From 78da33594bcbaf66de7477933f49bb49d1affb1c Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Tue, 3 Nov 2026 14:32:11 -0500 Subject: [PATCH 1/2] fix(server): failed Claude text generation shows the CLI's error list, not raw JSON Claude's error result subtypes, such as error_max_structured_output_retries, report through an `errors` array and have no `result` field, so they were not recognised as error results. The failure then read as the whole result JSON, or as whatever notice a wrapper wrote to stderr. Co-Authored-By: Claude Opus 5.5 --- .../ClaudeTextGeneration.test.ts | 38 +++++++++++++++++++ .../textGeneration/ClaudeTextGeneration.ts | 10 ++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index 197d412d93405..eb1fdb8713ae3 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -452,7 +452,24 @@ it.layer(layerClaudeTextGenerationTest)("ClaudeTextGeneration", (it) => { const apiError = "API Error: 400 Model blocked by guardrail: 4 endpoints excluded"; const wrapperNotice = "Using the OpenRouter credential from the global credential file."; + const structuredOutputError = "Failed to provide valid structured output after 5 attempts"; + const structuredOutputFailure = { + type: "result", + subtype: "error_max_structured_output_retries", + is_error: true, + errors: [structuredOutputError], + }; it.effect.each([ + [ + "the errors list of a result with no result text", + JSON.stringify(structuredOutputFailure), + structuredOutputError, + ], + [ + "the errors list from verbose output", + JSON.stringify([{ type: "system", subtype: "init" }, structuredOutputFailure]), + structuredOutputError, + ], [ "the stdout error over a stderr notice", JSON.stringify({ is_error: true, api_error_status: 400, result: apiError }), @@ -494,4 +511,25 @@ it.layer(layerClaudeTextGenerationTest)("ClaudeTextGeneration", (it) => { }), ), ); + + it.effect("does not report the raw result JSON when a failed run writes nothing to stderr", () => + withFakeClaudeEnv( + { output: JSON.stringify(structuredOutputFailure), exitCode: 1 }, + (textGeneration) => + Effect.gen(function* () { + const error = yield* Effect.flip( + textGeneration.generateThreadTitle({ + cwd: process.cwd(), + message: "Name this thread", + modelSelection: { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: SYNTHETIC_CLAUDE_STANDARD_MODEL, + }, + }), + ); + + expect(error.detail).toBe(`Claude CLI command failed: ${structuredOutputError}`); + }), + ), + ); }); diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index 51acced2745e5..2deb72cd138c2 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -67,7 +67,13 @@ const decodeClaudeOutput = Schema.decodeEffect( Schema.fromJsonString(Schema.Union([ClaudeOutputEnvelope, Schema.Array(ClaudeOutputMessage)])), ); -const ClaudeErrorResult = Schema.Struct({ is_error: Schema.Literal(true), result: Schema.String }); +// Only `success` results carry `result`; error subtypes such as +// error_max_structured_output_retries report through `errors` instead. +const ClaudeErrorResult = Schema.Struct({ + is_error: Schema.Literal(true), + result: Schema.optionalKey(Schema.String), + errors: Schema.optionalKey(Schema.Array(Schema.String)), +}); const isClaudeErrorResult = Schema.is(ClaudeErrorResult); const decodeJsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown)); @@ -77,7 +83,7 @@ function claudeErrorResult(stdout: string): string | undefined { const messages: ReadonlyArray = Array.isArray(output) ? output : [output]; return messages .filter(isClaudeErrorResult) - .map((message) => message.result.trim()) + .map((message) => (message.result ?? message.errors?.join("\n") ?? "").trim()) .findLast((result) => result.length > 0); } From 272e6bd73395d26c156ec8248f80e10190498667 Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Tue, 3 Nov 2026 14:33:43 -0500 Subject: [PATCH 2/2] fix(server): keep CLI telemetry out of Claude text generation errors Co-Authored-By: Claude Opus 5.5 --- .../src/textGeneration/ClaudeTextGeneration.test.ts | 9 +++++++++ apps/server/src/textGeneration/ClaudeTextGeneration.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts index eb1fdb8713ae3..45780dfcf56a0 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts @@ -470,6 +470,15 @@ it.layer(layerClaudeTextGenerationTest)("ClaudeTextGeneration", (it) => { JSON.stringify([{ type: "system", subtype: "init" }, structuredOutputFailure]), structuredOutputError, ], + [ + "the errors list without CLI telemetry", + JSON.stringify({ + ...structuredOutputFailure, + subtype: "error_during_execution", + errors: ["[ede_diagnostic] result_type=user last_content_type=n/a", apiError], + }), + apiError, + ], [ "the stdout error over a stderr notice", JSON.stringify({ is_error: true, api_error_status: 400, result: apiError }), diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index 2deb72cd138c2..63d151455e212 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -83,7 +83,14 @@ function claudeErrorResult(stdout: string): string | undefined { const messages: ReadonlyArray = Array.isArray(output) ? output : [output]; return messages .filter(isClaudeErrorResult) - .map((message) => (message.result ?? message.errors?.join("\n") ?? "").trim()) + .map((message) => + ( + message.result ?? + // error_during_execution mixes CLI telemetry into the list. + message.errors?.filter((error) => !error.startsWith("[ede_diagnostic]")).join("\n") ?? + "" + ).trim(), + ) .findLast((result) => result.length > 0); }