From 050502f8d040ab5192b118bd438edd0d51357694 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sat, 4 Apr 2026 21:57:37 -0700 Subject: [PATCH 1/4] fix(background-agent): retry with fallback agent on Agent not found error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a model/mode switch happens while a background task is in-flight, the oh-my-openagent agent registry can be rebuilt without custom agents (e.g., Sisyphus-Junior). The SDK then rejects the promptAsync call with "Agent not found", killing the task. This adds retry logic: when promptAsync fails with "Agent not found", retry with the "general" agent (always available in opencode). The original prompt, model, and skill content are preserved — only the agent routing changes. Fixes both the spawner (startTask/resumeTask) and manager (inline launch) code paths. Also improves the error message detection in manager.ts to recognize "Agent not found" alongside the existing "agent.name"/"undefined" checks. Related: #2052, #2875, #2882 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/features/background-agent/manager.ts | 59 ++++-- src/features/background-agent/spawner.test.ts | 178 ++++++++++++++++++ src/features/background-agent/spawner.ts | 101 +++++++--- 3 files changed, 293 insertions(+), 45 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index b66659abf..c4c86b23c 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1,5 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" +import { isAgentNotFoundError } from "./spawner" import type { BackgroundTask, LaunchInput, @@ -546,32 +547,54 @@ export class BackgroundManager { applySessionPromptParams(sessionID, input.model) } + const FALLBACK_AGENT = "general" + + const promptBody = { + agent: input.agent, + ...(launchModel ? { model: launchModel } : {}), + ...(launchVariant ? { variant: launchVariant } : {}), + system: input.skillContent, + tools: (() => { + const tools = { + task: false, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(input.agent), + } + setSessionTools(sessionID, tools) + return tools + })(), + parts: [createInternalAgentTextPart(input.prompt)], + } + promptWithModelSuggestionRetry(this.client, { path: { id: sessionID }, - body: { - agent: input.agent, - ...(launchModel ? { model: launchModel } : {}), - ...(launchVariant ? { variant: launchVariant } : {}), - system: input.skillContent, - tools: (() => { - const tools = { - task: false, - call_omo_agent: true, - question: false, - ...getAgentToolRestrictions(input.agent), - } - setSessionTools(sessionID, tools) - return tools - })(), - parts: [createInternalAgentTextPart(input.prompt)], - }, + body: promptBody, }).catch(async (error) => { + // Retry with fallback agent if the original agent was unregistered (e.g., after a model switch) + if (isAgentNotFoundError(error) && input.agent !== FALLBACK_AGENT) { + log("[background-agent] Agent not found, retrying with fallback agent", { + original: input.agent, + fallback: FALLBACK_AGENT, + taskId: task.id, + }) + try { + await promptWithModelSuggestionRetry(this.client, { + path: { id: sessionID }, + body: { ...promptBody, agent: FALLBACK_AGENT }, + }) + return + } catch (retryError) { + log("[background-agent] Fallback agent also failed:", retryError) + } + } + log("[background-agent] promptAsync error:", error) const existingTask = this.findBySession(sessionID) if (existingTask) { existingTask.status = "interrupt" const errorMessage = error instanceof Error ? error.message : String(error) - if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) { + if (errorMessage.includes("agent.name") || errorMessage.includes("undefined") || isAgentNotFoundError(error)) { existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.` } else { existingTask.error = errorMessage diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index f223aa300..214e15e4f 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -6,6 +6,184 @@ import { getSessionPromptParams, } from "../../shared/session-prompt-params-state" +describe("background-agent spawner agent-not-found fallback", () => { + afterEach(() => { + clearSessionPromptParams("session-fallback") + }) + + test("retries with 'general' agent when promptAsync fails with Agent not found", async () => { + //#given + const promptCalls: any[] = [] + let callCount = 0 + + const client = { + session: { + get: async () => ({ data: { directory: "/tmp/test" } }), + create: async () => ({ data: { id: "session-fallback" } }), + promptAsync: async (args: any) => { + callCount++ + promptCalls.push({ body: { ...args.body }, path: { ...args.path } }) + if (callCount === 1) { + throw new Error('Agent not found: "Sisyphus-Junior". Available agents: build, explore, general, plan') + } + return { data: {} } + }, + }, + } as any + + const onTaskError = mock(() => {}) + + const task = createTask({ + description: "Implement feature", + prompt: "Please implement the break-even analysis", + agent: "Sisyphus-Junior", + parentSessionID: "ses_parent", + parentMessageID: "msg_parent", + }) + + const item = { + task, + input: { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + parentModel: task.parentModel, + parentAgent: task.parentAgent, + model: task.model, + }, + } + + const ctx = { + client, + directory: "/tmp/test", + concurrencyManager: { release: () => {} }, + tmuxEnabled: false, + onTaskError, + } + + //#when + await startTask(item as any, ctx as any) + + // Wait for the fire-and-forget prompt chain to settle + await new Promise(resolve => setTimeout(resolve, 50)) + + //#then + // Should have called promptAsync twice: once with original agent, once with fallback + expect(promptCalls).toHaveLength(2) + expect(promptCalls[0].body.agent).toBe("Sisyphus-Junior") + expect(promptCalls[1].body.agent).toBe("general") + // Original prompt content preserved in fallback + expect(promptCalls[1].body.parts).toEqual(promptCalls[0].body.parts) + // Task should not have errored + expect(onTaskError).not.toHaveBeenCalled() + }) + + test("does not retry for non-agent-not-found errors", async () => { + //#given + const promptCalls: any[] = [] + + const client = { + session: { + get: async () => ({ data: { directory: "/tmp/test" } }), + create: async () => ({ data: { id: "session-fallback" } }), + promptAsync: async (args: any) => { + promptCalls.push(args) + throw new Error("Connection timeout") + }, + }, + } as any + + const onTaskError = mock(() => {}) + + const task = createTask({ + description: "Implement feature", + prompt: "Do work", + agent: "Sisyphus-Junior", + parentSessionID: "ses_parent", + parentMessageID: "msg_parent", + }) + + const item = { + task, + input: { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + }, + } + + const ctx = { + client, + directory: "/tmp/test", + concurrencyManager: { release: () => {} }, + tmuxEnabled: false, + onTaskError, + } + + //#when + await startTask(item as any, ctx as any) + await new Promise(resolve => setTimeout(resolve, 50)) + + //#then + // Only one attempt — no retry for non-agent errors + expect(promptCalls).toHaveLength(1) + expect(onTaskError).toHaveBeenCalled() + }) + + test("calls onTaskError if fallback agent also fails", async () => { + //#given + const client = { + session: { + get: async () => ({ data: { directory: "/tmp/test" } }), + create: async () => ({ data: { id: "session-fallback" } }), + promptAsync: async () => { + throw new Error('Agent not found: "Sisyphus-Junior". Available agents: build, explore, general, plan') + }, + }, + } as any + + const onTaskError = mock(() => {}) + + const task = createTask({ + description: "Implement feature", + prompt: "Do work", + agent: "Sisyphus-Junior", + parentSessionID: "ses_parent", + parentMessageID: "msg_parent", + }) + + const item = { + task, + input: { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + }, + } + + const ctx = { + client, + directory: "/tmp/test", + concurrencyManager: { release: () => {} }, + tmuxEnabled: false, + onTaskError, + } + + //#when + await startTask(item as any, ctx as any) + await new Promise(resolve => setTimeout(resolve, 50)) + + //#then + expect(onTaskError).toHaveBeenCalled() + }) +}) + describe("background-agent spawner fallback model promotion", () => { afterEach(() => { clearSessionPromptParams("session-123") diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index e8fc49e32..c1e3ccf7e 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -8,6 +8,13 @@ import { getTaskToastManager } from "../task-toast-manager" import { isInsideTmux } from "../../shared/tmux" import type { ConcurrencyManager } from "./concurrency" +const FALLBACK_AGENT = "general" + +export function isAgentNotFoundError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error) + return message.includes("Agent not found") +} + export interface SpawnerContext { client: OpencodeClient directory: string @@ -138,22 +145,42 @@ export async function startTask( applySessionPromptParams(sessionID, input.model) + const promptBody = { + agent: input.agent, + ...(launchModel ? { model: launchModel } : {}), + ...(launchVariant ? { variant: launchVariant } : {}), + system: input.skillContent, + tools: { + task: false, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(input.agent), + }, + parts: [createInternalAgentTextPart(input.prompt)], + } + promptWithModelSuggestionRetry(client, { path: { id: sessionID }, - body: { - agent: input.agent, - ...(launchModel ? { model: launchModel } : {}), - ...(launchVariant ? { variant: launchVariant } : {}), - system: input.skillContent, - tools: { - task: false, - call_omo_agent: true, - question: false, - ...getAgentToolRestrictions(input.agent), - }, - parts: [createInternalAgentTextPart(input.prompt)], - }, - }).catch((error) => { + body: promptBody, + }).catch(async (error) => { + if (isAgentNotFoundError(error) && input.agent !== FALLBACK_AGENT) { + log("[background-agent] Agent not found, retrying with fallback agent", { + original: input.agent, + fallback: FALLBACK_AGENT, + taskId: task.id, + }) + try { + await promptWithModelSuggestionRetry(client, { + path: { id: sessionID }, + body: { ...promptBody, agent: FALLBACK_AGENT }, + }) + return + } catch (retryError) { + log("[background-agent] Fallback agent also failed:", retryError) + onTaskError(task, retryError instanceof Error ? retryError : new Error(String(retryError))) + return + } + } log("[background-agent] promptAsync error:", error) onTaskError(task, error instanceof Error ? error : new Error(String(error))) }) @@ -228,21 +255,41 @@ export async function resumeTask( applySessionPromptParams(task.sessionID, task.model) + const resumeBody = { + agent: task.agent, + ...(resumeModel ? { model: resumeModel } : {}), + ...(resumeVariant ? { variant: resumeVariant } : {}), + tools: { + task: false, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(task.agent), + }, + parts: [createInternalAgentTextPart(input.prompt)], + } + client.session.promptAsync({ path: { id: task.sessionID }, - body: { - agent: task.agent, - ...(resumeModel ? { model: resumeModel } : {}), - ...(resumeVariant ? { variant: resumeVariant } : {}), - tools: { - task: false, - call_omo_agent: true, - question: false, - ...getAgentToolRestrictions(task.agent), - }, - parts: [createInternalAgentTextPart(input.prompt)], - }, - }).catch((error) => { + body: resumeBody, + }).catch(async (error) => { + if (isAgentNotFoundError(error) && task.agent !== FALLBACK_AGENT) { + log("[background-agent] Resume agent not found, retrying with fallback agent", { + original: task.agent, + fallback: FALLBACK_AGENT, + taskId: task.id, + }) + try { + await client.session.promptAsync({ + path: { id: task.sessionID! }, + body: { ...resumeBody, agent: FALLBACK_AGENT }, + }) + return + } catch (retryError) { + log("[background-agent] Resume fallback agent also failed:", retryError) + onTaskError(task, retryError instanceof Error ? retryError : new Error(String(retryError))) + return + } + } log("[background-agent] resume prompt error:", error) onTaskError(task, error instanceof Error ? error : new Error(String(error))) }) From 9470cbe090ab90b62f3a6fc86cc59643ed0522eb Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sat, 4 Apr 2026 22:13:05 -0700 Subject: [PATCH 2/4] fix: address systems review findings for agent-not-found fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/features/background-agent/manager.ts | 19 +++++++++++--- src/features/background-agent/spawner.test.ts | 8 ++++++ src/features/background-agent/spawner.ts | 26 ++++++++++++++++--- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index c4c86b23c..2c58dda06 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1,6 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { isAgentNotFoundError } from "./spawner" +import { isAgentNotFoundError, FALLBACK_AGENT, buildFallbackBody } from "./spawner" import type { BackgroundTask, LaunchInput, @@ -547,8 +547,6 @@ export class BackgroundManager { applySessionPromptParams(sessionID, input.model) } - const FALLBACK_AGENT = "general" - const promptBody = { agent: input.agent, ...(launchModel ? { model: launchModel } : {}), @@ -579,10 +577,13 @@ export class BackgroundManager { taskId: task.id, }) try { + const fallbackBody = buildFallbackBody(promptBody, FALLBACK_AGENT) + setSessionTools(sessionID, fallbackBody.tools as Record) await promptWithModelSuggestionRetry(this.client, { path: { id: sessionID }, - body: { ...promptBody, agent: FALLBACK_AGENT }, + body: fallbackBody, }) + task.agent = FALLBACK_AGENT return } catch (retryError) { log("[background-agent] Fallback agent also failed:", retryError) @@ -1225,6 +1226,16 @@ export class BackgroundManager { }): Promise { 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")) { return } diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index 214e15e4f..34a70873c 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -76,6 +76,14 @@ describe("background-agent spawner agent-not-found fallback", () => { expect(promptCalls[1].body.agent).toBe("general") // Original prompt content preserved in fallback 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 expect(onTaskError).not.toHaveBeenCalled() }) diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index c1e3ccf7e..c412e7c3a 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -8,13 +8,29 @@ import { getTaskToastManager } from "../task-toast-manager" import { isInsideTmux } from "../../shared/tmux" import type { ConcurrencyManager } from "./concurrency" -const FALLBACK_AGENT = "general" +export const FALLBACK_AGENT = "general" export function isAgentNotFoundError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error) return message.includes("Agent not found") } +export function buildFallbackBody( + originalBody: Record, + fallbackAgent: string, +): Record { + return { + ...originalBody, + agent: fallbackAgent, + tools: { + task: false, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(fallbackAgent), + }, + } +} + export interface SpawnerContext { client: OpencodeClient directory: string @@ -172,8 +188,9 @@ export async function startTask( try { await promptWithModelSuggestionRetry(client, { path: { id: sessionID }, - body: { ...promptBody, agent: FALLBACK_AGENT }, + body: buildFallbackBody(promptBody, FALLBACK_AGENT), }) + task.agent = FALLBACK_AGENT return } catch (retryError) { log("[background-agent] Fallback agent also failed:", retryError) @@ -279,10 +296,11 @@ export async function resumeTask( taskId: task.id, }) try { - await client.session.promptAsync({ + await promptWithModelSuggestionRetry(client, { path: { id: task.sessionID! }, - body: { ...resumeBody, agent: FALLBACK_AGENT }, + body: buildFallbackBody(resumeBody, FALLBACK_AGENT), }) + task.agent = FALLBACK_AGENT return } catch (retryError) { log("[background-agent] Resume fallback agent also failed:", retryError) From 51508c4949f105e071a13692b06297ce61ffc12c Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sat, 4 Apr 2026 22:17:22 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20cubic=20review=20=E2=80=94?= =?UTF-8?q?=20broaden=20error=20detection,=20add=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. isAgentNotFoundError now handles: - Plain objects with .message field (not just Error instances) - "agent.name"/"undefined" error variants from SDK validation - The original "Agent not found" format 2. New tests: - agent.name/undefined error variant triggers fallback - Plain object errors with .message field trigger fallback - "fallback also fails" test now verifies retry was attempted (callCount=2) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/features/background-agent/spawner.test.ts | 129 ++++++++++++++++++ src/features/background-agent/spawner.ts | 15 +- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index 34a70873c..d3896e55f 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -144,11 +144,13 @@ describe("background-agent spawner agent-not-found fallback", () => { test("calls onTaskError if fallback agent also fails", async () => { //#given + let callCount = 0 const client = { session: { get: async () => ({ data: { directory: "/tmp/test" } }), create: async () => ({ data: { id: "session-fallback" } }), promptAsync: async () => { + callCount++ throw new Error('Agent not found: "Sisyphus-Junior". Available agents: build, explore, general, plan') }, }, @@ -188,8 +190,135 @@ describe("background-agent spawner agent-not-found fallback", () => { await new Promise(resolve => setTimeout(resolve, 50)) //#then + // Verify retry was attempted (2 calls: original + fallback) + expect(callCount).toBe(2) expect(onTaskError).toHaveBeenCalled() }) + + test("retries on agent.name/undefined error variant", async () => { + //#given + const promptCalls: any[] = [] + let callCount = 0 + + const client = { + session: { + get: async () => ({ data: { directory: "/tmp/test" } }), + create: async () => ({ data: { id: "session-fallback" } }), + promptAsync: async (args: any) => { + callCount++ + promptCalls.push({ body: { ...args.body } }) + if (callCount === 1) { + throw new Error("Cannot read properties of undefined (reading 'agent.name')") + } + return { data: {} } + }, + }, + } as any + + const onTaskError = mock(() => {}) + + const task = createTask({ + description: "Test task", + prompt: "Do work", + agent: "Sisyphus-Junior", + parentSessionID: "ses_parent", + parentMessageID: "msg_parent", + }) + + const item = { + task, + input: { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + parentModel: task.parentModel, + parentAgent: task.parentAgent, + model: task.model, + }, + } + + const ctx = { + client, + directory: "/tmp/test", + concurrencyManager: { release: () => {} }, + tmuxEnabled: false, + onTaskError, + } + + //#when + await startTask(item as any, ctx as any) + await new Promise(resolve => setTimeout(resolve, 50)) + + //#then + expect(promptCalls).toHaveLength(2) + expect(promptCalls[0].body.agent).toBe("Sisyphus-Junior") + expect(promptCalls[1].body.agent).toBe("general") + expect(onTaskError).not.toHaveBeenCalled() + }) + + test("detects agent error from plain object with message field", async () => { + //#given + const promptCalls: any[] = [] + let callCount = 0 + + const client = { + session: { + get: async () => ({ data: { directory: "/tmp/test" } }), + create: async () => ({ data: { id: "session-fallback" } }), + promptAsync: async (args: any) => { + callCount++ + promptCalls.push({ body: { ...args.body } }) + if (callCount === 1) { + throw { message: 'Agent not found: "Custom-Agent"', name: "UnknownError" } + } + return { data: {} } + }, + }, + } as any + + const onTaskError = mock(() => {}) + + const task = createTask({ + description: "Test task", + prompt: "Do work", + agent: "Custom-Agent", + parentSessionID: "ses_parent", + parentMessageID: "msg_parent", + }) + + const item = { + task, + input: { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + parentModel: task.parentModel, + parentAgent: task.parentAgent, + model: task.model, + }, + } + + const ctx = { + client, + directory: "/tmp/test", + concurrencyManager: { release: () => {} }, + tmuxEnabled: false, + onTaskError, + } + + //#when + await startTask(item as any, ctx as any) + await new Promise(resolve => setTimeout(resolve, 50)) + + //#then + expect(promptCalls).toHaveLength(2) + expect(promptCalls[1].body.agent).toBe("general") + expect(onTaskError).not.toHaveBeenCalled() + }) }) describe("background-agent spawner fallback model promotion", () => { diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index c412e7c3a..1ae9f078d 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -11,8 +11,19 @@ import type { ConcurrencyManager } from "./concurrency" export const FALLBACK_AGENT = "general" export function isAgentNotFoundError(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error) - return message.includes("Agent not found") + const message = + typeof error === "string" + ? error + : error instanceof Error + ? error.message + : typeof error === "object" && error !== null && typeof (error as { message?: unknown }).message === "string" + ? (error as { message: string }).message + : String(error) + return ( + message.includes("Agent not found") || + message.includes("agent.name") || + (message.includes("agent") && message.includes("undefined")) + ) } export function buildFallbackBody( From f8d086ded1bbd71300cc676326ef15512b824a09 Mon Sep 17 00:00:00 2001 From: Jim Park Date: Sat, 4 Apr 2026 22:31:22 -0700 Subject: [PATCH 4/4] fix: remove overly broad agent+undefined error pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The (message.includes("agent") && message.includes("undefined")) pattern could match unrelated errors like "The agent returned undefined for the configuration", triggering a false fallback that hides the real failure. The two precise patterns are sufficient: - "Agent not found" — canonical SDK validation error - "agent.name" — property access error on undefined agent config Co-Authored-By: Claude Opus 4.6 (1M context) --- src/features/background-agent/spawner.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index 1ae9f078d..3c2fd7e73 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -21,8 +21,7 @@ export function isAgentNotFoundError(error: unknown): boolean { : String(error) return ( message.includes("Agent not found") || - message.includes("agent.name") || - (message.includes("agent") && message.includes("undefined")) + message.includes("agent.name") ) }