2026-07-26 12:43:14 +08:00
# Subprocess
2026-07-26 06:59:01 +08:00
2026-07-26 22:00:38 +08:00
English | [中文 ](subprocess.zh.md )
2026-07-26 15:27:59 +08:00
The subprocess seam is split across interface ([dsh-subprocess ](../../packages/subprocess/subprocess ), `ctx.subprocess` ) and implementation ([dsh-subprocess-local ](../../packages/subprocess/subprocess-local )); its consumers are other capability seams and out-of-process backends — the [bash executor family ](bash.md ) (collect-mode batch output), the LSP host (piped protocol streams + a collected stderr tail), and the ACP subagent backend (piped protocol streams + inherited stderr). This seam owns the managed `DSH_*` environment namespace, the shared credential scrub (`scrubbedParentEnv` ), and the `CollectedOutput` shape; [dsh-bash ](../../packages/bash/bash ) re-exports the vocabulary so bash consumers keep one import root.
2026-07-26 06:59:01 +08:00
2026-07-26 12:43:14 +08:00
Source: [`packages/subprocess/subprocess/src/types.ts` ](../../packages/subprocess/subprocess/src/types.ts )
## Managed environment namespace and captured output
2026-07-27 04:14:51 +08:00
`DSH_*` variables are Harness-owned child-process facts; implementations discard ambient `DSH_*` names before the caller's explicit `env` merges, so a current fact arrives only as a deliberate entry, and each collected stream reports its truncation and spill-recovery state through `CollectedOutput` .
2026-07-26 12:43:14 +08:00
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = ` ${typeof DSH_ENV_PREFIX}${string}`
` ``
` ``ts type-equiv
/** Trusted DeepSeek Harness variables for one child-process execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
` ``
` ``ts type-equiv
/** One captured stream: the (possibly truncated) text plus recovery info. */
interface CollectedOutput {
/** Collected text — the TAIL of the stream when truncated. */
text: string
/** True when bytes were dropped from ` text`. */
truncated: boolean
/** Path to a file holding the COMPLETE stream, when truncated and available. */
spillPath?: string
}
` ``
2026-07-26 06:59:01 +08:00
2026-07-26 15:27:59 +08:00
## Node-shaped stdio dispositions
Each stream's disposition is explicit, chosen per consumer: raw pipes for protocol framing (LSP JSON-RPC, ACP ndjson), inherit for pass-through diagnostics, and collect mode for bounded batch output — with the spill file optional, so a diagnostic tail (a language server's stderr) buffers without leaving files behind.
` ``ts type-equiv
/**
* stdin disposition. ` 'ignore'` leaves fd 0 on ` /dev/null`; ` 'pipe'` exposes
* {@link SubprocessHandle.stdin} for the caller's ongoing protocol writes;
* ` { data }` writes the bytes and closes (the batch shape).
*/
type SubprocessStdinMode = 'ignore' | 'pipe' | { readonly data: string }
` ``
` ``ts type-equiv
/**
* Bounded in-memory collection for one output stream, with an optional
* full-stream spill file. Omitting ` spill` keeps only the in-memory tail —
* the diagnostic-tail shape (a language server's stderr); including it makes
* the complete stream recoverable up to its cap (the bash tool shape).
*/
interface SubprocessCollect {
/** In-memory cap in bytes; overflow keeps the TAIL. */
maxBytes: number
/** Full-stream spill file; absent disables spilling entirely. */
spill?: {
/** Whole-stream byte cap; a larger stream discards its now-incomplete spill. */
maxBytes: number
}
}
` ``
` ``ts type-equiv
/**
* stdout/stderr disposition. ` 'pipe'` exposes the raw ` Readable` for the
* caller's protocol decoding; ` 'inherit'` passes the parent's descriptor
* through (child diagnostics land on the harness's own stream); a
* {@link SubprocessCollect} object buffers boundedly with offset-based reads.
*/
type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect
` ``
` ``ts type-equiv
/** Per-stream stdio dispositions, all explicit — this seam applies no defaults. */
interface SubprocessStdio {
stdin: SubprocessStdinMode
stdout: SubprocessOutputMode
stderr: SubprocessOutputMode
}
` ``
2026-07-26 06:59:01 +08:00
## The fully-explicit spawn spec
2026-07-26 15:27:59 +08:00
The seam applies no defaults: every disposition, limit, and directory is explicit on the spec, so the caller's own config — not a hidden subprocess-service default — decides them. ` argv` is never shell-interpreted.
2026-07-26 06:59:01 +08:00
` ``ts type-equiv
/**
2026-07-26 15:27:59 +08:00
* A fully-specified spawn request. This seam applies no defaults: every
* disposition, limit, and directory is explicit, so the caller's own config —
* not a hidden subprocess-service default — decides them (the ` dsh-bash`
* request/spec split is the owning template).
2026-07-26 06:59:01 +08:00
*/
2026-07-26 12:43:14 +08:00
interface SubprocessSpawnSpec {
2026-07-26 06:59:01 +08:00
/** Executable and arguments; ` argv[0]` is the program. Never shell-interpreted here. */
argv: readonly string[]
/** Working directory for the child. */
cwd: string
2026-07-26 15:27:59 +08:00
/** Per-stream stdio dispositions. */
stdio: SubprocessStdio
2026-07-26 06:59:01 +08:00
/**
2026-07-26 15:27:59 +08:00
* Grace period in milliseconds for the {@link SubprocessHandle.terminate}
* escalation and for draining still-open collected pipes after the process
* exits (an inherited descriptor held by a surviving descendant cannot hold
* the outcome open indefinitely).
2026-07-26 06:59:01 +08:00
*/
2026-07-26 15:27:59 +08:00
graceMs: number
2026-07-26 06:59:01 +08:00
/**
2026-07-26 15:27:59 +08:00
* Abort signal — starts the terminate escalation on the process tree when
* it fires. The caller owns deadlines and cause classification; this seam
* only reacts to the abort.
2026-07-26 06:59:01 +08:00
*/
2026-07-26 15:27:59 +08:00
signal?: AbortSignal | undefined
2026-07-26 06:59:01 +08:00
/**
2026-07-27 04:14:51 +08:00
* Explicit environment entries merged onto the implementation's scrubbed
* parent base (see ` scrubbedParentEnv`), with no namespace validation:
* every entry is a deliberate caller opt-in, so a forwarded
* credential-shaped entry or a current ` DSH_*` fact survives precisely
* because this layer merges after the scrub that drops its ambient
* namesake.
2026-07-26 06:59:01 +08:00
*/
env?: Record<string, string> | undefined
}
` ``
2026-07-26 15:27:59 +08:00
## Handles: streams, readers, and tree-scoped termination
2026-07-26 06:59:01 +08:00
2026-07-27 11:22:21 +08:00
A spawn returns a live handle immediately. Collect-mode readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; piped streams belong to the caller. Termination is tree-scoped on every platform: ` terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL, and ` waitForExit()` observes the whole tree — enough for a consumer to build its own teardown ladder (the ACP backend's stdin-EOF-first ` disposeAcpChild` is the template).
2026-07-26 06:59:01 +08:00
` ``ts type-equiv
/**
2026-07-26 15:27:59 +08:00
* A live child process rooted in its own process tree. Collected output
* remains readable after exit; piped streams belong to the caller.
*
* Termination is tree-scoped everywhere: POSIX signals the detached process
* group (falling back to the direct child when the group is gone), Windows
* terminates the tree via ` taskkill /T`, so helper processes cannot outlive
* the handle unnoticed.
2026-07-26 06:59:01 +08:00
*/
2026-07-26 12:43:14 +08:00
interface SubprocessHandle {
2026-07-26 15:27:59 +08:00
/** Process id (tree root); -1 when the spawn itself failed. */
2026-07-26 06:59:01 +08:00
readonly pid: number
2026-07-26 15:27:59 +08:00
/** The child's stdin, present iff spawned with ` stdin: 'pipe'`. */
readonly stdin: Writable | undefined
/** The child's raw stdout, present iff spawned with ` stdout: 'pipe'`. */
readonly stdout: Readable | undefined
/** The child's raw stderr, present iff spawned with ` stderr: 'pipe'`. */
readonly stderr: Readable | undefined
/** Offset-based readers for collect-mode streams (also readable after exit). */
readonly collected: SubprocessCollectedOutputs
/** Resolves at process close with exit facts; rejects only for spawn-level failures. */
2026-07-26 12:43:14 +08:00
readonly done: Promise<SubprocessOutcome>
2026-07-26 15:27:59 +08:00
/**
* Begin the SIGTERM → ` graceMs` → SIGKILL escalation on the process tree
2026-07-27 04:41:04 +08:00
* (Windows force-terminates immediately) — the seam's only termination
* verb. Idempotent, a no-op once the tree is gone (the pid may be reused),
* and also triggered by the spec's abort signal.
2026-07-26 15:27:59 +08:00
*/
terminate(): void
/**
* Wait until the process tree has exited — the tree, not just the direct
* child, so a still-running helper is observable before teardown returns.
* @param signal - optional bound for the wait.
* @returns ` true` when the tree exited, ` false` when the signal aborted first.
*/
waitForExit(signal?: AbortSignal): Promise<boolean>
2026-07-26 06:59:01 +08:00
}
` ``
` ``ts type-equiv
/**
2026-07-26 15:27:59 +08:00
* Cursor-free incremental access to one collected output stream. Offsets are
2026-07-26 06:59:01 +08:00
* whole-stream byte coordinates owned by the caller, so independent readers
2026-07-26 15:27:59 +08:00
* cannot consume one another's output; ` readFrom(0)` after settlement is the
* batch result (` lossy` then means the in-memory tail lost its head — the
* {@link CollectedOutput.truncated} fact).
2026-07-26 06:59:01 +08:00
*/
2026-07-26 12:43:14 +08:00
interface SubprocessOutputReader {
2026-07-26 06:59:01 +08:00
/**
* Read everything captured since ` fromByte`. When that offset has slid out
* of the in-memory tail window the read is ` lossy` — it returns the whole
* retained tail and the gap is only recoverable from the spill file.
* @param fromByte - whole-stream offset to resume from (a prior read's ` nextOffset`; 0 for the first read).
* @returns the delta text, the next offset, the ` lossy` flag, and the spill path when one exists.
*/
2026-07-26 12:43:14 +08:00
readFrom(fromByte: number): SubprocessOutputRead
2026-07-26 06:59:01 +08:00
}
` ``
` ``ts type-equiv
2026-07-26 12:43:14 +08:00
/** One incremental {@link SubprocessOutputReader.readFrom} read. */
interface SubprocessOutputRead {
2026-07-26 06:59:01 +08:00
/** Stream text from the requested offset (the whole retained tail when lossy). */
text: string
/** Whole-stream offset to resume from on the next read. */
nextOffset: number
/** True when the requested offset slid out of the in-memory tail window. */
lossy: boolean
/** Path to the full-stream spill file, when one was created and remains intact. */
spillPath?: string
}
` ``
2026-07-26 15:27:59 +08:00
` ``ts type-equiv
/** Offset-based readers for the streams spawned in collect mode. */
interface SubprocessCollectedOutputs {
/** Present iff stdout is a {@link SubprocessCollect}. */
readonly stdout?: SubprocessOutputReader
/** Present iff stderr is a {@link SubprocessCollect}. */
readonly stderr?: SubprocessOutputReader
}
` ``
## Outcomes carry exit facts only
2026-07-26 06:59:01 +08:00
2026-07-26 15:27:59 +08:00
` done` reports Node's close-event vocabulary and no cause classification — the service kills on abort but never decides why (the caller reads the deadline signal it owns, e.g. the bash executor's ` timedOut`/` aborted` split). Collected output stays readable through ` handle.collected` after settlement, so batch and streaming callers share one access path.
2026-07-26 06:59:01 +08:00
` ``ts type-equiv
/**
2026-07-26 15:27:59 +08:00
* Exit facts of one closed process — Node's ` close`-event vocabulary.
* Deliberately carries NO timeout or cancellation classification (the caller
* reads the signal it owns to classify causes) and NO output: collected
* streams stay readable through {@link SubprocessHandle.collected} after
* settlement, so batch and streaming callers share one access path.
2026-07-26 06:59:01 +08:00
*/
2026-07-26 12:43:14 +08:00
interface SubprocessOutcome {
2026-07-26 06:59:01 +08:00
/** Exit code; null when the process died from a signal. */
exitCode: number | null
/** Terminating signal (e.g. 'SIGTERM'); null on normal exit. */
signal: NodeJS.Signals | null
}
` ``
## Service behavior
2026-07-26 15:27:59 +08:00
The abstract [` SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam defines ` spawn` only; [` LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) is the local implementation (detached trees, per-disposition wiring, credential scrub, terminate-and-join disposal). See [` dsh-subprocess`](../../packages/subprocess/subprocess/README.md) for the seam contract and [` dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md) for the mechanics.