From 039713c3e7969ced6ccd0b12fee05294297af32f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 25 Feb 2026 01:36:19 +0000 Subject: [PATCH] fix: suppress reasoning payload leakage in whatsapp replies --- CHANGELOG.md | 1 + src/web/auto-reply/deliver-reply.test.ts | 50 ++++++++++++++++++++++++ src/web/auto-reply/deliver-reply.ts | 17 ++++++++ 3 files changed, 68 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb19ccd6bf..32de2c31f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai - Security/Voice Call: add Telnyx webhook replay detection and canonicalize replay-key signature encoding (Base64/Base64URL equivalent forms dedupe together), so duplicate signed webhook deliveries no longer re-trigger side effects. (#25832) Thanks @bmendonca3. - Providers/OpenRouter/Auth profiles: bypass auth-profile cooldown/disable windows for OpenRouter, so provider failures no longer put OpenRouter profiles into local cooldown and stale legacy cooldown markers are ignored in fallback and status selection paths. (#25892) Thanks @alexanderatallah for raising this and @vincentkoc for the fix. - WhatsApp/Web reconnect: treat close status `440` as non-retryable (including string-form status values), stop reconnect loops immediately, and emit operator guidance to relink after resolving session conflicts. (#25858) Thanks @markmusson. +- WhatsApp/Reasoning safety: suppress outbound payloads marked as reasoning and hard-drop text payloads that begin with `Reasoning:` before WhatsApp delivery, preventing hidden thinking blocks from leaking to end users through final-message paths. (#25804, #25214, #24328) - Onboarding/Telegram: keep core-channel onboarding available when plugin registry population is missing by falling back to built-in adapters and continuing wizard setup with actionable recovery guidance. (#25803) Thanks @Suko. - Models/Bedrock auth: normalize additional Bedrock provider aliases (`bedrock`, `aws-bedrock`, `aws_bedrock`, `amazon bedrock`) to canonical `amazon-bedrock`, ensuring auth-mode resolution consistently selects AWS SDK fallback. (#25756) Thanks @fwhite13. - Providers/SiliconFlow: normalize `thinking="off"` to `thinking: null` for `Pro/*` model payloads to avoid provider-side 400 loops and misleading compaction retries. (#25435) Thanks @Zjianru. diff --git a/src/web/auto-reply/deliver-reply.test.ts b/src/web/auto-reply/deliver-reply.test.ts index 24f6e2eb82e..e3dfe6126bb 100644 --- a/src/web/auto-reply/deliver-reply.test.ts +++ b/src/web/auto-reply/deliver-reply.test.ts @@ -70,6 +70,56 @@ const replyLogger = { }; describe("deliverWebReply", () => { + it("suppresses payloads flagged as reasoning", async () => { + const msg = makeMsg(); + + await deliverWebReply({ + replyResult: { text: "Reasoning:\n_hidden_", isReasoning: true }, + msg, + maxMediaBytes: 1024 * 1024, + textLimit: 200, + replyLogger, + skipLog: true, + }); + + expect(msg.reply).not.toHaveBeenCalled(); + expect(msg.sendMedia).not.toHaveBeenCalled(); + }); + + it("suppresses payloads that start with reasoning prefix text", async () => { + const msg = makeMsg(); + + await deliverWebReply({ + replyResult: { text: " \n Reasoning:\n_hidden_" }, + msg, + maxMediaBytes: 1024 * 1024, + textLimit: 200, + replyLogger, + skipLog: true, + }); + + expect(msg.reply).not.toHaveBeenCalled(); + expect(msg.sendMedia).not.toHaveBeenCalled(); + }); + + it("does not suppress messages that mention Reasoning: mid-text", async () => { + const msg = makeMsg(); + + await deliverWebReply({ + replyResult: { text: "Intro line\nReasoning: appears in content but is not a prefix" }, + msg, + maxMediaBytes: 1024 * 1024, + textLimit: 200, + replyLogger, + skipLog: true, + }); + + expect(msg.reply).toHaveBeenCalledTimes(1); + expect(msg.reply).toHaveBeenCalledWith( + "Intro line\nReasoning: appears in content but is not a prefix", + ); + }); + it("sends chunked text replies and logs a summary", async () => { const msg = makeMsg(); diff --git a/src/web/auto-reply/deliver-reply.ts b/src/web/auto-reply/deliver-reply.ts index 664e8acee85..7866fea0c8a 100644 --- a/src/web/auto-reply/deliver-reply.ts +++ b/src/web/auto-reply/deliver-reply.ts @@ -12,6 +12,19 @@ import { whatsappOutboundLog } from "./loggers.js"; import type { WebInboundMsg } from "./types.js"; import { elide } from "./util.js"; +const REASONING_PREFIX = "reasoning:"; + +function shouldSuppressReasoningReply(payload: ReplyPayload): boolean { + if (payload.isReasoning === true) { + return true; + } + const text = payload.text; + if (typeof text !== "string") { + return false; + } + return text.trimStart().toLowerCase().startsWith(REASONING_PREFIX); +} + export async function deliverWebReply(params: { replyResult: ReplyPayload; msg: WebInboundMsg; @@ -29,6 +42,10 @@ export async function deliverWebReply(params: { }) { const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params; const replyStarted = Date.now(); + if (shouldSuppressReasoningReply(replyResult)) { + whatsappOutboundLog.debug(`Suppressed reasoning payload to ${msg.from}`); + return; + } const tableMode = params.tableMode ?? "code"; const chunkMode = params.chunkMode ?? "length"; const convertedText = markdownToWhatsApp(