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})` : "";