diff --git a/CHANGELOG.md b/CHANGELOG.md index 62f1f6774e6..b5167cb19d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/replies: forward sanitized underlying agent failure details on external + channels instead of replacing unknown failures with a generic retry message. + Thanks @steipete. - Agents/TTS: preserve `[[audio_as_voice]]` directives on trusted text tool-result `MEDIA:` payloads so generated audio still delivers as a voice note. (#46535) Thanks @azade-c. diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 8ef3fa8bdf9..b25fca49f72 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -1662,7 +1662,9 @@ describe("runAgentTurnWithFallback", () => { expect(result.kind).toBe("final"); if (result.kind === "final") { - expect(result.payload.text).toContain("Something went wrong while processing your request"); + expect(result.payload.text).toContain("Agent failed before reply"); + expect(result.payload.text).toContain("All models failed"); + expect(result.payload.text).toContain("402 (billing)"); expect(result.payload.text).not.toContain("Rate-limited"); } }); @@ -1877,7 +1879,7 @@ describe("runAgentTurnWithFallback", () => { expect(failMock).not.toHaveBeenCalled(); }); - it("returns a friendly generic error on external chat channels", async () => { + it("forwards sanitized generic errors on external chat channels", async () => { state.runEmbeddedPiAgentMock.mockRejectedValueOnce( new Error("INVALID_ARGUMENT: some other failure"), ); @@ -1910,7 +1912,47 @@ describe("runAgentTurnWithFallback", () => { expect(result.kind).toBe("final"); if (result.kind === "final") { expect(result.payload.text).toBe( - "⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.", + "⚠️ Agent failed before reply: INVALID_ARGUMENT: some other failure. Please try again, or use /new to start a fresh session.", + ); + } + }); + + it("formats raw Codex API payloads before forwarding external errors", async () => { + state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + new Error( + 'Codex error: {"type":"error","error":{"type":"server_error","message":"Something exploded"},"sequence_number":2}', + ), + ); + + const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); + const result = await runAgentTurnWithFallback({ + commandBody: "hello", + followupRun: createFollowupRun(), + sessionCtx: { + Provider: "whatsapp", + MessageSid: "msg", + } as unknown as TemplateContext, + opts: {}, + typingSignals: createMockTypingSignaler(), + blockReplyPipeline: null, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + applyReplyToMode: (payload) => payload, + shouldEmitToolResult: () => true, + shouldEmitToolOutput: () => false, + pendingToolTasks: new Set(), + resetSessionAfterCompactionFailure: async () => false, + resetSessionAfterRoleOrderingConflict: async () => false, + isHeartbeat: false, + sessionKey: "main", + getActiveSessionEntry: () => undefined, + resolvedVerboseLevel: "off", + }); + + expect(result.kind).toBe("final"); + if (result.kind === "final") { + expect(result.payload.text).toBe( + "⚠️ Agent failed before reply: LLM error server_error: Something exploded. Please try again, or use /new to start a fresh session.", ); } }); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index f0e7dc3ecea..3ec304e4c4b 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -352,6 +352,7 @@ function collapseRepeatedFailureDetail(message: string): string { } const SAFE_MISSING_API_KEY_PROVIDERS = new Set(["anthropic", "google", "openai", "openai-codex"]); +const EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS = 900; function buildMissingApiKeyFailureText(message: string): string | null { const normalizedMessage = collapseRepeatedFailureDetail(message); @@ -369,6 +370,22 @@ function buildMissingApiKeyFailureText(message: string): string | null { return "⚠️ Missing API key for the selected provider on the gateway. Configure provider auth, then try again."; } +function formatForwardedExternalRunFailureText(message: string): string { + const sanitized = sanitizeUserFacingText(message, { errorContext: true }) + .trim() + .replace(/^⚠️\s*/u, "") + .replace(/\s+/gu, " "); + if (!sanitized) { + return "⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session."; + } + const detail = + sanitized.length > EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS + ? `${sanitized.slice(0, EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS - 1).trimEnd()}…` + : sanitized; + const suffix = /[.!?]$/u.test(detail) ? "" : "."; + return `⚠️ Agent failed before reply: ${detail}${suffix} Please try again, or use /new to start a fresh session.`; +} + function buildExternalRunFailureText(message: string): string { const normalizedMessage = collapseRepeatedFailureDetail(message); if (isToolResultTurnMismatchError(normalizedMessage)) { @@ -386,7 +403,7 @@ function buildExternalRunFailureText(message: string): string { } return `⚠️ Model login failed on the gateway${oauthRefreshFailure.provider ? ` for ${oauthRefreshFailure.provider}` : ""}. Please try again. If this keeps happening, re-auth with \`${loginCommand}\`.`; } - return "⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session."; + return formatForwardedExternalRunFailureText(normalizedMessage); } function shouldApplyOpenAIGptChatGuard(params: { provider?: string; model?: string }): boolean {