fix: surface concrete ai error details
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user