The host's message listener trusted the compile-time WorkerToHost shape on
traffic from a peer that runs model code: postMessage(null) threw in the
listener and crashed the host process; forged log/done messages bypassed
maxLogBytes/maxValueBytes (the worker-side LogBuffer and prepareValue cap
only honest flows); and the error-reply renegotiation re-echoed a forged
non-cloneable call id, throwing outside any catch.
Every inbound message now passes a runtime shape gate that validates and
REBUILDS it field by field (junk drops without a throw; call ids must be
numbers, so replies are always clone-plain; forged extra fields never ride
along). One host-side ledger bounds everything landing in logs — honest
port entries, forged ones, and stray pipe bytes — at the single documented
maxLogBytes, with the shared in-band truncation marker emitted host-side
when the ledger trips first; the completion value is re-capped host-side
through the same prepareValue (with exactly the truncation suffix as slack
so honest worker-capped values pass unchanged), and done error text is
bounded. Also folds the stray-capture budget into that shared ledger
(round-1 finding B: it was a second maxLogBytes on top of the documented
shared cap).
The shipped backend of the code-execution seam, per the Code Mode RFC's
worker-thread section: one fresh Node worker per run, executing the
model's TypeScript after a host-side type-strip (wrapped in an
async-function shell so top-level return/await parse, sliced back out
position-preserved), bindings bridged over the message port under
hostile-peer rules (own-property name lookup, at-most-once replies,
post-settlement drops, null-prototype namespaces), logs streamed eagerly
with an in-band truncation marker, and two independent budgets — measured
event-loop busy time (computeMs) plus a never-pausing wall ceiling
(maxWallMs) — funneling into worker.terminate(). env: {} and execArgv: []
keep the isolate hermetic; disposal aborts in-flight runs and awaits
worker exits.
The worker entry loads unbuilt via Node's native type stripping
(src/worker.ts, erasable-only) and ships built as a sibling tsdown bundle
(lib/worker.js); tests/built-lib.e2e.ts pins the built load path under
plain node and joins the built-artifact smoke gate. Unit suites cover the
bootstrap in-process (fake port) and the runtime over real workers,
per-file 100%.
The seam's structured-clone boundary admits values JSON does not (BigInt,
Map, circulars), while tool/code-dispatch events must be JSON-appendable —
left unhandled, a sub-call could execute and then fail at logging time.
The bridge now JSON-normalizes binding arguments BEFORE dispatch (a value
that does not survive rejects that one call), so the dispatched form and
the logged form are the same JSON value by construction.
Adds the missing core-data-structures coverage the catalog policy
requires for non-spine seam vocabulary: the code-runtime.md sub-page
with drift-checked type-equiv blocks for all six seam types, the core.md
sub-page row, the type-equiv manifest entries, and LINK_MAP entries so
the generated service signature links CodeRunRequest/CodeRunResult;
cordis/config catalogs regenerated.
New group packages/code-runtime/ with the interface package
@deepseek-ai/dsh-code-runtime, per the Code Mode RFC: abstract CodeRuntime
service (run() resolves program failures as an error field, rejects only
for seam misuse), the CodeRunRequest/CodeBindingNamespace/CodeRunResult/
CodeLogEntry/CodeRunFailure vocabulary, and readonly language/isolation
backend descriptors. Registered in the tsconfig maps, packages/README,
architecture service map, and the doc-graph service-role classification;
catalogs regenerated.
The RFC's one forward path token to the worker package becomes an npm-name
mention until PR3 creates that directory (verify-package-paths is
drift-scoped: the now-existing group made the token checkable).
docs/architecture.md ceiling 1630 -> 1640: the doc gained a genuinely new
capability-service row; the row itself is already minimal.
Budget expiry terminated the worker but nothing cancelled an in-flight
host-side sub-dispatch, and a late dispatch could append events after
run_code returned. The bridge now owns a run-scoped AbortController
(follows exec.signal; fired on any run settlement), sub-dispatches get
the run signal, and run_code returns only after the dispatch queue
drains — no post-settlement appends, per dispose-to-quiescence.
(A1) Scope the wire-collapse guarantee honestly: systemPrompt.tools() is
a public multi-provider API, so the mode governs the registry's
contribution (the only shipped source); deliberate extra providers own
what they add, and the shipped-configuration invariant is test-pinned.
(A2) Replace pause-on-pending-RPC timeout with two independent budgets:
computeMs metered by worker.performance.eventLoopUtilization() busy time
(unfoolable by an un-awaited decoy dispatch; probe-verified) plus a
never-pausing maxWallMs ceiling.
(A3) Specify sub-call additionalContext as deliberately suppressed in
the MVP (immediate inject would break call/result adjacency; the plural
channel is named follow-up work).
(B) Orphan-process caveat vs bash-local's group kill; null-prototype
binding namespaces (__proto__/constructor names); per-PR doc artifacts
(packages/README row, architecture service map in PR2, config/tool/
persistence catalogs per owning PR); engines range corrected to
^22.19.0 || >=24.0.0.
Research finding: a SessionEventMap member is a log event — JSDoc prose
required, @mode is a hard error there, and docs/persistence-catalog.md
must be regenerated (todo/write is the log-only precedent). PR4's plan
now names both.
Own adversarial pass finding: a static inject on the registry would gate
ctx.tools (and every tool plugin) on a code runtime existing even under
mode 'native'. The RFC now names the sanctioned pattern: soft
ctx.get('codeRuntime') at use time (the agent-loop sessionPersistence
precedent) with absence failing loud in the provider thunk.
Supersedes the add-on-plugin + node:vm-stub draft in place (still proposed/):
code mode becomes a first-class ToolRegistry presentation mode
('native' | 'code' | 'both'), execution goes behind a new ctx.codeRuntime
capability seam whose shipped backend is one fresh Node worker thread per
run (type-strip, empty env, resource limits, hard terminate), at
bash-equivalent trust with no unsafe-flag ceremony. Renames the file to
2026-06-15-code-mode.md and regenerates the RFC index.
Three Codex findings on json-schema.ts, one discipline:
- required-declared and every value check now use Object.hasOwn — 'in' let
inherited names (toString) satisfy required, dodge additionalProperties:
false, and validate a declared property against the value's prototype
member instead of a carried one
- isObjectLike now means PLAIN JSON object (proto chain of at most one link,
realm-agnostic): a Date annotation or a Map-as-properties no longer passes
structurally and serializes lossily — they fail loud as subset violations
- startInProcessRun asserts BEFORE the defensive structuredClone, so a
hostile schema fails as OutputSchemaError, never a raw DataCloneError
Also the type-equiv catalog gap: tools.md gains the structured-output subset
vocabulary (4 blocks) with matching manifest entries. The driver index also
drops the runtime internals from its public re-export (runs acquire it
internally; no external consumer remains — see the following commit).
Review follow-up (#196): a listed name with no registered tool was silently
ignored; misconfiguration must block work instead. The check lives in the
assembly — the earliest moment the registered tool set exists (tool plugins
register after the service constructs) and the only universal one (cordis has
no "all plugins loaded" event; registrations change at any time). assemble()
is now async so the throw surfaces as a rejection rather than a synchronous
escape from a Promise-returning method.
Blast radius, pinned by a loop-level test: the rejection reaches the turn's
outer catch — the turn closes balanced with an `error` reason, agent/error
mirrors it, no step opens, no request/header is logged, no request reaches
the adapter, and the agent returns to idle; every turn fails identically
until the config is fixed. A boot-time validation pass was considered and
rejected (recorded in the RFC). The general principle — misconfiguration
fails loud, never a silent skip — is added to AGENTS.md.
A three-dot rest entry reads as elision in a cordis.yml; the spelled-out
sentinel says what lands there. The literal now appears once in code (the
constant) and once in the value-pinning test; every other reference — the
forwarding tests included — imports TOOL_ORDER_REST, which adds the
dsh-system-prompt devDependency to the two app packages.
Review follow-up on #196.
- run-gates.ts docSyncLeafGates() gains verify-export-jsdoc — CI lanes
and the pre-push hook execute this leaf list, not the doc-sync npm
script, so the gate was previously unenforced there (proven by
SessionForkErrorCode landing undocumented via a master merge while
checks stayed green; now documented). Same wiring gap fixed for
master's verify-config-catalog, which was also missing from the list.
- The heritage exemption now recovers the base's return surface: a void
base return carried no @returns duty, so an override returning a
concrete result documents it itself (annotated overrides run the
standard check; unannotated ones are classified by the checker so
faithful void overrides need no boilerplate annotation). Three new
negative-path tests pin it; RFC and module doc updated.
- AGENTS.md states each principle inline instead of citing RFCs (eight
citations removed; high-level doc links kept) and the editing section
now carries the self-containedness rule.
- Generated catalogs/graphs regenerated for the shifted line pointers.
Master's generated config catalog (#188, flattened paths #191) now records
plugin Configs; the nudge removal dropped structuredNudgeRetries from both
backends, so the regenerated catalog loses those rows.
Residuals from the Codex re-review:
1. A system delta's insert was flattened to one token, so deltas differing
only in inserted-line count compared equal. Now one {{system}} token per
inserted line — position AND extent survive, content does not.
2. The live uniformity guard folded only request/header snapshots, so a
mid-run header CHANGE (request/header-delta) could diverge from the pin
invisibly. Non-pinning runs now assert zero header-delta events: a
scenario that legitimately changes its header mid-run exists to show
that change, so it must pin (fail-loud until it does).
Codex review findings on the pinned-header change:
1. scrubRequestHeaders flattened a request/header-delta's whole
system/tools payload to one token, so two meaningfully different deltas
compared equal. Now the structural facts survive — keepStart/keepEnd
line positions, added/removed/changed tool NAMES — and only the bulk
(inserted prompt lines, schema bodies) is tokenized.
2. The one-pin design rested on an unasserted premise (all sessions
compose the same header). Every non-pinning scenario now asserts, live,
that each request/header its run produces equals the pinned fixture's
header (both sides normalized against their own volatile values), so a
session-dependent header fails loud until it gets its own pin.
Verified the guard bites: perturbing the pinned fixture's prompt fails
a non-pinned scenario with the intended message.
3. RFC de-slopped per docs/AGENTS.md: no PR reference, no SHOULD
spec-speak; Decision/Verification/Consequences updated for 1 and 2.
Codex round-3: alias prose matches the gate's strength only when the
target's own contract is prose-only. An export-import alias to a
function, class, or namespace target (or an unresolvable one) is now
refused — those carry signature/member contracts the alias cannot
hold; export the declaration directly instead. Const/enum/interface/
type-alias targets keep the self-documentation contract.
Tests pin the refusal for function, class, and namespace targets;
module doc and RFC updated.
Codex round-2 review found three adjacent fail-open shapes:
- Wrapped function expressions escaped classification: parentheses,
as/satisfies casts, and non-null assertions are now peeled before the
arrow/function test (initializers and default exports), and a
single-call-signature type literal counts as the surface signature.
A literal mixing call/construct signatures with anything else is
refused outright — no single signature to hold the tags against.
- The blanket 'export import X = N.member' skip was unsound (the target
can be a non-exported namespace member no walk visits): an alias now
documents itself.
- The heritage extra-parameter duty missed binding-pattern extras,
which no base declaration can name: they now trigger the standard
binding-pattern violation.
Five new negative-path tests pin the closed shapes; module doc and RFC
updated.
Every session.jsonl fixture embedded the full composed system prompt and
complete tool-schema list in its request/header event (~8 KB on one line,
identical across the suite), so any prompt or tool-schema edit forced a
re-record or hand-edit of every fixture — see the dynamic-workflows PR for
the churn pattern this removes.
Now exactly one scenario (text-turn, flagged pinsHeader) commits and
compares that content verbatim; every other fixture stores and compares it
as {{system}}/{{tools}} tokens via the new pure scrubRequestHeaders
normalizer (applied to both compare sides and to record-mode writes, so a
re-record cannot reintroduce the content). request/header-delta payloads
are scrubbed the same way; config/reason stay verbatim — a model swap
SHOULD churn every fixture, a prompt edit should not. Replay is unaffected:
script derivation reads only assistant/chunk events.
Fixture meta-guards enforce the split: non-pinning fixtures must be fixed
points of the scrub, the pinning fixture must not be, and exactly one
scenario pins. Committed fixtures migrated through the same function.
Docs: pinned-header RFC (implemented/testing), base snapshot RFC + testing
policy + llm-replay module doc/README updated.