From 564bb20f6a628b4e2c7304b1777eba3b4586f71d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 9 Feb 2026 21:11:48 +0900 Subject: [PATCH] fix(cli/run): move error check before idle/tool gates in pollForCompletion --- src/cli/run/poll-for-completion.test.ts | 47 +++++++++++++++++++++++++ src/cli/run/poll-for-completion.ts | 21 +++++------ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/cli/run/poll-for-completion.test.ts b/src/cli/run/poll-for-completion.test.ts index 97260ff50..d4821f3c6 100644 --- a/src/cli/run/poll-for-completion.test.ts +++ b/src/cli/run/poll-for-completion.test.ts @@ -216,4 +216,51 @@ describe("pollForCompletion", () => { //#then - should NOT have exited with 0 (tool blocked it, then aborted) expect(result).toBe(130) }) + + it("returns 1 when session errors while not idle (error not masked by idle gate)", async () => { + //#given - mainSessionIdle=false, mainSessionError=true, lastError="crash" + spyOn(console, "log").mockImplementation(() => {}) + spyOn(console, "error").mockImplementation(() => {}) + const ctx = createMockContext() + const eventState = createEventState() + eventState.mainSessionIdle = false + eventState.mainSessionError = true + eventState.lastError = "crash" + eventState.hasReceivedMeaningfulWork = true + const abortController = new AbortController() + + //#when - pollForCompletion runs + const result = await pollForCompletion(ctx, eventState, abortController, { + pollIntervalMs: 10, + requiredConsecutive: 3, + }) + + //#then - returns 1 (not 130/timeout), error message printed + expect(result).toBe(1) + const errorCalls = (console.error as ReturnType).mock.calls + expect(errorCalls.some((call) => call[0]?.includes("Session ended with error"))).toBe(true) + }) + + it("returns 1 when session errors while tool is active (error not masked by tool gate)", async () => { + //#given - mainSessionIdle=true, currentTool="bash", mainSessionError=true + spyOn(console, "log").mockImplementation(() => {}) + spyOn(console, "error").mockImplementation(() => {}) + const ctx = createMockContext() + const eventState = createEventState() + eventState.mainSessionIdle = true + eventState.currentTool = "bash" + eventState.mainSessionError = true + eventState.lastError = "error during tool" + eventState.hasReceivedMeaningfulWork = true + const abortController = new AbortController() + + //#when + const result = await pollForCompletion(ctx, eventState, abortController, { + pollIntervalMs: 10, + requiredConsecutive: 3, + }) + + //#then - returns 1 + expect(result).toBe(1) + }) }) diff --git a/src/cli/run/poll-for-completion.ts b/src/cli/run/poll-for-completion.ts index 585131c50..947b692cc 100644 --- a/src/cli/run/poll-for-completion.ts +++ b/src/cli/run/poll-for-completion.ts @@ -25,16 +25,7 @@ export async function pollForCompletion( while (!abortController.signal.aborted) { await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) - if (!eventState.mainSessionIdle) { - consecutiveCompleteChecks = 0 - continue - } - - if (eventState.currentTool !== null) { - consecutiveCompleteChecks = 0 - continue - } - + // ERROR CHECK FIRST — errors must not be masked by other gates if (eventState.mainSessionError) { console.error( pc.red(`\n\nSession ended with error: ${eventState.lastError}`) @@ -45,6 +36,16 @@ export async function pollForCompletion( return 1 } + if (!eventState.mainSessionIdle) { + consecutiveCompleteChecks = 0 + continue + } + + if (eventState.currentTool !== null) { + consecutiveCompleteChecks = 0 + continue + } + if (!eventState.hasReceivedMeaningfulWork) { consecutiveCompleteChecks = 0 continue