fix(talk): use truncateUtf16Safe for LLM-prompt-facing text truncation (#102477)

* fix(talk): use truncateUtf16Safe for snippet text truncation

fast-context-runtime.ts normalizeSnippet and heartbeat-events-filter.ts
buildExecEventPrompt used raw .slice(0, N), which can produce lone
surrogates when truncation boundaries cross emoji surrogate pairs.

* test(talk): cover UTF-16 snippet boundary

---------

Co-authored-by: hailory <hailory@xydigit.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
xydt-tanshanshan
2026-07-09 15:39:38 +08:00
committed by GitHub
parent e5aefd5656
commit f0d4d4f2fc
2 changed files with 42 additions and 1 deletions
+40
View File
@@ -79,4 +79,44 @@ describe("resolveRealtimeVoiceFastContextConsult", () => {
);
expect(vi.getTimerCount()).toBe(0);
});
it("does not split a surrogate pair at the fast-context snippet limit", async () => {
const safePrefix = "x".repeat(698);
mocks.getActiveMemorySearchManager.mockResolvedValue({
manager: {
search: vi.fn().mockResolvedValue([
{
path: "memory/test.md",
startLine: 1,
endLine: 1,
snippet: `${safePrefix}🚀tail`,
source: "memory",
score: 1,
},
]),
},
});
const result = await resolveRealtimeVoiceFastContextConsult({
cfg: {},
agentId: "main",
sessionKey: "voice:15550001234",
config: {
enabled: true,
timeoutMs: 1_000,
maxResults: 1,
sources: ["memory"],
fallbackToConsult: true,
},
args: { question: "What do you remember?" },
logger: {},
});
expect(result).toEqual({
handled: true,
result: {
text: expect.stringContaining(`1. [memory] memory/test.md:1-1\n${safePrefix}...`),
},
});
});
});
+2 -1
View File
@@ -6,6 +6,7 @@
* back to the normal consult flow.
*/
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { getActiveMemorySearchManager } from "../plugins/memory-runtime.js";
@@ -69,7 +70,7 @@ function normalizeSnippet(text: string): string {
}
// Keep individual memory snippets bounded so several hits still fit in a
// short realtime response prompt.
return `${normalized.slice(0, MAX_SNIPPET_CHARS - 1).trimEnd()}...`;
return `${truncateUtf16Safe(normalized, MAX_SNIPPET_CHARS - 1).trimEnd()}...`;
}
function buildSearchQuery(args: unknown): string {