fix(run): set default stabilization to 1s and coerce non-positive values

- 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)
This commit is contained in:
YeonGyu-Kim 2026-02-19 03:38:06 +09:00
parent bd2e23584b
commit f9c78de171
2 changed files with 59 additions and 21 deletions

View File

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

View File

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