diff --git a/src/cli/run/continuation-state.ts b/src/cli/run/continuation-state.ts index 17066ab4a..733978913 100644 --- a/src/cli/run/continuation-state.ts +++ b/src/cli/run/continuation-state.ts @@ -1,4 +1,4 @@ -import { getPlanProgress, readBoulderState, resolveBoulderPlanPath } from "../../features/boulder-state" +import { getPlanProgress, normalizeSessionId, readBoulderState, resolveBoulderPlanPath } from "../../features/boulder-state" import { getSessionAgent } from "../../features/claude-code-session-state" import { getActiveContinuationMarkerReason, @@ -51,19 +51,25 @@ async function hasActiveBoulderContinuation( if (progress.isComplete) return false if (!client) return false - const isTrackedSession = boulder.session_ids.includes(sessionID) - const sessionOrigin = boulder.session_origins?.[sessionID] - if (!isTrackedSession) { + const normalizedSessionID = normalizeSessionId(sessionID) + const normalizedTrackedSessionIDs = boulder.session_ids.map((trackedSessionID) => normalizeSessionId(trackedSessionID)) + if (!normalizedTrackedSessionIDs.includes(normalizedSessionID)) { return false } - const isTrackedDescendant = await isTrackedDescendantSession(client, sessionID, boulder.session_ids) - - if (isTrackedSession && sessionOrigin === "direct") { + const sessionOrigin = boulder.session_origins?.[sessionID] ?? boulder.session_origins?.[normalizedSessionID] + if (sessionOrigin === "direct") { return true } - if (isTrackedSession && sessionOrigin !== "direct" && !isTrackedDescendant) { + const trackedAncestorSessionIDs = normalizedTrackedSessionIDs + .filter((trackedSessionID) => trackedSessionID !== normalizedSessionID) + if (trackedAncestorSessionIDs.length === 0) { + return true + } + + const isTrackedDescendant = await isTrackedDescendantSession(client, sessionID, trackedAncestorSessionIDs) + if (!isTrackedDescendant) { return false } @@ -82,23 +88,22 @@ async function hasActiveBoulderContinuation( return false } - return isTrackedSession || isTrackedDescendant + return true } async function isTrackedDescendantSession( client: RunContext["client"], sessionID: string, - trackedSessionIDs: string[], + trackedAncestorSessionIDs: string[], ): Promise { - const ancestorSessionIDs = trackedSessionIDs.filter((trackedSessionID) => trackedSessionID !== sessionID) - if (ancestorSessionIDs.length === 0) { + if (trackedAncestorSessionIDs.length === 0) { return false } return isSessionInBoulderLineage({ client, sessionID, - boulderSessionIDs: ancestorSessionIDs, + boulderSessionIDs: trackedAncestorSessionIDs, }) } diff --git a/src/hooks/atlas/session-last-agent.ts b/src/hooks/atlas/session-last-agent.ts index 7c60d5c96..e3a14897d 100644 --- a/src/hooks/atlas/session-last-agent.ts +++ b/src/hooks/atlas/session-last-agent.ts @@ -64,6 +64,48 @@ function getLastAgentFromMessageDir(messageDir: string): string | null { return null } +async function getLastAgentFromSessionMessages( + sessionID: string, + client: SessionMessagesClient, + deps: SessionLastAgentDeps, +): Promise { + try { + const response = await client.session.messages({ path: { id: sessionID } }) + const messages = deps.normalizeSDKResponse(response, [] as Array<{ + id?: string + info?: { agent?: string; time?: { created?: number } } + parts?: Array<{ type?: string }> + }>, { + preferResponseOnMissingData: true, + }).sort((left, right) => { + const leftTime = (left as { info?: { time?: { created?: number } } }).info?.time?.created ?? Number.NEGATIVE_INFINITY + const rightTime = (right as { info?: { time?: { created?: number } } }).info?.time?.created ?? Number.NEGATIVE_INFINITY + if (leftTime !== rightTime) { + return rightTime - leftTime + } + + const leftId = typeof left.id === "string" ? left.id : "" + const rightId = typeof right.id === "string" ? right.id : "" + return rightId.localeCompare(leftId) + }) + + for (const message of messages) { + if (deps.isCompactionMessage(message)) { + continue + } + + const agent = message.info?.agent + if (typeof agent === "string") { + return agent.toLowerCase() + } + } + } catch { + return null + } + + return null +} + export async function getLastAgentFromSession( sessionID: string, client?: SessionMessagesClient, @@ -75,44 +117,13 @@ export async function getLastAgentFromSession( } if (resolvedDeps.isSqliteBackend() && client) { - try { - const response = await client.session.messages({ path: { id: sessionID } }) - const messages = resolvedDeps.normalizeSDKResponse(response, [] as Array<{ - id?: string - info?: { agent?: string; time?: { created?: number } } - parts?: Array<{ type?: string }> - }>, { - preferResponseOnMissingData: true, - }).sort((left, right) => { - const leftTime = (left as { info?: { time?: { created?: number } } }).info?.time?.created ?? Number.NEGATIVE_INFINITY - const rightTime = (right as { info?: { time?: { created?: number } } }).info?.time?.created ?? Number.NEGATIVE_INFINITY - if (leftTime !== rightTime) { - return rightTime - leftTime - } - - const leftId = typeof left.id === "string" ? left.id : "" - const rightId = typeof right.id === "string" ? right.id : "" - return rightId.localeCompare(leftId) - }) - - for (const message of messages) { - if (resolvedDeps.isCompactionMessage(message)) { - continue - } - - const agent = message.info?.agent - if (typeof agent === "string") { - return agent.toLowerCase() - } - } - } catch { - return null - } - - return null + return getLastAgentFromSessionMessages(sessionID, client, resolvedDeps) } const messageDir = resolvedDeps.getMessageDir(sessionID) + if (!messageDir && client) { + return getLastAgentFromSessionMessages(sessionID, client, resolvedDeps) + } if (!messageDir) return null try { @@ -149,5 +160,9 @@ export async function getLastAgentFromSession( return null } + if (client) { + return getLastAgentFromSessionMessages(sessionID, client, resolvedDeps) + } + return null }