Merge pull request #4282 from SpencerJung/fix/issue-4149-terminal-continuation-guard

fix(atlas): honor stopped continuation after boulder completion
This commit is contained in:
YeonGyu-Kim
2026-05-23 01:51:01 +09:00
committed by GitHub
2 changed files with 61 additions and 0 deletions
+51
View File
@@ -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<PluginInput>({
directory: testDirectory,
client: {
session: {
promptAsync: promptAsyncMock,
},
},
})
const retryTimer = setTimeout(() => {}, 60_000)
const sessionStateById = new Map<string, SessionState>([
[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")
})
})
+10
View File
@@ -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