fix(feishu): truncate streaming-card summary on code-point boundaries#97462
Conversation
truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
Codex review: needs maintainer review before merge. Reviewed June 28, 2026, 6:04 AM ET / 10:04 UTC. Summary PR surface: Source +4, Tests +66. Total +70 across 2 files. Reproducibility: yes. Current main's Review metrics: none identified. Stored data model 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 helper-based Feishu truncation fix after normal maintainer review; keep the earlier closed duplicate closed. Do we have a high-confidence way to reproduce the issue? Yes. Current main's Is this the best way to solve the issue? Yes. Reusing the existing SDK text helper inside Feishu's private summary truncation function is the narrowest maintainable fix; broader grapheme handling or a new abstraction is not needed for this UTF-16 surrogate-boundary bug. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 8d168c836af3. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +4, Tests +66. Total +70 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
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…openclaw#97462) truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…openclaw#97462) truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…openclaw#97462) truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…openclaw#97462) truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…openclaw#97462) truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> (cherry picked from commit 2064575)
…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.
…openclaw#97462) truncateSummary used clean.slice(0, max - 3), which can cut between the two UTF-16 halves of a surrogate pair (emoji / astral char) straddling the limit. The serialized card summary then carries a lone high surrogate that Feishu renders as the replacement char. Slice with the surrogate-safe sliceUtf16Safe helper instead, matching the pattern already used in extensions/slack/src/truncate.ts, so a straddling code point is dropped whole. Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> (cherry picked from commit 2064575)
What Problem This Solves
Feishu streaming-card summary truncation could slice directly through a UTF-16 surrogate pair when a long summary placed an emoji at the boundary.
Why This Change Was Made
The streaming-card summary now uses the shared surrogate-safe truncation helper so Feishu receives valid text while the existing summary limit and fallback behavior stay unchanged.
User Impact
Long Feishu streaming-card summaries containing emoji or other astral characters no longer render replacement characters or malformed text at the truncation boundary.
Evidence
After-fix Proof
Ran on the PR branch:
Additional boundary probe against the PR branch source:
{ "usesSafeSlice": true, "inputLength": 68, "outputLength": 49, "hasLoneSurrogate": false, "outputTailCodeUnits": [ "61", "61", "61", "2e", "2e", "2e" ] }The probe uses the same 50-code-unit summary limit and an emoji at the truncation boundary.
hasLoneSurrogate: falseconfirms the after-fix summary no longer leaves a dangling UTF-16 surrogate before the ellipsis.