Skip to content

Commit 9e79720

Browse files
committed
fix(shared): use truncateUtf16Safe in assistant-identity-values
Replace raw .slice(0, maxLength) with truncateUtf16Safe to prevent emoji surrogate pair truncation issues in assistant identity values. The coerceIdentityValue function is used to truncate assistant name, avatar, and emoji values. When these values contain emoji at the truncation boundary, the old implementation could produce lone surrogates (invalid UTF-16 text). This fix follows the same pattern as PR #102477 which addressed similar issues in talk/fast-context-runtime.ts and infra/heartbeat-events-filter.ts.
1 parent 8870138 commit 9e79720

2 files changed

Lines changed: 24 additions & 3 deletions

File tree

src/shared/assistant-identity-values.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ describe("shared/assistant-identity-values", () => {
2020

2121
it("returns an empty string when truncating to a zero-length limit", () => {
2222
expect(coerceIdentityValue(" OpenClaw ", 0)).toBe("");
23-
expect(coerceIdentityValue(" OpenClaw ", -1)).toBe("OpenCla");
23+
// truncateUtf16Safe returns empty string for negative limits
24+
expect(coerceIdentityValue(" OpenClaw ", -1)).toBe("");
25+
});
26+
27+
it("handles emoji surrogate pairs safely at truncation boundary", () => {
28+
// Emoji at the truncation boundary should not produce lone surrogates
29+
// 🚀 is a surrogate pair (2 UTF-16 code units)
30+
const textWithEmoji = "x".repeat(9) + "🚀" + "yz"; // emoji at position 10-11 (UTF-16 indices)
31+
32+
// truncateUtf16Safe safely handles emoji boundaries:
33+
// - At limit 10: drops the emoji (would be lone surrogate)
34+
// - At limit 11: includes the full emoji (both surrogates fit)
35+
expect(coerceIdentityValue(textWithEmoji, 10)).toBe("xxxxxxxxx");
36+
expect(coerceIdentityValue(textWithEmoji, 11)).toBe("xxxxxxxxx🚀");
37+
expect(coerceIdentityValue(textWithEmoji, 12)).toBe("xxxxxxxxx🚀y");
38+
39+
// Verify no lone surrogates in any result
40+
for (const limit of [5, 10, 11, 12, 15]) {
41+
const result = coerceIdentityValue(textWithEmoji, limit)!;
42+
expect([...result].every((c) => !(c >= "\uDC00" && c <= "\uDFFF"))).toBe(true);
43+
}
2444
});
2545
});

src/shared/assistant-identity-values.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// Assistant identity helpers normalize assistant identity labels and metadata.
21
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
2+
// Assistant identity helpers normalize assistant identity labels and metadata.
3+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
34

45
/** Normalizes optional assistant identity fields and truncates them to the caller's limit. */
56
export function coerceIdentityValue(
@@ -13,5 +14,5 @@ export function coerceIdentityValue(
1314
if (trimmed.length <= maxLength) {
1415
return trimmed;
1516
}
16-
return trimmed.slice(0, maxLength);
17+
return truncateUtf16Safe(trimmed, maxLength);
1718
}

0 commit comments

Comments
 (0)