fix: address systems review findings for agent-not-found fallback
1. [HIGH] Tool restrictions recomputed for fallback agent via buildFallbackBody() — no longer inherits original agent's restrictions. 2. [HIGH] Double-retry race prevented — handleSessionErrorEvent now returns early for agent-not-found errors, since the prompt catch block already handles them with agent fallback. This prevents tryFallbackRetry from racing with a model-level retry on the same error (the "not found" pattern in RETRYABLE_MESSAGE_PATTERNS). 3. [MEDIUM] task.agent updated to FALLBACK_AGENT after successful fallback — notifications, toast, and logging reflect actual agent. 4. [MEDIUM] FALLBACK_AGENT exported from spawner.ts and imported into manager.ts — single source of truth. 5. [LOW] resumeTask fallback now uses promptWithModelSuggestionRetry (consistent with startTask), getting timeout protection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { isAgentNotFoundError } from "./spawner"
|
import { isAgentNotFoundError, FALLBACK_AGENT, buildFallbackBody } from "./spawner"
|
||||||
import type {
|
import type {
|
||||||
BackgroundTask,
|
BackgroundTask,
|
||||||
LaunchInput,
|
LaunchInput,
|
||||||
@@ -547,8 +547,6 @@ export class BackgroundManager {
|
|||||||
applySessionPromptParams(sessionID, input.model)
|
applySessionPromptParams(sessionID, input.model)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK_AGENT = "general"
|
|
||||||
|
|
||||||
const promptBody = {
|
const promptBody = {
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
...(launchModel ? { model: launchModel } : {}),
|
...(launchModel ? { model: launchModel } : {}),
|
||||||
@@ -579,10 +577,13 @@ export class BackgroundManager {
|
|||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
const fallbackBody = buildFallbackBody(promptBody, FALLBACK_AGENT)
|
||||||
|
setSessionTools(sessionID, fallbackBody.tools as Record<string, boolean>)
|
||||||
await promptWithModelSuggestionRetry(this.client, {
|
await promptWithModelSuggestionRetry(this.client, {
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: { ...promptBody, agent: FALLBACK_AGENT },
|
body: fallbackBody,
|
||||||
})
|
})
|
||||||
|
task.agent = FALLBACK_AGENT
|
||||||
return
|
return
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
log("[background-agent] Fallback agent also failed:", retryError)
|
log("[background-agent] Fallback agent also failed:", retryError)
|
||||||
@@ -1225,6 +1226,16 @@ export class BackgroundManager {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { task, errorInfo, errorMessage, errorName } = args
|
const { task, errorInfo, errorMessage, errorName } = args
|
||||||
|
|
||||||
|
// Agent-not-found errors are handled by the prompt catch block with agent fallback.
|
||||||
|
// Do not also trigger model fallback retry — that would race with the agent retry.
|
||||||
|
if (isAgentNotFoundError({ message: errorInfo.message } as Error)) {
|
||||||
|
log("[background-agent] Skipping session.error fallback for agent-not-found (handled by prompt catch)", {
|
||||||
|
taskId: task.id,
|
||||||
|
errorMessage: errorInfo.message?.slice(0, 100),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (await this.tryFallbackRetry(task, errorInfo, "session.error")) {
|
if (await this.tryFallbackRetry(task, errorInfo, "session.error")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ describe("background-agent spawner agent-not-found fallback", () => {
|
|||||||
expect(promptCalls[1].body.agent).toBe("general")
|
expect(promptCalls[1].body.agent).toBe("general")
|
||||||
// Original prompt content preserved in fallback
|
// Original prompt content preserved in fallback
|
||||||
expect(promptCalls[1].body.parts).toEqual(promptCalls[0].body.parts)
|
expect(promptCalls[1].body.parts).toEqual(promptCalls[0].body.parts)
|
||||||
|
// Tool restrictions recomputed for fallback agent (general has no restrictions)
|
||||||
|
expect(promptCalls[1].body.tools).toEqual({
|
||||||
|
task: false,
|
||||||
|
call_omo_agent: true,
|
||||||
|
question: false,
|
||||||
|
})
|
||||||
|
// Task agent identity updated to reflect fallback
|
||||||
|
expect(task.agent).toBe("general")
|
||||||
// Task should not have errored
|
// Task should not have errored
|
||||||
expect(onTaskError).not.toHaveBeenCalled()
|
expect(onTaskError).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,13 +8,29 @@ import { getTaskToastManager } from "../task-toast-manager"
|
|||||||
import { isInsideTmux } from "../../shared/tmux"
|
import { isInsideTmux } from "../../shared/tmux"
|
||||||
import type { ConcurrencyManager } from "./concurrency"
|
import type { ConcurrencyManager } from "./concurrency"
|
||||||
|
|
||||||
const FALLBACK_AGENT = "general"
|
export const FALLBACK_AGENT = "general"
|
||||||
|
|
||||||
export function isAgentNotFoundError(error: unknown): boolean {
|
export function isAgentNotFoundError(error: unknown): boolean {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
return message.includes("Agent not found")
|
return message.includes("Agent not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildFallbackBody(
|
||||||
|
originalBody: Record<string, unknown>,
|
||||||
|
fallbackAgent: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...originalBody,
|
||||||
|
agent: fallbackAgent,
|
||||||
|
tools: {
|
||||||
|
task: false,
|
||||||
|
call_omo_agent: true,
|
||||||
|
question: false,
|
||||||
|
...getAgentToolRestrictions(fallbackAgent),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpawnerContext {
|
export interface SpawnerContext {
|
||||||
client: OpencodeClient
|
client: OpencodeClient
|
||||||
directory: string
|
directory: string
|
||||||
@@ -172,8 +188,9 @@ export async function startTask(
|
|||||||
try {
|
try {
|
||||||
await promptWithModelSuggestionRetry(client, {
|
await promptWithModelSuggestionRetry(client, {
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: { ...promptBody, agent: FALLBACK_AGENT },
|
body: buildFallbackBody(promptBody, FALLBACK_AGENT),
|
||||||
})
|
})
|
||||||
|
task.agent = FALLBACK_AGENT
|
||||||
return
|
return
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
log("[background-agent] Fallback agent also failed:", retryError)
|
log("[background-agent] Fallback agent also failed:", retryError)
|
||||||
@@ -279,10 +296,11 @@ export async function resumeTask(
|
|||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await client.session.promptAsync({
|
await promptWithModelSuggestionRetry(client, {
|
||||||
path: { id: task.sessionID! },
|
path: { id: task.sessionID! },
|
||||||
body: { ...resumeBody, agent: FALLBACK_AGENT },
|
body: buildFallbackBody(resumeBody, FALLBACK_AGENT),
|
||||||
})
|
})
|
||||||
|
task.agent = FALLBACK_AGENT
|
||||||
return
|
return
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
log("[background-agent] Resume fallback agent also failed:", retryError)
|
log("[background-agent] Resume fallback agent also failed:", retryError)
|
||||||
|
|||||||
Reference in New Issue
Block a user