diff --git a/bun.lock b/bun.lock index 4a416c88d2..713fc4f35d 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 d4821f3c6b..0d7376ca7f 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 f6eac1089b..47245fd95d 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++