mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 02:31:39 +00:00
fix(agents): keep read warning previews UTF-16 safe (#106399)
This commit is contained in:
@@ -372,7 +372,69 @@ describe("handleToolExecutionStart read path checks", () => {
|
||||
await handleToolExecutionStart(ctx, evt);
|
||||
|
||||
const warnMeta = warn.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
|
||||
expect(warnMeta?.argsPreview).toBe(`${"x".repeat(200)}…`);
|
||||
const argsPreview = warnMeta?.argsPreview;
|
||||
expect(typeof argsPreview).toBe("string");
|
||||
expect(argsPreview).toBe(`${"x".repeat(200)}…`);
|
||||
});
|
||||
|
||||
it("keeps read warning args previews on UTF-16 boundaries", async () => {
|
||||
const { ctx, warn } = createTestContext();
|
||||
const emoji = "😀";
|
||||
|
||||
const evt: ToolExecutionStartEvent = {
|
||||
type: "tool_execution_start",
|
||||
toolName: "read",
|
||||
toolCallId: "tool-surrogate-args",
|
||||
args: `${"x".repeat(200)}${emoji}tail`,
|
||||
};
|
||||
|
||||
await handleToolExecutionStart(ctx, evt);
|
||||
|
||||
const warnMeta = warn.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
|
||||
const argsPreview = warnMeta?.argsPreview;
|
||||
expect(typeof argsPreview).toBe("string");
|
||||
expect(argsPreview).toBe(`${"x".repeat(200)}…`);
|
||||
expect(argsPreview).not.toMatch(
|
||||
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("marks astral-only read warning args previews as truncated", async () => {
|
||||
const { ctx, warn } = createTestContext();
|
||||
const emoji = "😀";
|
||||
|
||||
const evt: ToolExecutionStartEvent = {
|
||||
type: "tool_execution_start",
|
||||
toolName: "read",
|
||||
toolCallId: "tool-astral-args",
|
||||
args: emoji.repeat(101),
|
||||
};
|
||||
|
||||
await handleToolExecutionStart(ctx, evt);
|
||||
|
||||
const warnMeta = warn.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
|
||||
const argsPreview = warnMeta?.argsPreview;
|
||||
expect(typeof argsPreview).toBe("string");
|
||||
expect(argsPreview).toBe(`${emoji.repeat(100)}…`);
|
||||
expect(argsPreview).not.toMatch(
|
||||
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not scan visible preview content beyond the raw warning bound", async () => {
|
||||
const { ctx, warn } = createTestContext();
|
||||
|
||||
const evt: ToolExecutionStartEvent = {
|
||||
type: "tool_execution_start",
|
||||
toolName: "read",
|
||||
toolCallId: "tool-bounded-args",
|
||||
args: `${" ".repeat(200)}hidden`,
|
||||
};
|
||||
|
||||
await handleToolExecutionStart(ctx, evt);
|
||||
|
||||
const warnMeta = warn.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
|
||||
expect(warnMeta).not.toHaveProperty("argsPreview");
|
||||
});
|
||||
|
||||
it("does not split surrogate pairs when bounding read warning preview", async () => {
|
||||
@@ -389,7 +451,7 @@ describe("handleToolExecutionStart read path checks", () => {
|
||||
await handleToolExecutionStart(ctx, evt);
|
||||
|
||||
const warnMeta = warn.mock.calls[0]?.[1] as Record<string, unknown> | undefined;
|
||||
expect(warnMeta?.argsPreview).toBe("x".repeat(198));
|
||||
expect(warnMeta?.argsPreview).toBe(`${"x".repeat(198)}…`);
|
||||
});
|
||||
|
||||
it("awaits onBlockReplyFlush before continuing tool start processing", async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
normalizeOptionalLowercaseString,
|
||||
readStringValue,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import {
|
||||
HEARTBEAT_RESPONSE_TOOL_NAME,
|
||||
normalizeHeartbeatToolResponse,
|
||||
@@ -242,7 +242,17 @@ function traceToolExecutionStart(params: {
|
||||
}
|
||||
|
||||
const TOOL_START_WARNING_PREVIEW_MAX_CHARS = 200;
|
||||
const TOOL_START_WARNING_RAW_PREVIEW_MAX_CHARS = TOOL_START_WARNING_PREVIEW_MAX_CHARS + 1;
|
||||
|
||||
function buildToolStartWarningArgsPreview(rawArgsPreview: string | undefined): string | undefined {
|
||||
if (rawArgsPreview == null) {
|
||||
return undefined;
|
||||
}
|
||||
// Bound before regex normalization so malformed tool args cannot make warning work unbounded.
|
||||
const wasTruncated = rawArgsPreview.length > TOOL_START_WARNING_PREVIEW_MAX_CHARS;
|
||||
const bounded = truncateUtf16Safe(rawArgsPreview, TOOL_START_WARNING_PREVIEW_MAX_CHARS);
|
||||
const preview = sanitizeForConsole(bounded, TOOL_START_WARNING_PREVIEW_MAX_CHARS);
|
||||
return wasTruncated && preview ? `${preview}…` : preview;
|
||||
}
|
||||
|
||||
type ToolStartRecord = {
|
||||
startTime: number;
|
||||
@@ -984,12 +994,7 @@ export function handleToolExecutionStart(
|
||||
if (!filePath) {
|
||||
const argsType = typeof args;
|
||||
const rawArgsPreview = readStringValue(args);
|
||||
const argsPreview = sanitizeForConsole(
|
||||
rawArgsPreview
|
||||
? sliceUtf16Safe(rawArgsPreview, 0, TOOL_START_WARNING_RAW_PREVIEW_MAX_CHARS)
|
||||
: undefined,
|
||||
TOOL_START_WARNING_PREVIEW_MAX_CHARS,
|
||||
);
|
||||
const argsPreview = buildToolStartWarningArgsPreview(rawArgsPreview);
|
||||
const safeRunId = sanitizeForConsole(runId) ?? "-";
|
||||
const safeSessionKey = sanitizeForConsole(ctx.params.sessionKey);
|
||||
const safeSessionId = sanitizeForConsole(ctx.params.sessionId);
|
||||
|
||||
Reference in New Issue
Block a user