Skip to content

Commit 5f4bc6e

Browse files
committed
fix: surface external agent errors
1 parent f545872 commit 5f4bc6e

3 files changed

Lines changed: 66 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Docs: https://docs.openclaw.ai
2525

2626
### Fixes
2727

28+
- Agents/replies: forward sanitized underlying agent failure details on external
29+
channels instead of replacing unknown failures with a generic retry message.
30+
Thanks @steipete.
2831
- Agents/TTS: preserve `[[audio_as_voice]]` directives on trusted text
2932
tool-result `MEDIA:` payloads so generated audio still delivers as a voice
3033
note. (#46535) Thanks @azade-c.

src/auto-reply/reply/agent-runner-execution.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,7 +1662,9 @@ describe("runAgentTurnWithFallback", () => {
16621662

16631663
expect(result.kind).toBe("final");
16641664
if (result.kind === "final") {
1665-
expect(result.payload.text).toContain("Something went wrong while processing your request");
1665+
expect(result.payload.text).toContain("Agent failed before reply");
1666+
expect(result.payload.text).toContain("All models failed");
1667+
expect(result.payload.text).toContain("402 (billing)");
16661668
expect(result.payload.text).not.toContain("Rate-limited");
16671669
}
16681670
});
@@ -1877,7 +1879,7 @@ describe("runAgentTurnWithFallback", () => {
18771879
expect(failMock).not.toHaveBeenCalled();
18781880
});
18791881

1880-
it("returns a friendly generic error on external chat channels", async () => {
1882+
it("forwards sanitized generic errors on external chat channels", async () => {
18811883
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
18821884
new Error("INVALID_ARGUMENT: some other failure"),
18831885
);
@@ -1910,7 +1912,47 @@ describe("runAgentTurnWithFallback", () => {
19101912
expect(result.kind).toBe("final");
19111913
if (result.kind === "final") {
19121914
expect(result.payload.text).toBe(
1913-
"⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.",
1915+
"⚠️ Agent failed before reply: INVALID_ARGUMENT: some other failure. Please try again, or use /new to start a fresh session.",
1916+
);
1917+
}
1918+
});
1919+
1920+
it("formats raw Codex API payloads before forwarding external errors", async () => {
1921+
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
1922+
new Error(
1923+
'Codex error: {"type":"error","error":{"type":"server_error","message":"Something exploded"},"sequence_number":2}',
1924+
),
1925+
);
1926+
1927+
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
1928+
const result = await runAgentTurnWithFallback({
1929+
commandBody: "hello",
1930+
followupRun: createFollowupRun(),
1931+
sessionCtx: {
1932+
Provider: "whatsapp",
1933+
MessageSid: "msg",
1934+
} as unknown as TemplateContext,
1935+
opts: {},
1936+
typingSignals: createMockTypingSignaler(),
1937+
blockReplyPipeline: null,
1938+
blockStreamingEnabled: false,
1939+
resolvedBlockStreamingBreak: "message_end",
1940+
applyReplyToMode: (payload) => payload,
1941+
shouldEmitToolResult: () => true,
1942+
shouldEmitToolOutput: () => false,
1943+
pendingToolTasks: new Set(),
1944+
resetSessionAfterCompactionFailure: async () => false,
1945+
resetSessionAfterRoleOrderingConflict: async () => false,
1946+
isHeartbeat: false,
1947+
sessionKey: "main",
1948+
getActiveSessionEntry: () => undefined,
1949+
resolvedVerboseLevel: "off",
1950+
});
1951+
1952+
expect(result.kind).toBe("final");
1953+
if (result.kind === "final") {
1954+
expect(result.payload.text).toBe(
1955+
"⚠️ Agent failed before reply: LLM error server_error: Something exploded. Please try again, or use /new to start a fresh session.",
19141956
);
19151957
}
19161958
});

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ function collapseRepeatedFailureDetail(message: string): string {
352352
}
353353

354354
const SAFE_MISSING_API_KEY_PROVIDERS = new Set(["anthropic", "google", "openai", "openai-codex"]);
355+
const EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS = 900;
355356

356357
function buildMissingApiKeyFailureText(message: string): string | null {
357358
const normalizedMessage = collapseRepeatedFailureDetail(message);
@@ -369,6 +370,22 @@ function buildMissingApiKeyFailureText(message: string): string | null {
369370
return "⚠️ Missing API key for the selected provider on the gateway. Configure provider auth, then try again.";
370371
}
371372

373+
function formatForwardedExternalRunFailureText(message: string): string {
374+
const sanitized = sanitizeUserFacingText(message, { errorContext: true })
375+
.trim()
376+
.replace(/^\s*/u, "")
377+
.replace(/\s+/gu, " ");
378+
if (!sanitized) {
379+
return "⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.";
380+
}
381+
const detail =
382+
sanitized.length > EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS
383+
? `${sanitized.slice(0, EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS - 1).trimEnd()}…`
384+
: sanitized;
385+
const suffix = /[.!?]$/u.test(detail) ? "" : ".";
386+
return `⚠️ Agent failed before reply: ${detail}${suffix} Please try again, or use /new to start a fresh session.`;
387+
}
388+
372389
function buildExternalRunFailureText(message: string): string {
373390
const normalizedMessage = collapseRepeatedFailureDetail(message);
374391
if (isToolResultTurnMismatchError(normalizedMessage)) {
@@ -386,7 +403,7 @@ function buildExternalRunFailureText(message: string): string {
386403
}
387404
return `⚠️ Model login failed on the gateway${oauthRefreshFailure.provider ? ` for ${oauthRefreshFailure.provider}` : ""}. Please try again. If this keeps happening, re-auth with \`${loginCommand}\`.`;
388405
}
389-
return "⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.";
406+
return formatForwardedExternalRunFailureText(normalizedMessage);
390407
}
391408

392409
function shouldApplyOpenAIGptChatGuard(params: { provider?: string; model?: string }): boolean {

0 commit comments

Comments
 (0)