From f9ff04a35438374d0bb462157a995f91da23926a Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 24 Apr 2026 17:11:20 +0000 Subject: [PATCH] fix: skip assistant/user events in buildPartialRunError to avoid raw JSON blobs in error messages (FAR-32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a model produces assistant events with output_tokens=0 but no result event (e.g. MiniMax-M2.7 thinking-only output), the partial-run error previously surfaced the raw assistant JSON blob verbatim, producing an unreadable message like "Claude exited with code -1: {\"type\":\"assistant\",...}". Fix: extend the content-line filter in buildPartialRunError to also skip assistant and user event types (intermediate streaming events), in addition to system events. result events are still retained since they may carry useful terminal error details. When all stdout lines are filtered, the existing initOnlyOutput branch triggers and surfaces a clean diagnostic: "Claude started but did not produce a result (model: MiniMax-M2.7) — check API credentials, model support, and adapter config". Co-Authored-By: Paperclip --- src/server/execute.test.ts | 33 +++++++++++++++++++++++++++++++++ src/server/execute.ts | 16 ++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/server/execute.test.ts b/src/server/execute.test.ts index bca0a60..d28b87a 100644 --- a/src/server/execute.test.ts +++ b/src/server/execute.test.ts @@ -178,6 +178,39 @@ describe("buildPartialRunError", () => { expect(msg).toContain("code 2"); }); + it("skips assistant events and surfaces model hint (FAR-32: MiniMax-M2.7 output_tokens=0)", () => { + // Reproduces the exact failure: init event + assistant event with only a + // thinking block and output_tokens=0, no result event. The assistant JSON + // blob must not be surfaced verbatim as the error message. + const assistantEvent = JSON.stringify({ + type: "assistant", + message: { + id: "063ad6038e4c889faa7c95168e007d73", + type: "message", + role: "assistant", + content: [{ type: "thinking", thinking: "Let me start…", signature: "abc123" }], + model: "MiniMax-M2.7", + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 11013, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }, + }, + }); + const stdout = [initLine, assistantEvent].join("\n"); + const msg = buildPartialRunError(null, "MiniMax-M2.7", stdout); + expect(msg).toContain("MiniMax-M2.7"); + expect(msg).toContain("did not produce a result"); + expect(msg).not.toContain("063ad6038e4c889faa7c95168e007d73"); + expect(msg).not.toContain("output_tokens"); + expect(msg).not.toContain("thinking"); + }); + + it("skips user events alongside system events", () => { + const userEvent = JSON.stringify({ type: "user", message: { role: "user", content: [] } }); + const stdout = [initLine, userEvent, "Error: API quota exceeded"].join("\n"); + const msg = buildPartialRunError(1, "claude-sonnet-4-6", stdout); + expect(msg).toBe("Claude exited with code 1: Error: API quota exceeded"); + }); + it("null exitCode renders as -1 in message", () => { const msg = buildPartialRunError(null, "", "Some plain error text"); expect(msg).toBe("Claude exited with code -1: Some plain error text"); diff --git a/src/server/execute.ts b/src/server/execute.ts index ea2e6f1..f8609c5 100644 --- a/src/server/execute.ts +++ b/src/server/execute.ts @@ -117,22 +117,30 @@ export function buildPartialRunError( ): string { if (exitCode === 0) return "Failed to parse Claude JSON output"; - // Walk stdout lines, skip system events, return the first real content line. + // Walk stdout lines, skip system and intermediate streaming events, return + // the first human-readable content line. assistant/user events are + // intermediate and contain raw JSON blobs that make poor error messages; + // result events are retained because they may carry useful error details + // (e.g. rate-limit messages). const firstContentLine = stdout.split(/\r?\n/) .map((l) => l.trim()) .find((l) => { if (!l) return false; try { const obj = JSON.parse(l); - if (typeof obj === "object" && obj !== null && (obj as Record).type === "system") return false; + if (typeof obj === "object" && obj !== null) { + const t = (obj as Record).type; + if (t === "system" || t === "assistant" || t === "user") return false; + } } catch { // not JSON — treat as content } return true; }) ?? ""; - // If we only have system/init events and nothing else, surface the model - // name so the operator can diagnose missing credentials or unsupported model. + // If the stream contains only system/init and intermediate events with no + // plain-text or result output, surface the model name so the operator can + // diagnose missing credentials or unsupported model. const initOnlyOutput = stdout.trim() !== "" && model !== "" && !firstContentLine; if (initOnlyOutput) { const modelHint = model ? ` (model: ${model})` : "";