fix(text): keep five src-side text truncations UTF-16 safe#102630
fix(text): keep five src-side text truncations UTF-16 safe#102630wangmiao0668000666 wants to merge 1 commit into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 9, 2026, 6:33 AM ET / 10:33 UTC. Summary PR surface: Source +3. Total +3 across 4 files. Reproducibility: yes. Current main has four fixed-cap raw Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Land the four helper substitutions after normal exact-head merge validation, while leaving the post-compaction UTF-16 fix owned by #102515 and avoiding broader helper churn. Do we have a high-confidence way to reproduce the issue? Yes. Current main has four fixed-cap raw Is this the best way to solve the issue? Yes. Reusing the existing AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c87b9a7cee90. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +3. Total +3 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (1 earlier review cycle)
|
5a1149b to
a8d0879
Compare
|
@clawsweeper re-review Addressed both P1 findings from the previous round:
Pre-flight clean on the rebased head
The branch is now also rebased onto current main so the CI "ensure-base-commit" gate can fetch the correct base SHA, which should clear the build-artifacts / checks-fast-contracts-* failures from the previous run. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Landed the low-risk plugin-list and hook-log parts in the wider canonical cleanup: 0ac8933 Your contributor credit was retained. The fallback-reason and dashboard conversation-label changes were intentionally not copied: those cross user-visible fallback/session state and UI ownership and need separate owner-level proof rather than riding in a mixed micro patch. Proof: 114 focused tests passed, AutoReview was clean, and full-checkout Testbox |
What Problem This Solves
Four
src/-side text-truncation call sites still use rawString.prototype.slice()(or template-string slicing) with no UTF-16 surrogate-pair awareness. When an emoji or other supplementary Unicode character lands exactly on the cap, the slice can leave a dangling high surrogate that downstream provider / prompt / log consumers may treat as malformed text.src/cli/plugins-list-format.ts:19plugin.descriptiontruncation in CLI rowsrc/hooks/fire-and-forget.ts:75formatHookErrorForLogtruncationMAX_HOOK_LOG_MESSAGE_LENGTH(500)src/auto-reply/fallback-state.ts:33truncateFallbackReasonPartfor fallback reason textsrc/auto-reply/reply/conversation-label-generator.ts:117generateConversationLabeloutput capmaxLength(default 128)Each follows the same pattern that has already landed in 30+ merged UTF-16 PRs (e.g. #102087, #102466, #102496, #102085, #102332, #102090, #102266, #102246, #102378, #102464, #102467, #102470, #102483, #102484, #102477).
Why This Change Was Made
Replace the raw
slice()/.slice(0, N)patterns withtruncateUtf16Safe()from@openclaw/normalization-core/utf16-slice(re-exported viasrc/utils.js). The helper preserves the existing cap for ordinary text and only backs up by one code unit when the cap would split a surrogate pair — matching the contract every other UTF-16 fix in this wave uses.The helper is the canonical one (already imported by
src/agents/provider-http-errors.ts,src/gateway/server-methods/config.ts,src/talk/fast-context-runtime.ts,src/agents/bash-process-references.ts,src/auto-reply/reply/bash-command.ts,src/auto-reply/reply/commands-acp/lifecycle.ts, etc.), so no new helper surface is added.Scope note: An earlier version of this PR also covered
src/auto-reply/reply/post-compaction-context.ts:120, but that site was already covered by #102515 (lsr911, merged) which importstruncateUtf16Safedirectly from@openclaw/normalization-core/utf16-slice. The current branch drops that site to avoid import churn around an already-covered file.User Impact
The four sites now keep their truncation caps but never produce a lone surrogate at the boundary. No behavior change for ASCII / CJK content; the only difference is at the exact boundary where the cap lands inside a surrogate pair.
No config, environment, default, SDK, protocol, or migration change. No public API change. No new dependency.
Evidence
Real behavior proof (after-fix, drives each changed truncation path with an emoji crossing the cap)
A standalone harness imports
truncateUtf16Safefromsrc/utils.tsexactly as the four PR sites use it, drives each production call through the helper with emoji-density input crafted so the cap lands inside an astral code-point's surrogate pair, and verifies the output has no lone surrogate. The harness also runs the same input through rawString.prototype.sliceto show what the bug used to look like.Two of the four sites (cap 57 and cap 79) show the bug clearly: a raw
slice()would split the emoji and leave a dangling surrogate, whiletruncateUtf16Safebacks off by one code unit. The other two sites happen to land just inside an emoji boundary on the chosen input; the helper still produces the expected length with no surrogate.The proof uses no private paths, tokens, or user data — only the public
truncateUtf16Safehelper and synthetic emoji-dense inputs. Reproduction command is in the harness file; the harness exits with a non-zero status on any unexpected length or surrogate.Behavior proof
${plugin.description.slice(0, 57)}...${truncateUtf16Safe(plugin.description, 57)}...(formatted || "unknown error").slice(0, MAX_HOOK_LOG_MESSAGE_LENGTH)truncateUtf16Safe(formatted || "unknown error", MAX_HOOK_LOG_MESSAGE_LENGTH)${text.slice(0, Math.max(0, max - 1)).trimEnd()}…${truncateUtf16Safe(text, Math.max(0, max - 1)).trimEnd()}…text.slice(0, maxLength)truncateUtf16Safe(text, maxLength)All four sites preserve their existing length-based
if (text.length <= max) return text;short-circuit; the helper only operates on the truncate branch.Quantitative read-out
cli/plugins-list-format.ts:19(description)57chars57UTF-16-safetruncateUtf16Safesrc/cli/plugins-list-format.test.tshooks/fire-and-forget.ts:75(hook error)MAX_HOOK_LOG_MESSAGE_LENGTH(500)src/hooks/fire-and-forget.test.tsauto-reply/fallback-state.ts:33(fallback reason)FALLBACK_REASON_PART_MAX(80)src/auto-reply/fallback-state.test.tsauto-reply/reply/conversation-label-generator.ts:117(label)maxLength(default 128)src/auto-reply/reply/conversation-label-generator.test.tsTest coverage
All pre-existing tests pass on the rebased head
a8d0879089:Pre-flight also clean:
node scripts/run-tsgo.mjs -p tsconfig.core.json --incrementalexits 0 on the rebased head;git diff --checkis clean.Risk checklist
truncateUtf16Safeis already exported fromsrc/utils.js.if (text.length <= max) return text;short-circuit preserved; helper only runs on the truncate branch.Closes / siblings
fix(agents): keep tool-result truncation UTF-16 safe(chengzhichao-xydt 7/9)fix(agents): keep tool-result context guard truncation UTF-16 safe(chengzhichao-xydt 7/9)fix(agents): keep provider error detail truncation UTF-16 safe(chengzhichao-xydt 7/9)fix(agents): keep steering metadata truncation UTF-16 safe(chengzhichao-xydt 7/9)fix(agents): keep chunkString and buildResumeMessage truncation UTF-16 safefix(agents): keep truncation surrogate-safefix(gateway): keep session title and preview text truncation UTF-16 safefix(security): keep channel-metadata and install-policy truncation surrogate-safefix(discord): keep gateway close reasons UTF-16 safefix(acp): keep session update text truncation surrogate-safefix(tool-policy-audit): use truncateUtf16Safe for audit field truncationfix(native-hook-relay): use truncateUtf16Safe for hook display text truncationfix(gateway): keep chat history display text truncation surrogate-safefix(auto-reply,infra): keep startup context and heartbeat event text UTF-16 safefix(gateway): keep live chat assistant buffer tail truncation UTF-16 safefix(talk): use truncateUtf16Safe for LLM-prompt-facing text truncationfix(android): preserve UTF-16 boundaries in notification textfix(channels): keep inbound log previews UTF-16 safe(miorbnli 7/9)fix(codex): app inventory error diagnostics stay UTF-16 safefix: keep task title truncation UTF-16 safe in restart/diagnostic outputfix(matrix): keep HTTP error and tool-progress truncations UTF-16 safefix(telegram): keep DM topic auto-rename user message UTF-16 safe[AI] fix(memory): use truncateUtf16Safe for dreaming snippet truncationfix(acp): keep background-task summaries UTF-16 safe at truncation boundariesfix(ui): keep clampText/truncateText surrogate-safe at emoji boundariesfix(doctor): keep scrubbed error truncation UTF-16 safefix(logging): keep bounded log text UTF-16 safefix(agent-core,memory-core): keep compaction summary and memory snippet truncation UTF-16 safefix(agents): keep cleanup timeout details UTF-16 safefix(compaction): use truncateUtf16Safe for post-compaction context text(lsr911, merged — covers the site originally included in this PR)fix(channels): prevent metadata caches from growing without bound(cache cap, related infrastructure pattern)fix(config): use truncateUtf16Safe for allowed-values hint textfix(gateway): use truncateUtf16Safe for config path display truncationfix(slack): keep inbound message preview truncation surrogate-safefix(codex): use truncateUtf16Safe for tool transcript output truncationfix(memory-core): use truncateUtf16Safe for diary context truncationfix(discord): use truncateUtf16Safe for deploy error body truncationfix(oc-path): use truncateUtf16Safe for error message path truncationfix(mcp-runtime): use truncateUtf16Safe for MCP metadata text truncationtruncateUtf16Safeis defined inpackages/normalization-core/src/utf16-slice.tsand re-exported bysrc/utils.ts:65. Already used by 30+ merged UTF-16 sibling PRs.cli/plugins-list-format.ts,hooks/fire-and-forget.ts,auto-reply/fallback-state.ts, orauto-reply/reply/conversation-label-generator.tsfor UTF-16 truncation. The only fire-and-forget related OPEN PR is fix: catch unhandled promise rejections in fire-and-forget async calls #102185 (unhandled promise rejections, unrelated). The recent burst (fix(auto-reply): keep ACP steer output UTF-16 safe #102583) coversauto-reply/reply/bash-command.tsandauto-reply/reply/commands-acp/lifecycle.ts, not the files in this PR.src/auto-reply/reply/post-compaction-context.ts— current main already importstruncateUtf16Safedirectly from@openclaw/normalization-core/utf16-slice(via fix(compaction): use truncateUtf16Safe for post-compaction context text #102515), so this PR no longer touches that file.Re-review
@clawsweeper re-review — addressed both P1 findings from the previous round:
4be6fa7413; the import-churn conflict aroundpost-compaction-context.tsis gone. The file is no longer touched by this PR./tmp/proof-102630.mjsdrives all four remaining sites with emoji-cross-boundary inputs. Site 1 (cap 57) and Site 3 (cap 79) demonstrate the bug-vs-fix difference (raw slice lone surrogate = true, helper lone surrogate = false); Sites 2 and 4 confirm length and surrogate safety on dense emoji input. Full output is in the PR body under "Real behavior proof (after-fix…)".Pre-flight clean on the rebased head
a8d0879089:node scripts/run-tsgo.mjs -p tsconfig.core.json --incremental— exit 0node scripts/test-projects.mjsoncli/plugins-list-format.test.ts,hooks/fire-and-forget.test.ts,auto-reply/fallback-state.test.ts,auto-reply/reply/conversation-label-generator.test.ts— 4 + 22 tests pass in 41.44sgit diff --check— cleangit diff origin/main --stat— 4 files changed, 8 insertions(+), 5 deletions(-)