From 0de5d37f92fc1721596a7d1a0197cb23d699329a Mon Sep 17 00:00:00 2001 From: lsr911 Date: Thu, 9 Jul 2026 17:11:27 +0800 Subject: [PATCH] fix(logging): keep bounded log text UTF-16 safe (#102560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(logging): use truncateUtf16Safe in diagnostic log text clamp Replace naive .slice(0, maxChars) with truncateUtf16Safe() in clampDiagnosticLogText() — the core helper used by all diagnostic log output (sanitizeDiagnosticLogText, formatDiagnosticAttributes, etc.). This prevents surrogate pair splitting across the entire diagnostic logging subsystem. Co-Authored-By: Claude * fix(logging): preserve UTF-16 in bounded log text --------- Co-authored-by: Claude Co-authored-by: Peter Steinberger --- src/logging/diagnostic-log-events.test.ts | 17 +++++++++++++++++ src/logging/logger-redaction-behavior.test.ts | 12 ++++++++++++ src/logging/logger.ts | 5 +++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/logging/diagnostic-log-events.test.ts b/src/logging/diagnostic-log-events.test.ts index 58d9cb444f4..14f8ea1484d 100644 --- a/src/logging/diagnostic-log-events.test.ts +++ b/src/logging/diagnostic-log-events.test.ts @@ -143,6 +143,23 @@ describe("diagnostic log events", () => { expect(Object.hasOwn(event, "argsJson")).toBe(false); }); + it("keeps bounded diagnostic messages UTF-16 safe", async () => { + const received: Array> = []; + const unsubscribe = onInternalDiagnosticEvent((event) => { + if (event.type === "log.record") { + received.push(event); + } + }); + const prefix = "x".repeat(4_095); + + getChildLogger({ subsystem: "diagnostic" }).info(`${prefix}😀tail`); + await flushDiagnosticEvents(); + unsubscribe(); + + // The post-redaction bound keeps the first dot from the initial truncation marker. + expect(received.at(-1)?.message).toBe(`${prefix}....(truncated)`); + }); + it("drops sensitive, blocked, and excess log attribute keys without copying large objects", async () => { const received: Array> = []; const unsubscribe = onInternalDiagnosticEvent((evt) => { diff --git a/src/logging/logger-redaction-behavior.test.ts b/src/logging/logger-redaction-behavior.test.ts index 220ef472f2c..6c3ad4c22c3 100644 --- a/src/logging/logger-redaction-behavior.test.ts +++ b/src/logging/logger-redaction-behavior.test.ts @@ -192,6 +192,18 @@ describe("file log redaction", () => { expect(record.message).toBe("request completed"); }); + it("keeps bounded file-log messages UTF-16 safe", () => { + const logPath = logPathTracker.nextPath(); + setLoggerOverride({ level: "info", file: logPath }); + const prefix = "x".repeat(4_095); + + getLogger().info(`${prefix}😀tail`); + + const [line] = fs.readFileSync(logPath, "utf8").trim().split("\n"); + const record = JSON.parse(line ?? "{}") as Record; + expect(record.message).toBe(`${prefix}...(truncated)`); + }); + it("retries hostname resolution after an empty value and caches the first real value", () => { const logPath = logPathTracker.nextPath(); setLoggerOverride({ level: "info", file: logPath }); diff --git a/src/logging/logger.ts b/src/logging/logger.ts index 78c0f839143..099105bc8ac 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { Logger as TsLogger } from "tslog"; import type { OpenClawConfig } from "../config/types.js"; import { @@ -90,7 +91,7 @@ let cachedHostname: string | null = null; type DiagnosticLogAttributes = Record; function clampDiagnosticLogText(value: string, maxChars: number): string { - return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value; + return value.length > maxChars ? `${truncateUtf16Safe(value, maxChars)}...(truncated)` : value; } function sanitizeDiagnosticLogText(value: string, maxChars: number): string { @@ -217,7 +218,7 @@ function getSortedNumericLogArgs(logObj: TsLogRecord): unknown[] { } function clampFileLogText(value: string, maxChars: number): string { - return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value; + return value.length > maxChars ? `${truncateUtf16Safe(value, maxChars)}...(truncated)` : value; } function normalizeFileLogContextValue(value: unknown): string | undefined {