Skip to content

Release v0.0.17 — trust-boundary & commit-integrity hardening, story-mode opening, Node 26#24

Merged
ackness merged 32 commits into
mainfrom
v0.0.17-dev
Jul 21, 2026
Merged

Release v0.0.17 — trust-boundary & commit-integrity hardening, story-mode opening, Node 26#24
ackness merged 32 commits into
mainfrom
v0.0.17-dev

Conversation

@ackness

@ackness ackness commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Release PR for v0.0.17 — 31 commits since v0.0.16. Full notes in docs/CHANGELOG.md.

What changed

Consolidated audit remediation (2026-07-20, Batches 0–2 + follow-ups)

  • Trust boundary: canonical plugin identity (scoped-npm impersonation → 409), exact-action two-phase community approval (runtimes, declarative RPC handlers, entry imports), executor-side tool authorization, proposal identity rebinding, recursiveCall/PostRuntime/PreSchedule identity freeze, function-runtime capability revocation on timeout/abort.
  • Commit integrity: TurnCommitOutcome is authoritative on every completion path (proposal.failed SSE, withheld completion barrier, RPC 500 turn-commit-failed, no followers onto rolled-back state); turn_results.commit_status + origin stamping so turnCount counts committed player turns only; post-commit fan-out buffered until the enclosing transaction commits; SSE subscription established inside the session lock.
  • Store correctness: SQLite/Memory backends serialize outside writes against open transactions via a per-connection write gate (vector + mirror-media stores included); world-data sync got single-transaction atomicity + in-transaction CAS + post-commit media deletion.
  • Domain fixes: NPC graph moved onto a real turn clock with versioned relationship edges; event-runtime throttling (maxTriggerCount/cooldownTurns) restored; prompt hygiene (single-pass interpolation, XML-escaped core memory, demoted compacted summaries, pair-aware budget pruning that re-enables the hard budget for tool-using agents).

Story-mode opening experience

  • Stage media (scene backdrops, sprites/avatars) pre-warmed into the browser cache during pre-game.
  • The stage mounts as soon as the player submits the opening character form (world hero backdrop until the narrator's first scene.set), instead of waiting for pre-game to fully complete.

Toolchain / runtime

  • pnpm 11.9 / TypeScript 7.0 / Vite 8 (rolldown) / Vitest 4.1 / Electron 43 / Hono 4.12 / Undici 8.6.
  • Node runtime baseline 22 → 26 across engines, mise, CI workflows, and the digest-pinned Docker base image (node:26-alpine = 26.5.0-alpine3.24).

Why

The 2026-07-20 consolidated audit found the pre-0.0.17 build could report turn success on failed commits, let plugin-controlled code assume framework identity, and lose cross-session writes on single-connection rollbacks. Separately, players stared at a chat stream through the whole pre-game while stage art only started downloading afterwards.

How it was validated

  • release:preflight all green (lockfile sync, imports, plugin/world/prompt structure, actionlint).
  • Full-workspace turbo lint 21/21; full test suite green across remediation batches (40/40 tasks); web suite 406 tests.
  • Fault-injection tests for the commit domain (blocked proposal → proposal.failed + withheld barrier), cross-backend write-gate suite (verified red with the gate disabled), install/approval/authorization contract matrices.
  • Electron 43 unpacked build with native-addon rebuild validated during the toolchain upgrade.

Known risks / follow-ups

  • Electron packaging under Node 26 is validated by this PR's release CI for the first time (unpacked build was verified locally under the older baseline).
  • session.turnCount semantics changed (committed player turns only) — existing sessions with inflated counts keep their historical numbers; new turns count correctly.
  • Deprecated PLUGIN.md registration fields (tools.local/hooks/rpc/wires) were scheduled for removal in v0.0.17 but remain — removal moves to a later release.

Post-merge steps

Annotated tag v0.0.17 on the merge commit → release workflow builds installers and publishes the GitHub Release with the CHANGELOG section above.

ackness added 30 commits July 19, 2026 00:51
…table set

Upgrade the dependency baseline within the seven-day release-age policy:
pnpm 11.9.0 (onlyBuiltDependencies -> allowBuilds), TypeScript 7.0.2,
Vite 8.1.4 (rollupOptions -> rolldownOptions), Vitest 4.1.10, Undici 8.6.0,
Hono 4.12.29, Electron 43.1.0 + electron-builder 26.15.6, Radix umbrella
package replaced with per-package imports, unused optional langfuse removed.
Prettier 3.9.5 reformats 80 files. CI gains a 'pnpm peers check' step.

Networked validation fixes on top of the offline upgrade pass:

- Pin transitive @tanstack/router-core to 1.171.14 via workspace override;
  the offline resolution locked 1.171.15, which violates minimumReleaseAge
  (published 2026-07-13). Override can be dropped after 2026-07-20.
- Declare electron-winstaller: false in allowBuilds; pnpm 11 hard-fails on
  unlisted build scripts where pnpm 10 silently ignored them.
- Fix Button asChild crash: @radix-ui/react-slot exports Root as a sibling
  alias of Slot, so Slot.Root was undefined after leaving the umbrella pkg.

Validated: frozen install + supply-chain policy (901 entries), pnpm peers
check, full lint (TS7, 21 tasks), full tests (40 tasks), knip, i18n check,
release preflight, Vite 8 web production build, Electron 43 unpacked build
with native-addon rebuild (ABI 148).
…ad pipeline removal

2026-07-20 consolidated audit (audits/2026-07-20-consolidated-remediation),
Batch 0 quick fixes:

- H-04: character-tracker / npc-graph extractor / codex switch from exact
  `upstreamRequired: [narrator]` to `{ capability: narrative-engine }` with
  dual-engine injects (guide/scene-prompts pattern) — they were permanently
  skipped in dialogue mode, so character state and the relationship graph
  never updated. Contract test now locks the engine-agnostic shape and the
  chat-mode e2e asserts the tracker actually runs.
- H-05: remove `{{ player.message }}` interpolation from narrator and
  chat-mode-narrator PLUGIN bodies (zh+en) — player input rides the user
  role only, never the system prompt. Tests assert the inversion.
- M-18: drop raw `{{ inputs.* }}` body interpolation duplicating the
  escaped input.inject XML block (tracker/codex/extractor + player-init
  found by the new static check); added static regression check for both
  H-05 and M-18 in core-plugin-manifest-contract.test.ts.
- M-07: delete the dead `trigger_event` action (payload never read, no UI
  caller) across server SUPPORTED_ACTIONS, web api/store pipeline and
  types; `retry_runtime` now honors `payload.runtimeId` via the
  manual-trigger path. New actions-contract tests cover 400 + scoped retry.
- M-11: add missing `defer` field to the strict toolsConfigSchema so real
  PLUGin.md `tools.defer` declarations parse; parse-level tests added.
- L-01: fix `{ { locale } }` placeholder in generate-world.md — root cause
  was Prettier's embedded-YAML pass; quoted form is stable.
- L-02: codex prompt no longer references the unavailable plugin-data-list.
- L-03: collapse per-call legacy InMemoryToolClient wrapper to a direct
  module execute; unknown manifest-declared tools are warn-and-dropped from
  LLM advertisement instead of advertised schema-less (framework sentinels
  keep a minimal def); remove redundant renderWorkingMemory ternary.
- en PLUGIN.md variants synced with canonical frontmatter where they had
  drifted (priority/tags/upstream/inject).
- Docs synced: plugins.md, api.md, protocol.md.
…-03)

2026-07-20 consolidated audit, Batch 1 (P0 trust chain):

- C-01 canonical plugin identity: the install route now derives one
  canonical ID (manifest root name) — the reserved-builtin check, install
  dir, and API id all use it, and the package.json basename only
  participates after stripping the exact `plugin-` prefix. The scoped-npm
  bypass (@covel/plugin-narrator + name:narrator impersonating the builtin
  narrator) returns 409. Boot discovery hard-fails a dir/frontmatter
  identity mismatch (plugin registers as status:error, zero runtimes)
  instead of warn-and-continue.
- H-01 two-phase community approval restored: RpcApprovalGate.hasGrant is
  exact-action only (the optional-action prefix match let ANY grant unlock
  everything); entry import honors only the exact covel:plugin-server-code
  grant; loading a community runtime requires server-code AND
  runtime:<name> grants; plugin-rpc runtime dispatch walks the two phases
  (server-code approval first when missing, then the runtime grant).
- H-02 executor-side tool authorization: the agent loop passes the
  runtime's exact authorization set (declared tools + runtime-done
  contract; defer names included) via ToolCallContext.authorizedToolNames,
  and ToolExecutor.execute rejects out-of-set names with UNAUTHORIZED
  before resolution/approval — checked on the FINAL name, after session
  overrides and PreToolUse replacement. findTool fails closed for local
  tools when no context is provided.
- H-03 identity rebinding: tool-carried proposals (withPendingProposals)
  are rebound to the executing runtime's sessionId/turnId/source before
  commit; PreStateCommit replacement is payload-only (envelope pinned);
  PreSchedule replacement is strictly filter-only (reconciled by stable
  runtime name against the original set, original manifest objects
  re-used); isTrustedPluginSource drops the manifest pluginType fallback
  and trusts only the discovery registry source.

Tests: install reserved-bypass + canonical-id contract, discovery identity
gate, approval exact-action matrix, two-phase runtime rpc flow, executor
UNAUTHORIZED matrix, proposal rebinding, payload-only hook replacement,
PreSchedule filter-only, trust-source matrix. Full workspace lint + tests
green (40/40 tasks).

Docs synced: api.md (canonical identity, two-phase runtime approval),
tools.md (execution-side authorization), plugins.md (PreSchedule
filter-only contract).
…08/H-09/H-10/M-03/M-04)

2026-07-20 consolidated audit, Batch 2 (turn commit domain), first half:

- H-07 step 1: proposal commit failures are no longer reported as turn
  success. Each failure surfaces as a new `proposal.failed` SSE/trace
  event (CovelEvent union + web handler); any failure — or an auto-snapshot
  failure — withholds the completion barrier (turn.completed, memory
  ingestion) on both the main action path and the plugin-rpc manual /
  deferred-follower path. Web maps proposal.failed to a visible execution
  error.
- H-08: nested `ctx.recursiveCall` executions bubble their runtime
  results to the top-level TurnResult (`nestedRuntimeResults`, flattened
  across depths); the commit-owning callers process them through the same
  proposal barrier — leaf-runtime proposals were previously dropped.
- H-09: function-runtime capability revocation. Handler deadline + player
  abort merge into one AbortController (ctx.signal); every capability
  (store/pluginData/media/images/speech/gateway/utils/recursiveCall) is
  wrapped in a revocation proxy that rejects calls after the race settles,
  so a detached losing handler can no longer write after the lock releases.
- H-10: resume atomicity. The runtime no longer resolves the suspension or
  appends assistant history; the resume route folds proposal commit +
  assistant turn message + resolved marker into ONE store transaction
  (fallback: ordered writes), releases the claim on failure (retryable),
  announces turn.resumed only after the transaction lands, and gates on
  active session status under the lock.
- M-03: paused/ended sessions reject actions — fast 409 before the SSE
  stream opens plus an authoritative status re-read under the session lock
  before any history/interaction/compaction write (both /api/actions and
  /resume).
- M-04: PostRuntime hooks run BEFORE persistence on both agent and
  function paths — prompt history, commit proposals, and SSE now share the
  same finalized output; a hook that rewrites success→failed keeps the
  narrative out of history.
…M-02/M-05)

2026-07-20 consolidated audit, Batch 2 second half:

- M-02 turn accounting: `turn_results` gains `origin`
  (player|manual|follower|recursive) + `parent_turn_id`, stamped from
  `TurnInput.origin` at each producing site (main action path = player,
  plugin-rpc manual trigger = manual, deferred follower = follower, nested
  recursiveCall = recursive with parentTurnId). `computeSessionTurnCount`
  counts DISTINCT player-origin turnIds only, so a manual RPC, a background
  follower, or a recursive call no longer inflates `session.turnCount`
  (which drives UI turn display, auto-snapshot cadence, and numbering);
  legacy rows without `origin` still count as player. Idempotent column
  migrations added for SQLite and PG.
- M-05 SSE isolation: the eventBus→action-stream subscription is now
  established INSIDE the session lock. Subscribing before the lock meant a
  second action queued on the same session received the first action's
  events, rewrapped them in its own turnId/traceId envelope, and streamed
  them to its client as its own turn.
- Found while adding the H-07 fault-injection tests: the main turn path
  never passed `eventBus` into `executeTurn`, so every `emitSubEvent`
  inside the executor — including the completion barrier's
  `turn.completed` — silently no-opped on the player-facing path. Wired.

Tests: turn-count origin/dedup/back-compat matrix; commit-domain fault
injection (clean-turn baseline, blocked proposal → proposal.failed +
withheld barrier + no snapshot, paused/ended 409 with no writes).

Docs: transactions.md corrected (per-runtime transaction boundary, not
whole-turn — with the failure-visibility contract), CLAUDE.md turnCount
semantics (preGameCompleted is the band selector, not turnCount),
plugins.md `scheduled.interval` basis (player messages, not turnCount),
protocol.md `proposal.failed` event.
…polation

Comment style: audit finding IDs (H-0x / M-0x / …) are reused across audit
rounds, so they identify nothing durable in code. Comments now state the
reason directly; trivial fixes carry no comment. Docs still reference the
dated audit where that is the right place for provenance.

Also in this commit:
- Inject blocks are no longer re-interpolated during prompt assembly. Their
  content is XML-escaped upstream, but the escape does not touch , so a
  second template pass expanded any  appearing inside model- or
  player-authored data and inserted the expansion raw, bypassing escaping.
  The template is now interpreted exactly once, over the plugin's own body.
- Core-memory blocks are escaped and their label validated before use as an
  XML tag. Blocks hold text the model itself wrote in an earlier turn: an
  unescaped `</story_state>` could close the envelope early and the rest
  would read as framework instruction; a crafted label could inject markup.
  Unsafe labels fall back to a generic tag rather than failing the build.
…ection backends

SqliteStore and MemoryStore run everything on one connection / one shared
state, so `withTransaction` cannot isolate: a write issued elsewhere while a
transaction was open landed inside it — harmless on COMMIT, silently lost on
ROLLBACK. The session lock orders writes belonging to one session's turn, but
it is per-session and several routes hold no session lock at all (world
imports, character edits, settings), so a write from another session could
vanish when an unrelated transaction rolled back.

Both backends now share a serialized write gate: transactions and
outside-of-transaction writes queue on one chain, so an outside write waits
for the transaction instead of joining it. Writes issued from inside the
callback still run inline — they belong to that transaction, and queueing
them would deadlock against it. Inside vs outside is decided by
AsyncLocalStorage, so a concurrent caller that starts while a transaction is
suspended at an await is not mistaken for a nested one.

The mutating-method list is derived from the map MemoryStore already
maintains for touched-only snapshotting, so there is one list rather than two
that can drift. Throughput is unaffected (better-sqlite3 is synchronous, so
its statements were already serialized). Per-transaction connections, as
PgStore has, remain the long-term answer for real concurrency; this closes
the correctness gap without that rearchitecture. IdbStore is deliberately
excluded — browser single-user local mode.

Tests: cross-backend suite covering an outside write surviving a rollback,
inside writes still rolling back with the transaction, and queued writes
landing after a commit. Verified red by disabling the gate.
… agents

- world-init no longer seeds a new session by copying another session's
  plugin-data. It scanned every session of the same world, picked whichever
  had the most rows, and copied its whole schema + entries namespaces — but
  session plugin-data is player-writable through the generic PUT route, and
  on hosted tiers those sessions can belong to another user, so the copy both
  leaked and poisoned. Worlds that declare character attributes or ship
  dimensions never reached that branch; the cost is one schema-gen call per
  session on a world supplying neither.

- The generic plugin-data write API now rejects framework-reserved `_`
  namespaces (`_jobs` drives background-job scheduling, `_logs` is the log
  ring) and any core plugin's namespace. A forged world-init schema entry
  would have redefined the world's character attributes for every later turn.

- Budget pruning is pair-aware: it drops `tool` messages orphaned by the
  prefix cut instead of emitting a transcript that starts with a tool result
  whose assistant message is gone. That was the reason tool-declaring
  runtimes — i.e. every main agent, the runtimes that dominate long-session
  token spend — were excluded from hard budget enforcement entirely; the
  exclusion is now gone.

- Working memory is capped when rendered into the prompt (entry count and
  per-value length, with a visible truncation notice). It is unbounded
  storage that every runtime pays for on every turn.

- plugin-data get/list overlay this tool loop's own uncommitted proposals.
  Writes commit at end of turn, so a runtime that set a key and read it back
  in the same loop saw the pre-write value and either wrote twice or
  'corrected' an already-correct value. Scoped to the calling plugin's own
  pending writes.

- Corrected the player-abort comment in turn-executor: aborting stops
  proposal commits, but the character and memory builtin tools write straight
  to the store, so whatever they wrote before the abort is already durable
  and is not rolled back. PreStateCommit cannot veto those writes either.

Docs: world-data.md / plugins.md (reuse branch removed), tools.md
(read-your-own-write overlay).
…rop activate-all fallback

- Event-runtime throttling was dead everywhere. Fan-out is the only place an
  `event` runtime can trigger — the main scheduler evaluates it with an empty
  topic list, so its topic match always fails there — but fan-out passed
  hardcoded 'never triggered' history to shouldTrigger. maxTriggerCount and
  cooldownTurns therefore never bit: a once-only follower re-fired on every
  matching emission. Real per-runtime counts now reach the fan-out gate.

- Schema-declared runtimes were told to call `runtime-done`, a tool they are
  never given (buildToolDefinitions withholds it so the early-exit branch
  cannot fire before the JSON envelope downstream consumers read). The
  completion preamble now branches on termination mode: JSON-envelope
  runtimes are told to emit their schema output and explicitly not to look
  for that tool.

- `start_session` with an empty plugin set no longer activates every
  registered plugin and persists it. That silently enabled community plugins
  the player never chose and mutually-exclusive ones (both narrative engines
  at once) for the life of the session. Session creation is what picks the
  set; an empty set means the caller skipped that step, so it now returns 400.

- Community media wires: registration is process-global and survives revoke.
  Documented as an operator-level grant, which matches who can actually
  select one — wires are chosen by operator-authored llm.toml, and the
  X-Slot-Config sanitizer's field whitelist means a browser-supplied preset
  cannot carry a wire selection at all. Pinned with a test so that stays true.

- Marked the legacy StateManager deprecated: no production caller registers a
  state table, and its two-step writes are not transactional.

- Documented that emit-event dedup is per tool loop, not per turn — parallel
  runtimes may each emit the same topic, and fan-out's first-emission-wins
  collection is what keeps downstream subscribers firing once.
…coped locale rule

Completes the VERIFY-FIRST items after reading each one against the code.

- sync-dimensions is a four-phase rewrite (delete stale plugin-data rows,
  batch set, upsert lorebook, delete stale lorebook) that ran bare. A failure
  between phases — or a turn executing concurrently — could leave half the
  canonical world data, which feeds straight into the next prompt. Now one
  session lock + one transaction, rolled back and reported on failure.

- sync-data's conflict scan runs before the apply transaction opens (it reads
  the world package off disk), so anything it declared unmodified could be
  edited in that window and a force:false sync would overwrite it. The
  transaction re-reads each target's hash and aborts on drift (409
  world_data_sync_conflict); the route also holds the session lock now.
  Regression test verified red without the CAS.

- Media materialization registers each asset with a compensation stack as it
  lands. It previously only returned its refs on full success, so a throw on
  the second file orphaned the first one in the MediaStore.

- Compactor writes its summary and the message tags in one transaction. An
  orphan summary renders as a system message while the untagged history is
  still injected — the same content twice, with the summary carrying system
  authority.

- The NPC graph filled firstSeenTurn/lastSeenTurn from the node COUNT, which
  measures how much has been recorded rather than when: a fast-growing graph
  looked old, and a character mentioned twice in one turn advanced as if turns
  had passed. Anything derived from those fields was meaningless. Tool
  contexts now carry the authoritative logical turn number.

- The framework locale instruction covered 'tool parameters' wholesale, which
  invites translating enum members, ids, topics and schema keys — each of
  which then fails Zod validation or dispatch. Scoped to natural-language
  content, with an explicit rule to copy machine values verbatim.

- turn_results gains commit_status: pending when the execution artifact is
  written (before commit), settled to committed/failed at the barrier. A row
  left pending is a crash signature, previously indistinguishable from a
  successful turn. Added the settle method across all four backends.

- Documented that segments 9/10 aggregate only the executing runtime's own
  authorsNote/postHistory, and why: postHistory is a runtime's private working
  instruction, so cross-plugin aggregation would leak one plugin's internal
  directives into another's prompt. The docs claimed session-wide aggregation
  that the implementation never did — the docs were wrong, not the code.
Close a set of trust-boundary gaps where plugin-controlled code could
assume or rewrite framework-owned identity:

- recursiveCall now takes a RecursiveCallDelta (sessionId/turnId/origin/
  parentTurnId stripped) and returns a NestedTurnResult (no completeTurn),
  so a nested call cannot hop sessions, forge a child turnId the parent
  can never settle, or fire the authoritative turn.completed early.
- A PostRuntime hook replace may rewrite output/status but its
  pluginId/runtimeId/runId/turnId are restored to the loaded manifest's
  identity — the commit rebinder keys proposals on these fields.
- The function-runtime revocation set now also covers logger and
  assetProgress, so a timed-out handler cannot keep writing after the
  session lock releases.
- Plugin discovery publishes its capability caches only after every
  manifest validates, and undoes them if registry registration itself
  throws (e.g. a dataSchemas conflict) — a plugin left in error status
  leaves nothing behind for the tool/hook/wire/RPC wiring.
… handlers

A runtime can report success while its proposals fail to commit. Introduce
a TurnCommitOutcome that every completion path consumes instead of runtime
status alone:

- Synchronous plugin-rpc returns 500 turn-commit-failed on an uncommitted
  turn; the plain background-job and expected-follower paths settle the job
  failed and schedule no follower onto rolled-back state.
- A community plugin's declarative manifest.rpc action dynamically imports
  its handler module, which is server code by the same definition as an
  entry/runtime module, so it now takes the two-phase approval: the
  covel:plugin-server-code grant first, then the action's own grant.
… store

The write gate that keeps an unrelated write from being folded into an open
transaction (and lost on its rollback) only wrapped the DataStore methods.
The optional sqlite-vec capability and the mirror media store share the same
better-sqlite3 connection, so their writes still joined another session's
open transaction. Resolve one gate per connection via getConnectionWriteGate
and route the vector and media mutators through it too.
- resume buffers post-commit fan-out (proposal.committed SSE, PostStateCommit)
  until the outer finalize transaction commits, so a later write that rolls
  the transaction back cannot leave clients and hooks having observed a commit
  that was undone. Non-transactional stores keep running the fan-out inline.
- Turn counting advances a monotonic counter only for a fully committed player
  execution, after commitStatus settles — a failed commit no longer moves the
  count, and a fork's inherited count is preserved instead of being recounted
  from persisted rows the child never copied.
- The action SSE subscription is torn down while the lock is still held, and
  every stream write (execution.started included) goes through one serial
  queue, so a following action can no longer receive the previous execution's
  events and a forwarded bus event cannot overtake an awaited write.
- The character REST write takes the session lock like the turn path, closing
  a read-modify-write race with update-character.
…unes

- Move the `_`-prefixed reserved plugin-data namespace check into one shared
  validator and apply it at every plugin-controlled write path: the REST API,
  the plugin.data / plugin.data.batch commit handlers, and the function-runtime
  ctx.pluginData writer. Framework writers (job runner, logger) keep using the
  raw store. A plugin can no longer fabricate framework bookkeeping like _jobs.
- Enforce a per-session working-memory entry quota at the commit handler.
- Emit a trace-only context.pruned event when a runtime's assembled prompt
  loses history to the slot budget, so /debug can explain a truncated turn
  instead of the budgetExceeded flag being silently dropped.
…d manifests

- Event fan-out passes the real preGameCompleted set so a completed Pre-Game
  runtime is not resurrected by a later emission of its topic. Fan-out is
  deliberately not band-filtered — it is a causal reaction, not a scheduled
  slot, and band filtering would silently drop a subscriber across bands.
- A compacted history summary is model-written, persisted, and re-injected
  every turn, so it is now carried as an XML-escaped user-role data envelope
  labelled as story record rather than a system message — one prompt injection
  can no longer gain a permanent system-authority channel.
- Localized manifests are checked field-by-field against the canonical
  manifest so only natural-language fields may differ by locale.
An edge was de-duplicated by (source, target, relation) and its validAt was
the count of stored edge rows — a measure of graph size, not time — so a
relationship could never change strength or fact and recency ranking read
garbage. Edges are now versioned: an unchanged relation is a no-op, a changed
one closes the open version at the current turn and opens a new one, with
validAt/invalidAt taken from the authoritative turn number. Superseded
versions stay for provenance and are filtered out of retrieval. The version id
suffix uses a unique token outside the truncated slug so long endpoint names
and multiple revisions in one turn cannot collide and overwrite history.
- recursiveCall's RecursiveCallDelta/NestedTurnResult contract, PostRuntime
  identity freeze, and function-runtime capability revocation on timeout.
- Two-phase approval for community declarative RPC actions; turn-commit-failed
  and the commit-is-authoritative rule for RPC and background jobs.
- Per-connection write gate covering vector and media writes.
- event fan-out band exception, NPC edge versioning, prune trace, and the rule
  that runtime inject is no longer re-interpolated (drop the {{ variable }}
  guidance that would reintroduce player input into the system prompt).
- Correct world-init guard comments: schema is derived or imported, never read
  from another session.
- Reserved builtin plugin IDs are derived from the registry, not the
  post-quarantine discoveryMap: a builtin whose load failed is dropped from
  discoveryMap but still registered as an `error` entry carrying its source,
  so its id stays reserved and a community package cannot claim it.
- Sync the two internal createRecursiveCall option signatures to the public
  RecursiveCallDelta / NestedTurnResult contract.
- Heal a relation that already has two open edge versions: a same-turn double
  revision (writes don't commit between tool calls) could leave two open rows
  that the retriever would then inject as contradictory facts forever. The
  upsert tool now closes all but the newest open version on its next write, and
  the retriever de-duplicates open edges by (source, target, relation) as a
  fallback.
- Take memoryBlocks[*].label from the canonical manifest even though `label` is
  translatable elsewhere — it is a stable working_memory key, not display text,
  so a translation must not split it per UI language.
- Correct the media-store write-gate comment: cleanup is deliberately un-gated
  so its inner deletes each queue; gating cleanup itself would deadlock.
- A failed auto-snapshot no longer fails an otherwise-committed turn. The
  proposals are already durable (commit_status was "committed"); the snapshot
  is a best-effort checkpoint. Reporting it as a failure made the sync RPC
  return 500 and the client retry, replaying already-committed proposals.
  `committed` now tracks proposal commit alone, matching commit_status.
- Reuse `isPreGamePriority` in turn accounting instead of a hand-written
  `priority <= 99` that had drifted from the [0,99] band definition.
- Pin the known one-off under-count when a session is forked at exactly
  turnCount 1 (the floor-vs-inherited ambiguity), documented rather than fixed.
- Prune closed edge ids from the NPC-graph adjacency index so a revised
  relation nets zero index growth instead of accumulating dead ids.
- Enforce the working-memory quota (200 entries / 8000 chars) at the REST
  write path too, sharing one definition with the commit handler; a plugin
  route could previously grow it without bound.
- Guard the reserved `_` namespace on the store handed to trusted (builtin /
  official) function handlers, agent guards, and RPC entries — community was
  already blocked, but official plugins could still write `_jobs` / `_logs`.
  Framework writers use the raw store and are unaffected.
- A scoped retry_runtime (payload.runtimeId set) reran one runtime but was
  stamped origin=player and committed, so retrying advanced turnCount a second
  time over the same logical turn (UI turn number / snapshot cadence drift).
  It is now stamped origin=manual and kept out of the counter.
- world-data sync deleted media (removeRef + an irreversible rmSync) inside the
  import transaction, so a later abort rolled back the DB rows but not the
  deleted file, leaving a committed reference to a missing asset. Deletion is
  now deferred to after commit, mirroring the put-side finalization: the
  transaction only collects the unreferenced media ids.
When a node was re-upserted from a call with no turnNumber (currentTurn = -1),
its lastSeenTurn was overwritten with -1, rewinding a real turn index — and the
same -1 could push an edge's invalidAt before its validAt. Both now take
Math.max against the existing value so an unknown turn never moves the clock
backwards.
- Delete the unused InMemoryToolClient (its only caller was already removed).
- Correct comments that drifted from the code: the event fan-out throttle doc
  block (now feeds real per-runtime history), the three snapshot-completion
  notes (a failed snapshot no longer withholds completion), and the
  RecursiveCallDelta doc (a nested call reuses the parent turnId).
- Fix comment fragments and sed-mangled test names left by an earlier
  audit-ID cleanup, and drop the audit-ID references still lingering in a
  handful of source files.
- Pin that runtime inject content and core-memory blocks are treated as data:
  a `{{ player.message }}` token in an inject stays literal (never
  re-interpolated back into the system prompt) and a forged `</tag>` in
  core-memory content is XML-escaped.
- Pin that every mutating store method is registered in STORE_WRITE_METHODS —
  a new mutator that forgets to register would silently skip the serialized
  write gate and risk a lost write.
- Remove the @tanstack/router-core override whose minimum-release-age window
  has elapsed, letting it resolve normally.
- Pin node in mise.toml to satisfy engines (>=22.19), aligned with the Docker
  runtime, so a mismatched local node fails early.
- Bump the plugin template's vitest to ^4.1.10 to match the workspace.
Sweep the remaining audit, spec-task, and PR reference numbers (RR-*, H-*/M-*/
C-*/S-*, HIGH-*/MEDIUM-*/LOW-*, S3-T*, P0-*, PR-*, …) out of source comments
and test describe/it names across the workspace. They are dead links for anyone
reading the code — the explanatory text stays, only the tracer is removed.
Also repair two pre-existing UTF-8 mojibake lines (a `→` and a box-drawing
separator) surfaced during the sweep.
StageView only mounts once turnCount >= 1, so scene backdrops and
character sprites started downloading only after the pre-game turn
fully ended. World-package media bytes are already in the MediaStore
at session creation — preload known presence sprites/avatars and
scene-registry backdrops into the IDB media cache as soon as the
session opens, so the opening turn paints them straight from cache.
Align every runtime pin on Node 26.5.0: mise.toml, engines
(>=26.0.0), CI/release workflows (24 -> 26), and the Docker base
image (node:26-alpine, digest-pinned to 26.5.0-alpine3.24). Docs
updated to match. Verified with a clean full turbo lint (21/21)
and the web test suite (403 tests) under 26.5.0.
StageView previously mounted only at turnCount >= 1, so after naming
their character the player stared at the chat stream while pre-game
finished and the narrator generated the opening. Mount the stage as
soon as a form-type interaction is submitted (framework-level signal,
no plugin ids): the world hero backdrop + thinking indicator show
immediately, and the narrator's first scene.set swaps in the real
scene art from the pre-warmed cache.

Also refresh the stale _createCharacter note in CLAUDE.md — the
framework no longer auto-creates characters from marked forms; the
character plugin's pre-game guard synthesises the player from the
submitted form values.
ackness added 2 commits July 21, 2026 23:02
Trust-boundary and commit-integrity release, plus a smoother
story-mode opening. A consolidated full-repository audit (2026-07-20)
drove three remediation batches: plugin identity and community-code
approval are enforced end to end, a turn's commit outcome is
authoritative everywhere, and single-connection stores serialize
writes against open transactions. The GalGame stage pre-warms its art
during pre-game and appears as soon as the player submits the opening
form. Toolchain moved to pnpm 11 / TypeScript 7 / Vite 8 /
Electron 43; runtime baseline is Node 26.
pnpm 11's implicit verifyDepsBeforeRun auto-install reinstalls only the
filtered subgraph on a `pnpm --filter <pkg> run` and prunes every other
package from node_modules. The Node 26 bump invalidated the deps-state
hash, so CI's check:i18n step (root script fans out through a filtered
run) pruned 420 packages mid-job and the follow-up root script crashed
on a missing 'yaml' import; locally the same mechanism aborted
release:preflight. CI and local flows install explicitly, so run-time
auto-installs are never wanted.
@ackness
ackness merged commit 668b89f into main Jul 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant