feat(atlas): add canContinueTrackedBoulderSession for lineage-aware continuation
- Implement canContinueTrackedBoulderSession helper function
- Add lineage validation for appended descendant sessions
- Add agent matching logic for tracked sessions
- Add comprehensive tests for lineage continuation scenarios
- Add persisted lineage tests for boulder state tracking
🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
@@ -5,7 +5,9 @@ import {
|
||||
readBoulderState,
|
||||
readCurrentTopLevelTask,
|
||||
} from "../../features/boulder-state"
|
||||
import { getSessionAgent, isAgentRegistered, subagentSessions } from "../../features/claude-code-session-state"
|
||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { getLastAgentFromSession } from "./session-last-agent"
|
||||
import { isSessionInBoulderLineage } from "./boulder-session-lineage"
|
||||
import { getAgentConfigKey } from "../../shared/agent-display-names"
|
||||
import { log } from "../../shared/logger"
|
||||
import { injectBoulderContinuation } from "./boulder-continuation-injector"
|
||||
@@ -57,6 +59,25 @@ async function injectContinuation(input: {
|
||||
? getTaskSessionState(input.ctx.directory, currentTask.key)
|
||||
: null
|
||||
|
||||
if (!currentBoulder) {
|
||||
return
|
||||
}
|
||||
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
client: input.ctx.client,
|
||||
sessionID: input.sessionID,
|
||||
sessionOrigin: currentBoulder.session_origins?.[input.sessionID],
|
||||
boulderSessionIDs: currentBoulder.session_ids,
|
||||
requiredAgent: currentBoulder.agent,
|
||||
})
|
||||
if (!canContinueSession) {
|
||||
log(`[${HOOK_NAME}] Skipped: tracked descendant agent does not match boulder agent`, {
|
||||
sessionID: input.sessionID,
|
||||
requiredAgent: currentBoulder.agent ?? "atlas",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await injectBoulderContinuation({
|
||||
ctx: input.ctx,
|
||||
sessionID: input.sessionID,
|
||||
@@ -145,6 +166,14 @@ function scheduleRetry(input: {
|
||||
const currentProgress = getPlanProgress(currentBoulder.active_plan)
|
||||
if (currentProgress.isComplete) return
|
||||
if (options?.isContinuationStopped?.(sessionID)) return
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
client: ctx.client,
|
||||
sessionID,
|
||||
sessionOrigin: currentBoulder.session_origins?.[sessionID],
|
||||
boulderSessionIDs: currentBoulder.session_ids,
|
||||
requiredAgent: currentBoulder.agent,
|
||||
})
|
||||
if (!canContinueSession) return
|
||||
if (hasRunningBackgroundTasks(sessionID, options)) {
|
||||
scheduleRetry({ ctx, sessionID, sessionState, options })
|
||||
return
|
||||
@@ -196,29 +225,19 @@ export async function handleAtlasSessionIdle(input: {
|
||||
})
|
||||
}
|
||||
|
||||
if (subagentSessions.has(sessionID)) {
|
||||
const sessionAgent = getSessionAgent(sessionID)
|
||||
const agentKey = getAgentConfigKey(sessionAgent ?? "")
|
||||
const requiredAgentName = boulderState.agent ?? (isAgentRegistered("atlas") ? "atlas" : undefined)
|
||||
if (!requiredAgentName || !isAgentRegistered(requiredAgentName)) {
|
||||
log(`[${HOOK_NAME}] Skipped: boulder agent is unavailable for continuation`, {
|
||||
sessionID,
|
||||
requiredAgent: boulderState.agent ?? "unknown",
|
||||
})
|
||||
return
|
||||
}
|
||||
const requiredAgentKey = getAgentConfigKey(requiredAgentName)
|
||||
const agentMatches =
|
||||
agentKey === requiredAgentKey ||
|
||||
(requiredAgentKey === getAgentConfigKey("atlas") && agentKey === getAgentConfigKey("sisyphus"))
|
||||
if (!agentMatches) {
|
||||
log(`[${HOOK_NAME}] Skipped: subagent agent does not match boulder agent`, {
|
||||
sessionID,
|
||||
agent: sessionAgent ?? "unknown",
|
||||
requiredAgent: requiredAgentName,
|
||||
})
|
||||
return
|
||||
}
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
client: ctx.client,
|
||||
sessionID,
|
||||
sessionOrigin: boulderState.session_origins?.[sessionID],
|
||||
boulderSessionIDs: boulderState.session_ids,
|
||||
requiredAgent: boulderState.agent,
|
||||
})
|
||||
if (!canContinueSession) {
|
||||
log(`[${HOOK_NAME}] Skipped: tracked descendant agent does not match boulder agent`, {
|
||||
sessionID,
|
||||
requiredAgent: boulderState.agent ?? "atlas",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const sessionState = getState(sessionID)
|
||||
@@ -283,3 +302,40 @@ export async function handleAtlasSessionIdle(input: {
|
||||
worktreePath: boulderState.worktree_path,
|
||||
})
|
||||
}
|
||||
|
||||
async function canContinueTrackedBoulderSession(input: {
|
||||
client: PluginInput["client"]
|
||||
sessionID: string
|
||||
sessionOrigin?: "direct" | "appended"
|
||||
boulderSessionIDs: string[]
|
||||
requiredAgent?: string
|
||||
}): Promise<boolean> {
|
||||
const ancestorSessionIDs = input.boulderSessionIDs.filter((trackedSessionID) => trackedSessionID !== input.sessionID)
|
||||
if (ancestorSessionIDs.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isTrackedDescendant = await isSessionInBoulderLineage({
|
||||
client: input.client,
|
||||
sessionID: input.sessionID,
|
||||
boulderSessionIDs: ancestorSessionIDs,
|
||||
})
|
||||
if (input.sessionOrigin === "direct") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isTrackedDescendant) {
|
||||
return false
|
||||
}
|
||||
|
||||
const sessionAgent = await getLastAgentFromSession(input.sessionID, input.client)
|
||||
?? getSessionAgent(input.sessionID)
|
||||
if (!sessionAgent) {
|
||||
return false
|
||||
}
|
||||
|
||||
const requiredAgentKey = getAgentConfigKey(input.requiredAgent ?? "atlas")
|
||||
const sessionAgentKey = getAgentConfigKey(sessionAgent)
|
||||
return sessionAgentKey === requiredAgentKey
|
||||
|| (requiredAgentKey === getAgentConfigKey("atlas") && sessionAgentKey === getAgentConfigKey("sisyphus"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user