From e8a4d5d9bd578509a2f497ec231d7d25167b8e9d Mon Sep 17 00:00:00 2001 From: justinhuangcode Date: Mon, 23 Feb 2026 17:30:59 +0000 Subject: [PATCH] fix(discord): strip reasoning tags from partial stream preview When streamMode is "partial", reasoning/thinking block content can leak into the Discord draft preview because the partial text is forwarded to the draft stream without filtering. Apply `stripReasoningTagsFromText` before updating the draft and skip pure-reasoning messages (those starting with "Reasoning:\n") so internal thinking traces never reach the user-visible preview. Fixes #24532 Co-Authored-By: Claude Opus 4.6 --- .../monitor/message-handler.process.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index 1c41fef76..60966cff3 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -30,6 +30,7 @@ import { convertMarkdownTables } from "../../markdown/tables.js"; import { buildAgentSessionKey } from "../../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../../routing/session-key.js"; import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js"; +import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js"; import { truncateUtf16Safe } from "../../utils.js"; import { chunkDiscordTextWithMode } from "../chunk.js"; import { resolveDiscordDraftStreamingChunking } from "../draft-chunking.js"; @@ -485,7 +486,13 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) if (!draftStream || !text) { return; } - if (text === lastPartialText) { + // Strip reasoning/thinking tags that may leak through the stream. + const cleaned = stripReasoningTagsFromText(text, { mode: "strict", trim: "both" }); + // Skip pure-reasoning messages (e.g. "Reasoning:\n…") that contain no answer text. + if (!cleaned || cleaned.startsWith("Reasoning:\n")) { + return; + } + if (cleaned === lastPartialText) { return; } hasStreamedMessage = true; @@ -493,30 +500,30 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) // Keep the longer preview to avoid visible punctuation flicker. if ( lastPartialText && - lastPartialText.startsWith(text) && - text.length < lastPartialText.length + lastPartialText.startsWith(cleaned) && + cleaned.length < lastPartialText.length ) { return; } - lastPartialText = text; - draftStream.update(text); + lastPartialText = cleaned; + draftStream.update(cleaned); return; } - let delta = text; - if (text.startsWith(lastPartialText)) { - delta = text.slice(lastPartialText.length); + let delta = cleaned; + if (cleaned.startsWith(lastPartialText)) { + delta = cleaned.slice(lastPartialText.length); } else { // Streaming buffer reset (or non-monotonic stream). Start fresh. draftChunker?.reset(); draftText = ""; } - lastPartialText = text; + lastPartialText = cleaned; if (!delta) { return; } if (!draftChunker) { - draftText = text; + draftText = cleaned; draftStream.update(draftText); return; }