From 1bd1483b62aadc97844e05f88d337b662f5ef09d Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 3 Jun 2026 13:43:14 +0530 Subject: [PATCH] refactor(auto-reply): unify transient failure visibility --- docs/concepts/messages.md | 10 ++- .../reply/agent-runner-execution.test.ts | 73 +++++++------------ .../reply/agent-runner-execution.ts | 8 +- 3 files changed, 35 insertions(+), 56 deletions(-) diff --git a/docs/concepts/messages.md b/docs/concepts/messages.md index 1b55d3f15010..7358866122bb 100644 --- a/docs/concepts/messages.md +++ b/docs/concepts/messages.md @@ -194,10 +194,12 @@ OpenClaw resolves that behavior by conversation type: `message(action=send)`. - Internal orchestration allows silence by default. -OpenClaw also uses silent replies for internal runner failures that happen -before any assistant reply in non-direct chats, so groups/channels do not see -gateway error boilerplate. Direct chats show compact failure copy by default; -raw runner details are shown only when `/verbose full` is enabled. +OpenClaw also uses silent replies for generic internal runner failures in +non-direct chats, so groups/channels do not see gateway error boilerplate. +Classified failures with user-facing recovery copy, such as missing auth, +rate-limit, or overload notices, can still be delivered. Direct chats show +compact failure copy by default; raw runner details are shown only when +`/verbose full` is enabled. Defaults live under `agents.defaults.silentReply`; `surfaces..silentReply` can override group/internal policy per surface. diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index a68d7f875687..ccb1312d4e7c 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -2950,51 +2950,37 @@ describe("runAgentTurnWithFallback", () => { ).toBeUndefined(); }); - it("surfaces model capacity errors from no-text mid-turn failures", async () => { - state.runEmbeddedAgentMock.mockResolvedValueOnce({ - payloads: [{ text: "thinking", isReasoning: true }], - meta: { - error: { - kind: "server_overloaded", - message: "Selected model is at capacity. Please try a different model.", + it.each(NON_DIRECT_FAILURE_SURFACE_CASES)( + "surfaces model capacity errors from no-text mid-turn failures in $label chats", + async (testCase) => { + state.runEmbeddedAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "thinking", isReasoning: true }], + meta: { + error: { + kind: "server_overloaded", + message: "Selected model is at capacity. Please try a different model.", + }, }, - }, - }); + }); - 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(), - resetSessionAfterRoleOrderingConflict: async () => false, - isHeartbeat: false, - sessionKey: "main", - getActiveSessionEntry: () => undefined, - resolvedVerboseLevel: "off", - }); + const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); + const result = await runAgentTurnWithFallback( + createMinimalRunAgentTurnParams({ + sessionCtx: createNonDirectFailureSessionCtx(testCase), + }), + ); - expect(result.kind).toBe("success"); - if (result.kind === "success") { - expect(result.runResult.payloads).toEqual([ - { - text: "⚠️ Selected model is at capacity. Try a different model, or wait and retry.", - isError: true, - }, - ]); - } - }); + expect(result.kind).toBe("success"); + if (result.kind === "success") { + expect(result.runResult.payloads).toEqual([ + { + text: "⚠️ Selected model is at capacity. Try a different model, or wait and retry.", + isError: true, + }, + ]); + } + }, + ); it("surfaces model capacity errors from pre-reply CLI failures", async () => { state.runWithModelFallbackMock.mockRejectedValueOnce( @@ -5244,9 +5230,6 @@ describe("runAgentTurnWithFallback", () => { it.each(NON_DIRECT_FAILURE_SURFACE_CASES)( "keeps default silent behavior in $label chats when silentReply policy is unset", async (testCase) => { - // Sanity check: explicit `{}` config (no silentReply) must still resolve - // to the documented default `group: "allow"` and produce a silent payload - // — the new policy hookup must not regress the default behavior. state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai/gpt-5.5 ended with an incomplete terminal response"), ); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index fdde7805070a..785d61475a92 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -652,17 +652,12 @@ function resolveExternalRunFailureTextForConversation(params: { text: string; sessionCtx: TemplateContext; isGenericRunnerFailure: boolean; - suppressInNonDirect?: boolean; cfg?: OpenClawConfig; }): string { if (!isNonDirectConversationContext(params.sessionCtx)) { return params.text; } - if ( - !params.suppressInNonDirect && - !params.isGenericRunnerFailure && - !params.text.includes(AGENT_FAILED_BEFORE_REPLY_TEXT) - ) { + if (!params.isGenericRunnerFailure && !params.text.includes(AGENT_FAILED_BEFORE_REPLY_TEXT)) { return params.text; } // Match normal reply routing: default group/channel failures stay silent, @@ -2995,7 +2990,6 @@ export async function runAgentTurnWithFallback(params: { text: formattedErrorCandidate, sessionCtx: params.sessionCtx, isGenericRunnerFailure: false, - suppressInNonDirect: true, cfg: params.followupRun.run.config, }), isError: true,