From 56e4044927038f45083cf4a5a8a45904a9087b1a Mon Sep 17 00:00:00 2001 From: tw-yshuang Date: Sun, 3 May 2026 17:24:46 +0800 Subject: [PATCH] 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 --- .../boulder-continuation-injector.test.ts | 38 +++++++++++++++++++ .../atlas/boulder-continuation-injector.ts | 4 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/hooks/atlas/boulder-continuation-injector.test.ts b/src/hooks/atlas/boulder-continuation-injector.test.ts index c72fdb782..d26b4b850 100644 --- a/src/hooks/atlas/boulder-continuation-injector.test.ts +++ b/src/hooks/atlas/boulder-continuation-injector.test.ts @@ -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[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) diff --git a/src/hooks/atlas/boulder-continuation-injector.ts b/src/hooks/atlas/boulder-continuation-injector.ts index 8f3e1a57d..9e340e69b 100644 --- a/src/hooks/atlas/boulder-continuation-injector.ts +++ b/src/hooks/atlas/boulder-continuation-injector.ts @@ -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) {