Skip to content

fix(agents): preserve UTF-16 boundaries in block chunks#104441

Merged
steipete merged 3 commits into
openclaw:mainfrom
mushuiyu886:fix/followup-104030
Jul 11, 2026
Merged

fix(agents): preserve UTF-16 boundaries in block chunks#104441
steipete merged 3 commits into
openclaw:mainfrom
mushuiyu886:fix/followup-104030

Conversation

@mushuiyu886

Copy link
Copy Markdown
Contributor

Summary

  • preserve UTF-16 boundaries when EmbeddedBlockChunker must split at maxChars
  • keep fence close/reopen handling aligned with the adjusted split index
  • add production-class regressions for plain, fenced, and one-code-unit budgets

What Problem This Solves

When no natural Markdown break existed, the chunker returned the raw UTF-16 code-unit maxChars index. If that index fell between an astral character's high and low surrogates, the emitted chunk ended with an isolated high surrogate and the buffered remainder began with an isolated low surrogate.

User Impact

Streamed block chunks no longer contain malformed UTF-16 when a forced size boundary lands inside emoji or another astral character.

Origin / follow-up

  • Follows: fix(twitch): preserve UTF-16 pairs in outbound chunks #104030, which preserved UTF-16 pairs in Twitch outbound chunks.
  • Gap this fills: the shared embedded-agent block chunker independently used forced code-unit indices before its local emit boundary.
  • Consistency: forced chunk boundaries now reuse the repository's shared sliceUtf16Safe utility.

Competition / linked PR analysis

A fresh exact-class search found #94216 and #94247 touching the same chunker files, but both address paragraph-separator preservation. Their diffs append or coordinate \n\n around paragraph breaks and do not align forced maxChars indices to UTF-16 boundaries. #68986 does not change the chunker source or target test.

No open PR covers the surrogate-boundary root cause, and the author's open PR scan found no overlap.

Merge risk

  • Why it matters / User impact: malformed chunk strings can render as replacement characters or fail later text handling.
  • Risk labels considered: session-state and streamed-text.
  • Risk explanation: a split can move one code unit earlier when maxChars bisects a surrogate pair. For the otherwise impossible maxChars=1 case with an astral first character, the complete two-unit code point is emitted so the chunker retains forward progress.
  • Why acceptable: normal budgets remain hard upper bounds; only a one-unit budget can be exceeded by one code unit because no valid non-empty UTF-16 chunk can contain half of a surrogate pair.
  • Maintainer-ready confidence: High; the production class is directly reproducible and all forced branches share one adjusted index.
  • Patch quality notes: focused two-file patch with no type escape, fallback serializer, public contract change, dependency change, or unrelated refactor.
  • Target test file: src/agents/embedded-agent-block-chunker.test.ts
  • Root cause: forced break branches used maxChars directly as a UTF-16 code-unit slice index and propagated that same unsafe index into chunk emission and remainder state.
  • Why this is root-cause fix: the source index is normalized once before fence lookup, fence split metadata, raw chunk slicing, emit, and buffered remainder reconstruction.
  • What did NOT change: paragraph/newline/sentence preference, natural-break selection, fence parsing, close/reopen syntax, flushOnParagraph, and callback APIs remain unchanged.
  • Architecture / source-of-truth check: #pickBreakIndex is the source of truth for forced split positions; downstream emit consumers are not patched to repair malformed strings.
  • Scenario locked in: a 20-unit plain split after 19 ASCII units, the same split inside an open Markdown fence, and a one-unit budget beginning with an emoji.
  • Why this is the smallest reliable guardrail: one public-class test observes emitted chunks and bufferedText across both forced branches and the minimum-budget progress case.

Real behavior proof

  • Behavior or issue addressed: forced block chunks and their buffered remainders retain complete UTF-16 surrogate pairs.
  • Canonical reachability path: input → schema/type BlockReplyChunking plus streamed text → ingestion EmbeddedBlockChunker.append → normalization fence parsing and #pickBreakIndex → runtime object BreakResult and internal buffer → emitted chunk and buffered-remainder effect. Concretely, appended assistant text reaches the forced maxChars branch, receives one shared safe index, passes through #emitBreakResult, and is exposed through the production emit callback and bufferedText.
  • Boundary crossed: the production class ingests streamed text and passes a chunk string to its local emit callback; the proof terminates at that callback and the retained runtime buffer.
  • Shared helper / provider constraint check: reused sliceUtf16Safe through the existing shared utility export; provider-specific constraints are not applicable.
  • Fix classification: Root cause fix
  • Real environment tested: Linux worktree on Node 22 with the repository pnpm toolchain.
  • Exact steps or command run after this patch: direct EmbeddedBlockChunker execution for plain, fenced, one-unit, and natural-paragraph fixtures, plus pnpm test src/agents/embedded-agent-block-chunker.test.ts.
  • Evidence after fix:
BEFORE plain: emitted chunk ended with lone high surrogate U+D83D; buffered remainder began with lone low surrogate U+DE00
AFTER plain:  emitted length=19; buffered remainder begins with complete emoji
AFTER fenced: emitted chunk closes the Markdown fence; reopened remainder begins with complete emoji
TINY BUDGET:  complete emoji emitted first and the chunker retained forward progress
CONTROL:      natural paragraph break output and remainder stayed unchanged
TESTS:        1 file passed, 9 tests passed
  • Observed result after fix: plain and fenced forced splits emit well-formed strings, the corresponding remainders begin with the complete emoji, the one-unit case drains without stalling, and the natural paragraph control is unchanged.
  • What was not tested: provider or channel consumption was not exercised; this defect and proof terminate at the local production emit callback and buffered state.

Regression Test Plan

  • run pnpm test src/agents/embedded-agent-block-chunker.test.ts
  • verify plain and fenced forced splits keep complete surrogate pairs on both sides
  • verify the one-unit budget retains forward progress
  • verify existing paragraph, fence, and max-length behavior remains green

Risk / Compatibility

No API, schema, configuration, or dependency changes. Typical forced chunks can be one code unit shorter only when the previous boundary split a surrogate pair. A configured one-unit budget can emit a two-unit astral code point because emitting either half would be malformed and emitting nothing would stall.

Out of scope

No changes to paragraph-separator reconstruction, downstream coalescing, channel formatting, provider output, or delivery transport. The two adjacent open PRs remain responsible for their separate paragraph-boundary behavior.

Root Cause

The forced fallback treated a UTF-16 code-unit budget as a universally safe string boundary. The final source.slice then materialized that unsafe index into both sides of the split, so the boundary must be corrected at #pickBreakIndex before fence and emission logic consume it.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Jul 11, 2026
@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. P2 Normal backlog priority with limited blast radius. labels Jul 11, 2026
@clawsweeper

clawsweeper Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 11, 2026, 8:24 AM ET / 12:24 UTC.

Summary
The PR makes forced embedded-agent block chunk boundaries surrogate-safe and adds plain, fenced, and one-code-unit regression coverage.

PR surface: Source +7, Tests +32. Total +39 across 2 files.

Reproducibility: yes. Current main's forced fallback can be exercised directly with an astral character straddling maxChars; source inspection establishes the failing path, although this review did not execute the reproduction locally.

Review metrics: none identified.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

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

Next step before merge

  • [P2] No repair lane is needed; this clean implementation PR only needs ordinary maintainer merge review.

Security
Cleared: The diff changes only local TypeScript chunking logic and tests, with no dependency, workflow, permission, secret, artifact, or supply-chain change.

Review details

Best possible solution:

Land the canonical forced-index normalization and its public-class regressions after ordinary exact-head maintainer review.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main's forced fallback can be exercised directly with an astral character straddling maxChars; source inspection establishes the failing path, although this review did not execute the reproduction locally.

Is this the best way to solve the issue?

Yes. Correcting the single canonical forced break index is narrower and safer than repairing malformed emitted chunks or buffered remainders downstream.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against a4ee15a851df.

Label changes

Label justifications:

  • P2: The PR fixes deterministic malformed streamed text at a narrow forced chunk boundary with limited blast radius.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body provides after-fix direct production-class output for plain, fenced, one-unit, and natural-break control cases, plus the focused nine-test result.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix direct production-class output for plain, fenced, one-unit, and natural-break control cases, plus the focused nine-test result.
Evidence reviewed

PR surface:

Source +7, Tests +32. Total +39 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 13 6 +7
Tests 1 32 0 +32
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 45 6 +39

What I checked:

Likely related people:

  • steipete: Introduced the extracted block chunker and authored most subsequent forced-split, fence-safety, performance, and clamp work in its predecessor lineage. (role: feature owner; confidence: high; commits: 86ad703f5328, 67bda21811b3, 3adec3563278; files: src/agents/pi-embedded-block-chunker.ts, src/agents/embedded-agent-block-chunker.ts, packages/normalization-core/src/utf16-slice.ts)
  • tyler6204: Authored paragraph-boundary flush behavior and a later correction to newline block streaming in the predecessor chunker. (role: adjacent behavior contributor; confidence: medium; commits: 9ef24fd400ba, 20728e103511; files: src/agents/pi-embedded-block-chunker.ts)
  • Ruby: Enabled Telegram block streaming through this shared chunker path, connecting its emitted text behavior to channel delivery. (role: adjacent channel contributor; confidence: medium; commits: b4fbf2fe0d6f; files: src/agents/pi-embedded-block-chunker.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.
Review history (2 earlier review cycles)
  • reviewed 2026-07-11T11:41:48.713Z sha 6797aa2 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-11T12:15:39.272Z sha 2a18699 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jul 11, 2026
@steipete steipete self-assigned this Jul 11, 2026
@steipete

Copy link
Copy Markdown
Contributor

Land-ready maintainer proof for exact head 400979a654e4fed70ea181f341a64ec4c369454d:

  • Refactor review: one UTF-16-safe forced index now drives fence safety, close/reopen metadata, emitted chunks, and retained remainder state. Natural paragraph/newline/sentence paths are unchanged.
  • Hot-path cleanup: the chunker imports sliceUtf16Safe directly from the node-free normalization leaf instead of the filesystem-heavy root utility barrel.
  • Sanitized AWS Crabbox: cbx_f8ef3d760a7e, run run_eab3d68a79e0 — all 9 production chunker tests passed, including plain, fenced, and maxChars=1 astral boundaries.
  • Targeted oxlint and oxfmt --check passed.
  • Independent autoreview: clean, confidence 0.97.
  • Exact-head hosted CI: run 29162325797, all relevant checks green.
  • Native OPENCLAW_TESTBOX=1 scripts/pr prepare-run 104441 passed with exact-head hosted evidence.

Known proof gaps: downstream channel rendering was not separately exercised; the defect terminates at this shared production chunker’s emit callback and buffered state, both covered directly.

@steipete
steipete merged commit 92870e5 into openclaw:main Jul 11, 2026
101 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

steipete added a commit that referenced this pull request Jul 11, 2026
* fix(agents): preserve UTF-16 boundaries in block chunks

* refactor(agents): import UTF-16 helper from leaf module

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 92870e5)
(cherry picked from commit 44c022d)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 12, 2026
)

* fix(agents): preserve UTF-16 boundaries in block chunks

* refactor(agents): import UTF-16 helper from leaf module

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor 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