From 03bec49299446162015a531a125c6c0cf7901513 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Fri, 23 Jan 2026 01:03:54 +0800 Subject: [PATCH] fix: sanitize tool call text in sessions-helpers extractAssistantText Adds sanitization to extractAssistantText in sessions-helpers.ts to prevent tool call text from leaking to users. Previously, messages retrieved from chat history via sessions-helpers.ts could expose: - Minimax XML tool calls (...) - Downgraded tool call markers ([Tool Call: name (ID: ...)]) - Thinking tags (...) This fix: - Exports the stripping functions from pi-embedded-utils.ts - Adds a new sanitizeTextContent helper in sessions-helpers.ts - Updates extractAssistantText to sanitize before returning - Updates extractMessageText in commands-subagents.ts to sanitize Fixes #1269 Co-Authored-By: Claude --- src/agents/pi-embedded-utils.ts | 6 ++--- src/agents/tools/sessions-helpers.ts | 27 ++++++++++++++++++---- src/auto-reply/reply/commands-subagents.ts | 11 ++++++--- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index 82020a149..1a392c2f1 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -9,7 +9,7 @@ import { formatToolDetail, resolveToolDisplay } from "./tool-display.js"; * - ... blocks * - closing tags */ -function stripMinimaxToolCallXml(text: string): string { +export function stripMinimaxToolCallXml(text: string): string { if (!text) return text; if (!/minimax:tool_call/i.test(text)) return text; @@ -28,7 +28,7 @@ function stripMinimaxToolCallXml(text: string): string { * downgraded to text blocks like `[Tool Call: name (ID: ...)]`. These should * not be shown to users. */ -function stripDowngradedToolCallText(text: string): string { +export function stripDowngradedToolCallText(text: string): string { if (!text) return text; if (!/\[Tool (?:Call|Result)/i.test(text)) return text; @@ -165,7 +165,7 @@ function stripDowngradedToolCallText(text: string): string { * This is a safety net for cases where the model outputs tags * that slip through other filtering mechanisms. */ -function stripThinkingTagsFromText(text: string): string { +export function stripThinkingTagsFromText(text: string): string { if (!text) return text; // Quick check to avoid regex overhead when no tags present. if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text; diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index a7f94b63b..7af647080 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -1,4 +1,10 @@ import type { ClawdbotConfig } from "../../config/config.js"; +import { sanitizeUserFacingText } from "../pi-embedded-helpers.js"; +import { + stripDowngradedToolCallText, + stripMinimaxToolCallXml, + stripThinkingTagsFromText, +} from "../pi-embedded-utils.js"; import { normalizeMainKey } from "../../routing/session-key.js"; export type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other"; @@ -100,6 +106,16 @@ export function stripToolMessages(messages: unknown[]): unknown[] { }); } +/** + * Sanitize text content to strip tool call markers and thinking tags. + * This ensures user-facing text doesn't leak internal tool representations. + */ +export function sanitizeTextContent(text: string): string { + return stripThinkingTagsFromText( + stripDowngradedToolCallText(stripMinimaxToolCallXml(text)), + ).trim(); +} + export function extractAssistantText(message: unknown): string | undefined { if (!message || typeof message !== "object") return undefined; if ((message as { role?: unknown }).role !== "assistant") return undefined; @@ -110,10 +126,13 @@ export function extractAssistantText(message: unknown): string | undefined { if (!block || typeof block !== "object") continue; if ((block as { type?: unknown }).type !== "text") continue; const text = (block as { text?: unknown }).text; - if (typeof text === "string" && text.trim()) { - chunks.push(text); + if (typeof text === "string") { + const sanitized = sanitizeTextContent(text); + if (sanitized) { + chunks.push(sanitized); + } } } - const joined = chunks.join("").trim(); - return joined ? joined : undefined; + const joined = chunks.join("\n").trim(); + return joined ? sanitizeUserFacingText(joined) : undefined; } diff --git a/src/auto-reply/reply/commands-subagents.ts b/src/auto-reply/reply/commands-subagents.ts index a1e33c642..7122dc01b 100644 --- a/src/auto-reply/reply/commands-subagents.ts +++ b/src/auto-reply/reply/commands-subagents.ts @@ -7,6 +7,7 @@ import { extractAssistantText, resolveInternalSessionKey, resolveMainSessionAlias, + sanitizeTextContent, stripToolMessages, } from "../../agents/tools/sessions-helpers.js"; import type { SubagentRunRecord } from "../../agents/subagent-registry.js"; @@ -110,7 +111,8 @@ function extractMessageText(message: ChatMessage): { role: string; text: string const role = typeof message.role === "string" ? message.role : ""; const content = message.content; if (typeof content === "string") { - const normalized = normalizeMessageText(content); + const sanitized = sanitizeTextContent(content); + const normalized = normalizeMessageText(sanitized); return normalized ? { role, text: normalized } : null; } if (!Array.isArray(content)) return null; @@ -119,8 +121,11 @@ function extractMessageText(message: ChatMessage): { role: string; text: string if (!block || typeof block !== "object") continue; if ((block as { type?: unknown }).type !== "text") continue; const text = (block as { text?: unknown }).text; - if (typeof text === "string" && text.trim()) { - chunks.push(text); + if (typeof text === "string") { + const sanitized = sanitizeTextContent(text); + if (sanitized) { + chunks.push(sanitized); + } } } const joined = normalizeMessageText(chunks.join(" "));