fix(msteams): truncate reflection prompt on a UTF-16 boundary#96578
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 26, 2026, 12:15 AM ET / 04:15 UTC. Summary PR surface: Source +1, Tests +25. Total +26 across 2 files. Reproducibility: yes. Current main and 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: Merge the narrow SDK-helper reuse after maintainer review and clean required checks; keep the sibling parent-context UTF-16 fix in #96569 as separate work. Do we have a high-confidence way to reproduce the issue? Yes. Current main and Is this the best way to solve the issue? Yes. Reusing the existing public AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against a6a4652c708a. Label changesLabel justifications:
Evidence reviewedPR surface: Source +1, Tests +25. Total +26 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
|
e8f027b to
d4da0d3
Compare
buildReflectionPrompt truncated the thumbed-down response with String.slice(0, 500) on a UTF-16 code-unit index, so an astral character straddling the 500-char cap was cut into a lone surrogate in the reflection prompt built for the LLM. Use truncateUtf16Safe so truncation never splits a surrogate pair, keeping the existing 500-char budget and '...' suffix. Adds tests asserting the prompt stays UTF-16 well formed when truncating and that a boundary emoji is preserved when it fits.
d4da0d3 to
32f9331
Compare
…#96578) buildReflectionPrompt truncated the thumbed-down response with String.slice(0, 500) on a UTF-16 code-unit index, so an astral character straddling the 500-char cap was cut into a lone surrogate in the reflection prompt built for the LLM. Use truncateUtf16Safe so truncation never splits a surrogate pair, keeping the existing 500-char budget and '...' suffix. Adds tests asserting the prompt stays UTF-16 well formed when truncating and that a boundary emoji is preserved when it fits.
…#96578) buildReflectionPrompt truncated the thumbed-down response with String.slice(0, 500) on a UTF-16 code-unit index, so an astral character straddling the 500-char cap was cut into a lone surrogate in the reflection prompt built for the LLM. Use truncateUtf16Safe so truncation never splits a surrogate pair, keeping the existing 500-char budget and '...' suffix. Adds tests asserting the prompt stays UTF-16 well formed when truncating and that a boundary emoji is preserved when it fits.
…#96578) buildReflectionPrompt truncated the thumbed-down response with String.slice(0, 500) on a UTF-16 code-unit index, so an astral character straddling the 500-char cap was cut into a lone surrogate in the reflection prompt built for the LLM. Use truncateUtf16Safe so truncation never splits a surrogate pair, keeping the existing 500-char budget and '...' suffix. Adds tests asserting the prompt stays UTF-16 well formed when truncating and that a boundary emoji is preserved when it fits.
…#96578) buildReflectionPrompt truncated the thumbed-down response with String.slice(0, 500) on a UTF-16 code-unit index, so an astral character straddling the 500-char cap was cut into a lone surrogate in the reflection prompt built for the LLM. Use truncateUtf16Safe so truncation never splits a surrogate pair, keeping the existing 500-char budget and '...' suffix. Adds tests asserting the prompt stays UTF-16 well formed when truncating and that a boundary emoji is preserved when it fits. (cherry picked from commit 6c5a9fd)
…#96578) buildReflectionPrompt truncated the thumbed-down response with String.slice(0, 500) on a UTF-16 code-unit index, so an astral character straddling the 500-char cap was cut into a lone surrogate in the reflection prompt built for the LLM. Use truncateUtf16Safe so truncation never splits a surrogate pair, keeping the existing 500-char budget and '...' suffix. Adds tests asserting the prompt stays UTF-16 well formed when truncating and that a boundary emoji is preserved when it fits.
…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 feedback reflection prompt was truncated with a raw UTF-16 slice, so an emoji at the boundary became a lone surrogate fed into the LLM prompt. This uses
truncateUtf16Safeto truncate on a code-point boundary.What Problem This Solves
The Microsoft Teams feedback reflection prompt truncated the thumbed-down response with raw UTF-16 slicing. When the response crossed the 500-code-unit cap at an emoji or other surrogate-pair boundary, the prompt could include a lone surrogate.
Trigger input used for proof:
"a".repeat(499) + "🦞" + "b".repeat(20).slice(0, 500)keeps only the high surrogated83e, then appends...Fix
extensions/msteams/src/feedback-reflection-prompt.tsnow reuses the sharedtruncateUtf16Safehelper fromopenclaw/plugin-sdk/text-utility-runtimewhen capping the thumbed-down response before appending the reflection prompt ellipsis.That keeps the existing 500-character range check and prompt escaping behavior, but avoids cutting a UTF-16 surrogate pair in half. If the boundary emoji does not fully fit, the helper trims before the pair and the prompt remains well-formed.
Evidence
Command, run from a temporary worktree on
fix/msteams-reflection-truncate-surrogate:The demo imports the real fixed function:
Terminal output:
The fixed
buildReflectionPromptoutput is well-formed for this surrogate-boundary emoji input: the lone-surrogate regex reportsfalse, and the boundary emoji is dropped rather than split.Tests
Focused Vitest coverage exercises the prompt builder regression cases:
does not split UTF-16 surrogate pairs when truncating a thumbed-down responsekeeps a boundary emoji when it fully fits before the truncation capValidation command:
Result: