Просмотр исходного кода

refactor(subagent): drop unused ACP cancel classification

pku-xht 1 месяц назад
Родитель
Сommit
0dcb514fc6

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

@@ -491,11 +491,13 @@ export async function startAcpRun(request: SubagentStartRequest, spec: AcpRunSpe
     // A child closing its protocol stream can precede whole-tree exit
     // observation. Local cancellation does not need the discarded startup
     // classification; other failures use the configured process grace.
-    const startupOutcome = cancelledBeforeCleanup
-      ? processOutcome
-      : await observeProcessOutcome()
-    const failure = startupFailure(error, startupStage, child, startupOutcome)
-    if (cancelledBeforeCleanup) {
+    const startup = cancelledBeforeCleanup
+      ? { kind: 'cancelled' } as const
+      : {
+        kind: 'failed',
+        failure: startupFailure(error, startupStage, child, await observeProcessOutcome()),
+      } as const
+    if (startup.kind === 'cancelled') {
       // Local cancellation owns the startup outcome; only cleanup failure is
       // reported below when teardown itself rejects.
     } else {
@@ -512,18 +514,18 @@ export async function startAcpRun(request: SubagentStartRequest, spec: AcpRunSpe
         category: processOutcome === undefined ? 'unknown' : 'process-exit',
         ...(processOutcome === undefined ? {} : { outcome: processOutcome }),
       }, cleanupError)
-      if (cancelledBeforeCleanup) {
+      if (startup.kind === 'cancelled') {
         throw new AggregateError([cleanupFailure], cleanupFailure.message)
       }
       throw new AggregateError(
-        [failure, cleanupFailure],
-        `${failure.message}; ${cleanupFailure.message}`,
+        [startup.failure, cleanupFailure],
+        `${startup.failure.message}; ${cleanupFailure.message}`,
       )
     }
-    if (cancelledBeforeCleanup) {
+    if (startup.kind === 'cancelled') {
       throw new Error('subagent request was aborted before the ACP child started')
     }
-    throw failure
+    throw startup.failure
   }
   // The startup transaction validates the returned id before it can fulfill.
   // This assertion carries that cross-closure invariant into TypeScript.

+ 8 - 0
packages/subagent/subagent-acp/tests/mock-acp-server.ts

@@ -23,6 +23,8 @@
  *                        provider's fixed permission fact.
  * - `MOCK_CRASH_ON_INITIALIZE` — exit while the unpublished initialize
  *                        operation is active.
+ * - `MOCK_CLOSE_PROTOCOL_ON_INITIALIZE` — close stdout while keeping the
+ *                        process alive, producing initialize-stage transport.
  * - `MOCK_CLOSE_PROTOCOL_ON_PROMPT` — close stdout while keeping the process
  *                        alive, producing a prompt-stage transport failure.
  * - `MOCK_CRASH_AFTER_CHUNK` — exit after streaming the assistant chunk, so
@@ -94,6 +96,7 @@ const IGNORE_PERMISSION_DECISION = process.env.MOCK_PERMISSION_IGNORE_DECISION =
 const NO_ALLOW = process.env.MOCK_NO_ALLOW === '1'
 const THOUGHT = process.env.MOCK_THOUGHT === '1'
 const CRASH_ON_INITIALIZE = process.env.MOCK_CRASH_ON_INITIALIZE === '1'
+const CLOSE_PROTOCOL_ON_INITIALIZE = process.env.MOCK_CLOSE_PROTOCOL_ON_INITIALIZE === '1'
 const CRASH_ON_CANCEL = process.env.MOCK_CRASH_ON_CANCEL === '1'
 const CRASH_ON_PROMPT = process.env.MOCK_CRASH_ON_PROMPT === '1'
 const CLOSE_PROTOCOL_ON_PROMPT = process.env.MOCK_CLOSE_PROTOCOL_ON_PROMPT === '1'
@@ -118,6 +121,11 @@ function makeAgent(conn: AgentSideConnection): Agent {
   return {
     initialize(_params: InitializeRequest): Promise<InitializeResponse> {
       if (CRASH_ON_INITIALIZE) process.exit(11)
+      if (CLOSE_PROTOCOL_ON_INITIALIZE) {
+        process.stdout.end()
+        setInterval(() => { /* keep the process alive after protocol EOF */ }, 1000)
+        return new Promise<InitializeResponse>(() => {})
+      }
       return Promise.resolve({
         protocolVersion: PROTOCOL_VERSION,
         agentCapabilities: { loadSession: false, promptCapabilities: { image: false, audio: false, embeddedContext: false } },

+ 17 - 0
packages/subagent/subagent-acp/tests/subagent-acp.spec.ts

@@ -573,6 +573,23 @@ describe('dsh-subagent-acp', () => {
     )
   })
 
+  it('reports initialize-stage transport when the child closes the protocol but stays alive', async () => {
+    const error = await startAcpRun(request(), {
+      command: process.execPath,
+      args: [mockServer],
+      cwd: process.cwd(),
+      permission: 'reject',
+      env: { MOCK_CLOSE_PROTOCOL_ON_INITIALIZE: '1' },
+      disposeEofGraceMs: 50,
+      disposeGraceMs: 50,
+      spawn: spawnSubprocess,
+    }).catch((cause: unknown) => cause)
+    expect(error).toBeInstanceOf(Error)
+    expect((error as Error).message).toBe(
+      `subagent-acp: ${expectedFailure('stage: initialize; category: transport')}`,
+    )
+  })
+
   it('reaps a child whose session/new response omits the session id', async () => {
     const tmp = mkdtempSync(join(tmpdir(), 'acp-malformed-session-'))
     const flushed = join(tmp, 'flushed')