diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index 6086cc3e..a2656061 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -55,6 +55,7 @@ import { isClaudeTransientUpstreamError, isClaudeUnknownSessionError, isClaudePoisonedPreviousMessageIdError, + isClaudeImageProcessingError, } from "./parse.js"; import { prepareClaudeConfigSeed, resolveSharedClaudeConfigDir } from "./claude-config.js"; import { resolveClaudeDesiredSkillNames } from "./skills.js"; @@ -983,6 +984,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise { @@ -185,6 +186,50 @@ describe("isClaudeUnknownSessionError", () => { }); }); +describe("isClaudeImageProcessingError", () => { + it("detects the 'Could not process image' 400 error in the result field", () => { + expect( + isClaudeImageProcessingError({ + subtype: "success", + is_error: true, + result: "API Error: 400 Could not process image: image source URL has expired", + }), + ).toBe(true); + }); + + it("detects the error in the errors array", () => { + expect( + isClaudeImageProcessingError({ + is_error: true, + result: "", + errors: [{ message: "400 Could not process image" }], + }), + ).toBe(true); + }); + + it("is case-insensitive", () => { + expect( + isClaudeImageProcessingError({ + is_error: true, + result: "could not process image attached to message", + }), + ).toBe(true); + }); + + it("returns false for unrelated errors", () => { + expect( + isClaudeImageProcessingError({ + is_error: true, + result: "No conversation found with session id abc-123", + }), + ).toBe(false); + }); + + it("returns false for empty parsed result", () => { + expect(isClaudeImageProcessingError({})).toBe(false); + }); +}); + describe("extractClaudeRetryNotBefore", () => { it("parses the 'resets 4pm' hint in its explicit timezone", () => { const now = new Date("2026-04-22T15:15:00.000Z"); diff --git a/packages/adapters/claude-local/src/server/parse.ts b/packages/adapters/claude-local/src/server/parse.ts index 36d13fd6..fbf66f79 100644 --- a/packages/adapters/claude-local/src/server/parse.ts +++ b/packages/adapters/claude-local/src/server/parse.ts @@ -209,6 +209,17 @@ export function isClaudePoisonedPreviousMessageIdError(parsed: Record): boolean { + const resultText = asString(parsed.result, "").trim(); + const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)] + .map((msg) => msg.trim()) + .filter(Boolean); + + return allMessages.some((msg) => + /could not process image/i.test(msg), + ); +} + function buildClaudeTransientHaystack(input: { parsed?: Record | null; stdout?: string | null; @@ -388,7 +399,7 @@ export function isClaudeTransientUpstreamError(input: { }): boolean { const parsed = input.parsed ?? null; // Deterministic failures are handled by their own classifiers. - if (parsed && (isClaudeMaxTurnsResult(parsed) || isClaudeUnknownSessionError(parsed) || isClaudePoisonedPreviousMessageIdError(parsed))) { + if (parsed && (isClaudeMaxTurnsResult(parsed) || isClaudeUnknownSessionError(parsed) || isClaudePoisonedPreviousMessageIdError(parsed) || isClaudeImageProcessingError(parsed))) { return false; } const loginMeta = detectClaudeLoginRequired({