# Conflicts: # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/input.json # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/bundle/acp-app/package.json # packages/examples/jsonrpc-demo/src/packaged-bin.ts # packages/subagent/subagent-acp/src/run.ts # packages/subagent/subagent-acp/tests/subagent-acp.spec.ts # packages/subprocess/subprocess-local/README.i18n.yaml # packages/subprocess/subprocess-local/README.md # packages/subprocess/subprocess-local/README.zh.md # pnpm-lock.yaml # scripts/smoke-python-runtime.py
@deepseek-ai/dsh-subprocess-local
English | 中文
Local Service Provider for the @deepseek-ai/dsh-subprocess seam. LocalSubprocessRuntime resolves local executables, gives ordinary Linux and Windows commands plus Linux terminal sessions an OS-owned managed range when the host supports it, and implements terminal processes through node-pty plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling capability seams (dsh-bash-local, dsh-lsp-stdio, and dsh-terminal-bash).
Behavior
- One managed range for signal and wait — Linux ordinary commands and terminal sessions use transient user-systemd scopes when the manager supports literal argv and readable scope state. On Windows the parent creates private named-pipe endpoints for non-inherited streams; the runner opens only the target-side handles, creates the target suspended, assigns it to its kill-on-close Job, resumes it, publishes startup, and then closes those pipe handles. The runner alone retains the original target process handle and Job, reports the direct result, and exits successfully only after
ActiveProcessesreaches zero; the parent never opens either native object. Linux scopes and POSIX process-group fallbacks receive TERM and then KILL aftergraceMs; Windows Job andtaskkillowners force-terminate on the first request.waitForExit()succeeds only after the selected owner proves the range empty and rejects when that proof is unavailable. After the direct result arrives,.donewaits up tograceMsfor every non-inherited output stream to close; at that bound, only collected streams are force-closed while raw pipes remain caller-owned. - Explicit weaker fallback — macOS, old or unavailable user-systemd, and unavailable Windows native support keep the existing detached PGID or
taskkill /Tpath. The provider warns once before the first affected command. It never retries through fallback after a native runner may have started the user command. - Per-stream dispositions —
'pipe'hands the raw stream to the caller untouched (protocol framing stays consumer-owned);'inherit'passes the parent descriptor through; collect mode keeps the in-memory TAIL beyond its cap (errors and results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a private temp file when a spill cap is configured — omittingspillkeeps only the tail, the diagnostic shape. A stream larger than the spill cap discards its now-incomplete spill and returns only the marked truncated tail; spill fds are sealed at settlement, and a failed final close withholds the path rather than advertising an incomplete file. Spill files are0600with random names under a lazily-created0700per-process directory. - Credential scrub + explicit merge —
process.envminus credential-shaped vars (*KEY*/*PASSWORD*/*SECRET*/*TOKEN*) and all ambientDSH_*names; the spec's explicitenvmerges after that scrub with no namespace validation, so a deliberately supplied credential or currentDSH_*fact wins while stale nested-harness identity cannot leak in ambiently. Supplied stdin is written and closed; otherwise fd 0 is/dev/null. See the stdin/env Agent Note and managed environment Agent Note. - Offset-based reads — collect-mode readers return deltas in whole-stream byte coordinates; the service never holds a cursor, so consumer-owned cursors (the bash background read path) and full-stream re-reads coexist, before and after settlement.
- Executable lookup —
resolveExecutablechecks absolute files or searches the scrubbed effective PATH with platform-aware executable extensions; relative paths containing separators are rejected at the seam, and relative PATH entries resolve from the host process cwd. - Terminal-process ownership —
spawnTerminalallocatesnode-pty, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation. Supported Linux hosts wrap the original terminal argv directly in the same user-systemd scope model as ordinary commands, preserving the node-pty PID, session leader, controlling terminal, and foreground-input inspection while the scope owns reparented orsetsiddescendants. Linux reports an exact input wait only when the waiting thread's own fd 0 identifies the shell's controlling terminal, including the/dev/ttyalias, so a pipeline reader blocked onpipe:[…]cannot publish terminal readiness. The syscall probe admits supported runtime architectures and matches every supported kernel ABI so user-mode emulation cannot hide the wait. When Linux denies/proc/<pid>/task/<tid>/syscall, the inspector reports no exact wait and leaves the higher PTY backend to its configured idle inference; process sleep state never substitutes for syscall evidence. On fallback hosts, each foreground inspection retains exact identities from the rooted tree, Linux also enumerates the POSIX session after its leader exits, and teardown sweeps observable descendants before and after terminating the top-level shell. A previously observed macOS descendant and any same-session fallback Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. On Windows the koffi-backed inspector enumerates the process table through Toolhelp32, combines GetProcessTimes start identities with zero-time process-handle waits for liveness, reports the shell pid as the pseudo foreground group (Windows has no POSIX groups), and teardown verifies the shell's termination because externally taskkilled shells may never fire node-pty's exit notification. The higher PTY backend owns prompt readiness, buffers, and model-facing operations. - Terminate-and-join disposal — the service retains live handles so its own disposal can run each provider-owned termination procedure and await its exit; quiescent and spawn-failed handles leave the live set only after managed-range, whole-tree, or terminal-session cleanup finishes.
- Synchronous host-exit finalization — while the service effect is active, a Node
exitlistener synchronously signals every ordinary managed range and terminal session still in the live sets. Linux native owners issue the scope KILL request; the Windows runner treats parent IPC disconnect as Job termination; fallback owners retain their PGID,taskkill, and captured-identity behavior. These local-only operations create no promise or timer, preserve the host exit code and diagnostic, contain each target failure, and do not claim quiescence. Normal disposal keeps the awaited managed-range or graceful fallback path above. See the host-exit cleanup decision.
Model Experience
Indirectly, through Consumers (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
- Native ownership has explicit host requirements — Linux needs a readable user manager and
systemd-run --expand-environment=no; older systemd versions use the warned PGID fallback. macOS always uses that fallback because no supported public persistent owner exists. - Native selection has bounded probe and runner costs — Linux rechecks the live user manager before every eligible ordinary or terminal spawn. Successful stable systemd-scope and ordinary-runner probes are cached for the provider lifetime, failed probes are retried, and terminal selection never probes the ordinary runner. Windows likewise caches only a successful Job-runner probe. Each synchronous probe command has a 5-second bound and completes before the user command can run. A native ordinary handle returns before target publication:
pidstarts asundefinedand updates from asynchronously polled runner events, while.donecarries target startup failure or direct outcome. There is no target-publication timeout; a runner that remains alive without a terminal event leavespidundefined and.donepending until it exits or the range is terminated. Each supported native ordinary command keeps one runner process alive until the OS-owned range is empty, and Windows additionally creates private per-spawn named-pipe endpoints. Linux terminal launch passes the scoped argv directly tonode-ptyand adds no runner. Runner events are polled asynchronously every 100 ms and Linux scope state every 200 ms. - Windows Job inheritance has defined exclusions — ordinary descendants inherit the Job by default, but breakaway processes are outside the guarantee. The target starts only after Job assignment; external termination of the runner in the narrow create-to-assignment interval can leave a suspended target behind.
- Windows terminal signalling is console-wide — SIGINT is delivered as a
\x03Ctrl-C input write that conhost turns into a console-wide CTRL_C event; SIGTSTP and SIGHUP are rejected as unavailable; ataskkillwithout/Fdoes not terminate console processes, so the teardown TERM tier is a grace wait before the/Fescalation. Windows readiness has no exact stdin-wait tier: the prompt-marker fast path compares the shell pid as the pseudo foreground group, and silence/timing tiers cover the rest. - Fallback terminal ownership remains observational — on macOS or Linux without usable user-systemd, a child that reparents before any foreground-inspection snapshot or leaves the owned terminal session can escape the process-table scan. The local provider does not add a continuous process-table monitor; supported Linux native mode instead retains these descendants through scope membership.
- In-process cleanup requires a JavaScript-observable exit — direct
process.exit(), default uncaught exceptions, and default unhandled rejections emit Node's synchronousexitevent. The default OS disposition for an unhandledSIGTERM,SIGINT, orSIGHUPbypasses that event; an application covers those signals only by installing a handler that performs normal disposal or callsprocess.exit().SIGKILL, fatal OOM,process.abort(), native crashes, power loss, and any failure that cannot run JavaScript require an external supervisor, container init, or equivalent OS owner. - The credential scrub is a name heuristic —
*KEY*/*PASSWORD*/*SECRET*/*TOKEN*only; differently-named secrets (e.g.*PASSPHRASE*) pass through, and a whitelist for over-scrubbed vars is noted future work. - Completed spill files are not deleted — bounded full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them; oversize incomplete spills are discarded and deletion is attempted immediately, but a cleanup failure can leave a bounded file behind.
Common process handling lives in src/spawn.ts; Linux scopes, Windows Jobs, and the private runner live in their platform modules; src/index.ts owns selection and service wiring.