fix(slack): truncate on code-point boundaries to avoid splitting surrogate pairs#96382
Conversation
…ogate pairs
truncateSlackText sliced by UTF-16 code unit ('trimmed.slice(0, max - 1)'), so an
emoji or other astral character straddling the limit was cut in half, leaving a
lone high surrogate before the ellipsis — e.g. truncateSlackText('abc😀def', 5)
returned 'abc\uD83D…' instead of 'abc…'. That invalid half-character is sent in
live Slack payloads (message text and Block Kit section/button/header/option
labels, which truncate at limits as small as 75).
Use the repo's canonical sliceUtf16Safe (already re-exported from
plugin-sdk/text-utility-runtime, the module slack code imports from) so a
straddling pair is dropped whole. Behavior is byte-identical for all-BMP input.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 7:26 AM ET / 11:26 UTC. Summary PR surface: Source +5, Tests +27. Total +32 across 2 files. Reproducibility: yes. Current main's 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 this narrow Slack helper fix after ordinary maintainer review and required CI, keeping UTF-16-safe truncation centralized on the SDK helper. Do we have a high-confidence way to reproduce the issue? Yes. Current main's raw Is this the best way to solve the issue? Yes. Reusing AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ae06d846faf3. Label changesLabel justifications:
Evidence reviewedPR surface: Source +5, Tests +27. Total +32 across 2 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
|
|
Merged via squash.
|
…one surrogates clipProgressMarkdownText used text.slice(0, MAX - 1), which splits UTF-16 surrogate pairs. When an astral character (e.g. an emoji) straddles the 299/300-code-unit boundary, the slice keeps only the high surrogate half (\uD83D), producing an invalid character that the Telegram Bot API rejects. Extract the helper to extensions/telegram/src/truncate.ts as clipTelegramProgressText and switch to sliceUtf16Safe (from openclaw/plugin-sdk/text-utility-runtime), mirroring the fix applied to the equivalent Slack helper in openclaw#96382. Adds truncate.test.ts with five vitest cases covering the straddle, fits-before-cut, unchanged-short, trimEnd, and ASCII-exact-limit paths.
…ogate pairs (openclaw#96382) truncateSlackText sliced by UTF-16 code unit ('trimmed.slice(0, max - 1)'), so an emoji or other astral character straddling the limit was cut in half, leaving a lone high surrogate before the ellipsis — e.g. truncateSlackText('abc😀def', 5) returned 'abc\uD83D…' instead of 'abc…'. That invalid half-character is sent in live Slack payloads (message text and Block Kit section/button/header/option labels, which truncate at limits as small as 75). Use the repo's canonical sliceUtf16Safe (already re-exported from plugin-sdk/text-utility-runtime, the module slack code imports from) so a straddling pair is dropped whole. Behavior is byte-identical for all-BMP input. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…ogate pairs (openclaw#96382) truncateSlackText sliced by UTF-16 code unit ('trimmed.slice(0, max - 1)'), so an emoji or other astral character straddling the limit was cut in half, leaving a lone high surrogate before the ellipsis — e.g. truncateSlackText('abc😀def', 5) returned 'abc\uD83D…' instead of 'abc…'. That invalid half-character is sent in live Slack payloads (message text and Block Kit section/button/header/option labels, which truncate at limits as small as 75). Use the repo's canonical sliceUtf16Safe (already re-exported from plugin-sdk/text-utility-runtime, the module slack code imports from) so a straddling pair is dropped whole. Behavior is byte-identical for all-BMP input. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
What Problem This Solves
truncateSlackText(extensions/slack/src/truncate.ts) slices by UTF-16 code unit:When an emoji or other astral character (a surrogate pair) straddles the limit,
slicekeeps only its leading high surrogate, emitting a lone, invalid\uD83Dright before the ellipsis:This text is sent in live Slack payloads —
chat.postMessagetext (send.ts) and Block Kit section text, button labels, option labels, and header titles (blocks-render.ts), which truncate at limits as small asSLACK_PLAIN_TEXT_MAX = 75. Any user/agent text ending with an emoji near a limit produces a broken half-character.Fix
Use the repo's canonical surrogate-safe slice,
sliceUtf16Safe(defined insrc/utils.ts, already re-exported fromopenclaw/plugin-sdk/text-utility-runtime— the module Slack code already imports from). It drops a straddling pair whole.truncateSlackTextwas the lone straggler still slicing raw; this aligns it with the rest of the codebase (truncateUtf16Safe, discordclampToCodePointBoundary,src/shared/text-chunking.ts, etc.).Behavior is byte-identical for all-BMP input —
sliceUtf16Safe(s, 0, n) === s.slice(0, n)whenever no pair straddles the edge.Evidence
Added
extensions/slack/src/truncate.test.ts(the function is exported; no test existed). Verified by running the exactsliceUtf16Safe+truncateSlackTextlogic:All-BMP and emoji-that-fits cases are unchanged; only the straddling-pair case changes, and the fixed output contains no lone surrogate.