fix: surface concrete ai error details

This commit is contained in:
Peter Steinberger
2026-01-22 22:24:25 +00:00
parent b709898fb3
commit 411ce7e231
7 changed files with 39 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ Docs: https://docs.clawd.bot
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. - 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. - 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. - 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 ## 2026.1.21-2

View File

@@ -43,8 +43,6 @@ describe("formatAssistantErrorText", () => {
const msg = makeAssistantError( const msg = makeAssistantError(
'{"type":"error","error":{"message":"Something exploded","type":"server_error"}}', '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}',
); );
expect(formatAssistantErrorText(msg)).toBe( expect(formatAssistantErrorText(msg)).toBe("LLM error server_error: Something exploded");
"The AI service returned an error. Please try again.",
);
}); });
}); });

View File

@@ -17,4 +17,10 @@ describe("formatRawAssistantErrorForUi", () => {
it("renders a generic unknown error message when raw is empty", () => { it("renders a generic unknown error message when raw is empty", () => {
expect(formatRawAssistantErrorForUi("")).toContain("unknown error"); expect(formatRawAssistantErrorForUi("")).toContain("unknown error");
}); });
it("formats plain HTTP status lines", () => {
expect(formatRawAssistantErrorForUi("500 Internal Server Error")).toBe(
"HTTP 500: Internal Server Error",
);
});
}); });

View File

@@ -19,12 +19,12 @@ describe("sanitizeUserFacingText", () => {
it("sanitizes HTTP status errors with error hints", () => { it("sanitizes HTTP status errors with error hints", () => {
expect(sanitizeUserFacingText("500 Internal Server Error")).toBe( 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", () => { it("sanitizes raw API error payloads", () => {
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}'; 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");
}); });
}); });

View File

@@ -201,6 +201,14 @@ export function formatRawAssistantErrorForUi(raw?: string): string {
const trimmed = (raw ?? "").trim(); const trimmed = (raw ?? "").trim();
if (!trimmed) return "LLM request failed with an unknown error."; 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); const info = parseApiErrorInfo(trimmed);
if (info?.message) { if (info?.message) {
const prefix = info.httpCode ? `HTTP ${info.httpCode}` : "LLM error"; 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."; return "The AI service is temporarily overloaded. Please try again in a moment.";
} }
if (isRawApiErrorPayload(raw)) { if (isLikelyHttpErrorText(raw) || isRawApiErrorPayload(raw)) {
return "The AI service returned an error. Please try again."; return formatRawAssistantErrorForUi(raw);
} }
// Never return raw unhandled errors - log for debugging but return safe message // 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)) { if (isRawApiErrorPayload(trimmed) || isLikelyHttpErrorText(trimmed)) {
return "The AI service returned an error. Please try again."; return formatRawAssistantErrorForUi(trimmed);
} }
if (ERROR_PREFIX_RE.test(trimmed)) { if (ERROR_PREFIX_RE.test(trimmed)) {
@@ -303,7 +311,7 @@ export function sanitizeUserFacingText(text: string): string {
if (isTimeoutErrorMessage(trimmed)) { if (isTimeoutErrorMessage(trimmed)) {
return "LLM request timed out."; return "LLM request timed out.";
} }
return "The AI service returned an error. Please try again."; return formatRawAssistantErrorForUi(trimmed);
} }
return stripped; return stripped;

View File

@@ -6,6 +6,7 @@ import { formatToolAggregate } from "../../../auto-reply/tool-meta.js";
import type { ClawdbotConfig } from "../../../config/config.js"; import type { ClawdbotConfig } from "../../../config/config.js";
import { import {
formatAssistantErrorText, formatAssistantErrorText,
formatRawAssistantErrorForUi,
getApiErrorPayloadFingerprint, getApiErrorPayloadFingerprint,
isRawApiErrorPayload, isRawApiErrorPayload,
normalizeTextForComparison, normalizeTextForComparison,
@@ -64,6 +65,12 @@ export function buildEmbeddedRunPayloads(params: {
const rawErrorFingerprint = rawErrorMessage const rawErrorFingerprint = rawErrorMessage
? getApiErrorPayloadFingerprint(rawErrorMessage) ? getApiErrorPayloadFingerprint(rawErrorMessage)
: null; : null;
const formattedRawErrorMessage = rawErrorMessage
? formatRawAssistantErrorForUi(rawErrorMessage)
: null;
const normalizedFormattedRawErrorMessage = formattedRawErrorMessage
? normalizeTextForComparison(formattedRawErrorMessage)
: null;
const normalizedRawErrorText = rawErrorMessage const normalizedRawErrorText = rawErrorMessage
? normalizeTextForComparison(rawErrorMessage) ? normalizeTextForComparison(rawErrorMessage)
: null; : null;
@@ -116,10 +123,15 @@ export function buildEmbeddedRunPayloads(params: {
if (trimmed === genericErrorText) return true; if (trimmed === genericErrorText) return true;
} }
if (rawErrorMessage && trimmed === rawErrorMessage) return true; if (rawErrorMessage && trimmed === rawErrorMessage) return true;
if (formattedRawErrorMessage && trimmed === formattedRawErrorMessage) return true;
if (normalizedRawErrorText) { if (normalizedRawErrorText) {
const normalized = normalizeTextForComparison(trimmed); const normalized = normalizeTextForComparison(trimmed);
if (normalized && normalized === normalizedRawErrorText) return true; if (normalized && normalized === normalizedRawErrorText) return true;
} }
if (normalizedFormattedRawErrorMessage) {
const normalized = normalizeTextForComparison(trimmed);
if (normalized && normalized === normalizedFormattedRawErrorMessage) return true;
}
if (rawErrorFingerprint) { if (rawErrorFingerprint) {
const fingerprint = getApiErrorPayloadFingerprint(trimmed); const fingerprint = getApiErrorPayloadFingerprint(trimmed);
if (fingerprint && fingerprint === rawErrorFingerprint) return true; if (fingerprint && fingerprint === rawErrorFingerprint) return true;

View File

@@ -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", () => { 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", () => { 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");
}); });
}); });