From 7dae2711fc801de80ec5e2dba7fddd7098e7c507 Mon Sep 17 00:00:00 2001 From: SpencerJung Date: Fri, 22 May 2026 16:20:39 +0900 Subject: [PATCH] fix(atlas): honor stopped continuation after boulder completion Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/atlas/idle-event.test.ts | 51 ++++++++++++++++++++++++++++++ src/hooks/atlas/idle-event.ts | 10 ++++++ 2 files changed, 61 insertions(+) diff --git a/src/hooks/atlas/idle-event.test.ts b/src/hooks/atlas/idle-event.test.ts index 58a8c7591..82911a90a 100644 --- a/src/hooks/atlas/idle-event.test.ts +++ b/src/hooks/atlas/idle-event.test.ts @@ -210,4 +210,55 @@ describe("handleAtlasSessionIdle completion nudge", () => { expect(promptAsyncMock).toHaveBeenCalledTimes(1) expect(getState(SESSION_ID).boulderCompletionNudgedAt?.[workId]).toBeNumber() }) + + it("does not send a completion nudge after continuation was explicitly stopped", async () => { + // given + const planPath = join(testDirectory, "plan.md") + writeFileSync(planPath, "## TODOs\n- [x] 1. Parse input\n") + + const boulder = createBoulderState(planPath, SESSION_ID, "atlas") + const workId = boulder.active_work_id + if (!workId) { + throw new Error("Expected active_work_id") + } + writeBoulderState(testDirectory, boulder) + + const promptAsyncMock = mock(async () => ({ data: {} })) + const ctx = unsafeTestValue({ + directory: testDirectory, + client: { + session: { + promptAsync: promptAsyncMock, + }, + }, + }) + const retryTimer = setTimeout(() => {}, 60_000) + const sessionStateById = new Map([ + [SESSION_ID, { promptFailureCount: 0, pendingRetryTimer: retryTimer }], + ]) + const getState = (sessionId: string): SessionState => { + let state = sessionStateById.get(sessionId) + if (!state) { + state = { promptFailureCount: 0 } + sessionStateById.set(sessionId, state) + } + return state + } + + // when + await handleAtlasSessionIdle({ + ctx, + sessionID: SESSION_ID, + getState, + options: { + isContinuationStopped: (sessionId) => sessionId === SESSION_ID, + }, + }) + + // then + expect(promptAsyncMock).not.toHaveBeenCalled() + expect(getState(SESSION_ID).pendingRetryTimer).toBeUndefined() + expect(getState(SESSION_ID).boulderCompletionNudgedAt?.[workId]).toBeUndefined() + expect(readBoulderState(testDirectory)?.works?.[workId]?.status).toBe("completed") + }) }) diff --git a/src/hooks/atlas/idle-event.ts b/src/hooks/atlas/idle-event.ts index 2dc06008d..93b5685de 100644 --- a/src/hooks/atlas/idle-event.ts +++ b/src/hooks/atlas/idle-event.ts @@ -246,6 +246,11 @@ export async function handleAtlasSessionIdle(input: { const { boulderState, progress, appendedSession } = activeBoulderSession if (progress.isComplete) { + if (sessionState.pendingRetryTimer) { + clearTimeout(sessionState.pendingRetryTimer) + sessionState.pendingRetryTimer = undefined + } + const work = getWorkForSession(ctx.directory, sessionID) if (work) { completeBoulder(ctx.directory, work.work_id) @@ -258,6 +263,11 @@ export async function handleAtlasSessionIdle(input: { return } + if (options?.isContinuationStopped?.(sessionID)) { + log(`[${HOOK_NAME}] Boulder completion nudge skipped because continuation stopped`, { sessionID, plan: boulderState.plan_name }) + return + } + if (sessionState.boulderCompletionNudgedAt?.[work.work_id]) { log(`[${HOOK_NAME}] Boulder complete`, { sessionID, plan: boulderState.plan_name }) return