diff --git a/src/gateway/chat-display-projection.ts b/src/gateway/chat-display-projection.ts index fb66d45fe0d3..3936760a9229 100644 --- a/src/gateway/chat-display-projection.ts +++ b/src/gateway/chat-display-projection.ts @@ -4,6 +4,7 @@ import { createHash } from "node:crypto"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE } from "../agents/internal-runtime-context.js"; import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js"; import { isHeartbeatOkResponse, isHeartbeatUserMessage } from "../auto-reply/heartbeat-filter.js"; @@ -57,7 +58,7 @@ function truncateChatHistoryText( return { text, truncated: false }; } return { - text: `${text.slice(0, maxChars)}\n...(truncated)...`, + text: `${truncateUtf16Safe(text, maxChars)}\n...(truncated)...`, truncated: true, }; } diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index 5dc3cb9f727e..69d5f2c32d86 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -660,7 +660,6 @@ describe("waitForAgentJob", () => { expect(agentJobTesting.getAgentRunCacheSize()).toBe(max); agentJobTesting.resetAgentRunCache(); }); - }); describe("augmentChatHistoryWithCanvasBlocks", () => { @@ -824,6 +823,28 @@ describe("injectTimestamp", () => { }); describe("sanitizeChatHistoryMessages", () => { + it("truncates display text without splitting surrogate pairs", () => { + const prefix = "a".repeat(7); + const result = sanitizeChatHistoryMessages( + [ + { + role: "assistant", + content: [{ type: "text", text: `${prefix}😀tail` }], + timestamp: 1, + }, + ], + 8, + ); + + expect(result).toEqual([ + { + role: "assistant", + content: [{ type: "text", text: `${prefix}\n...(truncated)...` }], + timestamp: 1, + }, + ]); + }); + it("redacts base64 audio content blocks from chat history", () => { const data = Buffer.from("voice-bytes").toString("base64"); const result = sanitizeChatHistoryMessages([