diff --git a/CHANGELOG.md b/CHANGELOG.md index 40306679b..73355315d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.clawd.bot - BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. - Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. - Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer. +- Agents: surface concrete API error details instead of generic AI service errors. ## 2026.1.21-2 diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index e0a4c897c..e8c514a32 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -43,8 +43,6 @@ describe("formatAssistantErrorText", () => { const msg = makeAssistantError( '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}', ); - expect(formatAssistantErrorText(msg)).toBe( - "The AI service returned an error. Please try again.", - ); + expect(formatAssistantErrorText(msg)).toBe("LLM error server_error: Something exploded"); }); }); diff --git a/src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts b/src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts index 09e5e443a..9dbadf777 100644 --- a/src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts +++ b/src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts @@ -17,4 +17,10 @@ describe("formatRawAssistantErrorForUi", () => { it("renders a generic unknown error message when raw is empty", () => { expect(formatRawAssistantErrorForUi("")).toContain("unknown error"); }); + + it("formats plain HTTP status lines", () => { + expect(formatRawAssistantErrorForUi("500 Internal Server Error")).toBe( + "HTTP 500: Internal Server Error", + ); + }); }); diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts index df84dd6cd..9fac95b9c 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts @@ -19,12 +19,12 @@ describe("sanitizeUserFacingText", () => { it("sanitizes HTTP status errors with error hints", () => { expect(sanitizeUserFacingText("500 Internal Server Error")).toBe( - "The AI service returned an error. Please try again.", + "HTTP 500: Internal Server Error", ); }); it("sanitizes raw API error payloads", () => { const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}'; - expect(sanitizeUserFacingText(raw)).toBe("The AI service returned an error. Please try again."); + expect(sanitizeUserFacingText(raw)).toBe("LLM error server_error: Something exploded"); }); }); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index d29842958..e8fb7a4d1 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -201,6 +201,14 @@ export function formatRawAssistantErrorForUi(raw?: string): string { const trimmed = (raw ?? "").trim(); if (!trimmed) return "LLM request failed with an unknown error."; + const httpMatch = trimmed.match(HTTP_STATUS_PREFIX_RE); + if (httpMatch) { + const rest = httpMatch[2].trim(); + if (!rest.startsWith("{")) { + return `HTTP ${httpMatch[1]}: ${rest}`; + } + } + const info = parseApiErrorInfo(trimmed); if (info?.message) { const prefix = info.httpCode ? `HTTP ${info.httpCode}` : "LLM error"; @@ -261,8 +269,8 @@ export function formatAssistantErrorText( return "The AI service is temporarily overloaded. Please try again in a moment."; } - if (isRawApiErrorPayload(raw)) { - return "The AI service returned an error. Please try again."; + if (isLikelyHttpErrorText(raw) || isRawApiErrorPayload(raw)) { + return formatRawAssistantErrorForUi(raw); } // Never return raw unhandled errors - log for debugging but return safe message @@ -293,7 +301,7 @@ export function sanitizeUserFacingText(text: string): string { } if (isRawApiErrorPayload(trimmed) || isLikelyHttpErrorText(trimmed)) { - return "The AI service returned an error. Please try again."; + return formatRawAssistantErrorForUi(trimmed); } if (ERROR_PREFIX_RE.test(trimmed)) { @@ -303,7 +311,7 @@ export function sanitizeUserFacingText(text: string): string { if (isTimeoutErrorMessage(trimmed)) { return "LLM request timed out."; } - return "The AI service returned an error. Please try again."; + return formatRawAssistantErrorForUi(trimmed); } return stripped; diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index be7ca41f5..005402775 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -6,6 +6,7 @@ import { formatToolAggregate } from "../../../auto-reply/tool-meta.js"; import type { ClawdbotConfig } from "../../../config/config.js"; import { formatAssistantErrorText, + formatRawAssistantErrorForUi, getApiErrorPayloadFingerprint, isRawApiErrorPayload, normalizeTextForComparison, @@ -64,6 +65,12 @@ export function buildEmbeddedRunPayloads(params: { const rawErrorFingerprint = rawErrorMessage ? getApiErrorPayloadFingerprint(rawErrorMessage) : null; + const formattedRawErrorMessage = rawErrorMessage + ? formatRawAssistantErrorForUi(rawErrorMessage) + : null; + const normalizedFormattedRawErrorMessage = formattedRawErrorMessage + ? normalizeTextForComparison(formattedRawErrorMessage) + : null; const normalizedRawErrorText = rawErrorMessage ? normalizeTextForComparison(rawErrorMessage) : null; @@ -116,10 +123,15 @@ export function buildEmbeddedRunPayloads(params: { if (trimmed === genericErrorText) return true; } if (rawErrorMessage && trimmed === rawErrorMessage) return true; + if (formattedRawErrorMessage && trimmed === formattedRawErrorMessage) return true; if (normalizedRawErrorText) { const normalized = normalizeTextForComparison(trimmed); if (normalized && normalized === normalizedRawErrorText) return true; } + if (normalizedFormattedRawErrorMessage) { + const normalized = normalizeTextForComparison(trimmed); + if (normalized && normalized === normalizedFormattedRawErrorMessage) return true; + } if (rawErrorFingerprint) { const fingerprint = getApiErrorPayloadFingerprint(trimmed); if (fingerprint && fingerprint === rawErrorFingerprint) return true; diff --git a/src/gateway/assistant-identity.test.ts b/src/gateway/assistant-identity.test.ts index 5085708e5..f966c7dc7 100644 --- a/src/gateway/assistant-identity.test.ts +++ b/src/gateway/assistant-identity.test.ts @@ -13,7 +13,9 @@ describe("resolveAssistantIdentity avatar normalization", () => { }, }; - expect(resolveAssistantIdentity({ cfg }).avatar).toBe(DEFAULT_ASSISTANT_IDENTITY.avatar); + expect(resolveAssistantIdentity({ cfg, workspaceDir: "" }).avatar).toBe( + DEFAULT_ASSISTANT_IDENTITY.avatar, + ); }); it("keeps short text avatars", () => { @@ -25,7 +27,7 @@ describe("resolveAssistantIdentity avatar normalization", () => { }, }; - expect(resolveAssistantIdentity({ cfg }).avatar).toBe("PS"); + expect(resolveAssistantIdentity({ cfg, workspaceDir: "" }).avatar).toBe("PS"); }); it("keeps path avatars", () => { @@ -37,6 +39,6 @@ describe("resolveAssistantIdentity avatar normalization", () => { }, }; - expect(resolveAssistantIdentity({ cfg }).avatar).toBe("avatars/clawd.png"); + expect(resolveAssistantIdentity({ cfg, workspaceDir: "" }).avatar).toBe("avatars/clawd.png"); }); });