Implements the execa Agent Note's four sub-changes:
- execa (root devDep + loader-smoke dep) replaces the hand-rolled
spawn-collect-timeout choreography in loader-smoke, apps/cli and
cli-demo/acp-demo built-bin e2e, lsp-local and code-runtime-worker
built-lib e2e, the tui pty-harness outer collector, the jsonrpc
keyless smoke, and crash-recovery's child spawn. Genuinely custom
parts stay custom: cli-demo's interrupt-on-marker, jsonrpc's
line-predicate protocol driving, crash-recovery's SIGKILL-at-failpoint.
The two loader-smoke /* v8 ignore */ OS-error branches are gone.
- llm-mock-server CLI tokenizes via node:util parseArgs; numeric
coercion/bounds/cross-option constraints stay manual; pinned
error-message tests updated to the parseArgs texts.
- both loadRootEnv copies in apps/web/tests are deleted: the owning
vitest configs (web unconditionally, snapshot in record mode)
already load the repo-root .env before these files run.
- the four poll loops (acp-snapshot harness waits + crash-recovery
waitForFile) ride vi.waitFor with explicit {interval, timeout}.
Conflict resolution: scripts/type-equiv.manifest.json takes master's new
paired-derivative format (one primary entry per pair) and re-adds this
stack's CodeDispatchLog entry in that format. zh README pairs brought
along for the dispatch-log arm (spill-policy behavior/limitations bullets,
tools limitation bullet now pointing at the shipped bounding).
The scenario inspects the tools service API; the round-2 content-contract
JSDoc change shifted its rendered output. Keyless DSH_SNAPSHOT=refresh —
the resulting fixture is byte-identical to the one the shiki branch
already carries (the downstream trees were green for this reason).
Responding to ds-review-bot round 2 on #661:
- logWork is bounded: past maxParallelSubCalls pending shaped-append tasks
the ordered commit lane holds (Promise.race drains one), so a slow spill
backend backpressures the run instead of accumulating unbounded pending
I/O and retained results. Tasks self-remove on settlement; run
settlement still drains every task inside the open turn. New spill test
drives three oversized reads against a hung backend at cap 1 and proves
the third dispatch cannot start until a save drains.
- shapeDispatchLog's catch uses errorMessage() (total), so a thrown value
with a throwing toString cannot escape the containment and lose the
settle event.
- CodeDispatchLog.content documented as the RENDERED result projection
(native tool/result vocabulary), not what the program received — the
program gets the structured value; doc pair + type-equiv re-synced.
- New RECORDED tui-agent snapshot scenario code-mode-dispatch-spill: the
real Loader-visible composition (worker runtime + spill-local + policy)
drives an oversized bash sub-call end-to-end; replay proves the durable
dispatch copy is bounded to preview + locator while the program value
stays whole (the outer result carries just the line count).
Agent Note updated (both languages).
The cordis_inspect api surface embeds the SessionHeader declaration, which
gained sandboxMode/approvalPolicy; keyless refresh, model transcript
untouched.
Resolutions: regenerate docs/capability-seams.md (the only conflict);
master widened the bilingual pairing scope to package READMEs, so this
merge adds the Chinese counterparts and i18n records for the three
telemetry READMEs and patches packages/README.zh.md with the telemetry
group row.
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.
The automation bridge inherited two master-era assumptions the message
machine no longer honors. A prompt blocked at pre-turn admission opens no
turn, so no turn/end could ever settle it — the bridge now watches
whenIdle() and reports a turnless slot as cancelled (the disposed-agent
guard moved to a registry identity check before send). A failed turn no
longer rejects at its turn/end either: agent.retry() closes the failed
turn and opens a successor on the same history, so the bridge holds the
terminal error and lets a retry-triggered turn/start adopt the prompt,
rejecting only at quiescence with no successor.
Also: refresh the empty-response-retry fixture for retry-as-turn logging,
adapt master-side tests to the unified send()/UserMessageData API and
registry-fact disposal, resync the doc pairs both sides touched, trim
architecture.md back under its word ceiling, and regenerate the event and
persistence catalogs.
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).
The previous hygiene fix re-serialized with json.dumps defaults (spaced
separators), leaving the line byte-identical; explicit compact
separators make the header-scrub guard pass.
The re-records for the SDK-prompt change had harvested live v4-pro
headers while every replay overlay pins v4-flash, so keyless replay
diverged on provenance; keyless refresh reconciles the affected
scenarios (dispatch-start pairs preserved).
This branch intentionally appends plugin-sourced context (plan-mode
notices, the skill catalog) after the prompt, so the request's last message
is no longer the user's text. The scripted adapter keyed every scenario off
messages.at(-1) only, missed its triggers, and fell through to the
ask_user_question default — the frozen 'Waiting for the first token' PTY
timeouts. It now scans all text since the last assistant message.
The scripted (non-recorded) fixtures' run_code calls predate the required
description parameter, so replay rejected them at validation before any
dispatch: patch the scripted programs' args (tool/call, message blocks,
and chunk deltas together) and refresh goldens keylessly. Also picks up
the v4-pro re-records of the code-mode scenario pair whose live model
drifted from the overlay pin, and drops tmp-path churn.
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 bridge replaces its serialization queue with a pool that reuses the
native concurrency contract: submissions classify through
registry.executionMode (fail-closed isConcurrencySafe), start strictly in
submission order, overlap up to the validated maxParallelSubCalls config
(default 10; 1 restores serial), and exclusive calls drain the pool, run
alone, and bar later calls. Each started sub-call logs a
tool/code-dispatch-start event at pool entry; the existing
tool/code-dispatch settles the pair (started ⇔ settles exactly once;
abandoned queued calls log neither). SDK prompt guidance now states the
true Promise.all contract — re-recorded across every code/both-mode
snapshot (plus the stale cordis-dynamic-toolchain fixture gaining the
required description arg).
Client: CodeSubCall widens to RunningToolCall | ToolResultNode — starts
land the running shape (rows wear the native running ring), settles
replace in place preserving start order, callTime pairs to the start
time. Fixture emits start/settle pairs; jsdom pins the running sub-row;
runtime specs pin in-place settlement and out-of-order completion.
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.
The origin/master merge added the session-query-spill and
escalation-approved acp scenarios, whose pinned tool-schemas still
carried additionalProperties: true on the todo_write item schema. This
PR tightens that to false (model-visible via the request header), so
re-record it in the two new expected outputs. session-sandbox-root,
escalation-rejected, and fs-escalation-approved compare against the
escalation-approved pinned header and pass once it is fixed.
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).
Integrate the Commander adapter with master's headless rework (dsh -p now boots
through AppCLIEntry — the same cordis.yml composition as dsh web, web-observable
while it runs — via toFetchHandler(ctx.apiProxy)).
- headless.ts: keep master's AppCLIEntry-based body but take the adapter's
pre-parsed `task` (drop the re-added parseArgs). Old startHost path and the
now-deleted packages/host/runtime dependency are gone.
- remove the stale packages/host/runtime dir left on disk by master's deletion
(mirrors the earlier packages/ui/acp cleanup).
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
Integrate the Commander argument adapter and dsh-front-door work with master's
config-tree `dsh web` (#601: AppCLIEntry + apps/cli/cordis.yml) and the
packages/ui/acp → packages/acp/acp relocation.
- web.ts: keep master's AppCLIEntry-based boot, but take the adapter's parsed
(host, port, dev) instead of an internal parseArgs. The adapter's host/port
defaults (127.0.0.1/3080) match cordis.yml, so always passing them is
behavior-equivalent to master's "undefined keeps the yml default".
- apps/cli/package.json: master's expanded config-tree dep set + commander.
- retire-readline Agent Note: point the TUI refusal proof at
apps/cli/tests/built-bin.e2e.ts (both languages), re-record the pair.
- READMEs reconciled (demo-bin removal + master's ACP/channel rewording).