Skip to content

fix(feishu): truncate streaming-card summary on a code-point boundary#96411

Closed
ly-wang19 wants to merge 1 commit into
openclaw:mainfrom
ly-wang19:fix/feishu-streaming-card-surrogate-truncate
Closed

fix(feishu): truncate streaming-card summary on a code-point boundary#96411
ly-wang19 wants to merge 1 commit into
openclaw:mainfrom
ly-wang19:fix/feishu-streaming-card-surrogate-truncate

Conversation

@ly-wang19

Copy link
Copy Markdown
Contributor

What Problem This Solves

truncateSummary in extensions/feishu/src/streaming-card.ts builds the
closeout summary that is embedded in the Card Kit settings PATCH (config.summary.content).
It truncated with raw UTF-16 indexing:

return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";

When an emoji or other astral-plane character (a UTF-16 surrogate pair)
straddles the max - 3 cut point, slice keeps the high surrogate and
drops the low surrogate, leaving a lone surrogate half in the summary.
Feishu renders that dangling half as the replacement character .

Before / after (repro): input is "a".repeat(46) + "😀" + "b".repeat(20)
(a 68-char string with 😀 / U+1F600 at UTF-16 indices 46–47), default max = 50,
so the slice happens at max - 3 = 47 — right between the two halves of the emoji.

  • Before: "aaaa…(46 a's)…" + "\uD83D" + "..." — the summary ends with a lone
    high surrogate \uD83D before ..., which serializes invalid and renders as .
  • After: "a".repeat(46) + "..." — the half-emoji is dropped whole.

The fix swaps the raw slice for the repo's surrogate-safe helper,
sliceUtf16Safe(clean, 0, max - 3) (from openclaw/plugin-sdk/text-utility-runtime),
which snaps the boundary off a surrogate pair so the straddling code point is
dropped whole. This mirrors the existing pattern in
extensions/slack/src/truncate.ts. ASCII / non-straddling inputs are unchanged.

Evidence

Node red-green proof replicating the exact sliceUtf16Safe helper plus the
OLD (buggy) and NEW (fixed) truncateSummary logic:

repro.length = 68
OLD output endsWith lone high surrogate \uD83D: true
OLD output JSON: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83d..."
NEW output JSON: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
PASS: OLD leaves a dangling high surrogate (bug present)
PASS: OLD does NOT equal expected (bug present)
PASS: NEW equals expected (half-emoji dropped whole)
PASS: NEW contains NO lone high surrogate
PASS: NEW is valid (no replacement char on encode roundtrip)
PASS: normal short unchanged
PASS: normal long ASCII unchanged vs old
PASS: normal long ASCII is 47 chars + ...
PASS: empty unchanged
PASS: non-straddling emoji preserved

ALL ASSERTIONS PASSED
  • RED: the old slice(0, 47) output is "…\uD83D..." — a dangling high
    surrogate (charCodeAt(len-4) === 0xD83D), and is not equal to the expected
    surrogate-clean summary.
  • GREEN: the fixed output equals "a".repeat(46) + "...", contains no lone
    surrogate, and survives a UTF-8 encode roundtrip without a .
  • No behavior change for unaffected inputs: short strings, long ASCII
    (still 47 chars + "..."), empty strings, and non-straddling emoji are all
    byte-identical to the previous behavior.

A unit test was added in extensions/feishu/src/streaming-card.test.ts
("drops a surrogate pair whole when truncating the closeout summary") that
drives session.close(finalText), captures the /settings PATCH body, and
asserts config.summary.content is "a".repeat(46) + "..." with no \uD83D.
It fails on the old code and passes after the fix.

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]>
@openclaw-barnacle openclaw-barnacle Bot added channel: feishu Channel integration: feishu size: S r: too-many-prs Auto-close: author has more than twenty active PRs. labels Jun 24, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 20 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu r: too-many-prs Auto-close: author has more than twenty active PRs. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant