Files
deepseek-harness/packages/subprocess/subprocess-local/src/index.ts
Tianyi Cui 776d666246 refactor(subprocess): the dispose ladder moves to its one consumer
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.
2026-07-27 03:52:41 +08:00

60 lines
2.5 KiB
TypeScript

/**
* Local implementation of the subprocess seam. Each spawn is a detached
* process tree with the spec's per-stream stdio dispositions; disposal
* terminates and joins live trees. It has no config: every disposition and
* limit arrives on the spec, so the deployment-varying choices stay with the
* calling seam's config (the bash executor's, the LSP host's, …).
* @module @deepseek-ai/dsh-subprocess-local
*/
import { Context } from 'cordis'
import { SubprocessService } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { spawnSubprocess } from './spawn.ts'
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, 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. */
private live = new Set<SubprocessHandle>()
/** Test seam: spill and platform knobs forwarded to spawnSubprocess. */
internals: SpawnInternals = {}
constructor(ctx: Context) {
super(ctx)
ctx.effect(() => async () => {
// Terminate (escalating), then await WHOLE-TREE exit — not just the
// direct child's settlement — so even a TERM-trapping descendant cannot
// outlive the fiber.
const pending: Promise<unknown>[] = []
for (const handle of this.live) {
handle.terminate()
// Spawn-failure rejections already settled and left the live set.
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
}
this.live.clear()
await Promise.all(pending)
}, 'local subprocess teardown')
}
spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
const handle = spawnSubprocess(spec, this.internals)
this.live.add(handle)
// Release ownership only once the whole TREE is gone, not at direct-child
// settlement — a TERM-trapping helper that outlives the leader must stay
// owned so teardown can still escalate it. For the common no-survivor
// case waitForExit resolves immediately after settlement.
const release = (): Promise<void> =>
handle.waitForExit().then(() => { this.live.delete(handle) })
handle.done.then(release, release)
return handle
}
}
export default LocalSubprocessService