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:
tw-yshuang
2026-05-07 08:34:52 +08:00
parent 6c51eac8bf
commit fac90d69f8
10 changed files with 573 additions and 102 deletions
+18 -5
View File
@@ -14,6 +14,11 @@ import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-
import { resolveMetadataModel } from "./resolve-metadata-model"
import { shouldRetryError } from "../../shared/model-error-classifier"
import type { ModelFallbackState } from "../../hooks/model-fallback/hook"
import { buildTaskPrompt } from "./prompt-builder"
import {
clearDelegatedChildSessionBootstrap,
registerDelegatedChildSessionBootstrap,
} from "../../shared/delegated-child-session-bootstrap"
export async function executeSyncTask(
args: DelegateTaskArgs,
@@ -36,6 +41,9 @@ export async function executeSyncTask(
| undefined
try {
const tddEnabled = executorCtx.sisyphusAgentConfig?.tdd
const delegatedPromptText = buildTaskPrompt(args.prompt, agentToUse, tddEnabled)
if (typeof manager?.reserveSubagentSpawn === "function") {
spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID)
}
@@ -80,11 +88,13 @@ export async function executeSyncTask(
subagentSessions.add(newSessionID)
syncSubagentSessions.add(newSessionID)
setSessionAgent(newSessionID, agentToUse)
executorCtx.modelFallbackControllerAccessor?.setSessionFallbackChain(newSessionID, fallbackChain)
if (args.category) {
SessionCategoryRegistry.register(newSessionID, args.category)
}
registerDelegatedChildSessionBootstrap({
sessionID: newSessionID,
promptText: delegatedPromptText,
fallbackChain,
category: args.category,
modelFallbackControllerAccessor: executorCtx.modelFallbackControllerAccessor,
})
if (onSyncSessionCreated) {
log("[task] Invoking onSyncSessionCreated callback", { sessionID: newSessionID, parentID: parentContext.sessionID })
@@ -150,6 +160,7 @@ export async function executeSyncTask(
sessionID,
agentToUse,
args,
promptText: delegatedPromptText,
systemContent,
toastManager,
taskId,
@@ -171,6 +182,7 @@ export async function executeSyncTask(
const cleanupRetrySession = (currentSessionID: string): void => {
subagentSessions.delete(currentSessionID)
syncSubagentSessions.delete(currentSessionID)
clearDelegatedChildSessionBootstrap(currentSessionID)
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(currentSessionID)
SessionCategoryRegistry.remove(currentSessionID)
}
@@ -308,6 +320,7 @@ ${buildTaskMetadataBlock({
if (syncSessionID) {
subagentSessions.delete(syncSessionID)
syncSubagentSessions.delete(syncSessionID)
clearDelegatedChildSessionBootstrap(syncSessionID)
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(syncSessionID)
SessionCategoryRegistry.remove(syncSessionID)
}