Release v0.0.17 — trust-boundary & commit-integrity hardening, story-mode opening, Node 26#24
Merged
Conversation
…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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
recursiveCall/PostRuntime/PreSchedule identity freeze, function-runtime capability revocation on timeout/abort.TurnCommitOutcomeis authoritative on every completion path (proposal.failedSSE, withheld completion barrier, RPC 500turn-commit-failed, no followers onto rolled-back state);turn_results.commit_status+originstamping soturnCountcounts committed player turns only; post-commit fan-out buffered until the enclosing transaction commits; SSE subscription established inside the session lock.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
scene.set), instead of waiting for pre-game to fully complete.Toolchain / runtime
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:preflightall green (lockfile sync, imports, plugin/world/prompt structure, actionlint).turbo lint21/21; full test suite green across remediation batches (40/40 tasks); web suite 406 tests.proposal.failed+ withheld barrier), cross-backend write-gate suite (verified red with the gate disabled), install/approval/authorization contract matrices.Known risks / follow-ups
session.turnCountsemantics changed (committed player turns only) — existing sessions with inflated counts keep their historical numbers; new turns count correctly.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.17on the merge commit → release workflow builds installers and publishes the GitHub Release with the CHANGELOG section above.