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