fix(run): preserve active boulder continuations

This commit is contained in:
YeonGyu-Kim
2026-05-31 03:57:13 +09:00
parent 2684d08ec9
commit baf4aeeae9
2 changed files with 68 additions and 48 deletions
+18 -13
View File
@@ -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 { getSessionAgent } from "../../features/claude-code-session-state"
import { import {
getActiveContinuationMarkerReason, getActiveContinuationMarkerReason,
@@ -51,19 +51,25 @@ async function hasActiveBoulderContinuation(
if (progress.isComplete) return false if (progress.isComplete) return false
if (!client) return false if (!client) return false
const isTrackedSession = boulder.session_ids.includes(sessionID) const normalizedSessionID = normalizeSessionId(sessionID)
const sessionOrigin = boulder.session_origins?.[sessionID] const normalizedTrackedSessionIDs = boulder.session_ids.map((trackedSessionID) => normalizeSessionId(trackedSessionID))
if (!isTrackedSession) { if (!normalizedTrackedSessionIDs.includes(normalizedSessionID)) {
return false return false
} }
const isTrackedDescendant = await isTrackedDescendantSession(client, sessionID, boulder.session_ids) const sessionOrigin = boulder.session_origins?.[sessionID] ?? boulder.session_origins?.[normalizedSessionID]
if (sessionOrigin === "direct") {
if (isTrackedSession && sessionOrigin === "direct") {
return true 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 return false
} }
@@ -82,23 +88,22 @@ async function hasActiveBoulderContinuation(
return false return false
} }
return isTrackedSession || isTrackedDescendant return true
} }
async function isTrackedDescendantSession( async function isTrackedDescendantSession(
client: RunContext["client"], client: RunContext["client"],
sessionID: string, sessionID: string,
trackedSessionIDs: string[], trackedAncestorSessionIDs: string[],
): Promise<boolean> { ): Promise<boolean> {
const ancestorSessionIDs = trackedSessionIDs.filter((trackedSessionID) => trackedSessionID !== sessionID) if (trackedAncestorSessionIDs.length === 0) {
if (ancestorSessionIDs.length === 0) {
return false return false
} }
return isSessionInBoulderLineage({ return isSessionInBoulderLineage({
client, client,
sessionID, sessionID,
boulderSessionIDs: ancestorSessionIDs, boulderSessionIDs: trackedAncestorSessionIDs,
}) })
} }
+50 -35
View File
@@ -64,6 +64,48 @@ function getLastAgentFromMessageDir(messageDir: string): string | null {
return null return null
} }
async function getLastAgentFromSessionMessages(
sessionID: string,
client: SessionMessagesClient,
deps: SessionLastAgentDeps,
): Promise<string | null> {
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( export async function getLastAgentFromSession(
sessionID: string, sessionID: string,
client?: SessionMessagesClient, client?: SessionMessagesClient,
@@ -75,44 +117,13 @@ export async function getLastAgentFromSession(
} }
if (resolvedDeps.isSqliteBackend() && client) { if (resolvedDeps.isSqliteBackend() && client) {
try { return getLastAgentFromSessionMessages(sessionID, client, resolvedDeps)
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
} }
const messageDir = resolvedDeps.getMessageDir(sessionID) const messageDir = resolvedDeps.getMessageDir(sessionID)
if (!messageDir && client) {
return getLastAgentFromSessionMessages(sessionID, client, resolvedDeps)
}
if (!messageDir) return null if (!messageDir) return null
try { try {
@@ -149,5 +160,9 @@ export async function getLastAgentFromSession(
return null return null
} }
if (client) {
return getLastAgentFromSessionMessages(sessionID, client, resolvedDeps)
}
return null return null
} }