fix(line): truncate outbound altText, location, menu, and code fields on code point boundaries#98994
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 4, 2026, 2:45 AM ET / 06:45 UTC. Summary PR surface: Source +26, Tests +107. Total +133 across 10 files. Reproducibility: yes. source-level: current main still hard-caps outbound LINE strings with raw Review metrics: 1 noteworthy metric.
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. Next step before merge
Security Review detailsBest possible solution: Land this focused LINE outbound truncation fix after normal maintainer and CI gates. Do we have a high-confidence way to reproduce the issue? Yes, source-level: current main still hard-caps outbound LINE strings with 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 cafbd745c1b1. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +26, Tests +107. Total +133 across 10 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 re-review Addressed the retained raw cap in 8fd9e7d: the /card receipt flex altText now goes through truncateUtf16Safe(…, 400) like the other card types, closing the last raw .slice string cap in the plugin (verified with a multi-line-aware scan across extensions/line/src — zero remaining). Added the requested regression in message-cards.test.ts ( |
|
🦞🧹 I asked ClawSweeper to review this item again. |
8fd9e7d to
8b8b72c
Compare
|
Land-ready review complete on exact head Maintainer repair:
Proof:
Known proof gap: no live LINE credential call. The touched boundary is pure payload serialization and is covered by mocked SDK request assertions, the pinned SDK forwarding implementation, and the current LINE character-counting contract. |
|
Merged via squash.
|
… on code point boundaries (openclaw#98994) * fix(line): truncate outbound altText, location, menu, and code fields on code-point boundaries * fix(line): use safe truncation for receipt card altText * fix(line): count rich menu limits by grapheme --------- Co-authored-by: Vincent Koc <[email protected]>
…void lone surrogates The Mattermost inbound handler in extensions/mattermost/src/mattermost/monitor.ts truncates the message body preview with bodyText.slice(0, 200), which can split a UTF-16 surrogate pair when an emoji (e.g. 🎉, 🦞) or other astral character lands at the 200-char boundary. The resulting lone surrogate half leaks into the verbose log preview and could propagate to downstream consumers that reject ill-formed strings. Replace the raw .slice(0, 200) with the shared truncateUtf16Safe helper (from openclaw/plugin-sdk/text-utility-runtime), which backs up one code unit when the cut would land inside a surrogate pair. This matches the same pattern already applied to the Slack, Discord, Telegram, IRC, Line, MS Teams, and Feishu channel previews (openclaw#96456, openclaw#96569, openclaw#96572, openclaw#96577, openclaw#96578, openclaw#97462, openclaw#97472, openclaw#98994). The preview is only used for the verbose inbound log line so the behavioral change is cosmetic: when the 200th code unit would split an emoji, the preview now includes one fewer code unit rather than emitting a broken surrogate.
…void lone surrogates The Mattermost inbound handler in extensions/mattermost/src/mattermost/monitor.ts truncates the message body preview with bodyText.slice(0, 200), which can split a UTF-16 surrogate pair when an emoji (e.g. 🎉, 🦞) or other astral character lands at the 200-char boundary. The resulting lone surrogate half leaks into the verbose log preview and could propagate to downstream consumers that reject ill-formed strings. Replace the raw .slice(0, 200) with the shared truncateUtf16Safe helper (from openclaw/plugin-sdk/text-utility-runtime), which backs up one code unit when the cut would land inside a surrogate pair. This matches the same pattern already applied to the Slack, Discord, Telegram, IRC, Line, MS Teams, and Feishu channel previews (openclaw#96456, openclaw#96569, openclaw#96572, openclaw#96577, openclaw#96578, openclaw#97462, openclaw#97472, openclaw#98994). The preview is only used for the verbose inbound log line so the behavioral change is cosmetic: when the 200th code unit would split an emoji, the preview now includes one fewer code unit rather than emitting a broken surrogate.
What Problem This Solves
Fixes an issue where several LINE outbound fields can be sent to the LINE API with a broken character (a lone UTF-16 surrogate) when an emoji straddles the field's hard length cap. Sixteen call sites across six files still capped user-facing strings with a raw
.slice(0, N):altText(400-char cap) —outbound.ts,send.ts,auto-reply-delivery.ts,card-command.ts(×4)title/address(100) —outbound.ts,send.tsname(300) andchatBarText(14) —rich-menu.tsmarkdown-to-line.tsA raw slice cuts an emoji's surrogate pair in half, leaving a dangling surrogate in the payload (rendered as a broken glyph; serializes as an invalid lone
\uD83D, which the LINE API can reject). The 14-charchatBarTextcap is especially easy to hit with emoji menu labels.Why This Change Was Made
Each cap now goes through
truncateUtf16Safe— the same helper this plugin's own merged fixes already use for the sibling fields: actionlabel/data(actions.ts, "fix(line): truncate action fields on code-point boundaries") and templatetitle/text/altText(#97428). This PR brings the remaining outbound fields in line with those precedents; it is a mechanical 1:1 replacement of.slice(0, N)withtruncateUtf16Safe(x, N)using the same limits — no behavior change for any string that fits or ends on a complete character.User Impact
LINE users whose flex alt text, location titles/addresses, rich-menu labels, or shared code blocks contain an emoji near a field's length cap get clean, valid messages instead of a broken glyph or a rejected API call. No configuration changes.
Evidence
Real terminal proof (standalone, non-test)
Drove the real caps (and the real
convertCodeBlockToFlexBubble) from a standalone script, inspecting trailing UTF-16 code units:0xd83dis the high-surrogate half of😀(U+1F600); onmainit is left dangling at the end of the field.Regression tests (fail on unpatched code, pass here)
Added two focused tests driving the real production paths:
markdown-to-line.test.ts—does not split a surrogate pair at the truncation boundary: code whose emoji straddles index 2000 through the realconvertCodeBlockToFlexBubble.auto-reply-delivery.test.ts—truncates flex altText on a surrogate boundary: altText whose emoji straddles index 400 through the realdeliverLineAutoReply, asserting the altText handed to the LINE message builder is ≤400 with no lone surrogate.On unpatched
mainboth fail (expected true to be false— a lone surrogate is present); with this PR both pass.Touched-module suites all green:
markdown-to-line(25),auto-reply-delivery(6),rich-menu+channel.sendPayload+reply-payload-transform(43),message-cards+outbound-media(51). oxlint andoxfmt --checkclean on all eight files.AI-assisted (Claude Code); change reviewed and understood by the author.