Auto-reply: improve user-facing error messages

This commit is contained in:
Sahil Satralkar 2026-02-19 22:55:21 +05:30
parent 546e4d940a
commit aed4d330f4
4 changed files with 56 additions and 16 deletions

View File

@ -2192,6 +2192,7 @@ Docs: https://docs.openclaw.ai
- Security/Exec: block grep safe-bin positional operand bypass by setting grep positional budget to zero, so `-e/--regexp` cannot smuggle bare filename reads (for example `.env`) via ambiguous positionals; safe-bin grep patterns must come from `-e/--regexp`. Thanks @athuljayaram for reporting.
- Security/Gateway/Agents: remove implicit admin scopes from agent tool gateway calls by classifying methods to least-privilege operator scopes, and enforce owner-only tooling (`cron`, `gateway`, `whatsapp_login`) through centralized tool-policy wrappers plus tool metadata to prevent non-owner DM privilege escalation. Ships in the next npm release. Thanks @Adam55A-code for reporting.
- Security/Gateway: centralize gateway method-scope authorization and default non-CLI gateway callers to least-privilege method scopes, with explicit CLI scope handling, full core-handler scope classification coverage, and regression guards to prevent scope drift.
- Auto-reply: show user-friendly error messages based on error type (rate limit, auth, billing, timeout) instead of exposing technical details.
- Security/Net: block SSRF bypass via NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 (`2002::/16`), and Teredo (`2001:0000::/32`) IPv6 transition addresses, and fail closed on IPv6 parse errors. Thanks @jackhax.
- Security/OTEL: sanitize OTLP endpoint URL resolution. (#13791) Thanks @vincentkoc.
- Security: patch Dependabot security issues in pnpm lock. (#20832) Thanks @vincentkoc.

View File

@ -154,13 +154,31 @@ describe("trigger handling", () => {
{
error: "sandbox is not defined.",
expected:
"⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow",
"Something unexpected happened. Try /new to start a fresh conversation, or try again in a moment.",
},
{
error: "Context window exceeded",
expected:
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.",
},
{
error: "rate_limit_exceeded: API rate limit exceeded",
expected: "The AI service is busy. Please wait a moment and try again.",
},
{
error: "401 Unauthorized: Invalid API key",
expected:
"I couldn't connect to the AI service. Please verify your API key is configured correctly.",
},
{
error: "402 Payment Required: billing limit exceeded",
expected:
"I've reached my limit with the AI service. Please check your account balance and try again.",
},
{
error: "408 Request Timeout: connection timed out",
expected: "The request timed out. Please try again, or start a fresh session with /new.",
},
] as const;
for (const testCase of errorCases) {
runEmbeddedPiAgentMock.mockClear();

View File

@ -6,13 +6,15 @@ import { getCliSessionId } from "../../agents/cli-session.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { isCliProvider } from "../../agents/model-selection.js";
import {
BILLING_ERROR_USER_MESSAGE,
isAuthErrorMessage,
isBillingErrorMessage,
isCompactionFailureError,
isContextOverflowError,
isBillingErrorMessage,
isLikelyContextOverflowError,
isOverloadedErrorMessage,
isRateLimitErrorMessage,
isTimeoutErrorMessage,
isTransientHttpError,
sanitizeUserFacingText,
} from "../../agents/pi-embedded-helpers.js";
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
import {
@ -534,6 +536,12 @@ export async function runAgentTurnWithFallback(params: {
const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
const isTransientHttp = isTransientHttpError(message);
const isRateLimit = isRateLimitErrorMessage(message);
const isAuthError = isAuthErrorMessage(message);
const isBillingError = isBillingErrorMessage(message);
const isTimeoutError = isTimeoutErrorMessage(message);
const isOverloaded = isOverloadedErrorMessage(message);
if (
isCompactionFailure &&
!didResetAfterCompactionFailure &&
@ -620,17 +628,30 @@ export async function runAgentTurnWithFallback(params: {
}
defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
const safeMessage = isTransientHttp
? sanitizeUserFacingText(message, { errorContext: true })
: message;
const trimmedMessage = safeMessage.replace(/\.\s*$/, "");
const fallbackText = isBilling
? BILLING_ERROR_USER_MESSAGE
: isContextOverflow
? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
: isRoleOrderingError
? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
: `⚠️ Agent failed before reply: ${trimmedMessage}.\nLogs: openclaw logs --follow`;
let fallbackText: string;
if (isContextOverflow) {
fallbackText =
"⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.";
} else if (isRoleOrderingError) {
fallbackText =
"⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session.";
} else if (isRateLimit || isOverloaded) {
fallbackText = "The AI service is busy. Please wait a moment and try again.";
} else if (isAuthError) {
fallbackText =
"I couldn't connect to the AI service. Please verify your API key is configured correctly.";
} else if (isBillingError) {
fallbackText =
"I've reached my limit with the AI service. Please check your account balance and try again.";
} else if (isTimeoutError) {
fallbackText =
"The request timed out. Please try again, or start a fresh session with /new.";
} else {
fallbackText =
"Something unexpected happened. Try /new to start a fresh conversation, or try again in a moment.";
}
return {
kind: "final",

View File

@ -1485,7 +1485,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
const res = await run();
expect(res).toMatchObject({
text: expect.stringContaining("Agent failed before reply"),
text: "Something unexpected happened. Try /new to start a fresh conversation, or try again in a moment.",
});
expect(sessionStore.main).toBeDefined();
await expect(fs.access(transcriptPath)).resolves.toBeUndefined();