From 960baf39bb8940f22e97105931d3f91e317e4cb0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 15 May 2026 11:30:11 +0900 Subject: [PATCH] fix(atlas): gate boulder continuation prompts --- .../atlas/boulder-continuation-injector.ts | 27 ++++++++++++----- src/hooks/atlas/idle-event.ts | 29 +++++++++++++++---- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/hooks/atlas/boulder-continuation-injector.ts b/src/hooks/atlas/boulder-continuation-injector.ts index 197c3a7d8..1931da6c9 100644 --- a/src/hooks/atlas/boulder-continuation-injector.ts +++ b/src/hooks/atlas/boulder-continuation-injector.ts @@ -5,7 +5,7 @@ import { } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" import { createInternalAgentContinuationTextPart, resolveInheritedPromptTools } from "../../shared" -import { isSessionActive } from "../shared/session-idle-settle" +import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" import { HOOK_NAME } from "./hook-name" import { BOULDER_CONTINUATION_PROMPT } from "./system-reminder-templates" import { resolveRecentPromptContextForSession } from "./recent-model-resolver" @@ -32,6 +32,7 @@ export async function injectBoulderContinuation(input: { preferredTaskTitle?: string backgroundManager?: BackgroundTaskStatusProvider sessionState: SessionState + idleSettleMs?: number }): Promise { const { ctx, @@ -45,6 +46,7 @@ export async function injectBoulderContinuation(input: { preferredTaskTitle, backgroundManager, sessionState, + idleSettleMs, } = input const hasRunningBgTasks = backgroundManager @@ -78,11 +80,6 @@ export async function injectBoulderContinuation(input: { } try { - if (await isSessionActive(ctx.client, sessionID)) { - log(`[${HOOK_NAME}] Skipped injection: session is active`, { sessionID }) - return "skipped_active_session" - } - log(`[${HOOK_NAME}] Injecting boulder continuation`, { sessionID, planName, remaining }) const promptContext = await resolveRecentPromptContextForSession(ctx, sessionID) @@ -93,7 +90,12 @@ export async function injectBoulderContinuation(input: { : undefined const launchVariant = promptContext.model?.variant - await ctx.client.session.promptAsync({ + const promptResult = await promptAsyncAfterSessionIdle({ + client: ctx.client, + sessionID, + source: HOOK_NAME, + settleMs: idleSettleMs, + input: { path: { id: sessionID }, body: { agent: continuationAgent, @@ -103,7 +105,18 @@ export async function injectBoulderContinuation(input: { parts: [createInternalAgentContinuationTextPart(prompt)], }, query: { directory: ctx.directory }, + }, }) + if (promptResult.status === "failed") { + throw promptResult.error + } + if (promptResult.status !== "dispatched") { + log(`[${HOOK_NAME}] Boulder continuation skipped by promptAsync gate`, { + sessionID, + status: promptResult.status, + }) + return "skipped_active_session" + } sessionState.promptFailureCount = 0 log(`[${HOOK_NAME}] Boulder continuation injected`, { sessionID }) diff --git a/src/hooks/atlas/idle-event.ts b/src/hooks/atlas/idle-event.ts index 5f98122c8..4dce6ab4b 100644 --- a/src/hooks/atlas/idle-event.ts +++ b/src/hooks/atlas/idle-event.ts @@ -20,6 +20,7 @@ import { createInternalAgentContinuationTextPart } from "../../shared" import { getAgentConfigKey } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { shouldPromptAfterSessionIdle } from "../shared/session-idle-settle" +import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" import { injectBoulderContinuation } from "./boulder-continuation-injector" import { HOOK_NAME } from "./hook-name" import { resolveActiveBoulderSession } from "./resolve-active-boulder-session" @@ -52,6 +53,7 @@ async function injectContinuation(input: { progress: { total: number; completed: number } agent?: string worktreePath?: string + idleSettleMs?: number }): Promise { const remaining = input.progress.total - input.progress.completed if (input.sessionState.isInjectingContinuation) { @@ -110,6 +112,7 @@ async function injectContinuation(input: { preferredTaskTitle: preferredTaskSession?.task_title, backgroundManager: input.options?.backgroundManager, sessionState: input.sessionState, + idleSettleMs: input.idleSettleMs, }) if (result === "injected") { @@ -288,14 +291,27 @@ export async function handleAtlasSessionIdle(input: { return } - await ctx.client.session.promptAsync({ - path: { id: sessionID }, - body: { - agent: atlasAgent, - parts: [createInternalAgentContinuationTextPart(prompt)], + const promptResult = await promptAsyncAfterSessionIdle({ + client: ctx.client, + sessionID, + source: HOOK_NAME, + settleMs: options?.idleSettleMs, + input: { + path: { id: sessionID }, + body: { + agent: atlasAgent, + parts: [createInternalAgentContinuationTextPart(prompt)], + }, + query: { directory: ctx.directory }, }, - query: { directory: ctx.directory }, }) + if (promptResult.status !== "dispatched") { + log(`[${HOOK_NAME}] Boulder completion nudge skipped by promptAsync gate`, { + sessionID, + status: promptResult.status, + }) + return + } sessionState.boulderCompletionNudgedAt = { ...(sessionState.boulderCompletionNudgedAt ?? {}), [work.work_id]: Date.now(), @@ -398,6 +414,7 @@ export async function handleAtlasSessionIdle(input: { progress, agent: boulderState.agent, worktreePath: boulderState.worktree_path, + idleSettleMs: options?.idleSettleMs ?? 0, }) }