fix: use boulder agent instead of hardcoded Atlas check for continuation

Address code review: continuation was blocked unless last agent was Atlas,
making the new agent parameter ineffective. Now the idle handler checks if
the last session agent matches boulderState.agent (defaults to 'atlas'),
allowing non-Atlas agents to resume when properly configured.

- Add getLastAgentFromSession helper for agent lookup
- Replace isCallerOrchestrator gate with boulder-agent-aware check
- Add test for non-Atlas agent continuation scenario
This commit is contained in:
Rishi Vhavle
2026-02-04 13:06:34 +05:30
parent d8137c0c90
commit 169ccb6b05
2 changed files with 54 additions and 6 deletions

View File

@@ -858,8 +858,8 @@ describe("atlas hook", () => {
expect(callArgs.body.parts[0].text).toContain("2 remaining")
})
test("should not inject when last agent is not Atlas", async () => {
// given - boulder state with incomplete plan, but last agent is NOT Atlas
test("should not inject when last agent does not match boulder agent", async () => {
// given - boulder state with incomplete plan, but last agent does NOT match
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
@@ -868,10 +868,11 @@ describe("atlas hook", () => {
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
agent: "atlas",
}
writeBoulderState(TEST_DIR, state)
// given - last agent is NOT Atlas
// given - last agent is NOT the boulder agent
cleanupMessageStorage(MAIN_SESSION_ID)
setupMessageStorage(MAIN_SESSION_ID, "sisyphus")
@@ -886,10 +887,44 @@ describe("atlas hook", () => {
},
})
// then - should NOT call prompt because agent is not Atlas
// then - should NOT call prompt because agent does not match
expect(mockInput._promptMock).not.toHaveBeenCalled()
})
test("should inject when last agent matches boulder agent even if non-Atlas", async () => {
// given - boulder state expects sisyphus and last agent is sisyphus
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",
agent: "sisyphus",
}
writeBoulderState(TEST_DIR, state)
cleanupMessageStorage(MAIN_SESSION_ID)
setupMessageStorage(MAIN_SESSION_ID, "sisyphus")
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when
await hook.handler({
event: {
type: "session.idle",
properties: { sessionID: MAIN_SESSION_ID },
},
})
// then - should call prompt for sisyphus
expect(mockInput._promptMock).toHaveBeenCalled()
const callArgs = mockInput._promptMock.mock.calls[0][0]
expect(callArgs.body.agent).toBe("sisyphus")
})
test("should debounce rapid continuation injections (prevent infinite loop)", async () => {
// given - boulder state with incomplete plan
const planPath = join(TEST_DIR, "test-plan.md")

View File

@@ -26,6 +26,13 @@ function isSisyphusPath(filePath: string): boolean {
const WRITE_EDIT_TOOLS = ["Write", "Edit", "write", "edit"]
function getLastAgentFromSession(sessionID: string): string | null {
const messageDir = getMessageDir(sessionID)
if (!messageDir) return null
const nearest = findNearestMessageWithFields(messageDir)
return nearest?.agent?.toLowerCase() ?? null
}
const DIRECT_WORK_REMINDER = `
---
@@ -549,8 +556,14 @@ export function createAtlasHook(
return
}
if (!isCallerOrchestrator(sessionID)) {
log(`[${HOOK_NAME}] Skipped: last agent is not Atlas`, { sessionID })
const requiredAgent = (boulderState.agent ?? "atlas").toLowerCase()
const lastAgent = getLastAgentFromSession(sessionID)
if (!lastAgent || lastAgent !== requiredAgent) {
log(`[${HOOK_NAME}] Skipped: last agent does not match boulder agent`, {
sessionID,
lastAgent: lastAgent ?? "unknown",
requiredAgent,
})
return
}