Merge ACP direct-outcome simplification into DSH SDK layer

This commit is contained in:
pku-xht
2026-08-21 08:06:43 +08:00
2 changed files with 22 additions and 9 deletions
+12 -6
View File
@@ -379,16 +379,22 @@ export async function startAcpRun(request: SubagentStartRequest, spec: AcpRunSpe
const observeProcessOutcome = async (signal?: AbortSignal): Promise<SubprocessOutcome | undefined> => {
if (processOutcome !== undefined || child.pid <= 0) return processOutcome
const timeout = AbortSignal.timeout(Math.ceil(spec.disposeGraceMs))
const bound = signal === undefined ? timeout : AbortSignal.any([signal, timeout])
const aborted = Promise.withResolvers<undefined>()
const onObservationAbort = (): void => { aborted.resolve(undefined) }
bound.addEventListener('abort', onObservationAbort, { once: true })
/* v8 ignore next -- closes the event-loop race between listener registration and the preceding derived-signal check. */
if (bound.aborted) onObservationAbort()
try {
const timeout = AbortSignal.timeout(Math.ceil(spec.disposeGraceMs))
const exited = await child.waitForExit(
signal === undefined ? timeout : AbortSignal.any([signal, timeout]),
)
if (exited) return await processDone
return await Promise.race([processDone, aborted.promise])
} catch {
// The active protocol failure remains authoritative when exit observation fails.
/* v8 ignore next -- a published child.done cannot reject; spawn rejection is consumed before publication. */
return processOutcome
} finally {
bound.removeEventListener('abort', onObservationAbort)
}
return processOutcome
}
// Startup rollback and the published handle share one process teardown.
@@ -959,7 +959,8 @@ describe('dsh-subagent-acp', () => {
it('lets local cancellation interrupt prompt-failure process observation', async () => {
const controller = new AbortController()
const observing = Promise.withResolvers<undefined>()
const protocolEnded = Promise.withResolvers<undefined>()
let boundedExitWaits = 0
const run = await startAcpRun(request('p', controller.signal), {
command: process.execPath,
args: [mockServer],
@@ -968,9 +969,14 @@ describe('dsh-subagent-acp', () => {
env: { MOCK_CLOSE_PROTOCOL_ON_PROMPT: '1' },
disposeEofGraceMs: 100,
disposeGraceMs: 5000,
spawn: spec => tapBoundedExitWait(spawnSubprocess(spec), () => { observing.resolve(undefined) }),
spawn: (spec) => {
const child = spawnSubprocess(spec)
child.stdout?.once('end', () => { protocolEnded.resolve(undefined) })
return tapBoundedExitWait(child, () => { boundedExitWaits += 1 })
},
})
await observing.promise
await protocolEnded.promise
await new Promise<void>((resolve) => { setImmediate(resolve) })
controller.abort()
await expect(Promise.race([
run.result,
@@ -978,6 +984,7 @@ describe('dsh-subagent-acp', () => {
setTimeout(() => { reject(new Error('cancellation waited for process observation')) }, 500)
}),
])).resolves.toEqual({ output: [], stopReason: 'aborted' })
expect(boundedExitWaits).toBe(0)
await run.dispose()
})