From f9c78de1714dddde65f3325b8212761404d0737e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 19 Feb 2026 03:38:06 +0900 Subject: [PATCH] fix(run): set default stabilization to 1s and coerce non-positive values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change MIN_STABILIZATION_MS from 0 to 1_000 to prevent premature exits - Coerce non-positive minStabilizationMs to default instead of treating as disabled - Fix stabilization logic: track firstWorkTimestamp inside the meaningful-work branch - Add tests for default stabilization behavior and zero-value coercion 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/cli/run/poll-for-completion.test.ts | 48 +++++++++++++++++++++++-- src/cli/run/poll-for-completion.ts | 32 ++++++++--------- 2 files changed, 59 insertions(+), 21 deletions(-) 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)