fix(matrix): truncate thread starter body on code-point boundaries (#97121)

Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.

Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.

Salvages the original fix from #96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
This commit is contained in:
Bartok 2026-06-26 19:27:38 -04:00 committed by GitHub
parent 6db4624f43
commit 289865b392
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 19 additions and 1 deletions

View file

@ -23,6 +23,23 @@ describe("matrix thread context", () => {
).toBe("Thread starter body");
});
it("truncates long thread starter bodies on code-point boundaries", () => {
const summary = summarizeMatrixThreadStarterEvent({
event_id: "$root",
sender: "@alice:example.org",
type: "m.room.message",
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
// 496 "a" + astral emoji (surrogate pair at units 496-497) + tail.
// A raw slice(0, 497) would cut the pair and leave a lone high surrogate.
body: `${"a".repeat(496)}\u{1F600}bcd`,
},
} as MatrixRawEvent);
expect(summary).toBe(`${"a".repeat(496)}...`);
expect(summary && /[\uD800-\uDFFF]/.test(summary)).toBe(false);
});
it("marks media-only thread starter events instead of returning bare filenames", () => {
expect(
summarizeMatrixThreadStarterEvent({

View file

@ -1,4 +1,5 @@
// Matrix plugin module implements thread context behavior.
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { MatrixClient } from "../sdk.js";
import { summarizeMatrixMessageContextEvent, trimMatrixMaybeString } from "./context-summary.js";
import type { MatrixRawEvent } from "./types.js";
@ -17,7 +18,7 @@ function truncateThreadStarterBody(value: string): string {
if (value.length <= MAX_THREAD_STARTER_BODY_LENGTH) {
return value;
}
return `${value.slice(0, MAX_THREAD_STARTER_BODY_LENGTH - 3)}...`;
return `${sliceUtf16Safe(value, 0, MAX_THREAD_STARTER_BODY_LENGTH - 3)}...`;
}
export function summarizeMatrixThreadStarterEvent(event: MatrixRawEvent): string | undefined {