fix(feishu): truncate streaming-card summary on a code-point boundary#96411
Closed
ly-wang19 wants to merge 1 commit into
Closed
fix(feishu): truncate streaming-card summary on a code-point boundary#96411ly-wang19 wants to merge 1 commit into
ly-wang19 wants to merge 1 commit into
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]>
|
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Problem This Solves
truncateSummaryinextensions/feishu/src/streaming-card.tsbuilds thecloseout summary that is embedded in the Card Kit
settingsPATCH (config.summary.content).It truncated with raw UTF-16 indexing:
When an emoji or other astral-plane character (a UTF-16 surrogate pair)
straddles the
max - 3cut point,slicekeeps the high surrogate anddrops 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."aaaa…(46 a's)…" + "\uD83D" + "..."— the summary ends with a lonehigh surrogate
\uD83Dbefore..., which serializes invalid and renders as�."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)(fromopenclaw/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
sliceUtf16Safehelper plus theOLD (buggy) and NEW (fixed)
truncateSummarylogic:slice(0, 47)output is"…\uD83D..."— a dangling highsurrogate (
charCodeAt(len-4) === 0xD83D), and is not equal to the expectedsurrogate-clean summary.
"a".repeat(46) + "...", contains no lonesurrogate, and survives a UTF-8 encode roundtrip without a
�.(still
47 chars + "..."), empty strings, and non-straddling emoji are allbyte-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") thatdrives
session.close(finalText), captures the/settingsPATCH body, andasserts
config.summary.contentis"a".repeat(46) + "..."with no\uD83D.It fails on the old code and passes after the fix.