From eaeced477421fc413473bbba39affb3c849d0179 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:20:11 -0700 Subject: [PATCH 1/3] fix(gsd): extract real error from message content when errorMessage is useless MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When errorMessage is uninformative (e.g. "success", "ok", "error"), fall back to the assistant message text content to surface the real provider error like "Invalid API key · Please run /login". Fixes #3588 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../extensions/gsd/bootstrap/agent-end-recovery.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts b/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts index ac8737d09..c5b9555a3 100644 --- a/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +++ b/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts @@ -95,8 +95,17 @@ export async function handleAgentEnd( return; } if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") { - const errorDetail = "errorMessage" in lastMsg && lastMsg.errorMessage ? `: ${lastMsg.errorMessage}` : ""; - const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : ""; + // #3588: errorMessage can be useless (e.g. "success") while the real error + // is in the assistant message text content. Fall back to content when + // errorMessage looks uninformative. + const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : ""; + const isUseless = !rawErrorMsg || /^(success|ok|true|error|unknown)$/i.test(rawErrorMsg.trim()); + let errorMsg = rawErrorMsg; + if (isUseless && "content" in lastMsg && Array.isArray(lastMsg.content)) { + const textBlock = lastMsg.content.find((b: any) => b.type === "text" && b.text); + if (textBlock) errorMsg = (textBlock as any).text.slice(0, 300); + } + const errorDetail = errorMsg ? `: ${errorMsg}` : ""; const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined; // ── 1. Classify ────────────────────────────────────────────────────── From 0b6dfd0bbfab26f47082f0fe3194115ebcb7ff29 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:22:02 -0700 Subject: [PATCH 2/3] fix(gsd): extract real error from message content when errorMessage is useless When errorMessage is uninformative (e.g. "success", "ok"), fall back to the assistant message text content for display while keeping rawErrorMsg for classification to avoid prose false-positives. Fixes #3588 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../extensions/gsd/bootstrap/agent-end-recovery.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts b/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts index c5b9555a3..b3c517356 100644 --- a/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +++ b/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts @@ -100,16 +100,19 @@ export async function handleAgentEnd( // errorMessage looks uninformative. const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : ""; const isUseless = !rawErrorMsg || /^(success|ok|true|error|unknown)$/i.test(rawErrorMsg.trim()); - let errorMsg = rawErrorMsg; + // #3588: When errorMessage is uninformative, extract the real error from + // the assistant message text content for display purposes only. + // Classification still uses rawErrorMsg to avoid false positives from prose. + let displayMsg = rawErrorMsg; if (isUseless && "content" in lastMsg && Array.isArray(lastMsg.content)) { const textBlock = lastMsg.content.find((b: any) => b.type === "text" && b.text); - if (textBlock) errorMsg = (textBlock as any).text.slice(0, 300); + if (textBlock) displayMsg = (textBlock as any).text.slice(0, 300); } - const errorDetail = errorMsg ? `: ${errorMsg}` : ""; + const errorDetail = displayMsg ? `: ${displayMsg}` : ""; const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined; - // ── 1. Classify ────────────────────────────────────────────────────── - const cls = classifyError(errorMsg, explicitRetryAfterMs); + // ── 1. Classify using rawErrorMsg to avoid prose false-positives ──── + const cls = classifyError(rawErrorMsg, explicitRetryAfterMs); // Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli) // which use per-user quotas with shorter windows (#2922). From 808a0e56bd6fe5ab8257087610d31a5088beac21 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:26:03 -0700 Subject: [PATCH 3/3] test: add regression test for error-success mask detection Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gsd/tests/error-success-mask.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/error-success-mask.test.ts diff --git a/src/resources/extensions/gsd/tests/error-success-mask.test.ts b/src/resources/extensions/gsd/tests/error-success-mask.test.ts new file mode 100644 index 000000000..d6dd9719a --- /dev/null +++ b/src/resources/extensions/gsd/tests/error-success-mask.test.ts @@ -0,0 +1,37 @@ +/** + * error-success-mask.test.ts — #3664 + * + * Verify that the agent-end-recovery error handler detects when errorMessage + * is uninformative (e.g. "success", "ok", "unknown") and falls back to + * extracting the real error from the assistant message text content. + */ + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const sourceFile = join(__dirname, "..", "bootstrap", "agent-end-recovery.ts"); + +describe("error-success mask detection (#3664)", () => { + const source = readFileSync(sourceFile, "utf-8"); + + test("detects useless errorMessage values with regex", () => { + assert.match(source, /success\|ok\|true\|error\|unknown/i); + }); + + test("extracts display message from content text block", () => { + assert.match(source, /textBlock/); + assert.match(source, /\.text\.slice\(0,\s*300\)/); + }); + + test("classifies using rawErrorMsg, not displayMsg", () => { + assert.match(source, /classifyError\(rawErrorMsg/); + }); + + test("references issue #3588 in comments", () => { + assert.match(source, /#3588/); + }); +});