The parent branch's zh translations were recorded against its pre-reshape
en text; this PR's Node-ward en deltas get minimal zh updates across the
subprocess catalog and the subprocess/lsp-local/subagent READMEs (all
type-equiv blocks byte-identical), with pairing records re-recorded.
(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.
Review direction (tianyicui, PR #660): in a stacked PR, change all other
process-running places to use the new service.
- lsp-local: LspConnection spawns through ctx.subprocess (piped protocol
streams + a no-spill collected stderr tail); its private process-tree
helpers (POSIX group signalling, Windows taskkill, liveness polling) are
deleted in favor of the seam's handle verbs, and its buildChildEnv now
rides scrubbedParentEnv (LSP children also stop inheriting stale DSH_*).
The plugin injects 'subprocess'; compositions/tests mount
dsh-subprocess-local.
- subagent-acp: the ACP child spawns through the seam (piped ndjson streams,
inherited stderr); spawn failure surfaces through done-rejection into the
same startup race; disposal is handle.dispose with the plugin's configured
graces. dsh-subagent-subprocess is DELETED — its dispose ladder and scrub
are the seam's, and the isolated-config-dir helper had no consumer.
- mcp-client, pty-local, sdk-helper: adopt scrubbedParentEnv as the one
scrub definition (their spawns stay put by ownership: the MCP SDK and
node-pty own those calls; the SDK wizard runs outside any composition).
- Coverage: per-file 100% over every touched src file, with each v8 ignore
carrying a platform or contract reason; new suites cover stdio
dispositions, the dispose ladder tiers, injected-win32 tree semantics,
waitForExit, settled-kill/terminate no-ops, and spawn-failure disposal.
- Docs: consumer-migration Agent Note (en; zh follows in this PR), seam note
updated in place, subprocess.md rewritten for the reshaped vocabulary
(type-equiv re-registered), READMEs and SERVICE_ROLES updated, taskkill
added to knip ignoreBinaries.
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).
Review feedback (tianyicui, PR #657 inline): the missing-service message
should mention dsh-tasks, which defines ctx.tasks, rather than promoting a
specific backend. The seam's own surfaces (README, the direct-mount fence)
keep pointing at implementations, so the pointer chain still lands on
dsh-tasks-local without the producer strings going stale when another
backend becomes the recommended default. Agent Note updated accordingly
(en+zh, re-recorded).
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).
The tasks/ family now matches the capability-seam shape: @deepseek-ai/dsh-tasks
keeps the abstract TaskService (ctx.tasks contract, vocabulary types, snapshot
invariant companion) and the new @deepseek-ai/dsh-tasks-local carries the
process-local registry (LocalTaskService: in-memory store, settlement,
owner-cleanup effects, teardown, TASK_WAIT_TIMEOUT). Compositions and test
harnesses now load dsh-tasks-local; producers, TaskKindMap merges, and
dsh-tool-tasks keep importing the seam only.
Producer misconfiguration diagnostics name dsh-tasks-local because loading the
implementation is the fix. The registry behavior suite moves to tasks-local;
the seam keeps a stub-subclass registration test and the probe-based invariant
suite.
Replace send/steer/inject with one Agent.send primitive over the
(target × wakeup) matrix; followup/steer/inject become fixed-preset
alias methods on the now-abstract Agent class. Coalesce context/message
into user/message (injected context is a non-user source). Replace
agent/queued with agent/inbox/enqueue/dequeue/discard, add cancel
keepInbox, and add a FIFO-conservation invariant.
Master landed the tool-subagent depth budget while this branch was in
flight: the numeric default maxDepth now fails the mount against the
ACP provider's depthLimit: false, so the composition fixture must state
maxDepth: 'provider-managed' — the documented opt-out for a provider
whose recursion budget lives in the child harness. The fixture also
moves off the retired stdio-demo REPL onto the current app-boot driver
pattern (runLoaderSmoke + one-shot cli), and the split isDirectory
statements gain the file-not-a-directory case the single-expression
form used to cover implicitly.
The inheritance branch ran only under hand-mounted plugin tests, and
the with-key e2e always configures an explicit cwd. Add a test-only
cordis.yml (scripted delegating model + the scripted mock ACP child)
booting the stdio app through the real Loader with cwd omitted: the
child proves it ran in — and was announced — the parent session's
workspace, keylessly.
SubagentStartRequest.parent, its core-data type-equiv mirror, and the
ACP backend Agent Note still said the out-of-process backend ignores
request.parent — no longer true: with cwd unconfigured it reads exactly
the session header's cwd, and a parent session without one now rejects
at start. State the one-field read at each owning contract, record the
workspace-resolution decision in the Agent Note, and align the README's
accessibility wording with the search-permission guard.
statSync().isDirectory() is true for a mode-600 directory, but a
subprocess cwd needs X_OK — spawn would fail EACCES, deferring a
self-contained configuration error past the load/start boundary the
resolution owns. Probe accessSync(X_OK) alongside the type check so
"not an accessible directory" is literally what the guard verifies.