fix(agents): keep exec auto-reviewer rationale truncation UTF-16 safe (#101513)

* fix(agents): keep exec auto-reviewer rationale truncation UTF-16 safe

String.prototype.slice at offset 500 can split surrogate pairs in
LLM-generated review rationale text, rendering broken U+FFFD in
exec approval UI payloads.

Replace raw slice(0, 500) with truncateUtf16Safe.

* fix(agents): keep exec reviewer rationale and prompt template description truncation UTF-16 safe

String.prototype.slice can split surrogate pairs at truncation boundaries,
producing broken U+FFFD in both exec approval rationale (500 chars) and
session prompt-template descriptions (60 chars).

Replace raw slice with truncateUtf16Safe in both locations.

* test(exec): cover UTF-16-safe reviewer rationale

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
wm0018
2026-07-07 19:04:19 +08:00
committed by GitHub
parent 78f9449d73
commit e7d617d4d9
2 changed files with 20 additions and 1 deletions
+18
View File
@@ -118,6 +118,24 @@ describe("parseExecAutoReviewResponse", () => {
});
}
});
it("does not split surrogate pairs when truncating rationale", () => {
const rationale = "x".repeat(499) + "🚀tail";
expect(
parseExecAutoReviewResponse(
JSON.stringify({
decision: "ask",
risk: "medium",
rationale,
}),
),
).toEqual({
decision: "ask",
risk: "medium",
rationale: "x".repeat(499),
});
});
});
describe("createModelExecAutoReviewer", () => {
+2 -1
View File
@@ -6,6 +6,7 @@
*/
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { z } from "zod";
import type { AgentModelConfig } from "../config/types.agents-shared.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -76,7 +77,7 @@ function buildReviewerUserPrompt(input: ExecAutoReviewInput): string {
function normalizeRationale(value: unknown, fallback: string): string {
const text = normalizeOptionalString(typeof value === "string" ? value : undefined);
return (text ?? fallback).slice(0, 500);
return truncateUtf16Safe(text ?? fallback, 500);
}
function textLooksLikeReviewerDirective(value: string): boolean {