fix(cli-run): prevent premature exit when background tasks are active
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
This commit is contained in:
@@ -20,6 +20,11 @@ export async function checkCompletionConditions(ctx: RunContext): Promise<boolea
|
||||
return false
|
||||
}
|
||||
|
||||
if (continuationState.hasActiveBackgroundTaskMarker) {
|
||||
logWaiting(ctx, continuationState.activeHookMarkerReason ?? "background tasks are active")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!await areAllChildrenIdle(ctx)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ContinuationState {
|
||||
hasActiveRalphLoop: boolean
|
||||
hasHookMarker: boolean
|
||||
hasTodoHookMarker: boolean
|
||||
hasActiveBackgroundTaskMarker: boolean
|
||||
hasActiveHookMarker: boolean
|
||||
activeHookMarkerReason: string | null
|
||||
}
|
||||
@@ -32,6 +33,7 @@ export async function getContinuationState(
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "./error-classifier"
|
||||
import { tryFallbackRetry } from "./fallback-retry-handler"
|
||||
import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup"
|
||||
import { setContinuationMarkerSource } from "../../features/run-continuation-state"
|
||||
import {
|
||||
findNearestMessageExcludingCompaction,
|
||||
resolvePromptContextFromSessionMessages,
|
||||
@@ -370,6 +371,9 @@ export class BackgroundManager {
|
||||
spawnReservation.commit()
|
||||
this.markPreStartDescendantReservation(task)
|
||||
|
||||
// Signal CLI run mode that background tasks are active
|
||||
this.updateBackgroundTaskMarker(input.parentSessionID)
|
||||
|
||||
// Trigger processing (fire-and-forget)
|
||||
void this.processKey(key)
|
||||
|
||||
@@ -657,6 +661,21 @@ export class BackgroundManager {
|
||||
return result
|
||||
}
|
||||
|
||||
private updateBackgroundTaskMarker(parentSessionID: string): void {
|
||||
const tasks = this.getTasksByParentSession(parentSessionID)
|
||||
const activeTasks = tasks.filter(t => t.status === "running" || t.status === "pending")
|
||||
if (activeTasks.length > 0) {
|
||||
setContinuationMarkerSource(
|
||||
this.directory, parentSessionID, "background-task", "active",
|
||||
`${activeTasks.length} background task(s) active`,
|
||||
)
|
||||
} else {
|
||||
setContinuationMarkerSource(
|
||||
this.directory, parentSessionID, "background-task", "idle",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
getAllDescendantTasks(sessionID: string): BackgroundTask[] {
|
||||
const result: BackgroundTask[] = []
|
||||
const directChildren = this.getTasksByParentSession(sessionID)
|
||||
@@ -1585,6 +1604,11 @@ export class BackgroundManager {
|
||||
|
||||
removeTaskToastTracking(task.id)
|
||||
|
||||
// Update continuation marker for CLI run mode
|
||||
if (task.parentSessionID) {
|
||||
this.updateBackgroundTaskMarker(task.parentSessionID)
|
||||
}
|
||||
|
||||
if (options?.skipNotification) {
|
||||
this.cleanupPendingByParent(task)
|
||||
this.scheduleTaskRemoval(task.id)
|
||||
@@ -1700,6 +1724,11 @@ export class BackgroundManager {
|
||||
SessionCategoryRegistry.remove(task.sessionID)
|
||||
}
|
||||
|
||||
// Update continuation marker for CLI run mode
|
||||
if (task.parentSessionID) {
|
||||
this.updateBackgroundTaskMarker(task.parentSessionID)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task))
|
||||
log(`[background-agent] Task completed via ${source}:`, task.id)
|
||||
@@ -1993,6 +2022,11 @@ export class BackgroundManager {
|
||||
SessionCategoryRegistry.remove(task.sessionID)
|
||||
}
|
||||
|
||||
// Update continuation marker for CLI run mode
|
||||
if (task.parentSessionID) {
|
||||
this.updateBackgroundTaskMarker(task.parentSessionID)
|
||||
}
|
||||
|
||||
this.markForNotification(task)
|
||||
this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task)).catch(err => {
|
||||
log("[background-agent] Error in notifyParentSession for crashed task:", { taskId: task.id, error: err })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ContinuationMarkerSource = "todo" | "stop"
|
||||
export type ContinuationMarkerSource = "todo" | "stop" | "background-task"
|
||||
|
||||
export type ContinuationMarkerState = "idle" | "active" | "stopped"
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function injectContinuation(args: {
|
||||
}
|
||||
|
||||
const hasRunningBgTasks = backgroundManager
|
||||
? backgroundManager.getTasksByParentSession(sessionID).some((task: { status: string }) => task.status === "running")
|
||||
? backgroundManager.getTasksByParentSession(sessionID).some((task: { status: string }) => task.status === "running" || task.status === "pending")
|
||||
: false
|
||||
|
||||
if (hasRunningBgTasks) {
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function handleSessionIdle(args: {
|
||||
}
|
||||
|
||||
const hasRunningBgTasks = backgroundManager
|
||||
? backgroundManager.getTasksByParentSession(sessionID).some((task: { status: string }) => task.status === "running")
|
||||
? backgroundManager.getTasksByParentSession(sessionID).some((task: { status: string }) => task.status === "running" || task.status === "pending")
|
||||
: false
|
||||
|
||||
if (hasRunningBgTasks) {
|
||||
|
||||
Reference in New Issue
Block a user