Implement the continuable background subagents RFC: a durable child
session with a series of Task-backed activations, each disposing its
run before the Task settles.
- dsh-subagent: rename SubagentRun.sendMessage to strict steer, drop
run-level resume, add SubagentProvider.resume dispatch via
SubagentService.resume, the continuation start field, and the
versioned model-hidden subagent/descriptor session event.
- dsh-subagent-inprocess/-spawn/-fork: publish the control-allocated
child id, append the descriptor inside the initial turn, implement
cold resume from the child's own transcript under the live parent
scope, and strict running-only steer.
- dsh-subagent-control (new): SubagentControlService owning stable
child ids, descriptor snapshot/fold/authorization, Task-backed
activation with settle-then-dispose ordering, the process-local
active-run association, and steer-or-resume sendMessage routing.
- dsh-tool-subagent: background route branches on the provider's
resume capability (continuable via the control service; one-shot
task for ACP), returning both child and task ids.
- dsh-tool-subagent-control (new): the globally named send_message
tool rendering steered/started routes.
Keyless coverage spans Task ownership and disposal ordering, running
delivery, cold follow-up, descriptor rejection and rollback, known-id
reconstruction, kill during lookup, admission races, and a new
subagent-continuable ACP snapshot scenario.
Resolve the agent-loop import conflict by retaining both durable request context and runtime policy context. Refresh the combined session fixtures and regenerate documentation catalogs. Mark PDF artifacts as binary so staged whitespace checks do not parse PDF bytes as text.
Two lifecycle holes the registry and the consumer helper left open.
`AdapterRegistrationHandle.replace` had no liveness guard: after the handle's
disposer ran, a replace put routes back into the registry with nothing left to
release them, so the adapter leaked permanently. `owned` being empty cannot
carry that fact, because `replace([])` is the legal empty-section state, so the
disposer records it explicitly.
`installSettingsSection`'s watcher lacked the guard its own disposer carries:
a stored change landing while the consumer unloads reached `onChange`, which
re-registers routes against a fiber whose resources are being released.
Also documents `withFileLock` in the atomic-write README (it claimed one
export), records the age-based lock takeover as a known limitation, and lists
ctx.settings and ctx.credentials in the architecture capability table.
The chat stats line took its token totals from the loaded conversation nodes,
so paging changed them and compaction erased the billing behind replaced
content. It also had no way to show context occupancy: the numerator and
capacity never reached the browser.
Both now come from token-meter session projections read through the standard
useProjection seat. Window nodes keep supplying turn and step counts plus LLM
and tool wall times, which are correctly window-scoped facts about what is on
screen; accounting no longer comes from there.
`tokenUsage` supplies billing and cache hit. `contextPressure` supplies
occupancy, pairing the newest provider-reported prompt size with the newest
capacity recorded by `request/context`. Deployments without token-meter drop
the token groups; a route whose adapter advertises no capacity drops the
occupancy group rather than rendering a placeholder.
Occupancy is deliberately approximate: the numerator and capacity are
independent last-wins fields, not one atomic request observation, so switching
models pairs a fresh capacity with the prior route's pressure until the next
request reports usage. It is a user-facing reference figure that nothing in the
harness makes decisions from, and it matches how the TUI status line has always
computed occupancy. The Agent Note and token-meter README state this as a
decision, including why the atomic alternative was implemented and rejected, so
it is not re-litigated as a defect.
Snapshot delta is one added `Context N% of 128K` segment across eight web
goldens; the preceding commit absorbed master's pre-existing golden drift.
These 15 goldens already failed replay at origin/master d17fcd3a6 before this
branch touched anything: master's committed expectations lag master's own code
(localized sidebar labels, the access-mode control becoming a button, message
IconActions and clock placement, context-injection affordances).
Recorded by running DSH_SNAPSHOT=refresh over the web suite in a pristine
worktree at that commit, then importing the result here, so the stats-line
change in the following commit shows up as its own reviewable delta rather than
mixed into pre-existing drift.
Two web e2e specs still fail at that same pristine commit for non-golden
reasons and are untouched here: queue-actions (counts 2 user/message events
where it expects 1) and details-session-lifecycle (times out waiting for a
New session button).
Conflict resolution notes:
- StatsLine: master redesigned the row into pipe-separated groups with LLM and
tool wall times. Kept that design and swapped only the token accounting
source, so counts and durations stay window-scoped while billing and context
occupancy read the durable projections.
- Generated artifacts (cordis catalog, module graph, event producer/consumer,
i18n pairing hashes) and web snapshots took master's side; they are
regenerated and re-recorded after this merge.
- Web e2e goldens and details-panel/timeline assertions took master's side:
that evolution is unrelated to this branch.
- ui-conversation package.json: kept master's devDependency ordering, re-adding
only the token-meter entry this branch needs.
- While a picking flow is open (native chooser pending, browse dialog up) or
its pick is being adopted, every other menu action disables: a late outcome
must not race a concurrent selection or creation (ds-review-bot warning).
- ctx.directoryPicker joins the architecture Capability Services map (both
languages); neighboring rows condensed to keep the doc inside its ceiling.
- directory-picker-browse documents that its client half lands in the next
stacked PR: a -browse composition today hides the picking affordance (the
documented empty-hole default) rather than misbehaving (ds-review-bot
critical; the dialog itself ships in #821).
(A1) terminate()/dispose()/service teardown keyed on direct-child settlement
could leak a TERM-trapping descendant that outlived the leader (Codex
reproduced it with a disowned trap-SIGTERM helper). kill()/terminate() now
gate on tree liveness instead of outcome settlement; the SIGKILL escalation
timer survives settle (unref'd, re-probing the tree); dispose's tier
quiescence is whole-tree exit via a bounded waitForExit; the service's live
set releases handles only when their tree is gone, and its teardown awaits
tree exit. Three new suites pin the survivor scenarios end to end
(terminate, dispose, service teardown).
(A2) the escalation branch is now real tested behavior — its ignore is gone;
the one remaining signalTree guard ignore states why it is unreachable
through the handle verbs.
(A3) docs contradictions fixed: the impl README's stale POSIX-only bullet
now states the contained best-effort Windows tree story; the lsp-local
README no longer claims taskkill failures stay visible (containment + the
tree-liveness wait is the actual contract); the architecture tables (en+zh)
list all three consumer families.
(B1) OutputCollector keeps a byte-exact tail across uneven chunk boundaries
(trim the head chunk instead of dropping it whole) — the LSP diagnostic-tail
contract; pinned by a cross-chunk test.
(B2) the subagent-acp coverage ignore is narrowed to exactly the
never-settling success arm.
The automation bridge inherited two master-era assumptions the message
machine no longer honors. A prompt blocked at pre-turn admission opens no
turn, so no turn/end could ever settle it — the bridge now watches
whenIdle() and reports a turnless slot as cancelled (the disposed-agent
guard moved to a registry identity check before send). A failed turn no
longer rejects at its turn/end either: agent.retry() closes the failed
turn and opens a successor on the same history, so the bridge holds the
terminal error and lets a retry-triggered turn/start adopt the prompt,
rejecting only at quiescence with no successor.
Also: refresh the empty-response-retry fixture for retry-as-turn logging,
adapt master-side tests to the unified send()/UserMessageData API and
registry-fact disposal, resync the doc pairs both sides touched, trim
architecture.md back under its word ceiling, and regenerate the event and
persistence catalogs.
Codex re-review (A): AGENTS.md layout tree, the architecture capability
tables (en+zh), the seam Agent Note's group-name sentence (en+zh), and the
SERVICE_ROLES entry still carried process/-era labels; regenerated the doc
graphs and config catalog from the corrected inputs.
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
concludesTurn is terminal: a completed step now carries the concluded fact
to the driver, which ends the turn immediately instead of letting steering
that arrived during the tool batch (or from an agent/stopping listener)
reopen it. The steering is not lost — it drained into the log as
steering/message before the decision point, so it feeds the next turn's
request. Also annotates the driver's unreachable invariant guards
(exhaustiveness default, abort-slot ownership) for the coverage gate.
- Mount LocalProcessManager in the sandbox e2e compositions (bwrap/landlock/
seatbelt + the spine multi-project e2e) and add the spine demo's
dsh-process-local devDependency, so SandboxBashExecutor's new inject
resolves when those suites are enabled.
- Extend the Windows test/coverage skip to packages/process/* — the POSIX
process-group suite moved there from packages/bash.
- Update the stale disposal contract: the bash seam JSDoc, BashProcess JSDoc,
and core bash doc (en+zh) now state that composition teardown (the process
manager's disposal) owns kill-and-await, and an executor-only reload leaves
background processes running.
- Record ctx.processes in the architecture capability table and extension map
(en+zh) and the root AGENTS.md layout tree; reword the timeout-library note
so it describes where the plumbing and classification live today.