fix: skip assistant/user events in buildPartialRunError to avoid raw JSON blobs in error messages (FAR-32)
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 <noreply@paperclip.ing>
This commit is contained in:
@@ -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");
|
||||
|
||||
+12
-4
@@ -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<string, unknown>).type === "system") return false;
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
const t = (obj as Record<string, unknown>).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})` : "";
|
||||
|
||||
Reference in New Issue
Block a user