fix(delegate-task): harden child-session fallback bootstrap and cleanup
Capture delegated child-session retry context before the first prompt so fallback recovery still works when session history is empty. Align background and sync launch paths around the same bootstrap contract, clear session-scoped fallback state on every terminal path, and lock the behavior with regression coverage for first-prompt retries, exhaustion, isolation, and cleanup. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -87,6 +87,10 @@ import {
|
||||
resolveSubagentSpawnContext,
|
||||
type SubagentSpawnContext,
|
||||
} from "./subagent-spawn-limits"
|
||||
import {
|
||||
clearDelegatedChildSessionBootstrap,
|
||||
registerDelegatedChildSessionBootstrap,
|
||||
} from "../../shared/delegated-child-session-bootstrap"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
@@ -249,6 +253,12 @@ export class BackgroundManager {
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupDelegatedSessionContext(sessionID: string): void {
|
||||
clearDelegatedChildSessionBootstrap(sessionID)
|
||||
this.modelFallbackControllerAccessor?.clearSessionFallbackChain(sessionID)
|
||||
SessionCategoryRegistry.remove(sessionID)
|
||||
}
|
||||
|
||||
async assertCanSpawn(parentSessionID: string): Promise<SubagentSpawnContext> {
|
||||
const spawnContext = await resolveSubagentSpawnContext(this.client, parentSessionID, this.directory)
|
||||
const maxDepth = getMaxSubagentDepth(this.config)
|
||||
@@ -618,6 +628,14 @@ export class BackgroundManager {
|
||||
return
|
||||
}
|
||||
|
||||
registerDelegatedChildSessionBootstrap({
|
||||
sessionID,
|
||||
promptText: input.prompt,
|
||||
fallbackChain: input.fallbackChain,
|
||||
category: input.category,
|
||||
modelFallbackControllerAccessor: this.modelFallbackControllerAccessor,
|
||||
})
|
||||
|
||||
task.progress = {
|
||||
toolCalls: 0,
|
||||
lastUpdate: new Date(),
|
||||
@@ -778,6 +796,7 @@ The fallback retry session is now created and can be inspected directly.
|
||||
// Abort the session to prevent infinite polling hang
|
||||
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
|
||||
await this.abortSessionWithLogging(sessionID, "launch error cleanup")
|
||||
this.cleanupDelegatedSessionContext(sessionID)
|
||||
|
||||
this.markForNotification(existingTask)
|
||||
this.enqueueNotificationForParent(existingTask.parentSessionId, () => this.notifyParentSession(existingTask)).catch(err => {
|
||||
@@ -1417,7 +1436,7 @@ The fallback retry session is now created and can be inspected directly.
|
||||
}
|
||||
|
||||
this.rootDescendantCounts.delete(sessionID)
|
||||
SessionCategoryRegistry.remove(sessionID)
|
||||
this.cleanupDelegatedSessionContext(sessionID)
|
||||
}
|
||||
|
||||
if (event.type === "session.status") {
|
||||
@@ -1521,7 +1540,7 @@ The fallback retry session is now created and can be inspected directly.
|
||||
}
|
||||
this.scheduleTaskRemoval(task.id)
|
||||
if (task.sessionId) {
|
||||
SessionCategoryRegistry.remove(task.sessionId)
|
||||
this.cleanupDelegatedSessionContext(task.sessionId)
|
||||
}
|
||||
|
||||
this.markForNotification(task)
|
||||
@@ -1571,6 +1590,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
||||
this.clearSessionOutputObserved(previousSessionID)
|
||||
this.clearSessionTodoObservation(previousSessionID)
|
||||
subagentSessions.delete(previousSessionID)
|
||||
this.cleanupDelegatedSessionContext(previousSessionID)
|
||||
}
|
||||
return retried
|
||||
})
|
||||
@@ -1743,7 +1763,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
||||
this.clearTaskHistoryWhenParentTasksGone(task.parentSessionId)
|
||||
if (task.sessionId) {
|
||||
subagentSessions.delete(task.sessionId)
|
||||
SessionCategoryRegistry.remove(task.sessionId)
|
||||
this.cleanupDelegatedSessionContext(task.sessionId)
|
||||
}
|
||||
log("[background-agent] Removed completed task from memory:", taskId)
|
||||
}, TASK_CLEANUP_DELAY_MS)
|
||||
@@ -1818,7 +1838,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
||||
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
|
||||
await this.abortSessionWithLogging(task.sessionId, `task cancellation (${source})`)
|
||||
|
||||
SessionCategoryRegistry.remove(task.sessionId)
|
||||
this.cleanupDelegatedSessionContext(task.sessionId)
|
||||
}
|
||||
|
||||
removeTaskToastTracking(task.id)
|
||||
@@ -1938,7 +1958,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
||||
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
|
||||
await this.abortSessionWithLogging(task.sessionId, `task completion (${source})`)
|
||||
|
||||
SessionCategoryRegistry.remove(task.sessionId)
|
||||
this.cleanupDelegatedSessionContext(task.sessionId)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2236,7 +2256,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
||||
removeTaskToastTracking(task.id)
|
||||
this.scheduleTaskRemoval(task.id)
|
||||
if (task.sessionId) {
|
||||
SessionCategoryRegistry.remove(task.sessionId)
|
||||
this.cleanupDelegatedSessionContext(task.sessionId)
|
||||
}
|
||||
|
||||
this.markForNotification(task)
|
||||
@@ -2414,7 +2434,7 @@ The task was re-queued on a fallback model after a retryable failure.
|
||||
|
||||
for (const sessionID of trackedSessionIDs) {
|
||||
subagentSessions.delete(sessionID)
|
||||
SessionCategoryRegistry.remove(sessionID)
|
||||
this.cleanupDelegatedSessionContext(sessionID)
|
||||
}
|
||||
|
||||
this.concurrencyManager.clear()
|
||||
|
||||
Reference in New Issue
Block a user