Skip to content

fix(matrix): truncate thread starter body on code-point boundaries#96407

Closed
ly-wang19 wants to merge 1 commit into
openclaw:mainfrom
ly-wang19:fix/matrix-thread-starter-surrogate-safe
Closed

fix(matrix): truncate thread starter body on code-point boundaries#96407
ly-wang19 wants to merge 1 commit into
openclaw:mainfrom
ly-wang19:fix/matrix-thread-starter-surrogate-safe

Conversation

@ly-wang19

Copy link
Copy Markdown
Contributor

What Problem This Solves

truncateThreadStarterBody in extensions/matrix/src/matrix/monitor/thread-context.ts (reached via the exported summarizeMatrixThreadStarterEvent) trims an over-length Matrix thread-root body with a raw UTF-16 slice:

return `${value.slice(0, MAX_THREAD_STARTER_BODY_LENGTH - 3)}...`;

String.prototype.slice operates on UTF-16 code units, so when the 500-char boundary lands in the middle of an astral character (emoji, many CJK extensions, etc.), the surrogate pair is cut in half. The result is a lone, unpaired high surrogate immediately before the ellipsis, which renders as a broken/replacement glyph (U+FFFD).

Repro — a thread-root m.room.message whose content.body is 496 a characters, then 😀 (U+1F600 — a surrogate pair at UTF-16 indices 496–497), then bcd (total length 501):

summarizeMatrixThreadStarterEvent({
  type: "m.room.message",
  content: { msgtype: "m.text", body: "a".repeat(496) + "😀" + "bcd" },
})
  • Before (wrong): slice(0, 497) cuts the emoji in half, leaving a lone high surrogate: "a".repeat(496) + "\uD83D" + "..." — a broken glyph followed by the ellipsis.
  • After (fixed): truncation stops on a code-point boundary, dropping the half emoji entirely: "a".repeat(496) + "..." — 496 as then the ellipsis, no orphaned surrogate.

The fix swaps the raw slice for the existing SDK helper sliceUtf16Safe (from openclaw/plugin-sdk/text-utility-runtime), which already backs the surrogate-safe truncation used by the Discord, Slack, Telegram, and other adapters. Behavior is unchanged for short bodies and for long bodies whose cut point is not inside a surrogate pair.

Evidence

A standalone Node script replicates the old (buggy) and new (fixed) truncateThreadStarterBody logic and asserts the red→green transition plus that unaffected inputs are unchanged:

PASS repro input length is 501
PASS RED: old code leaves a lone (orphaned) high surrogate
PASS RED: old code emits half-emoji + ellipsis
PASS GREEN: fixed code drops the half emoji entirely
PASS GREEN: fixed code has NO orphaned surrogate
PASS short body unchanged (old)
PASS short body unchanged (fixed)
PASS long ASCII truncation identical old vs fixed
PASS long ASCII fixed output is 497 x + ellipsis
PASS emoji before cut survives intact
PASS emoji-before-cut output has no lone surrogate

ALL CHECKS PASSED
  • RED confirms the pre-fix code emits "a".repeat(496) + "\uD83D" + "..." (a lone high surrogate).
  • GREEN confirms the fixed code emits "a".repeat(496) + "..." with no orphaned surrogate.
  • Unaffected inputs (short body, long ASCII body, an emoji that sits safely before the cut) are byte-for-byte unchanged.

A matching unit test was added to extensions/matrix/src/matrix/monitor/thread-context.test.ts that calls the exported summarizeMatrixThreadStarterEvent directly with the repro body and asserts the surrogate-safe output ("a".repeat(496) + "..." with no surrogate code unit surviving).

truncateThreadStarterBody (reached via summarizeMatrixThreadStarterEvent)
truncated the thread-root body with a raw `value.slice(0, 497)`. When the
500-char boundary falls in the middle of an astral character (emoji etc.),
the raw slice cuts the surrogate pair in half and leaves a lone high
surrogate before the ellipsis, rendering as a broken/replacement glyph.

Use the SDK's surrogate-safe `sliceUtf16Safe` so truncation stops on a
code-point boundary, dropping the half character entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added channel: matrix Channel integration: matrix size: XS 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.

vincentkoc pushed a commit that referenced this pull request Jun 26, 2026
…97121)

Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.

Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.

Salvages the original fix from #96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.

Co-authored-by: ly-wang19 <[email protected]>
@vincentkoc

Copy link
Copy Markdown
Member

Superseded by the clean replacement PR #97121, now merged to main as 289865b39226c685af382a81da55c38ce3df9049.

The landed diff preserves the original Matrix fix and contributor credit, touching only extensions/matrix/src/matrix/monitor/thread-context.ts and its regression test. Exact-main proof passed: node scripts/run-vitest.mjs extensions/matrix/src/matrix/monitor/thread-context.test.ts (6/6), and git diff --check was clean.

wangmiao0668000666 pushed a commit to wangmiao0668000666/openclaw that referenced this pull request Jun 27, 2026
…penclaw#97121)

Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.

Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.

Salvages the original fix from openclaw#96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.

Co-authored-by: ly-wang19 <[email protected]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 27, 2026
…penclaw#97121)

Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.

Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.

Salvages the original fix from openclaw#96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.

Co-authored-by: ly-wang19 <[email protected]>
xydigit-zt pushed a commit to xydigit-zt/xydigit-zt-openclaw that referenced this pull request Jun 28, 2026
…penclaw#97121)

Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.

Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.

Salvages the original fix from openclaw#96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.

Co-authored-by: ly-wang19 <[email protected]>
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
…penclaw#97121)

Matrix thread-starter previews truncated long bodies by raw UTF-16
slice, which could cut an astral character (e.g. emoji) and leave a lone
surrogate, rendering mojibake in the agent's thread context.

Reuse the existing sliceUtf16Safe helper so the cut backs up to a valid
surrogate boundary, preserving the 500-code-unit limit and '...' suffix.
Adds a regression test that fails against the raw-slice implementation.

Salvages the original fix from openclaw#96407 (auto-closed by the active-PR
queue cap). Preserves @ly-wang19's authorship; rebased clean onto main
by @Bartok9.

Co-authored-by: ly-wang19 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants