Skip to content

fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16#97428

Merged
vincentkoc merged 3 commits into
openclaw:mainfrom
ly-wang19:fix/line-template-surrogate-safe
Jun 28, 2026
Merged

fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16#97428
vincentkoc merged 3 commits into
openclaw:mainfrom
ly-wang19:fix/line-template-surrogate-safe

Conversation

@ly-wang19

Copy link
Copy Markdown
Contributor

What Problem This Solves

Line plugin template title/altText truncation operates on raw UTF-16 code units. When a template title contains an emoji or other astral character near the truncation boundary, the cut splits a surrogate pair and produces a malformed string with a lone surrogate.

Why This Change Was Made

Truncation boundaries should respect grapheme/code-point boundaries for the same reason other user-facing text does — a lone surrogate breaks downstream rendering, JSON encoding, and copy-paste. Switch the truncation to grapheme boundaries so a multi-code-point character never gets cut mid-character.

User Impact

Line templates with long titles or alt text that include emoji or astral characters no longer produce broken strings when truncated.

Evidence

  • Local: pnpm test on touched line-plugin test file
  • Touched files: 2 files, +23/-7
  • Branch: fix/line-template-surrogate-safe

…t raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added channel: line Channel integration: line size: XS labels Jun 28, 2026
@clawsweeper

clawsweeper Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 28, 2026, 1:49 PM ET / 17:49 UTC.

Summary
The PR routes LINE template title, confirm text, carousel title, and template altText truncation through the existing grapheme-aware helper and adds surrogate-boundary tests.

PR surface: Source +9, Tests +53. Total +62 across 2 files.

Reproducibility: yes. source-level: current main uses raw .slice(0, N) on the affected LINE template fields, and a focused Node probe shows that boundary can leave a lone surrogate. I did not run a live LINE send in this read-only review.

Review metrics: none identified.

Root-cause cluster
Relationship: canonical
Canonical: #97428
Summary: This PR is the active canonical candidate for LINE template-message title/text/altText surrogate-safe truncation; the older same-surface PR is closed unmerged, and action-field PRs share the Unicode-boundary class but touch a different builder surface.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • [P2] No repair job is needed because the branch is a focused implementation PR and I found no concrete patch defect to automate.

Security
Cleared: The diff only changes LINE template-building TypeScript and focused Vitest coverage; it does not touch dependencies, workflows, secrets, lockfiles, package metadata, or install/publish scripts.

Review details

Best possible solution:

Land this focused LINE template-message truncation fix after normal maintainer merge gates; keep adjacent LINE action-field cleanup in #97470 separate.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level: current main uses raw .slice(0, N) on the affected LINE template fields, and a focused Node probe shows that boundary can leave a lone surrogate. I did not run a live LINE send in this read-only review.

Is this the best way to solve the issue?

Yes: reusing the existing local grapheme-aware helper inside the LINE template-message builder is the narrowest maintainable fix for this surface. The related action-field cleanup is adjacent and tracked separately in #97470.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against c0883a531de9.

Label changes

Label justifications:

  • P2: This is a normal-priority LINE channel payload correctness fix with limited blast radius and no evidence of security, data-loss, or core-runtime impact.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The contributor posted branch-specific copied terminal/live output invoking production createButtonTemplate and showing title and altText truncation no longer leave lone surrogates after the fix.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor posted branch-specific copied terminal/live output invoking production createButtonTemplate and showing title and altText truncation no longer leave lone surrogates after the fix.
Evidence reviewed

PR surface:

Source +9, Tests +53. Total +62 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 16 7 +9
Tests 1 53 0 +53
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 69 7 +62

What I checked:

  • Repository policy read: Root AGENTS.md and the scoped extensions guide were read; their review-depth, plugin-boundary, and dependency-contract guidance apply to this LINE plugin PR. (AGENTS.md:1, c0883a531de9)
  • Scoped extension guide read: extensions/AGENTS.md keeps bundled plugin production code inside the plugin boundary; this PR stays within extensions/line and local/plugin-sdk surfaces. (extensions/AGENTS.md:1, c0883a531de9)
  • Current main still has the bug: Current main uses raw .slice(0, N) for confirm text, template altText, button title, carousel title, and image-carousel altText, so current main has not already solved the central problem. (extensions/line/src/template-messages.ts:89, c0883a531de9)
  • Latest release still has the bug: v2026.6.10 shows the same raw template-message slices, so this fix is not already shipped in the latest release. (extensions/line/src/template-messages.ts:89, aa69b12d0086)
  • Source-level reproduction: A read-only Node probe showed ('x'.repeat(39) + '😀').slice(0, 40) leaves trailing high surrogate 0xd83d, matching the raw-slice failure described by the PR. (c0883a531de9)
  • PR implementation: At the PR head, the affected template fields call truncateTemplateText or the optional wrapper instead of raw slices. (extensions/line/src/template-messages.ts:68, fc4f6dd063fb)

Likely related people:

  • plum-dawg: GitHub PR metadata and history identify the original LINE plugin PR as authored by plum-dawg, and the template-message builder originated in that feature history. (role: original feature contributor; confidence: medium; commits: c96ffa7186a4; files: src/line/template-messages.ts)
  • steipete: Live PR metadata shows steipete merged the original LINE plugin PR, and git history shows a later LINE action/template helper refactor by Peter Steinberger. (role: merger and adjacent refactor contributor; confidence: medium; commits: c96ffa7186a4, d71f6afb7f7b; files: src/line/template-messages.ts, src/line/actions.ts)
  • vincentkoc: Git history shows Vincent Koc moved the LINE template-message module from src/line into the bundled plugin boundary. (role: migration/refactor contributor; confidence: medium; commits: 213198123042; files: src/line/template-messages.ts, extensions/line/src/template-messages.ts)
  • llagy009: Current-main blame for the checked-out LINE template-message module points to recent work by llagy009 that carried the existing raw slices and grapheme helper forward in this checkout. (role: recent area contributor; confidence: medium; commits: 2e881ab1c679; files: extensions/line/src/template-messages.ts, extensions/line/src/message-cards.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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 keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jun 28, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jun 28, 2026
@ly-wang19

Copy link
Copy Markdown
Contributor Author

Real behavior proof — grapheme-safe title/altText truncation

The new surrogate-safe path is exercised by a focused regression test that asserts no lone high surrogate remains after truncation. Local run (Node 22.22.1):

$ pnpm test extensions/line/src/message-cards.test.ts

 Test Files  1 passed (1)
      Tests  25 passed (25)
   Duration  3.42s
[test] passed 1 Vitest shard in 28.31s

Regression case added (extensions/line/src/message-cards.test.ts)

it("drops a surrogate-pair emoji from the title instead of splitting it", () => {
  // 39 chars + an emoji land the truncation boundary inside the surrogate pair;
  // a raw code-unit slice would keep only the lone high surrogate.
  const template = createButtonTemplate(`${"x".repeat(39)}😀`, "Text", [messageAction("OK")]);
  const title = (template.template as { title: string }).title;

  expect(title).toBe("x".repeat(39));
  expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(title)).toBe(false);
});

Pre-fix vs post-fix

  • Pre-fix (main): title.slice(0, 40) on a 41-code-unit string (xxxx...x😀) would cut between the high and low surrogates of 😀, leaving a lone high surrogate (\uD83D) at position 40. The regex /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/ matches such an unpaired high surrogate, so the second assertion would fail.
  • Post-fix (this PR): truncateTemplateText(title, 40) walks grapheme boundaries via the existing helper, so the trailing emoji is dropped cleanly. Title is xxx...x (39 chars), no lone surrogate, both assertions pass.

The same wire-up is applied wherever a raw .slice(0, N) was previously used for LINE field limits: button title (40), altText (400), carousel column title (40), and confirm-dialog text (240). All routes through the existing truncateTemplateText grapheme-safe helper, with an additional truncateOptionalTemplateText wrapper for optional fields.

@ly-wang19

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@ly-wang19

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@ly-wang19

Copy link
Copy Markdown
Contributor Author

Real-behavior proof (post-fix) for the surrogate-boundary case, generated by invoking production createButtonTemplate from extensions/line/src/template-messages.ts via tsx.

Input title (41 UTF-16 code units, emoji at the 40-char boundary):

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx😀   (39 ASCII + U+1F600, 40 graphemes, 40 code units after dropping the emoji)

Before-fix behavior on main: raw .slice(0, 40)

result:              "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\ud83d"
length:              40 chars
has lone surrogate:  true

The last code unit is a lone high surrogate \ud83d — LINE renders it as or rejects the template payload.

After-fix behavior: createButtonTemplate(title, text, [action]) on this branch

template.title:      "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
length:              39 chars
has lone surrogate:  false

The grapheme segmenter drops the whole 😀 grapheme rather than slicing it.

After-fix: createButtonTemplate(..., { altText: 399*'y' + '🦀' }) (limit 400)

altText:             399 'y' chars (emoji dropped)
length:              399 chars
has lone surrogate:  false

Both title and altText paths route through the file's existing grapheme-safe truncateTemplateText after this PR, so no template field can leave a lone surrogate. The truncateOptionalTemplateText wrapper is a no-op for all-BMP input — only surrogate-straddling truncation changes.

Local: ./node_modules/.bin/tsx extensions/line/scripts/surrogate-proof.mts on Node v22.22.1 (script removed after running — one-off proof harness, not part of the patch).

@clawsweeper re-review

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 28, 2026
@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jun 28, 2026
@vincentkoc
vincentkoc merged commit 4b36cf4 into openclaw:main Jun 28, 2026
106 of 110 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 29, 2026
…t raw UTF-16 (openclaw#97428)

* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: retry OpenGrep scan (HTTP 502 infra flake)

* test(line): cover grapheme-safe template fields

---------

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]>
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
…t raw UTF-16 (openclaw#97428)

* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: retry OpenGrep scan (HTTP 502 infra flake)

* test(line): cover grapheme-safe template fields

---------

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]>
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 1, 2026
…t raw UTF-16 (openclaw#97428)

* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: retry OpenGrep scan (HTTP 502 infra flake)

* test(line): cover grapheme-safe template fields

---------

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]>
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
…t raw UTF-16 (openclaw#97428)

* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: retry OpenGrep scan (HTTP 502 infra flake)

* test(line): cover grapheme-safe template fields

---------

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]>
Rorqualx pushed a commit to Rorqualx/cortex that referenced this pull request Jul 4, 2026
…t raw UTF-16 (openclaw#97428)

* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: retry OpenGrep scan (HTTP 502 infra flake)

* test(line): cover grapheme-safe template fields

---------

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]>
(cherry picked from commit 4b36cf4)
Rorqualx pushed a commit to Rorqualx/cortex that referenced this pull request Jul 8, 2026
…t raw UTF-16 (openclaw#97428)

* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16

createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* chore: retry OpenGrep scan (HTTP 502 infra flake)

* test(line): cover grapheme-safe template fields

---------

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]>
(cherry picked from commit 4b36cf4)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: line Channel integration: line P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants