mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(gateway): keep chat history display text truncation surrogate-safe (#102470)
* fix(gateway): keep chat history display text truncation surrogate-safe truncateChatHistoryText uses slice(0, N) to truncate chat history text for the Control UI. When the truncation boundary falls inside a UTF-16 surrogate pair (emoji, CJK extended), the resulting string contains a dangling surrogate that browsers render as U+FFFD (�). Replace slice(0, maxChars) with truncateUtf16Safe(text, maxChars) so the truncation point always falls on a complete code-point boundary. * test(gateway): cover chat display UTF-16 boundary --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user