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
@@ -0,0 +1,55 @@
import type { FallbackEntry } from "./model-requirements"
import type { ModelFallbackControllerAccessor } from "../hooks/model-fallback"
import { createInternalAgentTextPart } from "./internal-initiator-marker"
import { SessionCategoryRegistry } from "./session-category-registry"
export type DelegatedChildSessionRetryPart = {
type: "text"
text: string
}
export type DelegatedChildSessionBootstrap = {
retryParts: DelegatedChildSessionRetryPart[]
}
const delegatedChildSessionBootstrapMap = new Map<string, DelegatedChildSessionBootstrap>()
export function createDelegatedChildSessionRetryParts(promptText: string): DelegatedChildSessionRetryPart[] {
return [createInternalAgentTextPart(promptText)]
}
export function registerDelegatedChildSessionBootstrap(args: {
sessionID: string
promptText: string
fallbackChain?: FallbackEntry[]
category?: string
modelFallbackControllerAccessor?: ModelFallbackControllerAccessor
}): void {
delegatedChildSessionBootstrapMap.set(args.sessionID, {
retryParts: createDelegatedChildSessionRetryParts(args.promptText),
})
args.modelFallbackControllerAccessor?.setSessionFallbackChain(args.sessionID, args.fallbackChain)
if (args.category) {
SessionCategoryRegistry.register(args.sessionID, args.category)
}
}
export function getDelegatedChildSessionBootstrap(sessionID: string): DelegatedChildSessionBootstrap | undefined {
const bootstrap = delegatedChildSessionBootstrapMap.get(sessionID)
if (!bootstrap) {
return undefined
}
return {
retryParts: bootstrap.retryParts.map((part) => ({ ...part })),
}
}
export function clearDelegatedChildSessionBootstrap(sessionID: string): void {
delegatedChildSessionBootstrapMap.delete(sessionID)
}
export function clearAllDelegatedChildSessionBootstrap(): void {
delegatedChildSessionBootstrapMap.clear()
}