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 return false
} }
if (continuationState.hasActiveBackgroundTaskMarker) {
logWaiting(ctx, continuationState.activeHookMarkerReason ?? "background tasks are active")
return false
}
if (!await areAllChildrenIdle(ctx)) { if (!await areAllChildrenIdle(ctx)) {
return false return false
} }
+2
View File
@@ -16,6 +16,7 @@ export interface ContinuationState {
hasActiveRalphLoop: boolean hasActiveRalphLoop: boolean
hasHookMarker: boolean hasHookMarker: boolean
hasTodoHookMarker: boolean hasTodoHookMarker: boolean
hasActiveBackgroundTaskMarker: boolean
hasActiveHookMarker: boolean hasActiveHookMarker: boolean
activeHookMarkerReason: string | null activeHookMarkerReason: string | null
} }
@@ -32,6 +33,7 @@ export async function getContinuationState(
hasActiveRalphLoop: hasActiveRalphLoopContinuation(directory, sessionID), hasActiveRalphLoop: hasActiveRalphLoopContinuation(directory, sessionID),
hasHookMarker: marker !== null, hasHookMarker: marker !== null,
hasTodoHookMarker: marker?.sources.todo !== undefined, hasTodoHookMarker: marker?.sources.todo !== undefined,
hasActiveBackgroundTaskMarker: marker?.sources["background-task"]?.state === "active",
hasActiveHookMarker: isContinuationMarkerActive(marker), hasActiveHookMarker: isContinuationMarkerActive(marker),
activeHookMarkerReason: getActiveContinuationMarkerReason(marker), activeHookMarkerReason: getActiveContinuationMarkerReason(marker),
} }
+46
View File
@@ -59,6 +59,7 @@ import {
startAttempt, startAttempt,
} from "./attempt-lifecycle" } from "./attempt-lifecycle"
import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup" import { registerManagerForCleanup, unregisterManagerForCleanup } from "./process-cleanup"
import { setContinuationMarkerSource } from "../../features/run-continuation-state"
import { import {
findNearestMessageExcludingCompaction, findNearestMessageExcludingCompaction,
resolvePromptContextFromSessionMessages, resolvePromptContextFromSessionMessages,
@@ -449,6 +450,9 @@ export class BackgroundManager {
spawnReservation.commit() spawnReservation.commit()
this.markPreStartDescendantReservation(task) this.markPreStartDescendantReservation(task)
// Signal CLI run mode that background tasks are active
this.updateBackgroundTaskMarker(input.parentSessionID)
// Trigger processing (fire-and-forget) // Trigger processing (fire-and-forget)
void this.processKey(key) void this.processKey(key)
@@ -512,6 +516,9 @@ export class BackgroundManager {
await this.abortSessionWithLogging(item.task.sessionId, "startTask error cleanup") 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.markForNotification(item.task)
this.enqueueNotificationForParent(item.task.parentSessionId, () => this.notifyParentSession(item.task)).catch(err => { this.enqueueNotificationForParent(item.task.parentSessionId, () => this.notifyParentSession(item.task)).catch(err => {
log("[background-agent] Failed to notify on startTask error:", 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 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[] { getAllDescendantTasks(sessionID: string): BackgroundTask[] {
const result: BackgroundTask[] = [] const result: BackgroundTask[] = []
const directChildren = this.getTasksByParentSession(sessionID) 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) SessionCategoryRegistry.remove(task.sessionId)
} }
// Update continuation marker for CLI run mode
if (task.parentSessionID) {
this.updateBackgroundTaskMarker(task.parentSessionID)
}
this.markForNotification(task) this.markForNotification(task)
this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)).catch(err => { this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)).catch(err => {
log("[background-agent] Error in notifyParentSession for errored task:", { taskId: task.id, error: 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) removeTaskToastTracking(task.id)
// Update continuation marker for CLI run mode
if (task.parentSessionID) {
this.updateBackgroundTaskMarker(task.parentSessionID)
}
if (options?.skipNotification) { if (options?.skipNotification) {
this.cleanupPendingByParent(task) this.cleanupPendingByParent(task)
this.scheduleTaskRemoval(task.id) 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) SessionCategoryRegistry.remove(task.sessionId)
} }
// Update continuation marker for CLI run mode
if (task.parentSessionID) {
this.updateBackgroundTaskMarker(task.parentSessionID)
}
try { try {
await this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)) await this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task))
log(`[background-agent] Task completed via ${source}:`, task.id) 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) this.cleanupPendingByParent(task)
// Update continuation marker for CLI run mode
if (task.parentSessionID) {
this.updateBackgroundTaskMarker(task.parentSessionID)
}
this.markForNotification(task) this.markForNotification(task)
this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)).catch(err => { this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)).catch(err => {
log("[background-agent] Error in notifyParentSession for stale-pruned task:", { taskId: task.id, error: 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) SessionCategoryRegistry.remove(task.sessionId)
} }
// Update continuation marker for CLI run mode
if (task.parentSessionID) {
this.updateBackgroundTaskMarker(task.parentSessionID)
}
this.markForNotification(task) this.markForNotification(task)
this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)).catch(err => { this.enqueueNotificationForParent(task.parentSessionId, () => this.notifyParentSession(task)).catch(err => {
log("[background-agent] Error in notifyParentSession for crashed task:", { taskId: task.id, error: 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" export type ContinuationMarkerState = "idle" | "active" | "stopped"
@@ -79,7 +79,7 @@ export async function injectContinuation(args: {
} }
const hasRunningBgTasks = backgroundManager 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 : false
if (hasRunningBgTasks) { if (hasRunningBgTasks) {
@@ -71,7 +71,7 @@ export async function handleSessionIdle(args: {
} }
const hasRunningBgTasks = backgroundManager 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 : false
if (hasRunningBgTasks) { if (hasRunningBgTasks) {