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.
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -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.",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user