fix: boulder continuation now respects /stop-continuation guard
Add isContinuationStopped check to atlas hook's session.idle handler so boulder continuation stops when user runs /stop-continuation. Previously, todo continuation and session recovery checked the guard, but boulder continuation did not — causing work to resume after stop. Fixes #1575
This commit is contained in:
parent
4d19a22679
commit
f980e256dd
@ -755,40 +755,71 @@ describe("atlas hook", () => {
|
|||||||
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should skip when background tasks are running", async () => {
|
test("should skip when background tasks are running", async () => {
|
||||||
// given - boulder state with incomplete plan
|
// given - boulder state with incomplete plan
|
||||||
const planPath = join(TEST_DIR, "test-plan.md")
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
writeFileSync(planPath, "# Plan\n- [ ] Task 1")
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1")
|
||||||
|
|
||||||
const state: BoulderState = {
|
const state: BoulderState = {
|
||||||
active_plan: planPath,
|
active_plan: planPath,
|
||||||
started_at: "2026-01-02T10:00:00Z",
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
session_ids: [MAIN_SESSION_ID],
|
session_ids: [MAIN_SESSION_ID],
|
||||||
plan_name: "test-plan",
|
plan_name: "test-plan",
|
||||||
}
|
}
|
||||||
writeBoulderState(TEST_DIR, state)
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
const mockBackgroundManager = {
|
const mockBackgroundManager = {
|
||||||
getTasksByParentSession: () => [{ status: "running" }],
|
getTasksByParentSession: () => [{ status: "running" }],
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockInput = createMockPluginInput()
|
const mockInput = createMockPluginInput()
|
||||||
const hook = createAtlasHook(mockInput, {
|
const hook = createAtlasHook(mockInput, {
|
||||||
directory: TEST_DIR,
|
directory: TEST_DIR,
|
||||||
backgroundManager: mockBackgroundManager as any,
|
backgroundManager: mockBackgroundManager as any,
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await hook.handler({
|
await hook.handler({
|
||||||
event: {
|
event: {
|
||||||
type: "session.idle",
|
type: "session.idle",
|
||||||
properties: { sessionID: MAIN_SESSION_ID },
|
properties: { sessionID: MAIN_SESSION_ID },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// then - should not call prompt
|
// then - should not call prompt
|
||||||
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should skip when continuation is stopped via isContinuationStopped", async () => {
|
||||||
|
// given - boulder state with incomplete plan
|
||||||
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
||||||
|
|
||||||
|
const state: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [MAIN_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
}
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
|
const mockInput = createMockPluginInput()
|
||||||
|
const hook = createAtlasHook(mockInput, {
|
||||||
|
directory: TEST_DIR,
|
||||||
|
isContinuationStopped: (sessionID: string) => sessionID === MAIN_SESSION_ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook.handler({
|
||||||
|
event: {
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: MAIN_SESSION_ID },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// then - should not call prompt because continuation is stopped
|
||||||
|
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
test("should clear abort state on message.updated", async () => {
|
test("should clear abort state on message.updated", async () => {
|
||||||
// given - boulder with incomplete plan
|
// given - boulder with incomplete plan
|
||||||
|
|||||||
@ -399,6 +399,7 @@ const CONTINUATION_COOLDOWN_MS = 5000
|
|||||||
export interface AtlasHookOptions {
|
export interface AtlasHookOptions {
|
||||||
directory: string
|
directory: string
|
||||||
backgroundManager?: BackgroundManager
|
backgroundManager?: BackgroundManager
|
||||||
|
isContinuationStopped?: (sessionID: string) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAbortError(error: unknown): boolean {
|
function isAbortError(error: unknown): boolean {
|
||||||
@ -573,6 +574,11 @@ export function createAtlasHook(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.isContinuationStopped?.(sessionID)) {
|
||||||
|
log(`[${HOOK_NAME}] Skipped: continuation stopped for session`, { sessionID })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const requiredAgent = (boulderState.agent ?? "atlas").toLowerCase()
|
const requiredAgent = (boulderState.agent ?? "atlas").toLowerCase()
|
||||||
const lastAgent = getLastAgentFromSession(sessionID)
|
const lastAgent = getLastAgentFromSession(sessionID)
|
||||||
if (!lastAgent || lastAgent !== requiredAgent) {
|
if (!lastAgent || lastAgent !== requiredAgent) {
|
||||||
|
|||||||
@ -335,7 +335,11 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const atlasHook = isHookEnabled("atlas")
|
const atlasHook = isHookEnabled("atlas")
|
||||||
? safeCreateHook("atlas", () => createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }), { enabled: safeHookEnabled })
|
? safeCreateHook("atlas", () => createAtlasHook(ctx, {
|
||||||
|
directory: ctx.directory,
|
||||||
|
backgroundManager,
|
||||||
|
isContinuationStopped: (sessionID: string) => stopContinuationGuard?.isStopped(sessionID) ?? false,
|
||||||
|
}), { enabled: safeHookEnabled })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
initTaskToastManager(ctx.client);
|
initTaskToastManager(ctx.client);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user