Przeglądaj źródła

Merge ACP direct-outcome simplification into DSH SDK layer

pku-xht 1 miesiąc temu
rodzic
commit
d5fe4e9307

+ 12 - 6
packages/subagent/subagent-acp/src/run.ts

@@ -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.

+ 10 - 3
packages/subagent/subagent-acp/tests/subagent-acp.spec.ts

@@ -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()
   })