fix(agents): keep tool_search_code stderr tail UTF-16 safe (#104767)

* fix(agents): keep tool_search_code stderr tail UTF-16 safe

* test(agents): add real child-exit UTF-16 safety proof

* test(agents): focus stderr UTF-16 regression

---------

Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
chengzhichao-xydt
2026-07-12 08:08:56 +08:00
committed by GitHub
parent 4e4cca2b99
commit dfeb15261d
2 changed files with 45 additions and 2 deletions

View File

@@ -87,4 +87,47 @@ describe("tool-search code-mode stream errors", () => {
});
expect(spawnedChild?.kill).toHaveBeenCalledOnce();
});
it("keeps stderr tail in exit error messages valid at UTF-16 boundaries", async () => {
toolSearch.testing.setToolSearchCodeModeSupportedForTest(true);
toolSearch.testing.setToolSearchMinCodeTimeoutMsForTest(1000);
spawnMock.mockImplementationOnce(
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
const { child, stderr } = createMockSpawnChild();
process.nextTick(() => {
stderr?.emit("data", `${"a".repeat(500)}😀${"a".repeat(499)}`);
process.nextTick(() => {
child.emit("exit", 1, null);
});
});
return child as unknown as ChildProcess;
},
);
const runtime = new toolSearch.ToolSearchRuntime(
{},
toolSearch.testing.resolveToolSearchConfig({}),
);
let caught: Error | undefined;
try {
await toolSearch.testing.runCodeModeChild({
code: "return 1;",
config: toolSearch.testing.resolveToolSearchConfig({}),
logs: [],
parentToolCallId: "call-stderr-utf16",
runtime,
});
} catch (error) {
caught = error instanceof Error ? error : new Error(String(error));
}
expect(caught).toBeDefined();
expect(caught?.message).toMatch(/tool_search_code child exited with 1/);
expect(caught?.message).not.toMatch(
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/,
);
expect(caught?.message.endsWith("a".repeat(499))).toBe(true);
});
});

View File

@@ -11,7 +11,7 @@ import {
uniqueStrings,
uniqueValues,
} from "@openclaw/normalization-core/string-normalization";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { Type } from "typebox";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { getPluginToolMeta, type PluginToolMcpMeta } from "../plugins/tools.js";
@@ -2199,7 +2199,7 @@ function runCodeModeChild(params: {
}
const rejectOnExit = () => {
const suffix = stderrTail.trim();
const detail = suffix ? `: ${suffix.slice(-500)}` : "";
const detail = suffix ? `: ${sliceUtf16Safe(suffix, -500)}` : "";
settle(() =>
reject(
new Error(