fix: suppress reasoning payload leakage in whatsapp replies

This commit is contained in:
Peter Steinberger
2026-02-25 01:36:19 +00:00
parent a177b10b79
commit 039713c3e7
3 changed files with 68 additions and 0 deletions

View File

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

View File

@@ -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();

View File

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