Merge remote-tracking branch 'origin/worktree-process-service-seam' into subprocess-simpl/c-one-termination-verb

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-26-subprocess-consumer-migration.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-26-subprocess-consumer-migration.md
#	.agents/notes/implemented/architecture/2026-07-26-subprocess-consumer-migration.zh.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/subprocess.i18n.yaml
#	docs/core-data-structures/subprocess.md
#	docs/core-data-structures/subprocess.zh.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/subprocess/subprocess-local/README.i18n.yaml
#	packages/subprocess/subprocess-local/README.md
#	packages/subprocess/subprocess-local/README.zh.md
#	packages/subprocess/subprocess/README.i18n.yaml
#	packages/subprocess/subprocess/README.md
#	packages/subprocess/subprocess/README.zh.md
#	packages/subprocess/subprocess/src/index.ts
This commit is contained in:
Tianyi Cui
2026-07-27 11:22:21 +08:00
32 changed files with 224 additions and 296 deletions

View File

@@ -16,8 +16,8 @@ import type { SpawnInternals } from './spawn.ts'
/**
* Local subprocess service: detached process trees, Node-shaped stdio
* dispositions (raw pipes, inherit, bounded tail-keep collection with spill
* files), credential-scrubbed environment, tree-scoped signalling with
* SIGTERM→grace→SIGKILL escalation, and the cooperative dispose ladder.
* files), credential-scrubbed environment, and tree-scoped signalling with
* SIGTERM→grace→SIGKILL escalation.
*/
export class LocalSubprocessService extends SubprocessService {
/** Live handles retained only so disposal can terminate and join them. */

View File

@@ -1,10 +1,9 @@
/**
* Process plumbing for the local subprocess service: detached process-tree
* spawn with per-stream stdio dispositions, tail-keep collection with spill
* files, tree-scoped signalling (POSIX groups; Windows taskkill), the
* SIGTERM→SIGKILL escalation, and the cooperative EOF-first dispose ladder.
* This layer reacts to an abort signal; callers own deadlines and classify
* causes.
* files, tree-scoped signalling (POSIX groups; Windows taskkill), and the
* SIGTERM→SIGKILL escalation. This layer reacts to an abort signal; callers
* own deadlines, teardown ladders, and cause classification.
* @module dsh-subprocess-local/spawn
*/
@@ -15,13 +14,11 @@ import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from 'node:fs
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { setTimeout as sleepMs } from 'node:timers/promises'
import { deadline } from '@deepseek-ai/dsh-timeout'
import { DSH_ENV_PREFIX, scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
import type {
CollectedOutput,
DshEnvironment,
SubprocessCollect,
SubprocessDisposeGraces,
SubprocessHandle,
SubprocessOutcome,
SubprocessOutputMode,
@@ -63,9 +60,6 @@ export interface SpawnInternals {
platform?: NodeJS.Platform
}
/** Timeout code marking a dispose-ladder tier bound (vs an external abort). */
const DISPOSE_TIER_TIMEOUT = 'SUBPROCESS_DISPOSE_TIER'
/**
* Liveness-poll cadence for tree-exit waits. The timer stays ref'd: an
* awaited teardown must keep the event loop alive until the tree really
@@ -392,8 +386,8 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
}
}
// The dispose ladder's tier primitive (not on the handle — terminate() is
// the only consumer-facing termination verb). Guards on TREE liveness, not
// The escalation's tier primitive (not on the handle — terminate() is the
// only consumer-facing termination verb). Guards on TREE liveness, not
// outcome settlement: a TERM-trapping helper can outlive the settled direct
// child and must stay signalable, while a fully-dead tree (possible pid
// reuse) must not be re-signalled by a later tier.
@@ -468,39 +462,6 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
return true
}
/**
* Wait, bounded, for whole-tree exit — the dispose ladder's quiescence test.
* Tree liveness, not direct-child settlement: a TERM-trapping helper that
* outlives the leader must hold the ladder on its tier until it exits.
*/
const treeExitsWithin = async (ms: number): Promise<boolean> => {
using bound = deadline(undefined, ms, DISPOSE_TIER_TIMEOUT)
return await waitForExit(bound.signal)
}
let disposal: Promise<void> | undefined
const dispose = (graces: SubprocessDisposeGraces): Promise<void> => (disposal ??= (async () => {
// A spawn failure has no process to tear down; observe the rejection so
// disposal in a finally block cannot surface it as unhandled.
if (pid <= 0) {
await done.catch(() => {})
return
}
// 1. Close a piped stdin and allow cooperative teardown and flush.
if (stdinMode === 'pipe') child.stdin?.end()
if (await treeExitsWithin(graces.eofGraceMs)) return
// 2. POSIX gets a catchable graceful signal; Windows taskkill force-terminates.
if (platform !== 'win32') {
kill('SIGTERM')
if (await treeExitsWithin(graces.graceMs)) return
}
// 3. Force-kill the tree and await a bounded exit edge.
kill('SIGKILL')
if (!(await treeExitsWithin(graces.graceMs))) {
throw new Error(`child process tree did not exit within ${graces.graceMs}ms after forced termination`)
}
})())
return {
pid,
/* v8 ignore start -- pipe-mode fds exist on every spawn Node returns; the null-coalesces guard a nonconforming ChildProcess only. */
@@ -515,6 +476,5 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
done,
terminate,
waitForExit,
dispose,
}
}