diff --git a/src/infra/session-cost-usage.test.ts b/src/infra/session-cost-usage.test.ts index 906646a98da2..ccd22934d540 100644 --- a/src/infra/session-cost-usage.test.ts +++ b/src/infra/session-cost-usage.test.ts @@ -2993,6 +2993,25 @@ example expect(logs?.[0]?.content).toBe("hello there"); }); + it("does not split surrogate pairs when truncating session log content", async () => { + const root = await makeSessionCostRoot("logs-utf16"); + const sessionFile = path.join(root, "session.jsonl"); + const content = "x".repeat(1999) + "🚀tail"; + await fs.writeFile( + sessionFile, + JSON.stringify({ + type: "message", + timestamp: "2026-02-21T17:47:00.000Z", + message: { role: "assistant", content }, + }), + "utf-8", + ); + + const logs = await loadSessionLogs({ sessionFile }); + + expect(logs?.[0]?.content).toBe(`${"x".repeat(1999)}…`); + }); + it("normalizes malformed log timestamps with the transcript timestamp rules", async () => { const root = await makeSessionCostRoot("logs-malformed-timestamp"); const sessionsDir = path.join(root, "agents", "main", "sessions"); diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts index 773fbb11c613..48700eaf18d0 100644 --- a/src/infra/session-cost-usage.ts +++ b/src/infra/session-cost-usage.ts @@ -4,6 +4,7 @@ import path from "node:path"; import readline from "node:readline"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { NormalizedUsage, UsageLike } from "../agents/usage.js"; import { normalizeUsage } from "../agents/usage.js"; import { stripInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js"; @@ -2749,10 +2750,10 @@ export async function loadSessionLogs(params: { continue; } - // Truncate very long content + // Truncate very long content. const maxLen = 2000; if (content.length > maxLen) { - content = content.slice(0, maxLen) + "…"; + content = truncateUtf16Safe(content, maxLen) + "…"; } // Get timestamp