From 59865836413d3c8d19bd0c23503d00006d977a53 Mon Sep 17 00:00:00 2001 From: Casey Howard Date: Fri, 20 Mar 2026 12:51:21 -0400 Subject: [PATCH] fix(background-agent): decrement spawn budget on task completion, cancellation, error, and interrupt rootDescendantCounts was incremented on every spawn but never decremented when tasks reached terminal states (completed, cancelled, error, interrupt, stale-pruned). This made maxDescendants=50 a session-lifetime quota instead of its intended semantics as a concurrent-active agent cap. Fix: add unregisterRootDescendant() in five terminal-state handlers: - tryCompleteTask(): task completes successfully - cancelTask(): running task cancelled (wasRunning guard prevents double-decrement for pending tasks already handled by rollbackPreStartDescendantReservation) - session.error handler: task errors - promptAsync catch (startTask): task interrupted on launch - promptAsync catch (resume): task interrupted on resume - onTaskPruned callback: stale task pruned (wasPending guard) Fixes: code-yeongyu/oh-my-openagent#2700 --- src/features/background-agent/manager.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index c4ea7528b..4de7298d6 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -543,6 +543,9 @@ export class BackgroundManager { existingTask.error = errorMessage } existingTask.completedAt = new Date() + if (existingTask.rootSessionID) { + this.unregisterRootDescendant(existingTask.rootSessionID) + } if (existingTask.concurrencyKey) { this.concurrencyManager.release(existingTask.concurrencyKey) existingTask.concurrencyKey = undefined @@ -813,6 +816,9 @@ export class BackgroundManager { const errorMessage = error instanceof Error ? error.message : String(error) existingTask.error = errorMessage existingTask.completedAt = new Date() + if (existingTask.rootSessionID) { + this.unregisterRootDescendant(existingTask.rootSessionID) + } // Release concurrency on error to prevent slot leaks if (existingTask.concurrencyKey) { @@ -1009,6 +1015,9 @@ export class BackgroundManager { task.status = "error" task.error = errorMsg task.completedAt = new Date() + if (task.rootSessionID) { + this.unregisterRootDescendant(task.rootSessionID) + } this.taskHistory.record(task.parentSessionID, { id: task.id, sessionID: task.sessionID, agent: task.agent, description: task.description, status: "error", category: task.category, startedAt: task.startedAt, completedAt: task.completedAt }) if (task.concurrencyKey) { @@ -1341,8 +1350,12 @@ export class BackgroundManager { log("[background-agent] Cancelled pending task:", { taskId, key }) } + const wasRunning = task.status === "running" task.status = "cancelled" task.completedAt = new Date() + if (wasRunning && task.rootSessionID) { + this.unregisterRootDescendant(task.rootSessionID) + } if (reason) { task.error = reason } @@ -1463,6 +1476,10 @@ export class BackgroundManager { task.completedAt = new Date() this.taskHistory.record(task.parentSessionID, { id: task.id, sessionID: task.sessionID, agent: task.agent, description: task.description, status: "completed", category: task.category, startedAt: task.startedAt, completedAt: task.completedAt }) + if (task.rootSessionID) { + this.unregisterRootDescendant(task.rootSessionID) + } + removeTaskToastTracking(task.id) // Release concurrency BEFORE any async operations to prevent slot leaks @@ -1701,6 +1718,9 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea task.status = "error" task.error = errorMessage task.completedAt = new Date() + if (!wasPending && task.rootSessionID) { + this.unregisterRootDescendant(task.rootSessionID) + } this.taskHistory.record(task.parentSessionID, { id: task.id, sessionID: task.sessionID, agent: task.agent, description: task.description, status: "error", category: task.category, startedAt: task.startedAt, completedAt: task.completedAt }) if (task.concurrencyKey) { this.concurrencyManager.release(task.concurrencyKey)