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.
ds-review-bot critical: compactIfNeeded estimated pressure from the
derived history + system prompt only, but every loop-built request also
carries EpochHeader.messagePrefix in front of the history — a
deployment at the window edge would under-estimate by exactly the
prefix, skip compaction, and ship an over-window request.
BasicCompactService now gates on estimatePressure(): the session prefix
read from the log's folded header + the derived history + the system
prompt. The fold is exact from the instance's second request on (and
from a resumed instance's first — the previous instance logged its
prefix); it is absent only before a fresh session's first request,
where the history is a single prompt and compaction is moot. Compaction
itself still shrinks history only — a prefix that alone approaches the
window is a configuration error no compactor fixes, same as the
documented single-unit-overflow stance.
The credential env scrub (SENSITIVE_ENV_PATTERN/buildChildEnv), the
spawn-failure capture (spawnFailure), the child-exit waits
(waitForExit/exitsWithin), and the stdin-EOF -> SIGTERM -> SIGKILL dispose
ladder move out of subagent-acp into a new pure library package (the
subagent-inprocess shape), with the ladder taking its two grace periods as
parameters — defaults stay in the plugin Config. New isolated-config-dir
helpers (mkdtemp create, best-effort remove; a pinned dir is never removed)
land alongside for the CLAUDE_CONFIG_DIR / CODEX_HOME redirection the RFC
names.
The ACP backend migrates onto the library with no semantic change: its suite
passes with import-path edits only. bash-local keeps its sibling copy, per
the RFC's blast-radius call.
RFC: docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
Two ds-review-bot findings:
The seam JSDoc claimed extending 'await next()' composes in
registration order — false for the append form: the waterfall unwinds
innermost-first, so appending places later-registered contributions
first. The canonical contribution is now documented as the PREPEND
'[mine, ...await next()]' (registration order on the wire), with the
append form's reverse-order behavior stated explicitly; the ordering
test now uses the canonical pattern in both listeners.
scrubRequestHeaders tokenized only system/tools, so a fixture recording
a composed session prefix would carry its raw text (workspace-specific
churn/leak). The scrubber now maps each header/delta messagePrefix
entry to a {{messagePrefix}} token — count stays a structural fact,
absence stays absent, the empty-array transition stays visible — with
normalize.spec coverage for the header, delta, absence, and odd-shape
paths.
Brings in the refreshed base (master merged through the stack after #203
and #205 landed), including the acp-snapshot extraction (#204), and
re-ports this PR's snapshot-suite extensions onto the extracted package:
- dsh-acp-snapshot's Scenario gains headerClass and configPath; the suite
factory pins the request header PER CLASS (construction rejects a
missing or duplicated class pin), forwards a scenario's configPath to
the harness (RunOptions.configPath overrides AgentUnderTest.configPath),
and a new fixtures meta-test asserts every pinning fixture carries
exactly one request/header and no deltas.
- The acp-agent example's thin scenario table re-registers code-mode-turn
and both-mode-turn with their overlay configs and per-class pins; the
committed fixtures replay unchanged.
- The package's synthetic suites cover the new surface (explicit
headerClass on one suite, the default on the other, a configPath
override through the fake bin, and the two construction throws).
The master merge added a code-runtime/ package row while this branch adds
the timeout/ row; together they push packages/README.md over its 605-word
ceiling. Condense the timeout/ row to the terse sibling style and raise the
ceiling 605->610 for the genuinely-new package group, mirroring how the
code-runtime work raised architecture.md's ceiling in the same spirit.
Review discussion converged on the industry shape (Claude Code caches
user context per conversation; Codex separates initial context from
diffs; Kimi appends at continuation boundaries to protect prompt
caching): stable openers belong in a compose-once prefix, mid-session
changes belong in append-only history — not in a per-request slot.
agent/session-prefix fires ONCE per loop instance, lazily on its first
request-building step: the composed Message[] is deep-frozen, cached on
the transmission bookkeeping, recorded as EpochHeader.messagePrefix on
the anchoring 'initial'/'resume' snapshot, and reused verbatim for
every request the instance sends — prefix stability is structural, not
a producer discipline, and a resume recomposes with attributable drift.
The request is messagePrefix + boundary snapshot.
The per-step RequestAdvice/RequestAdviceContext surface and the
messageSuffix header field are dropped: the tail slot had no consumer,
and every current update pattern (new AGENTS.md discovered, memory
update, skills change) routes through the existing append-only history
channels — inject(), tools/post-execute additionalContext,
prompt-submit additionalContext — each paid once and prefix-cached
thereafter. The messagePrefix delta arm stays for codec totality; the
loop never produces one in practice.
The zero-config enforcer no longer imports schemastery (its Config was
removed); knip flags the stale dependency. Remove it from the manifest
and sync the lockfile.
The RFC's deployment-policy decision is unchanged; state the current
mechanism in place — the per-tool budget is declared on ToolDefinition
(timeoutMs, set by the owning tool plugin from its config) and the
enforcer is zero-config, so a mistyped tool name is impossible.
Regenerate config-catalog (timeout-policy -> no-config; tool-web gains
fetch/searchTimeoutMs), the event graph (tools/change loses its
timeout-policy consumer), the ToolDefinition type-equiv block, and a
source-line drift in the cordis services catalog.
fetchTimeoutMs/searchTimeoutMs (default 30000) resolve to each tool's
ToolDefinition.timeoutMs, moving the budget's declaration home onto the
owning tool plugin and preserving per-tool deployment override without a
mistypable central tool-name map.
The enforcer now reads ctx.tools.get(exec.name).timeoutMs instead of a
free-text tool-name config map, so a mistyped name is impossible and the
tools/change warn-once apparatus is gone. exec.name always resolves in the
registry during dispatch, so there is no unknown-name path to warn about.
Both bot criticals verified against the code and rejected as exploit
paths — pinned instead of patched:
The bindings loop already excludes run_code (the skip predates the
finding), and the runtime host resolves forged port calls as own
properties of the bindings record, so an absent binding is unreachable
from a program under any mode. A new both-mode test pins the invariant:
the record has no run_code key on any lookup path.
The drain await cannot mask a run failure: `queue` is the folded tail
(every link swallows its rejection), so `await queue` never rejects and
the runtime's own result.error always reaches the CodeRunFailedError
conversion — the existing abort test exercises exactly the
queued-abandonment-plus-run-failure scenario. Stated at the drain site so
the fold's purpose is explicit.
A tool declares its cooperative timeout budget on its own definition
rather than a deployment naming it in a central config map. The field
never reaches the model (schemas() whitelists name/description/parameters)
and defineTool rejects a non-positive-finite value at authorship.
- syncTools: on paginated listTools failure, unregister any tools already
registered in the current sync before rethrowing (prevents orphans)
- Effect disposer: call client.close() directly without awaiting startup
completion — aborts a hanging connect promptly on HMR/dispose
Three findings from the PR-4 convergence round:
(A) A root-undefined binding argument passed normalization untouched, so
the sub-call DISPATCHED and only then failed the tool/code-dispatch append
(Session.append rejects undefined event data) — a sub-call executed with
no log record, violating the nothing-executes-unlogged contract. And the
tool received the SAME object later handed to the append, so a tool
mutating its args desynced the logged record from what was dispatched (or
re-poisoned the append). jsonNormalizeArgs now rejects undefined up front
with a model-correctable message and returns TWO independent parses of the
canonical JSON text: the tool gets one, the event logs the sibling —
identical by construction, mutation-proof.
(B) The bridge built its bindings record with plain-object assignment, so
a registered tool named __proto__ hit the prototype setter and silently
vanished (the runtime host resolves binding names as own properties). The
record is now null-prototype with defineProperty, mirroring the
worker-side namespace build.
(B) The header-pin sanity assertions ran only inside NON-pinning
scenarios, so a class consisting solely of its pinning scenario (the two
Code Mode classes) would accept a re-recorded pin carrying several headers
or a header-delta. A fixtures meta-test now asserts every pinning fixture
directly.
- syncTools: on paginated listTools failure, unregister any tools already
registered in the current sync before rethrowing (prevents orphans)
- Effect disposer: call client.close() directly without awaiting startup
completion — aborts a hanging connect promptly on HMR/dispose
- syncTools: on paginated listTools failure, unregister any tools already
registered in the current sync before rethrowing (prevents orphans)
- Effect disposer: call client.close() directly without awaiting startup
completion — aborts a hanging connect promptly on HMR/dispose
Add the mcp-client workspace override so knip recognises the e2e test
file, fixture-server entry, and the bin-only devDeps (server-everything,
server-filesystem) that are invoked at runtime rather than imported.
Prove the full MCP protocol flow works end-to-end against real servers:
- Self-written fixture server: tool discovery, execution, error handling,
image placeholder, toolPrefix, and clean disposal
- @modelcontextprotocol/server-everything: echo, get-sum, get-tiny-image
- @modelcontextprotocol/server-filesystem: write_file + read_file round-trip,
list_directory with world-verification
All 15 tests keyless and deterministic (no API key needed).
Addresses CI coverage gap: exercises the latest-wins resync coalescing
(pendingResync branch), the client.onclose callback, and ensures index.ts
is loaded without module mocks for stable v8 coverage across environments.
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
Two findings from the GitHub review bot on the ready PR:
The tsdown two-entry build emitted the shared bootstrap module as a
lib/bootstrap-*.js chunk imported by both bundles, which the package.json
files whitelist (deliberately exact) omitted — a packed install had
dangling imports. The package now runs two single-entry builds, so each
bundle inlines its own bootstrap copy and every shipped file is
self-contained.
prepareValue admitted any cloneable value whose BOUNDED inspect rendering
fit maxValueBytes, so a huge container with a compact rendering (a
50k-element array renders as '... N more items') crossed the port raw,
bypassing the cap on both sides. The cap now measures the value's real
cross-boundary size — exact bytes for strings, the structured-clone wire
size (v8.serialize) for everything else — and oversized containers cross
as their bounded rendering instead.