diff --git a/CHANGELOG.md b/CHANGELOG.md
index f83a95475..03350f799 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ Docs: https://docs.clawd.bot
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
+- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
- Agents: surface concrete API error details instead of generic AI service errors.
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
diff --git a/src/agents/tools/sessions-helpers.test.ts b/src/agents/tools/sessions-helpers.test.ts
new file mode 100644
index 000000000..cc6d1fbc7
--- /dev/null
+++ b/src/agents/tools/sessions-helpers.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest";
+
+import { extractAssistantText, sanitizeTextContent } from "./sessions-helpers.js";
+
+describe("sanitizeTextContent", () => {
+ it("strips minimax tool call XML and downgraded markers", () => {
+ const input =
+ 'Hello payload ' +
+ "[Tool Call: foo (ID: 1)] world";
+ const result = sanitizeTextContent(input).trim();
+ expect(result).toBe("Hello world");
+ expect(result).not.toContain("invoke");
+ expect(result).not.toContain("Tool Call");
+ });
+
+ it("strips thinking tags", () => {
+ const input = "Before secret after";
+ const result = sanitizeTextContent(input).trim();
+ expect(result).toBe("Before after");
+ });
+});
+
+describe("extractAssistantText", () => {
+ it("sanitizes blocks without injecting newlines", () => {
+ const message = {
+ role: "assistant",
+ content: [
+ { type: "text", text: "Hi " },
+ { type: "text", text: "secretthere" },
+ ],
+ };
+ expect(extractAssistantText(message)).toBe("Hi there");
+ });
+});
diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts
index 7af647080..c4ec120ed 100644
--- a/src/agents/tools/sessions-helpers.ts
+++ b/src/agents/tools/sessions-helpers.ts
@@ -111,9 +111,8 @@ export function stripToolMessages(messages: unknown[]): unknown[] {
* This ensures user-facing text doesn't leak internal tool representations.
*/
export function sanitizeTextContent(text: string): string {
- return stripThinkingTagsFromText(
- stripDowngradedToolCallText(stripMinimaxToolCallXml(text)),
- ).trim();
+ if (!text) return text;
+ return stripThinkingTagsFromText(stripDowngradedToolCallText(stripMinimaxToolCallXml(text)));
}
export function extractAssistantText(message: unknown): string | undefined {
@@ -128,11 +127,11 @@ export function extractAssistantText(message: unknown): string | undefined {
const text = (block as { text?: unknown }).text;
if (typeof text === "string") {
const sanitized = sanitizeTextContent(text);
- if (sanitized) {
+ if (sanitized.trim()) {
chunks.push(sanitized);
}
}
}
- const joined = chunks.join("\n").trim();
+ const joined = chunks.join("").trim();
return joined ? sanitizeUserFacingText(joined) : undefined;
}
diff --git a/src/auto-reply/reply/commands-subagents.test.ts b/src/auto-reply/reply/commands-subagents.test.ts
new file mode 100644
index 000000000..eaf7c3026
--- /dev/null
+++ b/src/auto-reply/reply/commands-subagents.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it } from "vitest";
+
+import { extractMessageText } from "./commands-subagents.js";
+
+describe("extractMessageText", () => {
+ it("preserves user text that looks like tool call markers", () => {
+ const message = {
+ role: "user",
+ content: "Here [Tool Call: foo (ID: 1)] ok",
+ };
+ const result = extractMessageText(message);
+ expect(result?.text).toContain("[Tool Call: foo (ID: 1)]");
+ });
+
+ it("sanitizes assistant tool call markers", () => {
+ const message = {
+ role: "assistant",
+ content: "Here [Tool Call: foo (ID: 1)] ok",
+ };
+ const result = extractMessageText(message);
+ expect(result?.text).toBe("Here ok");
+ });
+});
diff --git a/src/auto-reply/reply/commands-subagents.ts b/src/auto-reply/reply/commands-subagents.ts
index 7122dc01b..5009a30fa 100644
--- a/src/auto-reply/reply/commands-subagents.ts
+++ b/src/auto-reply/reply/commands-subagents.ts
@@ -107,12 +107,14 @@ function normalizeMessageText(text: string) {
return text.replace(/\s+/g, " ").trim();
}
-function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
+export function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
const role = typeof message.role === "string" ? message.role : "";
+ const shouldSanitize = role === "assistant";
const content = message.content;
if (typeof content === "string") {
- const sanitized = sanitizeTextContent(content);
- const normalized = normalizeMessageText(sanitized);
+ const normalized = normalizeMessageText(
+ shouldSanitize ? sanitizeTextContent(content) : content,
+ );
return normalized ? { role, text: normalized } : null;
}
if (!Array.isArray(content)) return null;
@@ -122,9 +124,9 @@ function extractMessageText(message: ChatMessage): { role: string; text: string
if ((block as { type?: unknown }).type !== "text") continue;
const text = (block as { text?: unknown }).text;
if (typeof text === "string") {
- const sanitized = sanitizeTextContent(text);
- if (sanitized) {
- chunks.push(sanitized);
+ const value = shouldSanitize ? sanitizeTextContent(text) : text;
+ if (value.trim()) {
+ chunks.push(value);
}
}
}