aa90cff9cc
When using `opencode run`, the process exits prematurely if the main agent dispatches background subtasks. The CLI completion checker relies on `session.children()` + `session.status()` APIs which cannot see tasks in "pending" state (no session created yet) or tasks whose sessions are momentarily idle between operations. This fix bridges BackgroundManager state to the CLI completion checker using the existing run-continuation-state marker system: - Add "background-task" continuation marker source - BackgroundManager writes/clears markers on task lifecycle events (launch, cancel, complete, crash) - CLI completion checker blocks exit when marker is active - Fix todo-continuation-enforcer to also check "pending" task status Closes #3452
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import { getPlanProgress, readBoulderState } from "../../features/boulder-state"
|
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
|
import {
|
|
getActiveContinuationMarkerReason,
|
|
isContinuationMarkerActive,
|
|
readContinuationMarker,
|
|
} from "../../features/run-continuation-state"
|
|
import { isSessionInBoulderLineage } from "../../hooks/atlas/boulder-session-lineage"
|
|
import { getLastAgentFromSession } from "../../hooks/atlas/session-last-agent"
|
|
import { getAgentConfigKey } from "../../shared/agent-display-names"
|
|
import { readState as readRalphLoopState } from "../../hooks/ralph-loop/storage"
|
|
import type { RunContext } from "./types"
|
|
|
|
export interface ContinuationState {
|
|
hasActiveBoulder: boolean
|
|
hasActiveRalphLoop: boolean
|
|
hasHookMarker: boolean
|
|
hasTodoHookMarker: boolean
|
|
hasActiveBackgroundTaskMarker: boolean
|
|
hasActiveHookMarker: boolean
|
|
activeHookMarkerReason: string | null
|
|
}
|
|
|
|
export async function getContinuationState(
|
|
directory: string,
|
|
sessionID: string,
|
|
client?: RunContext["client"],
|
|
): Promise<ContinuationState> {
|
|
const marker = readContinuationMarker(directory, sessionID)
|
|
|
|
return {
|
|
hasActiveBoulder: await hasActiveBoulderContinuation(directory, sessionID, client),
|
|
hasActiveRalphLoop: hasActiveRalphLoopContinuation(directory, sessionID),
|
|
hasHookMarker: marker !== null,
|
|
hasTodoHookMarker: marker?.sources.todo !== undefined,
|
|
hasActiveBackgroundTaskMarker: marker?.sources["background-task"]?.state === "active",
|
|
hasActiveHookMarker: isContinuationMarkerActive(marker),
|
|
activeHookMarkerReason: getActiveContinuationMarkerReason(marker),
|
|
}
|
|
}
|
|
|
|
async function hasActiveBoulderContinuation(
|
|
directory: string,
|
|
sessionID: string,
|
|
client?: RunContext["client"],
|
|
): Promise<boolean> {
|
|
const boulder = readBoulderState(directory)
|
|
if (!boulder) return false
|
|
|
|
const progress = getPlanProgress(boulder.active_plan)
|
|
if (progress.isComplete) return false
|
|
if (!client) return false
|
|
|
|
const isTrackedSession = boulder.session_ids.includes(sessionID)
|
|
const sessionOrigin = boulder.session_origins?.[sessionID]
|
|
if (!isTrackedSession) {
|
|
return false
|
|
}
|
|
|
|
const isTrackedDescendant = await isTrackedDescendantSession(client, sessionID, boulder.session_ids)
|
|
|
|
if (isTrackedSession && sessionOrigin === "direct") {
|
|
return true
|
|
}
|
|
|
|
if (isTrackedSession && sessionOrigin !== "direct" && !isTrackedDescendant) {
|
|
return false
|
|
}
|
|
|
|
const sessionAgent = await getLastAgentFromSession(sessionID, client)
|
|
?? getSessionAgent(sessionID)
|
|
if (!sessionAgent) {
|
|
return false
|
|
}
|
|
|
|
const requiredAgentKey = getAgentConfigKey(boulder.agent ?? "atlas")
|
|
const sessionAgentKey = getAgentConfigKey(sessionAgent)
|
|
if (
|
|
sessionAgentKey !== requiredAgentKey
|
|
&& !(requiredAgentKey === getAgentConfigKey("atlas") && sessionAgentKey === getAgentConfigKey("sisyphus"))
|
|
) {
|
|
return false
|
|
}
|
|
|
|
return isTrackedSession || isTrackedDescendant
|
|
}
|
|
|
|
async function isTrackedDescendantSession(
|
|
client: RunContext["client"],
|
|
sessionID: string,
|
|
trackedSessionIDs: string[],
|
|
): Promise<boolean> {
|
|
const ancestorSessionIDs = trackedSessionIDs.filter((trackedSessionID) => trackedSessionID !== sessionID)
|
|
if (ancestorSessionIDs.length === 0) {
|
|
return false
|
|
}
|
|
|
|
return isSessionInBoulderLineage({
|
|
client,
|
|
sessionID,
|
|
boulderSessionIDs: ancestorSessionIDs,
|
|
})
|
|
}
|
|
|
|
function hasActiveRalphLoopContinuation(directory: string, sessionID: string): boolean {
|
|
const state = readRalphLoopState(directory)
|
|
if (!state || !state.active) return false
|
|
|
|
if (state.session_id && state.session_id !== sessionID) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|