fix(msteams): keep truncated parent context text well-formed#96569
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 26, 2026, 2:32 AM ET / 06:32 UTC. Summary PR surface: Source +3, Tests +19. Total +22 across 2 files. Reproducibility: yes. Current main and v2026.6.10 both raw-slice the parent summary before appending the ellipsis, and the PR body includes terminal proof showing the legacy lone-surrogate/URIError case versus the fixed well-formed output. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this focused MS Teams parent-context helper reuse after exact-head CI completes, keeping the sibling reflection-prompt truncation fix in its separate PR. Do we have a high-confidence way to reproduce the issue? Yes. Current main and v2026.6.10 both raw-slice the parent summary before appending the ellipsis, and the PR body includes terminal proof showing the legacy lone-surrogate/URIError case versus the fixed well-formed output. Is this the best way to solve the issue? Yes. Reusing the exported AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4fc504d321b6. Label changesLabel justifications:
Evidence reviewedPR surface: Source +3, Tests +19. Total +22 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
|
b40fef9 to
0888744
Compare
summarizeParentMessage truncated the parent body with text.slice(0, PARENT_TEXT_MAX_CHARS - 1), which can cut a surrogate pair in half and emit a lone surrogate into the injected "Replying to @sender: …" system event. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add a regression asserting the summary stays isWellFormed() when the limit lands inside an emoji.
0888744 to
ca210f7
Compare
…w#96569) summarizeParentMessage truncated the parent body with text.slice(0, PARENT_TEXT_MAX_CHARS - 1), which can cut a surrogate pair in half and emit a lone surrogate into the injected "Replying to @sender: …" system event. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add a regression asserting the summary stays isWellFormed() when the limit lands inside an emoji.
…w#96569) summarizeParentMessage truncated the parent body with text.slice(0, PARENT_TEXT_MAX_CHARS - 1), which can cut a surrogate pair in half and emit a lone surrogate into the injected "Replying to @sender: …" system event. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add a regression asserting the summary stays isWellFormed() when the limit lands inside an emoji.
…w#96569) summarizeParentMessage truncated the parent body with text.slice(0, PARENT_TEXT_MAX_CHARS - 1), which can cut a surrogate pair in half and emit a lone surrogate into the injected "Replying to @sender: …" system event. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add a regression asserting the summary stays isWellFormed() when the limit lands inside an emoji.
…w#96569) summarizeParentMessage truncated the parent body with text.slice(0, PARENT_TEXT_MAX_CHARS - 1), which can cut a surrogate pair in half and emit a lone surrogate into the injected "Replying to @sender: …" system event. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add a regression asserting the summary stays isWellFormed() when the limit lands inside an emoji.
…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.
Summary: The Teams parent-message summary was truncated with a raw UTF-16 slice, so an emoji at the 400-char boundary became a lone surrogate in the injected
Replying to @sender: …context event. This uses the sharedtruncateUtf16Safehelper to truncate on a code-point boundary.What Problem This Solves
MS Teams thread parent context summarization truncated long parent-message text with raw UTF-16 slicing. When the 400-character parent preview limit landed between the high and low surrogate of an emoji, the generated
Replying to @sender: ...context could contain a lone surrogate.Trigger input:
🦞(U+1F99E, encoded as a UTF-16 surrogate pair)slice(0, 399) + "…"and kept only the high surrogate before the ellipsisThat leaves malformed text in the injected MS Teams parent-context event and can break consumers that require well-formed strings.
Fix
extensions/msteams/src/thread-parent-context.tsnow reuses the sharedtruncateUtf16Safehelper fromopenclaw/plugin-sdk/text-utility-runtimewhen shortening parent-message text.The fix keeps the existing 400-character parent-context limit and ellipsis behavior, but the truncation boundary is now validated before the ellipsis is appended. If the limit would split a surrogate pair, the helper backs up to the last complete UTF-16 boundary, so the final escaped/injected parent context remains well-formed.
Evidence
Command run from an isolated temporary worktree on
fix/msteams-parent-truncate:Terminal output:
This demo calls the real exported
summarizeParentMessagepath fromextensions/msteams/src/thread-parent-context.ts. The legacy reproduction leaves a lone high surrogate (d83e) and throwsURIErrorduring URI encoding; the fixed path returns well-formed text withloneSurrogate=false.Tests
extensions/msteams/src/thread-parent-context.test.tsadds focused Vitest regression coverage for surrogate-boundary truncation in MS Teams parent-context summaries.The test constructs the same boundary shape, verifies the summarized text does not match the unpaired-surrogate regex, confirms the emoji is not split before the ellipsis, and preserves the expected compact parent-context output shape.