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.
- 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

View File

@@ -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");
});
});

View File

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

View File

@@ -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");
});
});

View File

@@ -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;

View File

@@ -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;

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