fix(subagent-codex): await managed tree exit

This commit is contained in:
pku-xht
2026-08-04 22:36:31 +08:00
parent 74f2ab90e1
commit a93968bf93
7 changed files with 32 additions and 124 deletions
+4 -55
View File
@@ -24,54 +24,13 @@ import { CodexAppServerWire } from './wire.ts'
/** Default POSIX grace between subprocess termination tiers. */
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
/** Largest delay Node schedules without collapsing it to one millisecond. */
const MAX_TIMER_DELAY_MS = 2_147_483_647n
/**
* Bound final exit observation at twice a positive finite grace without
* narrowing the public config to Node's single-timer integer range.
*/
function doubledGraceWindow(graceMs: number): {
readonly signal: AbortSignal
readonly cancel: () => void
} {
const whole = Math.floor(graceMs)
let remaining = BigInt(whole) * 2n
+ BigInt(Math.ceil((graceMs - whole) * 2))
const controller = new AbortController()
let timer: ReturnType<typeof setTimeout> | undefined
const arm = (): void => {
const chunk = remaining > MAX_TIMER_DELAY_MS
? MAX_TIMER_DELAY_MS
: remaining
remaining -= chunk
timer = setTimeout(() => {
timer = undefined
if (remaining === 0n) {
controller.abort()
} else {
arm()
}
}, Number(chunk))
}
arm()
return {
signal: controller.signal,
cancel: () => {
if (timer === undefined) return
clearTimeout(timer)
timer = undefined
},
}
}
/** Fully resolved inputs for one Codex app-server run. */
export interface CodexRunSpec {
/** Parent Session workspace, also supplied to `thread/start`. */
readonly cwd: string
/** Explicit deployment/test environment layered after the shared scrub. */
readonly env: Record<string, string>
/** Subprocess termination grace and final tree-exit bound. */
/** Subprocess termination grace passed to the shared process-tree owner. */
readonly disposeGraceMs: number
/** Shared subprocess service spawn operation. */
readonly spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle
@@ -111,12 +70,10 @@ export function textTask(prompt: readonly ContentBlock[]): string[] {
* subprocess owner to prove it is gone.
* @param wire - private app-server protocol connection.
* @param child - shared-service handle that owns the process tree.
* @param graceMs - termination grace used to bound final exit observation.
*/
export async function disposeCodexChild(
wire: CodexAppServerWire,
child: SubprocessHandle,
graceMs: number,
): Promise<void> {
wire.close()
if (child.pid <= 0) {
@@ -129,14 +86,7 @@ export async function disposeCodexChild(
// A concurrently closed stdin does not change tree ownership below.
}
child.terminate()
const exitWindow = doubledGraceWindow(graceMs)
try {
if (!(await child.waitForExit(exitWindow.signal))) {
throw new Error('subagent-codex: app-server process tree did not exit within its dispose window')
}
} finally {
exitWindow.cancel()
}
await child.waitForExit()
await child.done
}
@@ -167,8 +117,7 @@ export async function startCodexRun(
child.stdout as NonNullable<SubprocessHandle['stdout']>,
child.stdin as NonNullable<SubprocessHandle['stdin']>,
)
const disposeProcess = (): Promise<void> =>
disposeCodexChild(wire, child, spec.disposeGraceMs)
const disposeProcess = (): Promise<void> => disposeCodexChild(wire, child)
const processFailure: Promise<never> = child.done.then(
outcome => Promise.reject(new Error(
@@ -205,7 +154,7 @@ export async function startCodexRun(
)
}
if (runAbort.signal.aborted) {
throw new Error('subagent-codex: request was aborted before app-server startup')
throw new Error('subagent-codex: request was aborted before run publication')
}
throw thrown(error)
}