From f786efddcd88445d6678124341a1c939d5a41bd8 Mon Sep 17 00:00:00 2001 From: lzyyzznl Date: Thu, 9 Jul 2026 20:05:51 +0800 Subject: [PATCH] fix(cron): keep bounded diagnostics UTF-16 safe (#102624) * fix(cron): use truncateUtf16Safe for diagnostic entry and summary truncation * test(cron): cover UTF-16 diagnostic boundaries --------- Co-authored-by: Peter Steinberger --- src/cron/run-diagnostics.test.ts | 23 +++++++++++++++++++++++ src/cron/run-diagnostics.ts | 5 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/cron/run-diagnostics.test.ts b/src/cron/run-diagnostics.test.ts index 39f3f8a89df..3e28ac3bd0d 100644 --- a/src/cron/run-diagnostics.test.ts +++ b/src/cron/run-diagnostics.test.ts @@ -31,6 +31,29 @@ describe("cron run diagnostics", () => { expect(diagnostics?.summary).toHaveLength(2_000); }); + it("keeps bounded diagnostic text valid at UTF-16 boundaries", () => { + const diagnostics = normalizeCronRunDiagnostics({ + summary: `${"s".repeat(1_998)}😀tail`, + entries: [ + { + ts: 1, + source: "exec", + severity: "error", + message: `${"m".repeat(998)}😀tail`, + }, + ], + }); + + expect(diagnostics?.summary).toBe(`${"s".repeat(1_998)}…`); + expect(diagnostics?.entries[0]).toEqual({ + ts: 1, + source: "exec", + severity: "error", + message: `${"m".repeat(998)}…`, + truncated: true, + }); + }); + it("preserves later terminal diagnostics when capping entries", () => { const diagnostics = normalizeCronRunDiagnostics({ entries: [ diff --git a/src/cron/run-diagnostics.ts b/src/cron/run-diagnostics.ts index d94829d606a..5f72a1d039f 100644 --- a/src/cron/run-diagnostics.ts +++ b/src/cron/run-diagnostics.ts @@ -1,5 +1,6 @@ /** Builds bounded, redacted diagnostics for cron run logs and UI surfaces. */ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { isToolAllowedByPolicyName } from "../agents/tool-policy-match.js"; import { normalizeToolName as normalizePolicyToolName } from "../agents/tool-policy.js"; import { getReplyPayloadMetadata } from "../auto-reply/reply-payload.js"; @@ -101,7 +102,7 @@ function normalizeDiagnosticMessage(value: unknown): { message?: string; truncat if (redacted.length <= MAX_ENTRY_CHARS) { return { message: redacted }; } - return { message: `${redacted.slice(0, MAX_ENTRY_CHARS - 1)}…`, truncated: true }; + return { message: `${truncateUtf16Safe(redacted, MAX_ENTRY_CHARS - 1)}…`, truncated: true }; } function trimSummary(value: string | undefined): string | undefined { @@ -112,7 +113,7 @@ function trimSummary(value: string | undefined): string | undefined { if (normalized.length <= MAX_SUMMARY_CHARS) { return normalized; } - return `${normalized.slice(0, MAX_SUMMARY_CHARS - 1)}…`; + return `${truncateUtf16Safe(normalized, MAX_SUMMARY_CHARS - 1)}…`; } /** Returns the operator-facing summary for persisted cron diagnostics. */