From 8c886e94384807053ec9f5c628fcb33a63e9596f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 28 Apr 2026 21:31:51 -0700 Subject: [PATCH] fix(telegram): suppress acknowledged mutating tool warning leaks Suppress raw failed edit/write warning payloads when the assistant already delivered a user-facing error reply for the same turn, while keeping the fallback warning for unresolved, ambiguous, or success-looking mutating failures. Fixes #39631. Refs #51065, #39636, #39717, and #39406. Validation: - Testbox tbx_01kqbqxw1yqpyyxb25vvjkrc90: OPENCLAW_TESTBOX=1 pnpm test:serial src/agents/pi-embedded-runner/run/payloads.errors.test.ts - Testbox tbx_01kqbqxw1yqpyyxb25vvjkrc90: OPENCLAW_TESTBOX=1 pnpm check:changed - CI run 25086475010: success on ea33538add18485df96e5a30a9dc3c1df28b9fbd - Parity gate run 25086474949: success on ea33538add18485df96e5a30a9dc3c1df28b9fbd --- CHANGELOG.md | 1 + .../run/payloads.errors.test.ts | 29 +++++++++++++++++++ src/agents/pi-embedded-runner/run/payloads.ts | 5 +++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb439f719c1..d8647a13f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai - CLI/status: fall back to a bounded local `status` RPC when loopback detail probes time out or report unknown capability, so reachable local gateways are no longer marked unreachable by slow read diagnostics. Fixes #73535; refs #48360, #62762, #51357, and #42019. Thanks @RacecarGuy, @justinschille, @DJBlackhawk, @tianyaqpzm, and @0xrsydn. - CLI/gateway: reuse cached paired-device auth during `gateway probe` and report post-connect diagnostic failures as degraded reachability, so healthy local gateways are no longer marked unreachable after loopback auth or read timeouts. Fixes #48360. Thanks @RacecarGuy. - Channels/Discord: give Discord Gateway WebSocket handshakes a 30s timeout so stalled TLS/network transitions emit an error and Carbon can continue its reconnect loop instead of leaving the bot silent until restart. Refs #50046. Thanks @codexGW. +- Channels/Telegram: suppress standalone failed edit/write warning payloads when a user-facing assistant error reply already covers the turn, while keeping unresolved mutating failures visible behind success-looking or suppressed-error replies. Fixes #39631; refs #73750; carries forward #39636 and #39717; leaves #39406 for configurable delivery policy. Thanks @Bartok9 and @Bortlesboat. - NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar. - Channels/Discord: let text-only configs drop the `GuildVoiceStates` gateway intent and expose a bounded `/gateway/bot` metadata timeout with rate-limited fallback logs, reducing idle CPU and warning floods. Fixes #73709 and #73585. Thanks @sanchezm86 and @trac3r00. - Agents/sessions: mark same-turn `sessions_send` and A2A reply prompts with an inter-session `isUser=false` envelope before they reach the model, so foreign session output no longer lands as bare active user text. Fixes #73702; refs #73698, #73609, #73595, and #73622. Thanks @alvelda. diff --git a/src/agents/pi-embedded-runner/run/payloads.errors.test.ts b/src/agents/pi-embedded-runner/run/payloads.errors.test.ts index b3238b11b07..2c8cb54b462 100644 --- a/src/agents/pi-embedded-runner/run/payloads.errors.test.ts +++ b/src/agents/pi-embedded-runner/run/payloads.errors.test.ts @@ -80,6 +80,35 @@ describe("buildEmbeddedRunPayloads", () => { expect(payloads.some((payload) => payload.text === errorJson)).toBe(false); }); + it("suppresses mutating tool warnings when an assistant error reply already covers the turn", () => { + const payloads = buildPayloads({ + assistantTexts: [errorJson], + lastAssistant: makeAssistant({}), + lastToolError: { toolName: "edit", error: "file missing" }, + sessionKey: "agent:main:telegram:direct:u123", + }); + + expectOverloadedFallback(payloads); + expect(payloads[0]?.isError).toBe(true); + expect(payloads.some((payload) => payload.text?.includes("Edit"))).toBe(false); + expect(payloads.some((payload) => payload.text?.includes("missing"))).toBe(false); + }); + + it("keeps mutating tool warnings when assistant error artifacts are not user-facing", () => { + const payloads = buildPayloads({ + assistantTexts: [errorJson], + lastAssistant: makeAssistant({}), + lastToolError: { toolName: "edit", error: "file missing" }, + didSendDeterministicApprovalPrompt: true, + sessionKey: "agent:main:telegram:direct:u123", + }); + + expectSingleToolErrorPayload(payloads, { + title: "Edit", + absentDetail: "missing", + }); + }); + it("suppresses pretty-printed error JSON that differs from the errorMessage", () => { const payloads = buildPayloads({ assistantTexts: [errorJsonPretty], diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 886930a3fda..693c41508bc 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -127,6 +127,7 @@ function shouldIncludeToolErrorDetails(params: { function resolveToolErrorWarningPolicy(params: { lastToolError: ToolErrorSummary; hasUserFacingReply: boolean; + hasUserFacingErrorReply: boolean; hasUserFacingFailureAcknowledgement: boolean; suppressToolErrors: boolean; suppressToolErrorWarnings?: boolean; @@ -152,7 +153,7 @@ function resolveToolErrorWarningPolicy(params: { params.lastToolError.mutatingAction ?? isLikelyMutatingToolName(params.lastToolError.toolName); if (isMutatingToolError) { return { - showWarning: !params.hasUserFacingFailureAcknowledgement, + showWarning: !params.hasUserFacingErrorReply && !params.hasUserFacingFailureAcknowledgement, includeDetails, }; } @@ -363,6 +364,7 @@ export function buildEmbeddedRunPayloads(params: { ).filter((text) => !shouldSuppressRawErrorText(text)); let hasUserFacingAssistantReply = false; + const hasUserFacingErrorReply = replyItems.some((item) => item.isError === true); let hasUserFacingFailureAcknowledgement = false; for (const text of answerTexts) { const { @@ -394,6 +396,7 @@ export function buildEmbeddedRunPayloads(params: { const warningPolicy = resolveToolErrorWarningPolicy({ lastToolError: params.lastToolError, hasUserFacingReply: hasUserFacingAssistantReply, + hasUserFacingErrorReply, hasUserFacingFailureAcknowledgement, suppressToolErrors: Boolean(params.config?.messages?.suppressToolErrors), suppressToolErrorWarnings: params.suppressToolErrorWarnings,