From 5c2a215fca7510fc6048d823060da2e39bc17a96 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 11 Feb 2026 16:32:35 +0900 Subject: [PATCH] fix(run): add stabilization period to prevent early exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oh-my-opencode run exits within ~1.5s of starting because pollForCompletion checks completion too early. When the agent outputs its first text but hasn't created todos yet, the empty state (0 todos, 0 children) passes all checks. Root cause timeline: 1. promptAsync fires 2. Agent outputs text (e.g. 'ULTRAWORK MODE ENABLED!') -> hasReceivedMeaningfulWork = true 3. Agent pauses before todowrite -> session briefly goes idle 4. pollForCompletion: idle=true, tool=null, work=true -> checkCompletionConditions 5. 0 todos = 'all complete', 0 children = 'all idle' -> true 6. 3 consecutive checks (1.5s) -> premature exit 0 Fix: Add a minimum stabilization period (10s) after the first meaningful work before checking completion conditions. This gives agents time to create todos and spawn child sessions. The period is configurable via PollOptions for tests. Note: todo 0 remaining 'all complete' is correct behavior — some agents don't use todos. The stabilization period is the proper fix, not changing completion semantics. --- bun.lock | 28 ++++++++++++------------- src/cli/run/poll-for-completion.test.ts | 26 +++++++++++++++++++++++ src/cli/run/poll-for-completion.ts | 19 +++++++++++++++++ 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 4a416c88..713fc4f3 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,13 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.3.1", - "oh-my-opencode-darwin-x64": "3.3.1", - "oh-my-opencode-linux-arm64": "3.3.1", - "oh-my-opencode-linux-arm64-musl": "3.3.1", - "oh-my-opencode-linux-x64": "3.3.1", - "oh-my-opencode-linux-x64-musl": "3.3.1", - "oh-my-opencode-windows-x64": "3.3.1", + "oh-my-opencode-darwin-arm64": "3.5.1", + "oh-my-opencode-darwin-x64": "3.5.1", + "oh-my-opencode-linux-arm64": "3.5.1", + "oh-my-opencode-linux-arm64-musl": "3.5.1", + "oh-my-opencode-linux-x64": "3.5.1", + "oh-my-opencode-linux-x64-musl": "3.5.1", + "oh-my-opencode-windows-x64": "3.5.1", }, }, }, @@ -226,19 +226,19 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-R+o42Km6bsIaW6D3I8uu2HCF3BjIWqa/fg38W5y4hJEOw4mL0Q7uV4R+0vtrXRHo9crXTK9ag0fqVQUm+Y6iAQ=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oH+c/+Z/ULIK+8T1jQFpzISHsvQPyYJfA6bceiD9sgFy1OY1NjRh4a3sFk8cXy6uRVKpivWDFOfbVTcZ2kbKWA=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7VTbpR1vH3OEkoJxBKtYuxFPX8M3IbJKoeHWME9iK6FpT11W1ASsjyuhvzB1jcxSeqF8ddMnjitlG5ub6h5EVw=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-wnBYQ9BZBLbzgSNIJZOIJS03zf+b4trAQeYmG+yCLn8y7FWXqw1KmjJ88/bbMXTuZ4RSMKWpXb1Afgdsred+DQ=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BZ/r/CFlvbOxkdZZrRoT16xFOjibRZHuwQnaE4f0JvOzgK6/HWp3zJI1+2/aX/oK5GA6lZxNWRrJC/SKUi8LEg=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-19KNJex1LeU/S14IsJbumOvZa9O6F7X4BLIY7MfjtHtTk0dRFL+tbbXmlafecBMigEKlLdJ+HTW3TnQgp7Ih8A=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-U90Wruf21h+CJbtcrS7MeTAc/5VOF6RI+5jr7qj/cCxjXNJtjhyJdz/maehArjtgf304+lYCM/Mh1i+G2D3YFQ=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-mCCnym3nBTJP+xzK+AS4YPFQiT2sZWmjhOhOy7PjNY6Is4jkfT1C2e9ZrIU/2VoVLV6V5q7hQGh1jgleU+FxwQ=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-sYzohSNdwsAhivbXcbhPdF1qqQi2CCI7FSgbmvvfBOMyZ8HAgqOFqYW2r3GPdmtywzkjOTvCzTG56FZwEjx15w=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-sDYt4adNuwb+p1RzHb7IR9zvbAnYYgZofjPvceirBorffp63f+aypYFxjFpfmbT87o/Eb/Hgzm4sHliJtd1UmQ=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aG5pZ4eWS0YSGUicOnjMkUPrIqQV4poYF+d9SIvrfvlaMcK6WlQn7jXzgNCwJsfGn5lyhSmjshZBEU+v79Ua3w=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-tz/0QSS5AKIiKj6cMom5VQSnEYpMIP/SRTaP5WYNOYhnUkXMwXEncQ7FIcj2vovMCXuqA9a8ujVY0zTs7TeALw=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-FGH7cnzBqNwjSkzCDglMsVttaq+MsykAxa7ehaFK+0dnBZArvllS3W13a3dGaANHMZzfK0vz8hNDUdVi7Z63cA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-zfpRS6HIkSwE8btajJzSYxhqsE5kDkop896/XGS3LLIAAZt0RtCmT3C1plxVfI9oAABfgcaiveCxJ5f9AlKPcQ=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/cli/run/poll-for-completion.test.ts b/src/cli/run/poll-for-completion.test.ts index d4821f3c..0d7376ca 100644 --- a/src/cli/run/poll-for-completion.test.ts +++ b/src/cli/run/poll-for-completion.test.ts @@ -45,6 +45,7 @@ describe("pollForCompletion", () => { const result = await pollForCompletion(ctx, eventState, abortController, { pollIntervalMs: 10, requiredConsecutive: 3, + minStabilizationMs: 0, }) //#then - exits with 0 but only after 3 consecutive checks @@ -53,6 +54,30 @@ describe("pollForCompletion", () => { expect(todoCallCount).toBeGreaterThanOrEqual(3) }) + it("does not check completion during stabilization period after first meaningful work", async () => { + //#given - 0 todos, 0 children, session idle, meaningful work done, short stabilization + spyOn(console, "log").mockImplementation(() => {}) + spyOn(console, "error").mockImplementation(() => {}) + const ctx = createMockContext() + const eventState = createEventState() + eventState.mainSessionIdle = true + eventState.hasReceivedMeaningfulWork = true + const abortController = new AbortController() + + //#when - abort before stabilization period ends + setTimeout(() => abortController.abort(), 50) + const result = await pollForCompletion(ctx, eventState, abortController, { + pollIntervalMs: 10, + requiredConsecutive: 3, + minStabilizationMs: 200, + }) + + //#then - should be aborted (130) because stabilization hadn't elapsed yet + expect(result).toBe(130) + const todoCallCount = (ctx.client.session.todo as ReturnType).mock.calls.length + expect(todoCallCount).toBe(0) + }) + it("does not exit when currentTool is set - resets consecutive counter", async () => { //#given spyOn(console, "log").mockImplementation(() => {}) @@ -110,6 +135,7 @@ describe("pollForCompletion", () => { const result = await pollForCompletion(ctx, eventState, abortController, { pollIntervalMs: 10, requiredConsecutive: 3, + minStabilizationMs: 0, }) const elapsedMs = Date.now() - startMs diff --git a/src/cli/run/poll-for-completion.ts b/src/cli/run/poll-for-completion.ts index f6eac108..47245fd9 100644 --- a/src/cli/run/poll-for-completion.ts +++ b/src/cli/run/poll-for-completion.ts @@ -6,10 +6,12 @@ import { checkCompletionConditions } from "./completion" const DEFAULT_POLL_INTERVAL_MS = 500 const DEFAULT_REQUIRED_CONSECUTIVE = 3 const ERROR_GRACE_CYCLES = 3 +const MIN_STABILIZATION_MS = 10_000 export interface PollOptions { pollIntervalMs?: number requiredConsecutive?: number + minStabilizationMs?: number } export async function pollForCompletion( @@ -21,8 +23,11 @@ export async function pollForCompletion( const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS const requiredConsecutive = options.requiredConsecutive ?? DEFAULT_REQUIRED_CONSECUTIVE + const minStabilizationMs = + options.minStabilizationMs ?? MIN_STABILIZATION_MS let consecutiveCompleteChecks = 0 let errorCycleCount = 0 + let firstWorkTimestamp: number | null = null while (!abortController.signal.aborted) { await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) @@ -61,6 +66,20 @@ export async function pollForCompletion( continue } + // Track when first meaningful work was received + if (firstWorkTimestamp === null) { + firstWorkTimestamp = Date.now() + } + + // Don't check completion until stabilization period has elapsed. + // Agents need time to set up todos and spawn child sessions after + // their first output. Without this, empty todos + no children + // triggers a false "all complete" within ~1.5s of starting. + if (Date.now() - firstWorkTimestamp < minStabilizationMs) { + consecutiveCompleteChecks = 0 + continue + } + const shouldExit = await checkCompletionConditions(ctx) if (shouldExit) { consecutiveCompleteChecks++