Skip to content

fix(zalouser): keep outbound message text truncation UTF-16 safe#101625

Closed
lsr911 wants to merge 1 commit into
openclaw:mainfrom
lsr911:fix/zalouser-utf16-safe-truncation
Closed

fix(zalouser): keep outbound message text truncation UTF-16 safe#101625
lsr911 wants to merge 1 commit into
openclaw:mainfrom
lsr911:fix/zalouser-utf16-safe-truncation

Conversation

@lsr911

@lsr911 lsr911 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

The Zalouser plugin truncates outbound message text with .slice(0, 2000) in two send paths — media messages and plain text messages. Zalo users in Vietnam commonly use emoji in messages. When the 2000-code-unit boundary lands mid-surrogate-pair, the dangling surrogate renders as U+FFFD in the message delivered to the Zalo recipient.

This is the highest-impact fix in the UTF-16 series — unlike log/display truncation, these sites directly affect production user messages sent through the Zalo platform.

Why This Change Was Made

truncateUtf16Safe is already available via openclaw/plugin-sdk/text-utility-runtime — the same SDK module the file already imports from for sleep.

Evidence

Branch prerun (commit cef6cca)

=== Zalo media message @2000 boundary ===
Raw .slice(0,2000) with "z".repeat(1998) + "🚀": Safe ends "z🚀" | len: 2000 | no U+FFFD: true

=== Zalo text message @2000 CJK within limit ===
1000 CJK chars → truncateUtf16Safe: len=1000 (unchanged, within limit)

Files Changed

File Change
extensions/zalouser/src/zalo-js.ts +truncateUtf16Safe import; 2 .slice(0, 2000)truncateUtf16Safe(...)

Replace two raw .slice(0, 2000) calls with truncateUtf16Safe
in Zalo media-message and plain-text send paths to prevent
dangling surrogate pairs when the 2000-code-unit boundary
splits an emoji or CJK supplementary character in outbound
user-visible messages.
@openclaw-barnacle openclaw-barnacle Bot added channel: zalouser Channel integration: zalouser triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. size: XS and removed triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jul 7, 2026
@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 7, 2026, 8:19 AM ET / 12:19 UTC.

Summary
The PR imports truncateUtf16Safe and replaces two raw 2000-code-unit truncations in extensions/zalouser/src/zalo-js.ts.

PR surface: Source +9. Total +9 across 1 file.

Reproducibility: yes. from source inspection and a focused JS probe: sendMessageZalouser can slice 1999 BMP characters plus an emoji into a length-2000 chunk ending with a high surrogate before sendZaloTextMessage runs.

Review metrics: 1 noteworthy metric.

  • UTF-16 boundary layers: 2 low-level truncations changed, 1 upstream chunker unchanged. The unchanged upstream chunker is the production caller that can create a dangling-surrogate chunk before the new helper runs.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦪 silver shellfish
Patch quality: 🧂 unranked krab
Result: blocked until stronger real behavior proof is added.

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

Rank-up moves:

  • Move the UTF-16-safe boundary into splitTextRanges or splitStyledText.
  • [P1] Add regression coverage for 1999 BMP characters plus an emoji through plain text and media/caption sends.
  • [P1] Add redacted real Zalouser send proof, runtime logs, or a transcript showing the fixed behavior.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR body includes copied console output, but it does not exercise a real Zalouser send and the sample does not demonstrate a split surrogate boundary; add redacted live output, logs, or a transcript after fixing. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] The unchanged splitStyledText/splitTextRanges path can still split a surrogate pair before the patched low-level send path runs.
  • [P1] The PR body's copied console output is not real Zalouser send proof and its 1998-BMP-plus-emoji example does not exercise a split surrogate boundary.

Maintainer options:

  1. Fix chunk boundaries before merge (recommended)
    Move the UTF-16-safe boundary decision into splitTextRanges or splitStyledText, keep the downstream guard, and cover plain text plus media/caption sends.
  2. Pause this branch
    Leave this PR open but blocked until the author adds the upstream chunker repair and real behavior proof.

Next step before merge

  • [P1] The PR needs contributor or maintainer follow-up because the remaining blocker is both a code repair and missing real behavior proof, which should not be treated as a ClawSweeper repair marker on this external PR.

Security
Cleared: The diff only imports an existing SDK text helper in a Zalouser runtime file and does not change dependencies, secrets, CI, or packaging.

Review findings

  • [P1] Make Zalouser chunk boundaries UTF-16 safe before send — extensions/zalouser/src/zalo-js.ts:1316
Review details

Best possible solution:

Make Zalouser chunk range selection UTF-16-safe before slicing, keep the low-level truncation as a guard, and prove text/media sends with focused regression coverage plus redacted real send output.

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

Yes from source inspection and a focused JS probe: sendMessageZalouser can slice 1999 BMP characters plus an emoji into a length-2000 chunk ending with a high surrogate before sendZaloTextMessage runs.

Is this the best way to solve the issue?

No; this is too low in the send stack because the unsafe boundary is selected upstream in splitTextRanges/splitStyledText before the patched truncation call.

Full review comments:

  • [P1] Make Zalouser chunk boundaries UTF-16 safe before send — extensions/zalouser/src/zalo-js.ts:1316
    This previous blocker is still present at the current head. sendMessageZalouser builds chunk.text with text.slice(start, end) before this new truncateUtf16Safe(text, 2000) call runs; if the 2000 boundary lands between surrogate halves, the chunk is already length 2000 with a dangling high surrogate, so the helper returns it unchanged and the outbound message can still contain U+FFFD. Move the safe-boundary logic into splitTextRanges or splitStyledText and cover the text/media chunk path.
    Confidence: 0.95

Overall correctness: patch is incorrect
Overall confidence: 0.93

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add merge-risk: 🚨 message-delivery: The PR changes outbound message truncation but leaves the production chunker able to deliver corrupted text at UTF-16 boundaries.

Label justifications:

  • P2: This is a normal-priority Zalouser message-delivery correctness fix with limited channel-specific blast radius.
  • merge-risk: 🚨 message-delivery: The PR changes outbound message truncation but leaves the production chunker able to deliver corrupted text at UTF-16 boundaries.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body includes copied console output, but it does not exercise a real Zalouser send and the sample does not demonstrate a split surrogate boundary; add redacted live output, logs, or a transcript after fixing. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +9. Total +9 across 1 file.

View PR surface stats
Area Files Added Removed Net
Source 1 15 6 +9
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 1 15 6 +9

What I checked:

  • Repository policy read: Root and scoped extension policy were read; bundled plugin production code may use openclaw/plugin-sdk/*, but review must inspect callers and sibling chunking surfaces before verdict. (AGENTS.md:11, 2ba622ca3019)
  • PR diff surface: The branch changes only two low-level sendZaloTextMessage truncations and import formatting; it does not change the upstream chunk range calculation. (extensions/zalouser/src/zalo-js.ts:1316, cef6cca7688f)
  • Production caller chunks first: sendMessageZalouser prepares chunks before calling sendZaloTextMessage, so the patched helper is not the first boundary decision for normal outbound sends. (extensions/zalouser/src/send.ts:42, 2ba622ca3019)
  • Unsafe boundary remains: splitStyledText still creates chunks with text.slice(start, end) after splitTextRanges chooses start + limit, which can split a UTF-16 surrogate pair before the PR's truncation runs. (extensions/zalouser/src/send.ts:161, 2ba622ca3019)
  • Helper cannot repair already bounded chunks: truncateUtf16Safe returns the input unchanged when input.length <= limit, so a length-2000 chunk ending in a high surrogate is preserved. (packages/normalization-core/src/utf16-slice.ts:44, 2ba622ca3019)
  • Focused source probe: A read-only JS probe showed 'a'.repeat(1999) + '🚀' sliced to 2000 code units ends in 0xd83d, and truncateUtf16Safe(chunk, 2000) returns that chunk unchanged.

Likely related people:

  • steipete: Current blame for the Zalouser send wrapper, chunker, and low-level send implementation points to the recent native zca-js migration in fix(zalo): accept opaque string chat IDs. (role: recent area contributor; confidence: high; commits: 6b76a306d472; files: extensions/zalouser/src/send.ts, extensions/zalouser/src/zalo-js.ts)
  • darkamenosa: History for splitStyledText and styled Zalouser sends also traces back to the markdown-to-Zalo text style parsing feature, which overlaps the chunk/style boundary involved here. (role: adjacent feature contributor; confidence: medium; commits: a6711afdc2bb; files: extensions/zalouser/src/send.ts, extensions/zalouser/src/send.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.
Review history (1 earlier review cycle)
  • reviewed 2026-07-07T12:13:18.074Z sha cef6cca :: needs real behavior proof before merge. :: [P1] Make Zalouser chunk boundaries UTF-16 safe before send

@clawsweeper clawsweeper Bot added 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. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jul 7, 2026
@steipete

steipete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Thanks @lsr911. The useful behavior from this PR was consolidated with the sibling UTF-16 boundary fixes into #101654 and landed on main in a9582a1bb62aa70bb0b0ceb72536d0d76f08eab8.

The landed fix uses the existing truncateUtf16Safe helper, preserves this caller's prior code-unit limit and output shape, and adds caller-level surrogate-boundary coverage. Closing this PR as superseded by the canonical landed change.

@steipete steipete closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: zalouser Channel integration: zalouser merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants