Skip to content

Commit 55d7b5b

Browse files
ly-wang19claude
andauthored
fix(matrix): truncate reply context on code-point boundaries (#97471)
truncateReplyBody used a raw value.slice(0, MAX_REPLY_BODY_LENGTH - 3) to shorten long reply bodies. When the cut index fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji), the slice left a lone high surrogate before the ellipsis, which renders as a broken glyph in the reply context shown to the agent. Replace the raw slice with sliceUtf16Safe from the plugin SDK so the truncation never cuts inside a surrogate pair. A normal (non-astral) body is unaffected. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
1 parent e445d61 commit 55d7b5b

2 files changed

Lines changed: 26 additions & 1 deletion

File tree

extensions/matrix/src/matrix/monitor/reply-context.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,30 @@ describe("matrix reply context", () => {
3939
expect(result.endsWith("...")).toBe(true);
4040
});
4141

42+
it("truncates on a code-point boundary without orphaning a surrogate half", () => {
43+
// Body is 496 'a' + 😀 (U+1F600, a surrogate pair at UTF-16 indices 496-497)
44+
// + "bcd". Raw `.slice(0, 497)` would split the emoji and leave a lone high
45+
// surrogate (\uD83D) before the ellipsis. The fix must drop the half emoji.
46+
const body = `${"a".repeat(496)}😀bcd`;
47+
expect(body.length).toBe(501);
48+
const result = summarizeMatrixReplyEvent({
49+
event_id: "$original",
50+
sender: "@alice:example.org",
51+
type: "m.room.message",
52+
origin_server_ts: Date.now(),
53+
content: {
54+
msgtype: "m.text",
55+
body,
56+
},
57+
} as MatrixRawEvent);
58+
if (result === undefined) {
59+
throw new Error("expected truncated reply context");
60+
}
61+
expect(result).toBe(`${"a".repeat(496)}...`);
62+
// No dangling high surrogate should survive the truncation.
63+
expect(result.includes("\uD83D")).toBe(false);
64+
});
65+
4266
it("handles media-only reply events", () => {
4367
expect(
4468
summarizeMatrixReplyEvent({

extensions/matrix/src/matrix/monitor/reply-context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Matrix plugin module implements reply context behavior.
2+
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
23
import type { MatrixClient } from "../sdk.js";
34
import { summarizeMatrixMessageContextEvent, trimMatrixMaybeString } from "./context-summary.js";
45
import type { MatrixRawEvent } from "./types.js";
@@ -16,7 +17,7 @@ function truncateReplyBody(value: string): string {
1617
if (value.length <= MAX_REPLY_BODY_LENGTH) {
1718
return value;
1819
}
19-
return `${value.slice(0, MAX_REPLY_BODY_LENGTH - 3)}...`;
20+
return `${sliceUtf16Safe(value, 0, MAX_REPLY_BODY_LENGTH - 3)}...`;
2021
}
2122

2223
export function summarizeMatrixReplyEvent(event: MatrixRawEvent): string | undefined {

0 commit comments

Comments
 (0)