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 ea33538add
- Parity gate run 25086474949: success on ea33538add
This commit is contained in:
Vincent Koc
2026-04-28 21:31:51 -07:00
committed by GitHub
parent 0fc3032325
commit 8c886e9438
3 changed files with 34 additions and 1 deletions

View File

@@ -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.

View File

@@ -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],

View File

@@ -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,