fix: sanitize user-facing errors and strip final tags
Co-authored-by: Drake Thomsen <drake.thomsen@example.com>
This commit is contained in:
@@ -32,6 +32,41 @@ export function isCompactionFailureError(errorMessage?: string): boolean {
|
||||
|
||||
const ERROR_PAYLOAD_PREFIX_RE =
|
||||
/^(?:error|api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error)[:\s-]+/i;
|
||||
const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/gi;
|
||||
const ERROR_PREFIX_RE =
|
||||
/^(?:error|api\s*error|openai\s*error|anthropic\s*error|gateway\s*error|request failed|failed|exception)[:\s-]+/i;
|
||||
const HTTP_STATUS_PREFIX_RE = /^(?:http\s*)?(\d{3})\s+(.+)$/i;
|
||||
const HTTP_ERROR_HINTS = [
|
||||
"error",
|
||||
"bad request",
|
||||
"not found",
|
||||
"unauthorized",
|
||||
"forbidden",
|
||||
"internal server",
|
||||
"service unavailable",
|
||||
"gateway",
|
||||
"rate limit",
|
||||
"overloaded",
|
||||
"timeout",
|
||||
"timed out",
|
||||
"invalid",
|
||||
"too many requests",
|
||||
"permission",
|
||||
];
|
||||
|
||||
function stripFinalTagsFromText(text: string): string {
|
||||
if (!text) return text;
|
||||
return text.replace(FINAL_TAG_RE, "");
|
||||
}
|
||||
|
||||
function isLikelyHttpErrorText(raw: string): boolean {
|
||||
const match = raw.match(HTTP_STATUS_PREFIX_RE);
|
||||
if (!match) return false;
|
||||
const code = Number(match[1]);
|
||||
if (!Number.isFinite(code) || code < 400) return false;
|
||||
const message = match[2].toLowerCase();
|
||||
return HTTP_ERROR_HINTS.some((hint) => message.includes(hint));
|
||||
}
|
||||
|
||||
type ErrorPayload = Record<string, unknown>;
|
||||
|
||||
@@ -170,8 +205,9 @@ export function formatAssistantErrorText(
|
||||
msg: AssistantMessage,
|
||||
opts?: { cfg?: ClawdbotConfig; sessionKey?: string },
|
||||
): string | undefined {
|
||||
if (msg.stopReason !== "error") return undefined;
|
||||
// Also format errors if errorMessage is present, even if stopReason isn't "error"
|
||||
const raw = (msg.errorMessage ?? "").trim();
|
||||
if (msg.stopReason !== "error" && !raw) return undefined;
|
||||
if (!raw) return "LLM request failed with an unknown error.";
|
||||
|
||||
const unknownTool =
|
||||
@@ -193,7 +229,8 @@ export function formatAssistantErrorText(
|
||||
);
|
||||
}
|
||||
|
||||
if (/incorrect role information|roles must alternate/i.test(raw)) {
|
||||
// Catch role ordering errors - including JSON-wrapped and "400" prefix variants
|
||||
if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(raw)) {
|
||||
return (
|
||||
"Message ordering conflict - please try again. " +
|
||||
"If this persists, use /new to start a fresh session."
|
||||
@@ -213,9 +250,50 @@ export function formatAssistantErrorText(
|
||||
return "The AI service returned an error. Please try again.";
|
||||
}
|
||||
|
||||
// Never return raw unhandled errors - log for debugging but return safe message
|
||||
if (raw.length > 600) {
|
||||
console.warn("[formatAssistantErrorText] Long error truncated:", raw.slice(0, 200));
|
||||
}
|
||||
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
|
||||
}
|
||||
|
||||
export function sanitizeUserFacingText(text: string): string {
|
||||
if (!text) return text;
|
||||
const stripped = stripFinalTagsFromText(text);
|
||||
const trimmed = stripped.trim();
|
||||
if (!trimmed) return stripped;
|
||||
|
||||
if (/incorrect role information|roles must alternate/i.test(trimmed)) {
|
||||
return (
|
||||
"Message ordering conflict - please try again. " +
|
||||
"If this persists, use /new to start a fresh session."
|
||||
);
|
||||
}
|
||||
|
||||
if (isContextOverflowError(trimmed)) {
|
||||
return (
|
||||
"Context overflow: prompt too large for the model. " +
|
||||
"Try again with less input or a larger-context model."
|
||||
);
|
||||
}
|
||||
|
||||
if (isRawApiErrorPayload(trimmed) || isLikelyHttpErrorText(trimmed)) {
|
||||
return "The AI service returned an error. Please try again.";
|
||||
}
|
||||
|
||||
if (ERROR_PREFIX_RE.test(trimmed)) {
|
||||
if (isOverloadedErrorMessage(trimmed) || isRateLimitErrorMessage(trimmed)) {
|
||||
return "The AI service is temporarily overloaded. Please try again in a moment.";
|
||||
}
|
||||
if (isTimeoutErrorMessage(trimmed)) {
|
||||
return "LLM request timed out.";
|
||||
}
|
||||
return "The AI service returned an error. Please try again.";
|
||||
}
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
|
||||
if (!msg || msg.stopReason !== "error") return false;
|
||||
return isRateLimitErrorMessage(msg.errorMessage ?? "");
|
||||
|
||||
Reference in New Issue
Block a user