Skip to content

Commit b581355

Browse files
committed
fix(talk): keep consult result truncation UTF-16 safe
1 parent 632efa2 commit b581355

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

src/talk/consult-question.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import {
66
readSpeakableRealtimeVoiceToolResult,
77
} from "./consult-question.js";
88

9+
function hasLoneSurrogate(value: string): boolean {
10+
for (let index = 0; index < value.length; index += 1) {
11+
const code = value.charCodeAt(index);
12+
if (code >= 0xd800 && code <= 0xdbff) {
13+
const next = value.charCodeAt(index + 1);
14+
if (next < 0xdc00 || next > 0xdfff) {
15+
return true;
16+
}
17+
index += 1;
18+
} else if (code >= 0xdc00 && code <= 0xdfff) {
19+
return true;
20+
}
21+
}
22+
return false;
23+
}
24+
925
describe("realtime voice consult question helpers", () => {
1026
it("reads common provider question fields", () => {
1127
expect(readRealtimeVoiceConsultQuestion({ question: " check status " })).toBe("check status");
@@ -40,4 +56,15 @@ describe("realtime voice consult question helpers", () => {
4056
),
4157
).toBe("abcdefgh [truncated]");
4258
});
59+
60+
it("does not split a boundary emoji in truncated speakable text", () => {
61+
const result = readSpeakableRealtimeVoiceToolResult(
62+
{ text: `${"a".repeat(7)}😀${"b".repeat(20)}` },
63+
{ maxChars: 23 },
64+
);
65+
66+
expect(result).toBe(`${"a".repeat(7)} [truncated]`);
67+
expect(result).toContain("[truncated]");
68+
expect(hasLoneSurrogate(result ?? "")).toBe(false);
69+
});
4370
});

src/talk/consult-question.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* pulling human-readable questions/results out of provider-owned payloads.
66
*/
77
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
8+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
89
import { readTrimmedStringAlias } from "../utils/string-readers.js";
910

1011
const REALTIME_VOICE_CONSULT_QUESTION_STOPWORDS = new Set([
@@ -166,5 +167,5 @@ function limitSpeakableRealtimeVoiceToolResult(
166167
}
167168
// Reserve space for the marker so callers can keep audio replies under a
168169
// provider or UX limit without losing the truncation signal.
169-
return `${trimmed.slice(0, Math.max(0, maxChars - 16)).trimEnd()} [truncated]`;
170+
return `${truncateUtf16Safe(trimmed, Math.max(0, maxChars - 16)).trimEnd()} [truncated]`;
170171
}

0 commit comments

Comments
 (0)