From e7d617d4d9369a41d059f3345d4ec1e455ff635e Mon Sep 17 00:00:00 2001 From: wm0018 Date: Tue, 7 Jul 2026 19:04:19 +0800 Subject: [PATCH] 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 --- src/agents/exec-auto-reviewer.test.ts | 18 ++++++++++++++++++ src/agents/exec-auto-reviewer.ts | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/agents/exec-auto-reviewer.test.ts b/src/agents/exec-auto-reviewer.test.ts index 1e6752fe6496..6009f68c4410 100644 --- a/src/agents/exec-auto-reviewer.test.ts +++ b/src/agents/exec-auto-reviewer.test.ts @@ -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", () => { diff --git a/src/agents/exec-auto-reviewer.ts b/src/agents/exec-auto-reviewer.ts index a0da89591f8e..d8ecc1fc3935 100644 --- a/src/agents/exec-auto-reviewer.ts +++ b/src/agents/exec-auto-reviewer.ts @@ -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 {