Every packages/*/* README now carries a canonical '## Known Limitations and
Deferred Work' section: condensed, evidence-backed bullets for consumer-visible
gaps (unimplemented features, platform caveats, MVP cuts) and consciously
postponed work (TODO/FIXME/XXX markers, RFC deferrals still open). The ten
pre-existing ad-hoc variants ('What is NOT here (TODO)', 'Deferred',
'Limitations (MVP)', 'Known limitations (tracked TODOs)', ...) are normalized
into the canonical heading.
A new doc-sync gate, scripts/verify-readme-limitations.ts, enforces the shape:
exactly one limitations-like heading per package README, byte-equal to the
canonical h2, with at least one bullet; near-miss headings fail so variants
cannot creep back. Packages with genuinely nothing to declare (dsh-brand,
dsh-timeout, dsh-subagent-mock, dsh-app-boot) are whitelisted in the script and
must NOT carry the section; whitelist entries are validated against the scanned
package set so a rename fails loud.
Wired into the doc-sync chain (package.json) and the run-gates doc-sync leaf
set; the standing rule lands in packages/AGENTS.md and the adding-a-package
cookbook; decision record in
docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
(RFC index regenerated).
Also fixes two stale '(deferred)' markers claiming dsh-compact-basic is
unimplemented (the dsh-compact seam README's package table and the seam's
module doc comment).
@deepseek-ai/dsh-subagent-inprocess
The shared in-process subagent run driver. A library with no provider or import-time registration that the in-process backends — spawn (a fresh child) and fork (a child seeded with a prefix of the parent's log) — both build on. Each accepted run installs one provider-owned cleanup effect. The backends are thin shells that differ ONLY in the session seed they pass; everything downstream lives here, so neither backend depends on the other.
What it exports
startInProcessRun(ctx, request, options): SubagentRun
Runs a child as a child Agent on the same cordis context (ctx.agents):
- snapshots the accepted request before asynchronous owner setup: the parent and signal remain identity capabilities but are never reread from the caller-owned record; tool filter, seed, agent options, output schema, and prompt are detached. It computes child depth =
depthOf(parent) + 1and rejectsrequest.maxDepthoverflow withSubagentDepthError;outputSchemais asserted before cloning so a hostile value fails asOutputSchemaError, while the prompt passes the session log's lossless-JSON check before and after cloning; - first installs provider ownership, then attaches the request abort listener and creates one run-owner Cordis fiber under
parent.ctx; an already-unloading provider therefore leaves no child or orphaned listener. Async child creation goes through that fiber'sctx.agentsservice with fresh IDs, lineage/seed, inherited model, and an unpublished setup transaction for persona, tool restriction, and structured output. Parent teardown, provider teardown, and manualrun.dispose()all dispose this exact node, preventing publication after it becomes inactive and awaiting the same quiescence boundary.startInProcessRunstill returns itsSubagentRunimmediately:run.startedresolves only afterctx.agents.create()has published the child (and rejects if publication never happens), while cancellation during creation is recorded and applied when a child exists; - drives the one-shot:
child.send(prompt)thenawait child.whenIdle()(ordering matters —sendenqueues synchronously, sowhenIdleobserves the queued work and resolves on the child'srunning → idletransition, never before the turn starts); there is deliberately NO re-prompt for a structured child that finished cleanly without callingstructured_output— the shortfall maps to anerrorresult for the parent; - reads the result, scoped to the child's OWN events (everything at or after
seedLength, so a seeded child that produced no message of its own never returns the seeded parent's last message): the lastassistant/messagecontent (deep-cloned — the log is frozen) and the lastturn/end.reasonmapped to aSubagentStopReason. A structured run surfaces the captured value asresult.structured; a structured child that finished cleanly WITHOUT ever capturing settleserror(a clean finish without the demanded result is a failure, not a success with a missing field).
SubagentService waits for run.started before emitting subagent/start, so a synchronous start observer can resolve the published child with ctx.agents.get(run.id); the result driver awaits the same boundary before sending the prompt. An attempt that never publishes rejects readiness and emits no false start/end pair; its result reports a deliberate cancel/dispose as aborted and propagates an infrastructure fault. dispose() awaits creation or rollback and then delegates to AgentHandle.dispose() (stop and drain → remove agent → detach session → unwind scope); cancel() records its request even before publication and cancels the child immediately once available. A cancel landing before any turn/end still settles aborted, honoring the cancel contract rather than the generic no-turn error.
InProcessRunOptions
{ seed?: SessionEvent[] } — the optional child-session seed: absent for spawn, or the parent's balanced completed-turn prefix for fork.
Structured output (package-internal runtime)
attachStructuredRuntime(childCtx, schema) registers the run's whole enforcement surface as SCOPED registrations on the child's agent.ctx — riding the child's fiber (a backend hot-reload mid-run cannot unregister anything; a disposed child leaves no residue) and visible to that child alone (two concurrent structured runs never interact; no placeholder schema, no strip-for-everyone-else, no refcounted global state):
- the
structured_outputcapture tool with the run's REAL schema as its registeredparameters, validating each call (validateStructuredValue) — violations become anINVALID_ARGSisError the model retries in-turn; a valid call STAGES the value in aWeakMapkeyed by that call'sToolExecutionobject; - the calling instruction as an ordinary order-190 scoped prompt section (the demand travels with the tool, as prompt state of exactly one agent);
- a scoped
systemPrompt.protect()registration making the capture instruction and schema canonical after the complete assembly waterfall. Canonical absence is protected too: pure Code Mode removesstructured_outputfrom the wire and declares it through the SDK. The tool registry separately owns and protects itstools:sdksection and reservedrun_codetransport; protection guarantees those named contributions, while unrelated listener-added schemas remain the listener's responsibility. The loop logs the finalized assembly as the step'srequest/header, so the demand remains reconstructable; - a scoped
tools/resultobserver as the commit point: it promotes a staged value only when that same execution's immutable, JSON-safe authoritative result after the complete pre-execute → guards → execute → post-execute pipeline succeeds. For a Code Mode SDK sub-dispatch, the child's opaqueparenttoken matches the enclosingrun_codeexecution's registry-assignedtoken, so promotion waits for that outer final result without exposing its live object; a runtime failure or post-policy block discards the value. Execution-object identity prevents call-id reuse or another execution from reaching the stage; - a scoped monotonic
tools.guard()denial for every call arriving after capture. Guards run after the extensible pre-execute waterfall and cannot return allow, so terminal means terminal within the step regardless of listener order; - a scoped
agent/turn-stopterminal policy stopping the child's turn once its output is captured. It runs after ordinary continuation and steering folding, and its terminal state survives turn close and flush, so later listeners cannot leak steering into another step or turn; ordinary queued prompts remain intact.
depthOf(agent): number
Delegation depth rides on a merge-extensible AgentOptions.subagentDepth field (0 for a top-level agent, parent + 1 for a child), so a nested spawn reads its parent's depth from parent.options.subagentDepth. depthOf reads it (absent ⇒ 0).
SubagentDepthError
Thrown by startInProcessRun when a spawn would exceed the request's maxDepth cap; carries attemptedDepth and maxDepth.
Known Limitations and Deferred Work
- The structured-output runtime is context-global — the tool registry and prompt assembly are context-wide while schemas differ per concurrent child, hence the final-assembly enforcement dance; per-agent/per-session scoping would dissolve it (the module-doc
FIXME). - Runs expose no
sendMessage/resume— the optional runtime capabilities are absent on in-process runs; the consumer collects synchronously. toolFilteris unimplemented in this driver — both in-process backends declare itfalse; scoping a child's tool set is deferred.