Files
workdsh/packages/subprocess/subprocess
Tianyi Cui f81fcccd93 refactor(subprocess): keep terminate() as the seam's only termination verb
Delete kill(signal?) from SubprocessHandle: consumers stop a process only
through terminate()'s tree-scoped SIGTERM→graceMs→SIGKILL escalation
(idempotent, also driven by the spec's abort signal, a no-op once the tree
is gone). The single-signal verb had exactly one consumer family —
lsp-local — and what it bought there was a private re-implementation of
the same escalation. The internal kill closure stays in spawn.ts as the
dispose ladder's tier primitive; terminate() now routes through it too.

lsp-local collapses onto the seam's escalation:
- LspConnection replaces its terminate()/kill() pair with one terminate()
  that delegates to handle.terminate(). Behavior change: the
  framing-failure path terminates instead of instant SIGKILL, so a
  misbehaving server now gets SIGTERM plus the killGraceMs window to
  flush before SIGKILL.
- ConnectionSpec.pipeDrainGraceMs becomes killGraceMs: one grace, the
  spawn spec's graceMs, drives both the escalation window and post-exit
  pipe draining (the provider already passed killGraceMs for it).
- LspInstance.forceTerminate() drops its hand-rolled bounded first wait
  (LSP_KILL_GRACE) and escalateProcessTree (deleted with its export and
  unit test): the seam's escalation already commits to SIGKILL after
  killGraceMs, so only the unbounded quiescence awaits stay load-bearing.

Tests: kill()-shaped spawn specs become terminate()-shaped or fold into
the terminate() suites (group-wide delivery; the settled no-op case was
already pinned by 'terminate() after the tree died'); tree-survivor
coverage is intact. A stderr-'inherit' disposition test completes the
stdout/stderr symmetry so the scoped subprocess+lsp coverage gate stands
alone instead of leaning on subagent-acp's cross-package runs.

Docs: SubprocessHandle type-equiv block, seam/impl/group READMEs, and the
consumer-migration Agent Note lose the kill(signal?) vocabulary (zh pairs
re-recorded); cordis api/services catalogs regenerated.
2026-07-27 04:41:04 +08:00
..

@deepseek-ai/dsh-subprocess

English | 中文

The subprocess seam (ctx.subprocess). The abstract SubprocessService exposes one method — spawn(spec): SubprocessHandle — plus the vocabulary shared by every consumer: the fully-explicit SubprocessSpawnSpec, SubprocessHandle with its non-consuming offset-based output readers, SubprocessOutcome, CollectedOutput, and the managed DSH_* environment namespace (DSH_ENV_PREFIX, DshEnvironment). The local implementation lives in dsh-subprocess-local.

Contract

  • spawn(spec) returns immediately with a live handle; done resolves at process close with exit facts (SubprocessOutcome carries no output and no cause classification) and rejects only for spawn-level failures.
  • The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the calling seam's config, not to a hidden subprocess-service default (the dsh-bash request/spec split is the owning template). argv is never shell-interpreted; a consumer that wants a shell passes ['bash', '-c', command] itself.
  • Stdio is Node-shaped per stream: 'pipe' hands the caller the raw stream for its own protocol framing (LSP JSON-RPC, ACP ndjson), 'inherit' passes the parent descriptor through for diagnostics, and collect mode ({ maxBytes, spill? }) buffers a bounded tail with an optional full-stream spill file. Collect readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; a read whose offset slid out of the in-memory tail is lossy and points at the spill file when one exists. Collected output stays readable after settlement.
  • Termination is tree-scoped on every platform (POSIX detached groups with direct-child fallback; Windows taskkill /T): terminate() — the only termination verb — escalates SIGTERM→grace→SIGKILL (idempotent, driven by the spec's abort signal too, a no-op once the tree is gone), waitForExit() observes the whole tree, and dispose(graces) runs the cooperative stdin-EOF→SIGTERM→SIGKILL ladder out-of-process children need — the manager reacts but never classifies why (callers own deadlines and cause classification).
  • scrubbedParentEnv() / SENSITIVE_ENV_PATTERN are the one shared scrub definition: ambient credential-shaped and DSH_* names are dropped, explicit env merges after the scrub (a deliberately forwarded key survives), and dshEnv carries current harness facts on its own validated channel; splitEnvChannels() partitions a consumer config's single mixed env map onto those two channels (lsp-local servers and the ACP backend expose one map, and a configured DSH_* fact must ride the managed channel the ordinary one rejects). Spawners that cannot route through the service (node-pty backends, SDK-managed transports) import the scrub.
  • Disposal of the service terminates all still-running managed processes and awaits their exit.

See the subprocess data-structure catalog and the seam Agent Note.

Model Experience

Indirectly, through consumer seams (today the bash executor family behind dsh-tool-bash), which own all model-facing rendering of process output and lifecycle.

KV Cache effect

No direct invalidation; the named consumers own any request-prefix changes.

Known Limitations and Deferred Work

  • node-pty and SDK-managed spawns share only the scrub — the PTY backend's terminal fork and the MCP SDK's own stdio transport cannot route their spawns through this seam (the library owns the fork/spawn call); they import scrubbedParentEnv so the environment policy stays single-sourced.
  • The dispose ladder assumes stdin-EOF cooperation — a child that quiesces on a different signal (SIGHUP conventions, control sockets) needs its own tier-1 before the generic ladder fits.