fix(background-agent): preserve parent agent on retry wakes

Background fallback retry notifications were queued as bare internal user messages, so OpenCode could treat the notification as a new default-agent turn. Reuse the same parent prompt context resolver used by completion notifications for retrying and retry-ready wakes, and pin regression coverage for Hephaestus parent sessions plus missing-context fallbacks.
This commit is contained in:
YeonGyu-Kim
2026-05-18 14:05:01 +09:00
parent 3b50b7c8ba
commit d44cd1c1a0
3 changed files with 287 additions and 77 deletions
+81 -70
View File
@@ -818,6 +818,7 @@ export class BackgroundManager {
? `\n- Error: ${failedError}`
: ""
const retryModel = formatAttemptModelSummary(boundAttempt) ?? task.retryNotification.nextModel
const parentPromptContext = await this.resolveParentWakePromptContext(task)
this.queuePendingParentWake(
task.parentSessionId,
`<system-reminder>
@@ -830,7 +831,7 @@ export class BackgroundManager {
The fallback retry session is now created and can be inspected directly.
</system-reminder>`,
{},
parentPromptContext,
false,
PENDING_PARENT_WAKE_DEBOUNCE_MS,
)
@@ -1920,6 +1921,7 @@ The fallback retry session is now created and can be inspected directly.
source: string,
): Promise<boolean> {
const previousSessionID = task.sessionId
let retryingNotification: string | undefined
const result = tryFallbackRetry({
task,
errorInfo,
@@ -1938,22 +1940,26 @@ The fallback retry session is now created and can be inspected directly.
const failedModelLine = failedModel ? `\n- Failed model: \`${failedModel}\`` : ""
const failedErrorLine = previousAttempt?.error ? `\n- Error: ${previousAttempt.error}` : ""
const nextModel = formatAttemptModelSummary(currentAttempt)
this.queuePendingParentWake(
task.parentSessionId,
`<system-reminder>
retryingNotification = `<system-reminder>
[BACKGROUND TASK RETRYING]
**ID:** \`${task.id}\`
**Description:** ${task.description}${sourceText}${failedSessionLine}${failedModelLine}${failedErrorLine}${nextModel ? `\n- Next model: \`${nextModel}\`` : ""}
The task was re-queued on a fallback model after a retryable failure.
</system-reminder>`,
{},
false,
PENDING_PARENT_WAKE_DEBOUNCE_MS,
)
</system-reminder>`
},
})
const retried = await result
if (retried && retryingNotification) {
const parentPromptContext = await this.resolveParentWakePromptContext(task)
this.queuePendingParentWake(
task.parentSessionId,
retryingNotification,
parentPromptContext,
false,
PENDING_PARENT_WAKE_DEBOUNCE_MS,
)
}
if (retried && previousSessionID) {
this.clearSessionOutputObserved(previousSessionID)
this.clearSessionTodoObservation(previousSessionID)
@@ -2412,76 +2418,18 @@ The task was re-queued on a fallback model after a retryable failure.
completedTasks,
})
let agent: string | undefined = task.parentAgent
let model: { providerID: string; modelID: string } | undefined
let tools: Record<string, boolean> | undefined = task.parentTools
let promptContext: ReturnType<typeof resolvePromptContextFromSessionMessages> = null
if (this.enableParentSessionNotifications) {
try {
const messagesResp = await messagesInDirectory(this.client, {
path: { id: task.parentSessionId },
}, this.directory)
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
info?: {
agent?: string
model?: { providerID: string; modelID: string }
modelID?: string
providerID?: string
tools?: Record<string, boolean | "allow" | "deny" | "ask">
}
}>)
promptContext = resolvePromptContextFromSessionMessages(
messages,
task.parentSessionId,
)
const normalizedTools = isRecord(promptContext?.tools)
? normalizePromptTools(promptContext.tools)
: undefined
if (promptContext?.agent || promptContext?.model || normalizedTools) {
agent = promptContext?.agent ?? task.parentAgent
model = promptContext?.model?.providerID && promptContext.model.modelID
? { providerID: promptContext.model.providerID, modelID: promptContext.model.modelID }
: undefined
tools = normalizedTools ?? tools
}
} catch (error) {
if (isAbortedSessionError(error)) {
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
taskId: task.id,
parentSessionID: task.parentSessionId,
})
}
const messageDir = join(MESSAGE_STORAGE, task.parentSessionId)
const currentMessage = messageDir
? findNearestMessageExcludingCompaction(messageDir, task.parentSessionId)
: null
agent = currentMessage?.agent ?? task.parentAgent
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
: undefined
tools = normalizePromptTools(currentMessage?.tools) ?? tools
}
const resolvedTools = resolveInheritedPromptTools(task.parentSessionId, tools)
const parentPromptContext = await this.resolveParentWakePromptContext(task)
log("[background-agent] notifyParentSession context:", {
taskId: task.id,
resolvedAgent: agent,
resolvedModel: model,
resolvedAgent: parentPromptContext.agent,
resolvedModel: parentPromptContext.model,
})
const isTaskFailure = task.status === "error" || task.status === "cancelled" || task.status === "interrupt"
const shouldReply = allComplete || isTaskFailure
const variant = promptContext?.model?.variant
const parentPromptContext: ParentWakePromptContext = {
...(agent !== undefined ? { agent } : {}),
...(model !== undefined ? { model } : {}),
...(variant !== undefined ? { variant } : {}),
...(resolvedTools ? { tools: resolvedTools } : {}),
}
const shouldDeferNotification = await this.isSessionActive(task.parentSessionId)
if (shouldDeferNotification) {
@@ -2519,6 +2467,69 @@ The task was re-queued on a fallback model after a retryable failure.
}
}
private async resolveParentWakePromptContext(task: BackgroundTask): Promise<ParentWakePromptContext> {
let agent: string | undefined = task.parentAgent
let model: { providerID: string; modelID: string } | undefined
let tools: Record<string, boolean> | undefined = task.parentTools
let variant: string | undefined
try {
const messagesResp = await messagesInDirectory(this.client, {
path: { id: task.parentSessionId },
}, this.directory)
const messages = normalizeSDKResponse(messagesResp, [] as Array<{
info?: {
agent?: string
model?: { providerID: string; modelID: string; variant?: string }
modelID?: string
providerID?: string
tools?: Record<string, boolean | "allow" | "deny" | "ask">
}
}>)
const promptContext = resolvePromptContextFromSessionMessages(
messages,
task.parentSessionId,
)
const normalizedTools = isRecord(promptContext?.tools)
? normalizePromptTools(promptContext.tools)
: undefined
if (promptContext?.agent || promptContext?.model || normalizedTools) {
agent = promptContext?.agent ?? task.parentAgent
model = promptContext?.model?.providerID && promptContext.model.modelID
? { providerID: promptContext.model.providerID, modelID: promptContext.model.modelID }
: undefined
variant = promptContext?.model?.variant
tools = normalizedTools ?? tools
}
} catch (error) {
if (isAbortedSessionError(error)) {
log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", {
taskId: task.id,
parentSessionID: task.parentSessionId,
})
}
const messageDir = join(MESSAGE_STORAGE, task.parentSessionId)
const currentMessage = messageDir
? findNearestMessageExcludingCompaction(messageDir, task.parentSessionId)
: null
agent = currentMessage?.agent ?? task.parentAgent
model = currentMessage?.model?.providerID && currentMessage?.model?.modelID
? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID }
: undefined
variant = currentMessage?.model?.variant
tools = normalizePromptTools(currentMessage?.tools) ?? tools
}
const resolvedTools = resolveInheritedPromptTools(task.parentSessionId, tools)
return {
...(agent !== undefined ? { agent } : {}),
...(model !== undefined ? { model } : {}),
...(variant !== undefined ? { variant } : {}),
...(resolvedTools ? { tools: resolvedTools } : {}),
}
}
private async isSessionActive(sessionID: string): Promise<boolean> {
return isOpenCodeSessionActive(this.client, sessionID)
}