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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user