diff --git a/src/process/supervisor/supervisor.test.ts b/src/process/supervisor/supervisor.test.ts index bba18d2d0df1..28b32dbbcedb 100644 --- a/src/process/supervisor/supervisor.test.ts +++ b/src/process/supervisor/supervisor.test.ts @@ -345,21 +345,25 @@ describe("process supervisor", () => { expect(exit.stdout).toBe(""); }); - it("bounds retained stdout and stderr while streaming full chunks", async () => { + it("bounds retained output on UTF-16 boundaries while streaming full chunks", async () => { const adapter = createStubChildAdapter(); createChildAdapterMock.mockResolvedValue(adapter); const supervisor = createProcessSupervisor(); let streamedStdout = ""; let streamedStderr = ""; - const stdoutChunk = `${"a".repeat(300)}stdout-tail`; - const stderrChunk = `${"b".repeat(300)}stderr-tail`; + const maxCapturedOutputChars = 256; + const stdoutMarker = `[openclaw: captured stdout truncated to last ${maxCapturedOutputChars} chars]\n`; + const stderrMarker = `[openclaw: captured stderr truncated to last ${maxCapturedOutputChars} chars]\n`; + const retainedChars = maxCapturedOutputChars - stdoutMarker.length - 1; + const stdoutChunk = `${"a".repeat(stdoutMarker.length)}😀${"s".repeat(retainedChars)}`; + const stderrChunk = `${"b".repeat(stderrMarker.length)}😀${"e".repeat(retainedChars)}`; const run = await spawnChild(supervisor, { sessionId: "s-capture-cap", argv: createWriteStdoutArgv(stdoutChunk), timeoutMs: 1_000, stdinMode: "pipe-closed", - maxCapturedOutputChars: 256, + maxCapturedOutputChars, onStdout: (chunk) => { streamedStdout += chunk; }, @@ -375,11 +379,7 @@ describe("process supervisor", () => { const exit = await run.wait(); expect(streamedStdout).toBe(stdoutChunk); expect(streamedStderr).toBe(stderrChunk); - expect(exit.stdout.length).toBeLessThanOrEqual(256); - expect(exit.stderr.length).toBeLessThanOrEqual(256); - expect(exit.stdout).toContain("captured stdout truncated"); - expect(exit.stderr).toContain("captured stderr truncated"); - expect(exit.stdout.endsWith("stdout-tail")).toBe(true); - expect(exit.stderr.endsWith("stderr-tail")).toBe(true); + expect(exit.stdout).toBe(`${stdoutMarker}${"s".repeat(retainedChars)}`); + expect(exit.stderr).toBe(`${stderrMarker}${"e".repeat(retainedChars)}`); }); }); diff --git a/src/process/supervisor/supervisor.ts b/src/process/supervisor/supervisor.ts index cacd82d372bc..984c3378b903 100644 --- a/src/process/supervisor/supervisor.ts +++ b/src/process/supervisor/supervisor.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import { performance } from "node:perf_hooks"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { getShellConfig } from "../../agents/shell-utils.js"; import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js"; import { createChildAdapter } from "./adapters/child.js"; @@ -54,7 +55,7 @@ function appendCapturedOutput( } const marker = `[openclaw: captured ${stream} truncated to last ${maxChars} chars]\n`; const tailChars = Math.max(0, maxChars - marker.length); - return `${marker}${next.slice(-tailChars)}`; + return `${marker}${sliceUtf16Safe(next, -tailChars)}`; } function isTimeoutReason(reason: TerminationReason) {