diff --git a/src/gateway/live-chat-projector.ts b/src/gateway/live-chat-projector.ts index 72faadc77d82..f0e432d9d1c8 100644 --- a/src/gateway/live-chat-projector.ts +++ b/src/gateway/live-chat-projector.ts @@ -1,3 +1,4 @@ +import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; // Gateway live chat projector. // Converts streaming assistant events into display-safe live chat text. import { stripInternalRuntimeContext } from "../agents/internal-runtime-context.js"; @@ -36,7 +37,7 @@ function capLiveAssistantBuffer(text: string): string { if (text.length <= MAX_LIVE_CHAT_BUFFER_CHARS) { return text; } - return text.slice(-MAX_LIVE_CHAT_BUFFER_CHARS); + return sliceUtf16Safe(text, -MAX_LIVE_CHAT_BUFFER_CHARS); } /** Merges assistant full-text and delta events into a capped live buffer. */ diff --git a/src/gateway/server-chat.stream-text-merge.test.ts b/src/gateway/server-chat.stream-text-merge.test.ts index 846bc4decff8..2e287f546d85 100644 --- a/src/gateway/server-chat.stream-text-merge.test.ts +++ b/src/gateway/server-chat.stream-text-merge.test.ts @@ -93,4 +93,15 @@ describe("server chat stream text merge", () => { expect(result).toHaveLength(MAX_LIVE_CHAT_BUFFER_CHARS); expect(result.endsWith("bbbb")).toBe(true); }); + + it("does not start the capped tail with the low half of a surrogate pair", () => { + const safeTail = "y".repeat(MAX_LIVE_CHAT_BUFFER_CHARS - 1); + const result = resolveMergedAssistantText({ + previousText: "", + nextText: `x🚀${safeTail}`, + nextDelta: "", + }); + + expect(result).toBe(safeTail); + }); });