Skip to content

fix(content): stop content truncation from splitting a surrogate pair - #391

Merged
steipete merged 1 commit into
steipete:mainfrom
devYRPauli:fix/clip-lone-surrogate
Aug 31, 2026
Merged

fix(content): stop content truncation from splitting a surrogate pair#391
steipete merged 1 commit into
steipete:mainfrom
devYRPauli:fix/clip-lone-surrogate

Conversation

@devYRPauli

@devYRPauli devYRPauli commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Problem

clipAtSentenceBoundary in packages/core/src/content/link-preview/content/cleaner.ts cuts with input.slice(0, maxLength). That is a UTF-16 code unit index.

Every non-BMP character is a surrogate pair of two code units. Emoji are the common case. When the cut lands between the two units, and no sentence break sits past maxLength * 0.5, the rescue branch does not run and the function returns a string that ends in a lone high surrogate.

applyContentBudget then calls .trim(), which does not remove a lone surrogate. Nothing later repairs it.

Measured with the function as it is on main, using "x".repeat(80) + "\u{1F600}" + " and more trailing text here" and maxLength 81:

output length   81
last code unit  0xd83d
isWellFormed()  false
UTF-8 tail      "xxxxx" followed by U+FFFD

The text is written to stdout, to --json output, and into the model request body. Each of those is a UTF-8 encode, so the reader sees a replacement character where the text should end cleanly.

applyContentBudget is exported from packages/core/src/content/index.ts and is called from:

  • packages/core/src/content/browser-html.ts:33
  • packages/core/src/content/link-preview/content/utils.ts:249,335,358,362
  • src/run/flows/asset/extract.ts:55,117
  • src/speaker-identification/identify.ts:194

So any page or transcript that carries an emoji near the budget boundary can hit this.

Change

Four lines. When the clipped text ends in an unpaired high surrogate, drop that one code unit.

The guard sits after the sentence-break return, so it only affects the plain truncation path. A cut at . , ! , ? or a blank line is already safe, because those characters are all BMP.

A trailing high surrogate is unpaired by definition, since its low half would follow it. A trailing low surrogate means the pair is complete, so it is left alone.

Scope

This fixes lone surrogates only. It does not add grapheme cluster handling.

A cut inside a ZWJ emoji sequence or before a combining mark still produces well-formed text, so it is a different question. A lone surrogate is invalid text. I kept the two separate to keep this change small. Say the word if you want the grapheme case handled too.

I did not use String.prototype.toWellFormed(). It replaces the lone surrogate with U+FFFD, which keeps the visible corruption instead of removing it.

Proof

cleaner.ts reverted, tests kept:

FAIL  tests/cleaner.test.ts > content cleaner utilities > keeps clipped content well-formed at a surrogate pair
FAIL  tests/cleaner.test.ts > content cleaner utilities > keeps budgeted content well-formed at a surrogate pair
Tests  2 failed | 8 passed (10)

With the change:

Tests  10 passed (10)

Full suite after the change: 3018 passed, 43 skipped, 0 failed, across 585 test files.

oxfmt --check reports the correct format. oxlint exits 0. tsc -p packages/core/tsconfig.build.json --noEmit exits 0.

The new tests also assert the opposite case. When the budget is 82 and both code units fit, the emoji is kept. The existing expectations for clipAtSentenceBoundary are unchanged, and a plain BMP truncation still returns the same result as before.

Runtime check on the real helper

The results above come from Vitest. This is a direct terminal run of the exported helper, on this branch and on main.

Command:

node --import ./scripts/register-typescript.mjs --input-type=module \
  -e 'import { applyContentBudget } from "./packages/core/src/content/link-preview/content/cleaner.ts"; const value = applyContentBudget("x".repeat(80) + "\u{1F600} and more", 81).content; console.log(`well-formed=${value.isWellFormed()} length=${value.length}`);'

main at 861fa4a9:

well-formed=false length=81

This branch at d88c576:

well-formed=true length=80

The input is 80 x characters plus one emoji. The budget is 81 UTF-16 code units. main returns the raw slice, which ends in a lone high surrogate. This branch drops that one code unit and returns 80 well-formed units.

The clip cut on a UTF-16 code unit. A cut inside a surrogate pair left a lone high surrogate at the end of the text.

The lone surrogate became U+FFFD after a UTF-8 round trip, so the output showed a replacement character.

The clip now drops the unpaired code unit. A budget that fits both code units still keeps the character.
@clawsweeper

clawsweeper Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. 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. labels Aug 19, 2026
@clawsweeper

clawsweeper Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 30, 2026, 3:42 PM ET / 19:42 UTC.

ClawSweeper review

What this changes

The PR prevents content clipping from returning a string that ends with an unpaired UTF-16 high surrogate, with regression tests for emoji-boundary and BMP clipping.

Merge readiness

⚠️ Ready for maintainer review - 1 item remains

Keep open for normal maintainer review: current main still has the UTF-16 clipping defect, and this focused PR repairs it with direct before/after proof and regression coverage.

Priority: P2
Reviewed head: d88c576bfb657a671d8f07010ec6befe995746e9

Review scores

Measure Result What it means
Overall readiness 🦞 diamond lobster (5/6) A focused repair with direct before/after helper proof, targeted regression coverage, and no identified correctness or security concern.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The changed production owner is the core content-budget helper; the PR body supplies a direct terminal trace of that helper on main and this exact PR head with a split-emoji boundary, showing malformed output becomes well-formed after the guard.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The changed production owner is the core content-budget helper; the PR body supplies a direct terminal trace of that helper on main and this exact PR head with a split-emoji boundary, showing malformed output becomes well-formed after the guard.
Evidence reviewed 6 items Current main retains the defect: Current main returns the raw UTF-16 slice after sentence-boundary fallback, so a budget can end after a high surrogate and return malformed text.
Narrow introduced repair: The verified PR delta checks the final code unit only on the ordinary clipping path, removing a trailing high surrogate while preserving complete pairs and BMP clipping.
Regression coverage: Tests exercise a split emoji boundary, retaining an emoji when both code units fit, unchanged BMP clipping, and the public content-budget path.
Findings None None.
Security None None.

How this fits together

The core content cleaner clips extracted webpage and transcript text to a character budget before downstream formatting and model-facing processing. Browser extraction, link-preview processing, asset extraction, and speaker identification consume its output.

flowchart LR
  A[Extracted page or transcript text] --> B[Content budget]
  B --> C[Sentence-aware clipping]
  C --> D[Surrogate-boundary check]
  D --> E[Well-formed clipped text]
  E --> F[Output and model input]
Loading

Before merge

  • Complete next step (P2) - No actionable repair remains; this PR needs ordinary maintainer approval to land.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test delta production +4, tests +17 The small core behavior change is accompanied by focused direct and public-helper regression coverage.

Technical review

Best possible solution:

Land the narrow boundary guard and regression coverage so every content-budget caller receives well-formed text without changing valid clipping behavior.

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

Yes. Current main still slices by UTF-16 code-unit count without a trailing-high-surrogate check, and the supplied direct-helper trace shows the split-emoji failure and fixed result on this PR head.

Is this the best way to solve the issue?

Yes. Removing only a trailing high surrogate preserves the existing code-unit budget while avoiding replacement-character corruption and retaining complete pairs.

AGENTS.md: found and applied where relevant.

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

Labels

Label changes:

  • add rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦞 diamond lobster, so this older rating label is no longer current.

Label justifications:

  • P2: This PR repairs a bounded text-integrity defect in budgeted extracted content.
  • 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 (terminal): The changed production owner is the core content-budget helper; the PR body supplies a direct terminal trace of that helper on main and this exact PR head with a split-emoji boundary, showing malformed output becomes well-formed after the guard.
  • proof: sufficient: Contributor real behavior proof is sufficient. The changed production owner is the core content-budget helper; the PR body supplies a direct terminal trace of that helper on main and this exact PR head with a split-emoji boundary, showing malformed output becomes well-formed after the guard.

Evidence

What I checked:

Likely related people:

  • Peter Steinberger: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
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.

Workflow

  • 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.

History

Review history (14 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-25T20:04:27.128Z sha d88c576 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-27T14:23:25.120Z sha d88c576 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-29T08:54:01.270Z sha d88c576 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-29T10:28:59.624Z sha d88c576 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T07:59:42.687Z sha d88c576 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T13:04:16.812Z sha d88c576 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T14:39:39.500Z sha d88c576 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T17:35:33.762Z sha d88c576 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed proof: sufficient Contributor real behavior proof is sufficient. 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. labels Aug 25, 2026
@devYRPauli

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

I added a runtime terminal transcript to the PR body. It calls the exported helper directly, on this branch and on main.

main at 861fa4a9 prints well-formed=false length=81. This branch at d88c576 prints well-formed=true length=80.

@clawsweeper

clawsweeper Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Aug 27, 2026
@steipete

Copy link
Copy Markdown
Owner

Triage recommendation: LAND. I reproduced this through the built core package's exported extractBrowserHtmlContent() API, fetching a synthetic page from a real local HTTP fixture server and applying a character budget to its text.

Input: 80 ASCII characters, one emoji, and trailing text.

budget 81, main:    length=81; wellFormed=false; tail ends in \ud83d
budget 81, patched: length=80; wellFormed=true
budget 82, patched: length=82; wellFormed=true; complete emoji retained

The defect is the UTF-16 slice ending between the emoji's two code units. The narrow trailing-high-surrogate guard fixes that boundary without changing the existing code-unit budget or valid sentence-boundary behavior.

Reviewed commit: d88c576bfb657a671d8f07010ec6befe995746e9.

Both content PRs also apply together cleanly on current main and pass all 12 cleaner regression tests. The baseline package build passed on Node 24.20.0, and each changed cleaner was compiled into the built core package for the integration checks. Local tests reused the installed dependency tree (Vitest 4.1.10); each original PR's exact-head CI is green. Codex autoreview was scoped-clean at the default P0 threshold.

No source repair or branch rewrite was needed. No merge performed. Preserve Co-authored-by: Yash Raj Pandey <yashpn62@gmail.com> when squashing.

Suggested landing changelog: “Content extraction: avoid splitting UTF-16 surrogate pairs when clipping to a character budget (#391, thanks @devYRPauli).”

@steipete
steipete merged commit cdcf604 into steipete:main Aug 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. 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