Merge pull request #3455 from CHLK/fix/cli-run-premature-exit-with-background-tasks

fix(cli-run): prevent premature exit when background tasks are active
This commit is contained in:
YeonGyu-Kim
2026-05-04 23:58:34 +09:00
committed by GitHub
6 changed files with 56 additions and 3 deletions
+5
View File
@@ -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
}
+2
View File
@@ -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),
}
+46
View File
@@ -59,6 +59,7 @@ import {
startAttempt,
} from "./attempt-lifecycle"
import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup"
import { setContinuationMarkerSource } from "../../features/run-continuation-state"
import {
findNearestMessageExcludingCompaction,
resolvePromptContextFromSessionMessages,
@@ -449,6 +450,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)
@@ -512,6 +516,9 @@ export class BackgroundManager {
await this.abortSessionWithLogging(item.task.sessionId, "startTask error cleanup")
}
// Update continuation marker for CLI run mode
this.updateBackgroundTaskMarker(item.task.parentSessionID)
this.markForNotification(item.task)
this.enqueueNotificationForParent(item.task.parentSessionId, () => this.notifyParentSession(item.task)).catch(err => {
log("[background-agent] Failed to notify on startTask error:", err)
@@ -814,6 +821,21 @@ The fallback retry session is now created and can be inspected directly.
return tasks
}
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)
@@ -1525,6 +1547,11 @@ The fallback retry session is now created and can be inspected directly.
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 errored task:", { taskId: task.id, error: err })
@@ -1824,6 +1851,11 @@ The task was re-queued on a fallback model after a retryable failure.
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)
@@ -1942,6 +1974,11 @@ The task was re-queued on a fallback model after a retryable failure.
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)
@@ -2178,6 +2215,10 @@ The task was re-queued on a fallback model after a retryable failure.
}
}
this.cleanupPendingByParent(task)
// 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 stale-pruned task:", { taskId: task.id, error: err })
@@ -2240,6 +2281,11 @@ The task was re-queued on a fallback model after a retryable failure.
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 -1
View File
@@ -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) {