From 42fc9e8a92f992c82a06ea52d125cffc7cd19c6f Mon Sep 17 00:00:00 2001 From: lsr911 Date: Tue, 7 Jul 2026 19:10:28 +0800 Subject: [PATCH] fix(acp): use truncateUtf16Safe for event mapper error text truncation (#101535) * fix(acp): use truncateUtf16Safe for event mapper error text truncation .slice(0, 100) on ACP event argument values can split surrogate pairs from emoji or CJK text, producing U+FFFD in error/warning notices shown to operators. Co-Authored-By: Claude * test(acp): cover UTF-16-safe tool titles * refactor(acp): keep event mapper import leaf-only --------- Co-authored-by: Claude Co-authored-by: Peter Steinberger --- src/acp/event-mapper.test.ts | 12 +++++++++++- src/acp/event-mapper.ts | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/acp/event-mapper.test.ts b/src/acp/event-mapper.test.ts index 88650811a53a..8d599b455fd5 100644 --- a/src/acp/event-mapper.test.ts +++ b/src/acp/event-mapper.test.ts @@ -1,6 +1,6 @@ /** Tests ACP tool-call location extraction limits. */ import { describe, expect, it } from "vitest"; -import { extractToolCallLocations } from "./event-mapper.js"; +import { extractToolCallLocations, formatToolTitle } from "./event-mapper.js"; describe("extractToolCallLocations", () => { it("enforces the global node visit cap across nested structures", () => { @@ -18,3 +18,13 @@ describe("extractToolCallLocations", () => { expect(locations).toEqual([{ path: "/tmp/file-0.txt" }, { path: "/tmp/file-1.txt" }]); }); }); + +describe("formatToolTitle", () => { + it("does not split surrogate pairs when truncating argument values", () => { + const title = formatToolTitle("exec", { + command: `${"x".repeat(99)}🚀tail`, + }); + + expect(title).toBe(`exec: command: ${"x".repeat(99)}...`); + }); +}); diff --git a/src/acp/event-mapper.ts b/src/acp/event-mapper.ts index 2862a8ae21cd..3a7b5600d90c 100644 --- a/src/acp/event-mapper.ts +++ b/src/acp/event-mapper.ts @@ -14,6 +14,7 @@ import { normalizeOptionalString, readStringValue, } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; type GatewayAttachment = { type: string; @@ -309,7 +310,7 @@ export function formatToolTitle( } const parts = Object.entries(args).map(([key, value]) => { const raw = typeof value === "string" ? value : JSON.stringify(value); - const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw; + const safe = raw.length > 100 ? `${truncateUtf16Safe(raw, 100)}...` : raw; return `${key}: ${safe}`; }); // Sanitize at the source so session updates and permission requests never