fix(shared): use UTF-16 safe truncation in assistant error formatting#97289
Conversation
Use truncateUtf16Safe from the existing utf16-slice utility instead of String.prototype.slice(0, 600), which can split a surrogate pair and produce malformed Unicode when the error message contains characters outside the Basic Multilingual Plane (emoji, rare CJK, etc.).
|
Codex review: needs maintainer review before merge. Reviewed June 27, 2026, 2:25 PM ET / 18:25 UTC. Summary PR surface: Source +2, Tests +27. Total +29 across 3 files. Reproducibility: yes. Current main uses raw UTF-16 slice(0, 600) at the reported formatter fallbacks, and a focused node check shows the same boundary split leaves a dangling surrogate. 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 narrow helper reuse after normal exact-head gates; leave broader truncation sweeps to separate surface-specific PRs. Do we have a high-confidence way to reproduce the issue? Yes. Current main uses raw UTF-16 slice(0, 600) at the reported formatter fallbacks, and a focused node check shows the same boundary split leaves a dangling surrogate. Is this the best way to solve the issue? Yes. Reusing the existing dependency-free truncateUtf16Safe helper at the two assistant fallback truncation sites is the narrowest maintainable fix; duplicating surrogate logic or adding a new utility would be worse. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 98ed83f848de. Label changesLabel justifications:
Evidence reviewedPR surface: Source +2, Tests +27. Total +29 across 3 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
|
Verify formatRawAssistantErrorForUi does not produce dangling surrogates when truncating a fallback raw error message with a non-BMP character (emoji) straddling the 600-code-unit boundary.
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Use truncateUtf16Safe instead of String.prototype.slice(0,600) in formatAssistantErrorText and its companion comparison in isRawAssistantErrorPassthrough, preventing dangling surrogates when non-BMP characters straddle the 600-code-unit truncation boundary.
…openclaw#97289) * fix(shared): truncate assistant error text on UTF-16 boundary Use truncateUtf16Safe from the existing utf16-slice utility instead of String.prototype.slice(0, 600), which can split a surrogate pair and produce malformed Unicode when the error message contains characters outside the Basic Multilingual Plane (emoji, rare CJK, etc.). * test(shared): add regression test for UTF-16 safe error truncation Verify formatRawAssistantErrorForUi does not produce dangling surrogates when truncating a fallback raw error message with a non-BMP character (emoji) straddling the 600-code-unit boundary. * fix(agents): truncate assistant error text fallback on UTF-16 boundary Use truncateUtf16Safe instead of String.prototype.slice(0,600) in formatAssistantErrorText and its companion comparison in isRawAssistantErrorPassthrough, preventing dangling surrogates when non-BMP characters straddle the 600-code-unit truncation boundary.
…openclaw#97289) * fix(shared): truncate assistant error text on UTF-16 boundary Use truncateUtf16Safe from the existing utf16-slice utility instead of String.prototype.slice(0, 600), which can split a surrogate pair and produce malformed Unicode when the error message contains characters outside the Basic Multilingual Plane (emoji, rare CJK, etc.). * test(shared): add regression test for UTF-16 safe error truncation Verify formatRawAssistantErrorForUi does not produce dangling surrogates when truncating a fallback raw error message with a non-BMP character (emoji) straddling the 600-code-unit boundary. * fix(agents): truncate assistant error text fallback on UTF-16 boundary Use truncateUtf16Safe instead of String.prototype.slice(0,600) in formatAssistantErrorText and its companion comparison in isRawAssistantErrorPassthrough, preventing dangling surrogates when non-BMP characters straddle the 600-code-unit truncation boundary.
…openclaw#97289) * fix(shared): truncate assistant error text on UTF-16 boundary Use truncateUtf16Safe from the existing utf16-slice utility instead of String.prototype.slice(0, 600), which can split a surrogate pair and produce malformed Unicode when the error message contains characters outside the Basic Multilingual Plane (emoji, rare CJK, etc.). * test(shared): add regression test for UTF-16 safe error truncation Verify formatRawAssistantErrorForUi does not produce dangling surrogates when truncating a fallback raw error message with a non-BMP character (emoji) straddling the 600-code-unit boundary. * fix(agents): truncate assistant error text fallback on UTF-16 boundary Use truncateUtf16Safe instead of String.prototype.slice(0,600) in formatAssistantErrorText and its companion comparison in isRawAssistantErrorPassthrough, preventing dangling surrogates when non-BMP characters straddle the 600-code-unit truncation boundary.
…openclaw#97289) * fix(shared): truncate assistant error text on UTF-16 boundary Use truncateUtf16Safe from the existing utf16-slice utility instead of String.prototype.slice(0, 600), which can split a surrogate pair and produce malformed Unicode when the error message contains characters outside the Basic Multilingual Plane (emoji, rare CJK, etc.). * test(shared): add regression test for UTF-16 safe error truncation Verify formatRawAssistantErrorForUi does not produce dangling surrogates when truncating a fallback raw error message with a non-BMP character (emoji) straddling the 600-code-unit boundary. * fix(agents): truncate assistant error text fallback on UTF-16 boundary Use truncateUtf16Safe instead of String.prototype.slice(0,600) in formatAssistantErrorText and its companion comparison in isRawAssistantErrorPassthrough, preventing dangling surrogates when non-BMP characters straddle the 600-code-unit truncation boundary.
Summary
Replace
String.prototype.slice(0, 600)withtruncateUtf16Safe()in two assistant error formatting functions —formatRawAssistantErrorForUiandformatAssistantErrorText(plus its companion comparison inisRawAssistantErrorPassthrough) — to prevent broken Unicode from surrogate-pair splitting when error messages contain emoji or non-BMP characters.What Problem This Solves
When
formatRawAssistantErrorForUireceives an error message containing characters outside the Basic Multilingual Plane (emoji like 🎉, rare CJK characters, etc.), the truncation at line 249 can split a UTF-16 surrogate pair in half. This produces a dangling surrogate code unit — a malformed Unicode character displayed as�(replacement character) in TUI and chat channels.Code evidence:
src/shared/assistant-error-format.ts:250,formatRawAssistantErrorForUiusestrimmed.slice(0, 600)for the fallback raw error previewsrc/agents/embedded-agent-helpers/errors.ts:1543,formatAssistantErrorTextuses the same pattern for its raw fallback, and its companion comparison inisRawAssistantErrorPassthroughat line 1563 also uses.slice(0, 600)Both operate on UTF-16 code units, not code points. Characters outside BMP (U+10000+) are encoded as two code units (surrogate pairs); slicing at an arbitrary index can cut between them.
Root Cause
397b0d85f57(2026-03-19, refactor: split assistant error formatting)String.prototype.slice(n)assumes all characters are single-code-unit BMP characters. In reality, JavaScript strings are UTF-16 — supplementary-plane characters occupy 2 code units.Why This Fix
truncateUtf16Safeutility fromsrc/shared/utf16-slice.ts(same directory, zero new dependencies).User Impact
src/tui/), auto-reply channels, and agent payload builders — any path that callsformatRawAssistantErrorForUi.�if the original message contained an emoji near the 600-char boundary.Evidence
Terminal proof script demonstrating the bug and fix using a 601-code-unit error message containing a 🎉 emoji at the truncation boundary:
Before (on main,
String.prototype.slice(0, 600)):…ellipsis)�in error messagesAfter (with fix,
truncateUtf16Safe):…ellipsis)Real Behavior Proof
Before (on main)
After (with fix)
Tests and Validation
pnpm checkpassedgit diff --checkclean)formatRawAssistantErrorForUiunit tests continue to passformatRawAssistantErrorForUiwith non-BMP character at the 600-char truncation boundary