Skip to content

Commit 4fa3118

Browse files
lsr911claudesteipete
authored
fix(native-hook-relay): use truncateUtf16Safe for hook display text truncation (#102467)
* fix(tool-policy-audit): use truncateUtf16Safe for audit field truncation Replace naive .slice(0, MAX) with truncateUtf16Safe() to prevent surrogate pair splitting in tool policy audit log output. Co-Authored-By: Claude <[email protected]> * fix(native-hook-relay): use truncateUtf16Safe for hook display text truncation Replace naive .slice(0, N) truncation with truncateUtf16Safe() in: - truncateText() helper (covers 4 call sites for tool display text) - Inline hook result truncation (...[truncated] suffix) Prevents surrogate pair splitting in native hook relay display output (tool names, commands, descriptions shown to users). Co-Authored-By: Claude <[email protected]> * test(agents): cover hook relay UTF-16 boundaries --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent 472e516 commit 4fa3118

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

src/agents/harness/native-hook-relay.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,32 @@ describe("native hook relay registry", () => {
13701370
expect(String(rawPayload.tool_response)).toContain("[truncated]");
13711371
});
13721372

1373+
it("retains payload snapshots without splitting surrogate pairs", async () => {
1374+
const relay = registerNativeHookRelay({
1375+
provider: "codex",
1376+
sessionId: "session-1",
1377+
runId: "run-1",
1378+
allowedEvents: ["post_tool_use"],
1379+
});
1380+
1381+
await invokeNativeHookRelay({
1382+
provider: "codex",
1383+
relayId: relay.relayId,
1384+
event: "post_tool_use",
1385+
rawPayload: {
1386+
tool_response: `${"a".repeat(3_999)}😀tail`,
1387+
},
1388+
});
1389+
1390+
const [recorded] = testing.getNativeHookRelayInvocationsForTests();
1391+
const rawPayload = readRecordField(
1392+
requireRecord(recorded, "native hook relay invocation"),
1393+
"rawPayload",
1394+
"invocation raw payload",
1395+
);
1396+
expect(rawPayload.tool_response).toBe(`${"a".repeat(3_999)}...[truncated]`);
1397+
});
1398+
13731399
it("removes retained invocations when a relay is unregistered", async () => {
13741400
const relay = registerNativeHookRelay({
13751401
provider: "codex",
@@ -3407,6 +3433,20 @@ describe("native hook relay registry", () => {
34073433
}),
34083434
).toContain("(1 omitted)");
34093435
});
3436+
3437+
it("truncates PermissionRequest approval previews without splitting surrogate pairs", () => {
3438+
expect(
3439+
testing.formatPermissionApprovalDescriptionForTests({
3440+
provider: "codex",
3441+
sessionId: "session-1",
3442+
runId: "run-1",
3443+
toolName: "exec",
3444+
toolInput: {
3445+
command: `${"a".repeat(236)}😀tail`,
3446+
},
3447+
}),
3448+
).toBe(`Tool: exec\nCommand: ${"a".repeat(236)}...`);
3449+
});
34103450
});
34113451

34123452
describe("native hook relay command builder", () => {

src/agents/harness/native-hook-relay.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
asDateTimestampMs,
2525
resolveExpiresAtMsFromDurationMs,
2626
} from "@openclaw/normalization-core/number-coercion";
27+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
2728
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2829
import { toErrorObject } from "../../infra/errors.js";
2930
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
@@ -1925,11 +1926,14 @@ function snapshotString(value: string, state: { remainingStringLength: number })
19251926
MAX_NATIVE_HOOK_RELAY_HISTORY_STRING_LENGTH,
19261927
state.remainingStringLength,
19271928
);
1928-
state.remainingStringLength -= limit;
19291929
if (limit >= value.length) {
1930+
state.remainingStringLength -= limit;
19301931
return value;
19311932
}
1932-
return `${value.slice(0, limit)}...[truncated]`;
1933+
const prefix = truncateUtf16Safe(value, limit);
1934+
// Charge the retained prefix; a safe boundary may back up one code unit.
1935+
state.remainingStringLength -= prefix.length;
1936+
return `${prefix}...[truncated]`;
19331937
}
19341938

19351939
function normalizeNativeHookInvocation(params: {
@@ -2223,7 +2227,7 @@ function truncateText(value: string, maxLength: number): string {
22232227
if (value.length <= maxLength) {
22242228
return value;
22252229
}
2226-
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
2230+
return `${truncateUtf16Safe(value, Math.max(0, maxLength - 3))}...`;
22272231
}
22282232

22292233
function resolveOpenClawCliExecutable(): string {

0 commit comments

Comments
 (0)