diff --git a/src/cli/run/poll-for-completion.test.ts b/src/cli/run/poll-for-completion.test.ts index 682db2b5..c64e168d 100644 --- a/src/cli/run/poll-for-completion.test.ts +++ b/src/cli/run/poll-for-completion.test.ts @@ -45,7 +45,7 @@ describe("pollForCompletion", () => { const result = await pollForCompletion(ctx, eventState, abortController, { pollIntervalMs: 10, requiredConsecutive: 3, - minStabilizationMs: 0, + minStabilizationMs: 10, }) //#then - exits with 0 but only after 3 consecutive checks @@ -136,7 +136,7 @@ describe("pollForCompletion", () => { const result = await pollForCompletion(ctx, eventState, abortController, { pollIntervalMs: 10, requiredConsecutive: 3, - minStabilizationMs: 0, + minStabilizationMs: 10, }) const elapsedMs = Date.now() - startMs @@ -227,7 +227,7 @@ describe("pollForCompletion", () => { const result = await pollForCompletion(ctx, eventState, abortController, { pollIntervalMs: 10, requiredConsecutive: 2, - minStabilizationMs: 0, + minStabilizationMs: 10, }) //#then - completion succeeds without idle event @@ -255,6 +255,48 @@ describe("pollForCompletion", () => { expect(result).toBe(0) }) + it("uses default stabilization to avoid indefinite wait when no meaningful work arrives", async () => { + //#given - idle with no meaningful work and no explicit minStabilization override + spyOn(console, "log").mockImplementation(() => {}) + spyOn(console, "error").mockImplementation(() => {}) + const ctx = createMockContext() + const eventState = createEventState() + eventState.mainSessionIdle = true + eventState.hasReceivedMeaningfulWork = false + const abortController = new AbortController() + + //#when + const result = await pollForCompletion(ctx, eventState, abortController, { + pollIntervalMs: 10, + requiredConsecutive: 1, + }) + + //#then - command exits without manual Ctrl+C + expect(result).toBe(0) + }) + + it("coerces non-positive stabilization values to default stabilization", async () => { + //#given - explicit zero stabilization should still wait for default window + spyOn(console, "log").mockImplementation(() => {}) + spyOn(console, "error").mockImplementation(() => {}) + const ctx = createMockContext() + const eventState = createEventState() + eventState.mainSessionIdle = true + eventState.hasReceivedMeaningfulWork = false + const abortController = new AbortController() + + //#when - abort before default 1s window elapses + setTimeout(() => abortController.abort(), 100) + const result = await pollForCompletion(ctx, eventState, abortController, { + pollIntervalMs: 10, + requiredConsecutive: 1, + minStabilizationMs: 0, + }) + + //#then - should not complete early + expect(result).toBe(130) + }) + it("simulates race condition: brief idle with 0 todos does not cause immediate exit", async () => { //#given - simulate Sisyphus outputting text, session goes idle briefly, then tool fires spyOn(console, "log").mockImplementation(() => {}) diff --git a/src/cli/run/poll-for-completion.ts b/src/cli/run/poll-for-completion.ts index 80385145..684670cb 100644 --- a/src/cli/run/poll-for-completion.ts +++ b/src/cli/run/poll-for-completion.ts @@ -7,7 +7,7 @@ import { normalizeSDKResponse } from "../../shared" const DEFAULT_POLL_INTERVAL_MS = 500 const DEFAULT_REQUIRED_CONSECUTIVE = 1 const ERROR_GRACE_CYCLES = 3 -const MIN_STABILIZATION_MS = 0 +const MIN_STABILIZATION_MS = 1_000 export interface PollOptions { pollIntervalMs?: number @@ -24,8 +24,10 @@ export async function pollForCompletion( const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS const requiredConsecutive = options.requiredConsecutive ?? DEFAULT_REQUIRED_CONSECUTIVE - const minStabilizationMs = + const rawMinStabilizationMs = options.minStabilizationMs ?? MIN_STABILIZATION_MS + const minStabilizationMs = + rawMinStabilizationMs > 0 ? rawMinStabilizationMs : MIN_STABILIZATION_MS let consecutiveCompleteChecks = 0 let errorCycleCount = 0 let firstWorkTimestamp: number | null = null @@ -75,27 +77,21 @@ export async function pollForCompletion( } if (!eventState.hasReceivedMeaningfulWork) { - if (minStabilizationMs <= 0) { - consecutiveCompleteChecks = 0 - continue - } - if (Date.now() - pollStartTimestamp < minStabilizationMs) { consecutiveCompleteChecks = 0 continue } - consecutiveCompleteChecks = 0 - } + } else { + // Track when first meaningful work was received + if (firstWorkTimestamp === null) { + firstWorkTimestamp = Date.now() + } - // Track when first meaningful work was received - if (firstWorkTimestamp === null) { - firstWorkTimestamp = Date.now() - } - - // Don't check completion during stabilization period - if (Date.now() - firstWorkTimestamp < minStabilizationMs) { - consecutiveCompleteChecks = 0 - continue + // Don't check completion during stabilization period + if (Date.now() - firstWorkTimestamp < minStabilizationMs) { + consecutiveCompleteChecks = 0 + continue + } } const shouldExit = await checkCompletionConditions(ctx)