fix(agents): truncate tool-display detail on code-point boundaries#96958
Conversation
coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
Codex review: needs maintainer review before merge. Reviewed June 26, 2026, 8:04 PM ET / 00:04 UTC. Summary Reproducibility: yes. Source inspection shows current main cuts tool-display details with raw UTF-16 Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the focused four-file UTF-16-safe tool-display fix after exact-head CI clears; keep sibling exec and Matrix truncation fixes in their own PRs. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main cuts tool-display details with raw UTF-16 Is this the best way to solve the issue? Yes for the current potential merge result. The fix is at the single compact tool-detail truncation producer, uses a dependency-free helper for the UI import graph, and preserves the existing AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ff35f3bb2cd6. Label changesLabel changes:
Label justifications:
Evidence reviewedWhat 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
|
tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface.
Real behavior proof + build-failure fixHost: macOS (Darwin 25.4.0, arm64), node v25.5.0, repo source via its own tsx. 1. Behavior proof — the patched
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
CI red shard is an unrelated flaky test, not this PRThe single failing check ( That shard's other 1287 tests passed; the failure is a transient @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Merged via squash.
|
…penclaw#96958) * fix(agents): truncate tool-display detail on code-point boundaries coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(build): import surrogate-safe slice from node-free leaf module tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface. * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…penclaw#96958) * fix(agents): truncate tool-display detail on code-point boundaries coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(build): import surrogate-safe slice from node-free leaf module tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface. * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…penclaw#96958) * fix(agents): truncate tool-display detail on code-point boundaries coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(build): import surrogate-safe slice from node-free leaf module tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface. * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…penclaw#96958) * fix(agents): truncate tool-display detail on code-point boundaries coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(build): import surrogate-safe slice from node-free leaf module tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface. * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…penclaw#96958) * fix(agents): truncate tool-display detail on code-point boundaries coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(build): import surrogate-safe slice from node-free leaf module tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface. * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…penclaw#96958) * fix(agents): truncate tool-display detail on code-point boundaries coerceDisplayValue truncated a long first-line detail value with raw UTF-16 slice() at half = floor((maxStringChars-1)/2). When an emoji (surrogate pair) straddles the cut boundary, the head kept a lone high surrogate and the tail could begin on a lone low surrogate, rendering as the replacement character. Use the existing sliceUtf16Safe helper so the whole code point is dropped at the boundary, matching the UTF-16-safe truncation used elsewhere in the repo. Behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(build): import surrogate-safe slice from node-free leaf module tool-display-common.ts is in the UI browser bundle graph; importing sliceUtf16Safe from utils.js dragged node:fs (via infra/fs-safe) into the bundle and broke build-artifacts/QA Smoke. Extract sliceUtf16Safe/truncateUtf16Safe into src/shared/utf16-slice.ts (dependency-free) and re-export from utils.js to preserve the existing import surface. * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module * fix(build): import surrogate-safe slice from node-free leaf module --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
What Problem This Solves
Salvages #96420 by @ly-wang19 onto current
main— clean single-commit rebase, original authorship preserved. The original was auto-closed byopenclaw-barnaclefor the author's >20-active-PR cap, not on merits; the bug is still live onmain.Tool-display middle-truncation (
src/agents/tool-display-common.ts) cuts the displayed detail string at a raw UTF-16 code-unit offset, so a truncated tool detail containing emoji/astral characters can end in a lone high surrogate or begin with a lone low surrogate — rendering mojibake in the UI/log around the….Root Cause
Symptom — A long tool detail with emoji near either truncation boundary shows a broken replacement glyph at the cut.
Root cause —
firstLine.slice(0, half)andfirstLine.slice(-(maxStringChars - 1 - half))operate on UTF-16 code units. Whenhalf(or the tail offset) lands inside a surrogate pair, the pair is severed.Evidence — On current
main, line 139:return \${firstLine.slice(0, half)}…${firstLine.slice(-(maxStringChars - 1 - half))}`;with no surrogate-aware back-off. The regression test fails against thismain` source.Fix + why this level — Add surrogate-aware helpers and back the head/tail offsets off to code-point boundaries before slicing. Fixing at the truncation site is correct: this is the single place that produces a non-aligned boundary for tool display; consumers just render the returned string.
Scope / risk — Touches only the middle-truncation path in
tool-display-common.ts. BMP-only details and details below the truncation threshold are unaffected (back-off is a no-op when already aligned).Evidence
Host: macOS (Darwin 25.4.0, arm64), node v25.5.0, repo source via its own tsx.
Regression test (new):
src/agents/tool-display-common.test.ts— asserts no lone surrogate at either boundary; fails without the fix.Behavior proof — patched
resolveToolVerbAndDetailForArgs(reachescoerceDisplayValue). Input: 200 UTF-16 units = 78a+ 😀 (U+1F600, surrogate pair at idx 78-79) + 120b. WithmaxStringChars=160,half=79, so the naivefirstLine.slice(0,79)keeps only the emoji's high surrogate.Before (current
upstream/mainline 139):After (this branch):
CI results:
build-artifacts + QA Smoke CI fix (commit
d862dfc1c1): an earlierimport { sliceUtf16Safe } from "../utils.js"pulledsrc/infra/fs-safe.ts→node:fsinto the browser bundle (ui/src/ui/tool-display.tsimportstool-display-common.ts), breakingui:buildwith[UNLOADABLE_DEPENDENCY] Could not load ../extensions/fs-safe/walk. Fixed by extractingsliceUtf16Safe/truncateUtf16Safeinto a dependency-free leaf modulesrc/shared/utf16-slice.ts(zeronode:imports — verified), re-exported fromutils.jsto preserve the public surface, and pointedtool-display-common.tsat the leaf module. build-artifacts and QA Smoke CI now pass on this branch.Changes from original
main(clean single commit, no conflicts).utf16-slice.tsleaf module to keep the browser bundle clean.Credit: Salvage of #96420 by @ly-wang19 — original closed by the >20-active-PR queue cap, not on merits.