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.
Drop SubprocessSpawnSpec.dshEnv and splitEnvChannels(); childEnv() is now
scrubbed-base + explicit entries with no namespace validation. The invariant
dropped is the reserved-namespace check on explicit entries (DSH_* rejected
from env, non-DSH_* rejected from dshEnv). Explicit-entry trust already
covers it: an explicit credential-shaped entry has always merged after the
scrub as a deliberate caller opt-in, and an explicit DSH_* entry is the same
deliberate act — the staleness invariant lives entirely in scrubbedParentEnv
dropping AMBIENT credential-shaped and DSH_* names, which stays. The
validation's only observed effect was rejecting legitimate explicit entries:
both recent CI breakages (DSH_GATE_CONCURRENCY exported into every job
crashing lsp specs, DSH_PERMISSION_MODE in acp config.env crashing the
child spawn) were this check firing on values a caller meant to pass, each
fixed by routing around the bureaucracy the seam itself imposed.
The bash seam keeps its own request/spec dshEnv field: that is bash-owned
trusted-plugin vocabulary (the ctx.bashEnv collected overlay) whose merge-last
position guarantees a caller env entry cannot displace a managed fact;
bash-local now flattens ENV_OVERRIDES -> spec.env -> spec.dshEnv into the
seam's one env map. subagent-acp and lsp-local pass their single config env
map straight through. DshEnvironment/DshEnvironmentKey/DSH_ENV_PREFIX stay on
the subprocess seam as the namespace vocabulary (bash re-exports them;
scrubbedParentEnv filters on the prefix).
Tests: the two channel-rejection specs and the splitEnvChannels partition
spec are deleted; one spawn spec now proves an explicit DSH_* env entry
reaches the child while an ambient one is scrubbed; the acp/lsp forwarding
specs keep their MOCK_ECHO_ENV / LSP_FAKE_ECHO_ENV assertions with the split
comments rewritten to merge-after-scrub. Docs (en+zh, re-recorded) and the
owning Agent Notes updated; cordis api/services catalogs regenerated.
SubprocessHandle loses dispose(graces) and SubprocessDisposeGraces: the
stdin-EOF→SIGTERM→SIGKILL sequence is teardown POLICY encoding one
consumer's cooperation shape, not process vocabulary — the seam keeps
kill/terminate/waitForExit, and waitForExit(signal?) is the quiescence
probe a consumer ladder needs. dsh-subagent-acp owns disposeAcpChild()
over those public verbs (tier tests move into its suite; a never-exits
stub pins the fail-loud path); dsh-subprocess-local sheds the ladder,
its deadline import, and the dsh-timeout dependency. Every future
backend now owes four verbs and no teardown policy. New bilingual
ladder-ownership Agent Note records the decision; catalogs regenerated.
Implements the approved simplification Agent Note: sse.ts now pipes the
response body through TextDecoderStream and EventSourceParserStream
(eventsource-parser/stream) and keeps only the DeepSeek protocol shim —
yield each event's data, terminate on [DONE], throw
LlmError('STREAM_CLOSED') on EOF without the sentinel. The SSE
spec-conformance tests are deleted; sse.spec.ts pins only the
[DONE]/STREAM_CLOSED/EOF contract, including the new spec-strict verdict
that an unterminated trailing event is truncation (the old parser
flushed it — a robustness nicety no real provider shape needs).
eventsource-parser@^3.1.0 becomes llm-deepseek's second runtime
dependency (already in the lockfile transitively via the MCP SDK).
Docs: the Agent Note moves proposed/ → implemented/ and is rewritten per
the lifecycle contract; the rejected NIH roll-up note's inbound links
follow. The twin-adapters note, dsh-llm LlmAdapter JSDoc (and its
type-equiv fences), cookbook, group/package READMEs, root AGENTS.md
layout line, sdk-helper comments, and the regenerated config catalog
drop the "hand-rolled fetch + SSE" claim in both languages; all eight
touched pairs re-recorded.
The public classifier existed to defend an exported reader against
arbitrary signals, but its only production caller is the loop reading
its own machine-private turn signal, where cancel() is the sole aborter
and always writes one frozen canonical cause. Delete the export and its
15-line structural validation: settle() states the slot invariant with
one cast, the boolean call sites ask signal.aborted directly, and the
retry veto drops entirely because a requested window already implies a
live signal (cancel() retires the window before aborting).
The abort(reason) channel and first-wins semantics are unchanged; only
the reader's publicness is gone, and with it the paranoia it required.
Faithfulness pass from the translation review: 接入 over 搭乘, first-mention
disposition annotation, 原始的 Readable/Writable, 留在原地, 评审 over 审阅;
pairing records re-recorded.
Adds the zh counterpart, aligns the seam note's zh side with its updated
interface bullet and follow-up sentence, records both pairs, and ratchets
the translation-pairing manifest.
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.
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).
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).
- The acp-demo and cli-demo built-bin e2e consumers link a pruned package
closure; add process/process and process/process-local so the built bins
boot now that bash-local injects ctx.processes.
- verify-package-paths scans backticked packages/* references, so the
timeout-note's historical mention of the deleted run.ts path is reworded
(en+zh, both sides now carry the same historical-vs-current split; the
earlier zh edit had silently missed its match); pair re-recorded.
The previous commit's English edit silently missed (its match pattern
targeted already-rewritten text); only the zh side carried the split. Both
sides now state it and the pair is re-recorded.
Codex round-2 (B): the retargeted bullet read as though spawn.ts carries the
full timeout plumbing today. Split the historical claim (run.ts, pre-library)
from the current split (spawn.ts reacts to aborts; bash-local owns the fused
deadline and classification), en+zh, pair re-recorded.
Review finding (Codex round 1): abstract erases at runtime and
@deepseek-ai/dsh-tasks used to be the mountable registry, so a stale
composition row would register a ctx.tasks with no method implementations
and fail far from the misconfiguration. The seam constructor now rejects
direct mounts with a load-time pointer at dsh-tasks-local; the seam suite
pins the fence, the Agent Note cost paragraph records the actual behavior,
and the stale tool-pty README requirement line names the implementation
package.
Review finding (Codex round 1): abstract erases at runtime and
@deepseek-ai/dsh-tasks used to be the mountable registry, so a stale
composition row would register a ctx.tasks with no method implementations
and fail far from the misconfiguration. The seam constructor now rejects
direct mounts with a load-time pointer at dsh-tasks-local; the seam suite
pins the fence, the Agent Note cost paragraph records the actual behavior,
and the stale tool-pty README requirement line names the implementation
package.
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).
Adds the Chinese counterpart of the new seam note, records both pairs
(new note + the updated background-task runtime note), and ratchets the
translation-pairing manifest.
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.
run_code gains a required bash-style description parameter: presentCall
titles the card with it and moves the program to rawInput, so every
surface gets a readable label. tool/code-dispatch now logs each
sub-call's complete content/isError (the tool/result vocabulary),
replacing the bounded resultSummary and deleting the summarize/cwd
machinery — a UI renders sub-calls through the identical path as native
results. The dsh config tree mounts the worker code runtime and reads
DSH_TOOLS_MODE (temporary seam until per-session mode selection lands).
Session format stays v0 (pre-release churn). Code-mode ACP/TUI fixtures
re-recorded; TUI presenter pin refreshed; catalogs regenerated. Keyless
web smoke pins the code-mode wire contract (tools=[run_code] + SDK
prompt section).