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 {