start({ kind, label, owner, run }) preflights everything that can fail
(the attachSurface fence, validation, the owner-cleanup attach) BEFORE
invoking the producer's run() starter, then commits atomically —
'work started but never got a collectable id' is now structurally
impossible instead of a producer try/catch rollback obligation (the
P1 review fix, rebuilt on #185's declare/execute split). Producers
lose their catch-wraps; the leak tests now pin the stronger property
that a failed preflight never spawns anything. TaskRegistration splits
into TaskStart (identity + run) and TaskHooks (cancel/done/readOutput);
docs, type-equiv manifest, catalogs, and both RFCs move with it.
ctx.tasks.wait arms a deadline() fusing the caller's abort with the
wait timeout and classifies the outcome with timeoutOf scoped to the
new TASK_WAIT_TIMEOUT code: a wait timeout resolves to the live
snapshot (the task keeps running), a caller abort rejects the wait —
same contract, no hand-rolled timer/listener plumbing, and a nested
foreign deadline can no longer misread as a wait timeout. task_output
deliberately declares NO ToolDefinition.timeoutMs: timeout-policy
turns a timed-out call into a structured TOOL_TIMEOUT failure, but a
timed-out wait is a SUCCESS that must still report [status: running]
(decision recorded in the runtime RFC alternatives).
Adopts #185 (dsh-timeout: clampTimeout/deadline/timeoutOf drive bash
run() timeout classification; runBash loses its own timer) and #108
(ask_user_question) across the task-runtime rework: bash-local keeps
the BashProcess handle shape with master's deadline mechanics, tool
catalogs/expectations carry both the task_* and ask-user tools, and
generated docs are regenerated on the union.
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
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.
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.
Model-facing tool-call budgets were tangled into each capability's schema
(bash timeoutMs, web_fetch timeout_ms) with no shared home. Add a
tools/execute around-dispatch waterfall to dsh-tools whose base next() is
the dispatch-with-normalization thunk, and a new @deepseek-ai/dsh-timeout-policy
plugin (packages/timeout/) that arms a per-tool deadline on exec.signal and
returns a structured TOOL_TIMEOUT when it wins. Migrate web_fetch (drop the
model-facing timeout_ms) and web_search onto it; the fetch provider keeps its
timeout only as a resource backstop for direct callers. bash and hook command
execution keep BASH_TIMEOUT unchanged.
Named the plugin timeout-policy (not the RFC's tool-timeout) so it does not
trip the gen-tool-catalog packages/*/tool-* completeness guard, and replace
exec.signal by in-place mutation before next() since cordis waterfall next()
ignores passed arguments. RFC moved to implemented/ recording both deviations.
A client-callback throw only becomes a JSON-RPC error RESPONSE to the
agent's session/request_permission — runScenario itself kept going, so a
tolerant agent could treat the error as a denial and the scenario would
pass, or worse, record: the impossible click baked into fixture and
golden, green on every replay. The mismatch is now captured as a harness
error while the agent is answered plain cancelled (a well-defined path
it cannot reinterpret), and the step loop rejects the run on it as soon
as the in-flight step settles. The spec asserts the rejection instead of
the agent-side error echo.
The package, coverage, and permission scripting all shipped on this
branch, so the RFC moves to implemented/ with the lifecycle rewrite:
Proposal becomes a present-tense Decision, Acceptance criteria and Risks
fold into Testing/Consequences with what actually pinned each one (the
zero-byte extraction parity, the 100% per-file coverage via the fake
bin, the vitest-in-src caveat, the per-suite pin cost).
The snapshot tier's machinery leaves examples/acp-agent/tests for
packages/support/acp-snapshot (@deepseek-ai/dsh-acp-snapshot), where the
coverage gate measures it and a second example can consume it instead of
forking it: harness.ts (runScenario, parameterized by an AgentUnderTest
{binScript, configPath, tsconfigPath} instead of module constants),
normalize.ts (moved verbatim), and suite.ts (defineAcpSnapshotSuite — the
per-scenario golden/log compares, record write-back, per-suite header pin
with its uniformity guard, and the fixture guard block, lifted from
acp.snapshot.ts). The example file collapses to its scenario table plus
one factory call; env reading (DSH_SNAPSHOT) stays at that edge.
The exactly-one-pin meta-test generalizes from the hardcoded text-turn
name to "exactly one per suite" — which scenario pins is the scenario
table's reviewable choice (per-suite pinning per the proposal RFC).
Extraction parity: pnpm run test:snapshot is 36 passed + fs-policy-reject
failing BEFORE AND AFTER (BSD-sed environment failure, reproduced at the
base commit in a clean worktree — the recorded golden's sed -i syntax is
GNU-only), with zero byte changes under examples/acp-agent/tests/snapshots/.
Coverage for the new src files lands in the next commit.
The harness, normalizers, and suite/guard logic live inside
examples/acp-agent/tests, outside the coverage gate and copyable-only
for a second suite. Propose @deepseek-ai/dsh-acp-snapshot under
packages/support: parameterized runScenario, verbatim normalizers, a
defineAcpSnapshotSuite factory with per-suite header pinning, and
scripted permissionAnswers so an approval round-trip is expressible at
the snapshot tier — the sandbox composition is the immediate consumer.
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.
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.
Out-of-process delegation to external coding agents as two new subagent
seam backends, exposed as subagent_claude_code / subagent_codex tools.
Verified against @anthropic-ai/claude-agent-sdk 0.3.202 and codex CLI
0.142.5 via keyless spikes; includes the dsh-subagent-process extraction
plan, isolation/permission stances, and tiered test coverage.