Skip to content

fix(feishu): truncate streaming-card summary on code-point boundaries#97462

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
ly-wang19:fix/feishu-streaming-card-surrogate-truncate
Jun 28, 2026
Merged

fix(feishu): truncate streaming-card summary on code-point boundaries#97462
vincentkoc merged 1 commit into
openclaw:mainfrom
ly-wang19:fix/feishu-streaming-card-surrogate-truncate

Conversation

@ly-wang19

@ly-wang19 ly-wang19 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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

  • git diff --check origin/main...HEAD
  • node scripts/run-vitest.mjs extensions/feishu/src/streaming-card.test.ts
  • .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

After-fix Proof

Ran on the PR branch:

node scripts/run-vitest.mjs extensions/feishu/src/streaming-card.test.ts --reporter=verbose
# exit 0

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: false confirms the after-fix summary no longer leaves a dangling UTF-16 surrogate before the ellipsis.

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 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, 6:04 AM ET / 10:04 UTC.

Summary
The PR switches Feishu streaming-card closeout summary truncation to sliceUtf16Safe and adds a regression test for an emoji split at the summary limit.

PR surface: Source +4, Tests +66. Total +70 across 2 files.

Reproducibility: yes. Current main's clean.slice(0, max - 3) path reproduces a dangling high surrogate before the ellipsis with the reported boundary input, and close() sends that string as Feishu CardKit summary content.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: extensions/feishu/src/streaming-card.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: canonical
Canonical: #97462
Summary: This PR is the live landing candidate for the Feishu streaming-card surrogate-safe summary truncation fix; the earlier identical PR was closed unmerged by queue-limit automation.

Members:

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

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

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

Rank-up moves:

  • none.

Next step before merge

  • [P2] No repair job is needed; the patch is focused and the remaining action is ordinary maintainer review or merge of the current PR.

Security
Cleared: The diff only changes Feishu text truncation and a colocated test; it does not touch secrets, workflows, dependencies, lockfiles, install scripts, or other code-execution surfaces.

Review details

Best 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 clean.slice(0, max - 3) path reproduces a dangling high surrogate before the ellipsis with the reported boundary input, and close() sends that string as Feishu CardKit summary content.

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 changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. Sufficient live output: the PR body now includes after-fix terminal/test output plus a copied boundary probe against the PR branch source showing no lone surrogate remains.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): Sufficient live output: the PR body now includes after-fix terminal/test output plus a copied boundary probe against the PR branch source showing no lone surrogate remains.
  • remove rating: 🧂 unranked krab: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: The PR fixes a bounded Feishu channel text-rendering bug in one streaming-card closeout path with limited blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): Sufficient live output: the PR body now includes after-fix terminal/test output plus a copied boundary probe against the PR branch source showing no lone surrogate remains.
  • proof: sufficient: Contributor real behavior proof is sufficient. Sufficient live output: the PR body now includes after-fix terminal/test output plus a copied boundary probe against the PR branch source showing no lone surrogate remains.
Evidence reviewed

PR surface:

Source +4, Tests +66. Total +70 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 5 1 +4
Tests 1 66 0 +66
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 71 1 +70

What I checked:

Likely related people:

  • giodl73-repo: git blame and PR metadata tie the current Feishu streaming-card implementation, raw summary truncation, shared sliceUtf16Safe helper, and sibling helper precedent to the merged feed-refactor commit. (role: introduced behavior and helper pattern; confidence: high; commits: 4010b81a77f4, 82d05bdc461e; files: extensions/feishu/src/streaming-card.ts, src/shared/utf16-slice.ts, src/plugin-sdk/text-utility-runtime.ts)
  • sliverp: CONTRIBUTING.md lists this handle for Chinese channels including Feishu, which is useful routing context for Feishu-specific behavior. (role: declared Feishu channel area owner; confidence: medium; files: CONTRIBUTING.md, extensions/feishu/src/streaming-card.ts)
  • Patrick-Erichsen: GitHub PR metadata shows this handle approved the merged PR that introduced the current Feishu streaming-card implementation and helper pattern. (role: reviewer of introducing PR; confidence: medium; commits: 82d05bdc461e; files: extensions/feishu/src/streaming-card.ts, src/shared/utf16-slice.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: 🧂 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. labels Jun 28, 2026
@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.

@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
@vincentkoc
vincentkoc merged commit 2064575 into openclaw:main Jun 28, 2026
260 of 277 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 29, 2026
…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]>
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
…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]>
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 1, 2026
…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]>
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
…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]>
Rorqualx pushed a commit to Rorqualx/cortex that referenced this pull request Jul 4, 2026
…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)
hugenshen added a commit to hugenshen/openclaw that referenced this pull request Jul 7, 2026
…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.
hugenshen added a commit to hugenshen/openclaw that referenced this pull request Jul 7, 2026
…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.
Rorqualx pushed a commit to Rorqualx/cortex that referenced this pull request Jul 8, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary 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