fix(agents): truncate exec command detail on code-point boundaries#96963
Conversation
compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is 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, 2:49 AM ET / 06:49 UTC. Summary PR surface: Source +11, Tests +22. Total +33 across 4 files. Reproducibility: yes. Current main raw-slices long exec details in Review metrics: none identified. 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:
Next step before merge
Security Review detailsBest possible solution: Land the focused exec-display Unicode truncation fix after normal maintainer review, while tracking the adjacent generic tool-display truncation surface in the sibling PR. Do we have a high-confidence way to reproduce the issue? Yes. Current main raw-slices long exec details in Is this the best way to solve the issue? Yes. Using the existing surrogate-safe slice semantics at the exec truncation boundary, while moving the helper into a Node-free shared module for browser-shared imports, is the narrow maintainable fix. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against e9f9a68d68c9. Label changesLabel justifications:
Evidence reviewedPR surface: Source +11, Tests +22. Total +33 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
|
ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle.
|
Addressed the P1 browser-safety finding: moved the surrogate-safe slice helpers into a new dependency-free module |
|
Merged via squash.
|
…penclaw#96963) * fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- 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#96963) * fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- 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#96963) * fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- 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#96963) * fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- 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#96963) * fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- 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#96963) * fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- 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 #96424 by @ly-wang19 onto current
main— 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.The exec command-detail middle-truncation (
src/agents/tool-display-exec.ts) cuts at a raw UTF-16 code-unit offset, so an exec detail containing emoji/astral characters near either boundary renders a lone surrogate (the�replacement glyph) around the….Root Cause
Symptom — A long exec command detail with emoji near the head or tail truncation boundary shows a broken glyph at the cut.
Root cause — Line 445:
return \${oneLine.slice(0, half)}…${oneLine.slice(-(maxLength - 1 - half))}`;`. Both slices are code-unit cuts; a boundary inside a surrogate pair severs it.Fix + why this level — Use the surrogate-safe
sliceUtf16Safefor both head/tail offsets. The truncation site is the only producer of the boundary, so it's the correct layer.Addressing the browser-safety review (ClawSweeper P1)
ClawSweeper correctly flagged that importing
sliceUtf16Safefromsrc/utils.tswould pullnode:fs/node:os/node:pathinto the Control UI browser-shared bundle (ui/vite.config.tstreatstool-display-exec.tsas browser-shared). Fixed (recommended option 1): the surrogate-safe helpers now live in a new self-contained, dependency-free modulesrc/shared/text/surrogate-safe-slice.ts.tool-display-exec.tsimports directly from the node-free module — no Node built-ins can leak into the browser bundle.src/utils.tsre-exportssliceUtf16Safe/truncateUtf16Safefrom the new module, so all existing Node-side callers are unchanged (zero churn).Evidence
Real behavior proof — reproduced the rendered exec-detail boundary with an astral character (U+1F600) straddling the head cut (
maxLength=120,half=59, emoji units at indices 58/59):The buggy raw-slice path keeps a lone high surrogate
\ud83d(renders as�); the fixed safe-slice path drops the whole emoji cleanly.Regression test:
src/agents/tool-display.test.ts— asserts no lone surrogate at either boundary of the exec detail; fails without the fix.Changes from original
main.Credit: Salvage of #96424 by @ly-wang19 — rebased onto current main. Original closed by the >20-active-PR queue cap, not on merits.