fix(atlas): block continuation while delegated tasks are pending

Atlas continuation only treated running background tasks as active work. When a delegated subagent had been launched but was still waiting for session creation, the task remained pending and Atlas could inject another continuation too early.

Treat pending tasks as active background work in the continuation injector and add regression coverage for the pending-session-creation race so delegated work is allowed to acquire a session before Atlas resumes the plan.

Tests: bun test src/hooks/atlas/boulder-continuation-injector.test.ts src/hooks/atlas/index.test.ts; bun run typecheck

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
tw-yshuang
2026-05-03 17:24:46 +08:00
parent 6c51eac8bf
commit 56e4044927
2 changed files with 41 additions and 1 deletions
@@ -91,6 +91,44 @@ describe("injectBoulderContinuation", () => {
expect(sessionState.lastContinuationInjectedAt).toBe(123)
})
test("#given a background task is still pending session creation #when injector checks again #then it still skips continuation", async () => {
// given
registerAgentName("atlas")
const promptAsyncMock = mock(async (_request: unknown) => undefined)
const messagesMock = mock(async () => ({ data: [] }))
const sessionState = { promptFailureCount: 1, lastContinuationInjectedAt: 456 }
const ctx = {
directory: "/tmp",
client: {
session: {
messages: messagesMock,
promptAsync: promptAsyncMock,
},
},
} as unknown as PluginInput
// when
const result = await injectBoulderContinuation({
ctx,
sessionID: "ses_test_pending",
planName: "test-plan",
remaining: 1,
total: 2,
agent: "atlas",
backgroundManager: {
getTasksByParentSession: () => [{ status: "pending" }],
} as unknown as Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"],
sessionState,
})
// then
expect(result).toBe("skipped_background_tasks")
expect(promptAsyncMock).not.toHaveBeenCalled()
expect(sessionState.promptFailureCount).toBe(1)
expect(sessionState.lastContinuationInjectedAt).toBe(456)
})
test("#given the continuation agent is unavailable #when injector runs #then it reports skipped agent unavailable without prompting", async () => {
// given
const promptAsyncMock = mock(async (_request: unknown) => undefined)
@@ -13,6 +13,8 @@ import type { SessionState } from "./types"
export type BoulderContinuationResult = "injected" | "skipped_background_tasks" | "skipped_agent_unavailable" | "failed"
const ACTIVE_BACKGROUND_TASK_STATUSES = new Set(["pending", "running"])
export async function injectBoulderContinuation(input: {
ctx: PluginInput
sessionID: string
@@ -41,7 +43,7 @@ export async function injectBoulderContinuation(input: {
} = input
const hasRunningBgTasks = backgroundManager
? backgroundManager.getTasksByParentSession(sessionID).some((t: { status: string }) => t.status === "running")
? backgroundManager.getTasksByParentSession(sessionID).some((t: { status: string }) => ACTIVE_BACKGROUND_TASK_STATUSES.has(t.status))
: false
if (hasRunningBgTasks) {