Skip to content

Commit c3d567d

Browse files
fix(memory): snippets split emoji when truncated (#102478)
* fix(memory): snippets split emoji when truncated * refactor(memory): reuse shared UTF-16 truncation --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 9aa19fe commit c3d567d

4 files changed

Lines changed: 48 additions & 3 deletions

File tree

packages/memory-host-sdk/src/host/read-file-shared.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,40 @@ describe("memory read result slicing", () => {
3434
lines: 2,
3535
});
3636
});
37+
38+
it("does not split surrogate pairs when truncating a single line", () => {
39+
expect(
40+
buildMemoryReadResultFromSlice({
41+
selectedLines: ["abc🤖tail"],
42+
relPath: "memory/test.md",
43+
startLine: 1,
44+
maxChars: 4,
45+
suggestReadFallback: true,
46+
}),
47+
).toEqual({
48+
text: "abc\n\n[More content available. Requested excerpt exceeded the default maxChars budget. If you need the full raw line, use read on the source file.]",
49+
path: "memory/test.md",
50+
from: 1,
51+
lines: 1,
52+
truncated: true,
53+
});
54+
});
55+
56+
it("keeps the continuation notice when a leading surrogate pair is dropped", () => {
57+
expect(
58+
buildMemoryReadResultFromSlice({
59+
selectedLines: ["🤖tail"],
60+
relPath: "memory/test.md",
61+
startLine: 1,
62+
maxChars: 1,
63+
suggestReadFallback: true,
64+
}),
65+
).toEqual({
66+
text: "\n\n[More content available. Requested excerpt exceeded the default maxChars budget. If you need the full raw line, use read on the source file.]",
67+
path: "memory/test.md",
68+
from: 1,
69+
lines: 1,
70+
truncated: true,
71+
});
72+
});
3773
});

packages/memory-host-sdk/src/host/read-file-shared.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Memory Host SDK module implements read file shared behavior.
2+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
23
import type { MemoryReadResult } from "./types.js";
34

45
// Shared memory-file read result shaping and truncation notices.
@@ -48,7 +49,7 @@ function fitLinesToCharBudget(params: { lines: string[]; maxChars: number }): {
4849
}
4950

5051
return {
51-
text: text.slice(0, maxChars),
52+
text: truncateUtf16Safe(text, maxChars),
5253
includedLines: 1,
5354
hardTruncatedSingleLine: true,
5455
};
@@ -85,7 +86,7 @@ export function buildMemoryReadResultFromSlice(params: {
8586
: undefined;
8687
const truncated = charCapTruncated || moreSourceLinesRemain;
8788
const text =
88-
truncated && fitted.text
89+
truncated && (fitted.text || fitted.hardTruncatedSingleLine)
8990
? `${fitted.text}${buildContinuationNotice({
9091
nextFrom,
9192
suggestReadFallback: fitted.hardTruncatedSingleLine && params.suggestReadFallback,

packages/memory-host-sdk/src/host/response-snippet.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ describe("readResponseTextSnippet", () => {
3535
expect(canceled).toBe(true);
3636
});
3737

38+
it("does not split surrogate pairs when truncating text snippets", async () => {
39+
await expect(readResponseTextSnippet(new Response("abc🤖tail"), { maxChars: 4 })).resolves.toBe(
40+
"abc... [truncated]",
41+
);
42+
});
43+
3844
it("cancels snippet body reads when the caller signal aborts", async () => {
3945
let canceled = false;
4046
const response = stallingResponse(() => {

packages/memory-host-sdk/src/host/response-snippet.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// Memory Host SDK module implements response snippet behavior.
2+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
3+
24
const DEFAULT_ERROR_BODY_MAX_BYTES = 8 * 1024;
35
const DEFAULT_ERROR_BODY_MAX_CHARS = 1_000;
46
const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024 * 1024;
@@ -41,7 +43,7 @@ export async function readResponseTextSnippet(
4143
return "";
4244
}
4345
if (prefix.truncated || collapsed.length > maxChars) {
44-
return `${collapsed.slice(0, maxChars)}${TRUNCATED_SUFFIX}`;
46+
return `${truncateUtf16Safe(collapsed, maxChars)}${TRUNCATED_SUFFIX}`;
4547
}
4648
return collapsed;
4749
}

0 commit comments

Comments
 (0)