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).
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).
Replace send/steer/inject with one Agent.send primitive over the
(target × wakeup) matrix; followup/steer/inject become fixed-preset
alias methods on the now-abstract Agent class. Coalesce context/message
into user/message (injected context is a non-user source). Replace
agent/queued with agent/inbox/enqueue/dequeue/discard, add cancel
keepInbox, and add a FIFO-conservation invariant.
context/message previously defaulted to a <context source="…">…</context>
wrapper. No model is trained on a <context> tag either, and message
framing does not belong on the session surface: the surface projects the
durable log, and a caller that wants a frame formats its own content —
which the one heavy producer (workspace-context) already does with its own
<system-reminder> frame, opting out via 'raw'. The tag only added
machinery — ContextEnvelope plus an envelope field threaded through
InjectOptions, HookContext, the context/message event, and the agent-loop
inject/additionalContexts plumbing.
context/message now projects its content verbatim as a user-role message,
sharing one deriveEventMessage case with user/message and steering/message.
ContextEnvelope and every envelope field are removed; context/message.meta
still carries durable, model-hidden JSON state. Regenerated catalogs and
website API; refreshed the three affected keyless snapshots (envelope field
only; timestamps unchanged).
Broadens and renames the steering Agent Note to cover both envelope
removals as one decision.
Agent Note: .agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
Codex review of the draft: the abort-on-dispose tests asserted only that
dispose() resolved and no warning fired — a regression back to untracked
fire-and-forget runs would pass both. The hook script now records its
shell PID before sleeping, and after dispose the test asserts
process.kill(pid, 0) throws: the drain resolves only after the killed
run settled (the child reaped), so ESRCH-by-dispose-return is
deterministic under the correct implementation, while an alive or
unreaped process fails it. Verified the discriminator by gutting drain()
locally: exactly these two tests fail.
The emit-shaped hook points (SessionStart, SubagentStart, SubagentStop)
run fire-and-forget: no seam awaits the run chain, so disposing a bridge
could strand a live hook process and let a late continuation inject into
a disposed context. The floating continuation also made the coverage
gate racy: the only coverage of the SubagentStart continuation's
no-context branch arm rode on an un-awaited .then, and on a loaded CI
runner the fork's per-file coverage snapshot beat it — master run
28798191671 failed the 100% branch gate on hooks-claude/src/index.ts at
99.03% (uncovered line 336) with the identical tree passing the PR run
three minutes earlier.
New shared primitive createDetachedRuns() in dsh-hook-protocol: a bridge
tracks each detached run chain, passes the tracker's abort signal to
runHook, and registers drain() as its effect disposer — drain aborts
still-running hook processes (a kill via the bash seam, not a wait out
to the 10-minute default hook timeout), then resolves once every chain
has settled. fiber.dispose() resolving now means the bridge's detached
work is quiescent (docs/defensive-patterns.md).
The subagent marker test disposes the bridge as its sync point, so the
formerly racy branch arm is executed deterministically before the file's
coverage snapshot; new tests pin abort-on-dispose promptness for both
bridges and the tracker's settle/drain contract in hook-protocol.
A Codex review pass on the draft caught four real gaps and two solid
suggestions; all addressed except one pushed back on the merits:
- hooks-claude/hooks-codex: stderrSummaryMaxChars was the one new knob
with NO range validation — a negative/NaN cap would silently
misbehave inside slice(). Both bridges now assert a positive integer
at the TOP of apply() (before the config-file parse's early return,
so a bad value fails the load loudly), with rejection tests.
- tool-fs: the read caps count lines/chars/bytes, so positive-FINITE
was too loose (a fractional readLimit would flow into windowing
arithmetic and the schema description). All four now require a
positive integer, matching tool-web's cap.
- Doc drift the gates cannot catch: tool-web's README tools table
still named WEB_SEARCH_MAX_RESULTS as the mechanism; compact-basic's
README/module doc and the compaction-capability-seam RFC still
described estimation as fixed char/4 rather than the charsPerToken
default.
- subagent-acp: the dispose graces were tested only at the
startAcpRun level, so a regression that stopped threading plugin
config into AcpRunSpec would have survived. A provider-path test now
drives the trap-escalation scenario through ctx.subagents.start with
small config graces and bounds dispose at 4s.
Pushed back on: converting compact-basic's charsPerToken to a
schemastery field. The package's whole config is deliberately
hand-rolled (resolveConfig, every threshold REQUIRED with no default —
a documented design posture); one schemastery field beside it would be
incoherent. The knob is cordis.yml-reachable, defaulted, and validated,
which is what the convention requires; migrating the package to
schemastery wholesale is pre-existing config-surface hygiene out of
this change's scope.