Repository navigation
Description
Summary: tail_overlap in crates/embeddings/src/chunker.rs slices text[text.len() - n..] using byte offsets. On multibyte text (CJK, Greek, emoji), that offset can land inside a multibyte character, which panics with "byte index N is not a char boundary." This is reachable from the public chunk_document API whenever a chunk flush occurs on multibyte text — i.e. any user document in a non-Latin script (or with emoji) long enough to need more than one chunk.
Severity: S2 — user-facing broken flow. A user uploading/editing a CJK, Greek, Cyrillic, Arabic, or emoji-containing document long enough to span multiple chunks (default chunk_size = 2048) will crash the indexing/embedding pipeline for that document; the document itself likely persists, but it will not be searchable via semantic/hybrid search and the import/index job will fail.
Environment:
- Deploy: local / any (code-level bug)
- Commit: 6411ddb
- User / role: any user editing or importing a document in a non-Latin script
Repro steps:
- Direct function-level repro (guaranteed panic): call
tail_overlap(&"α".repeat(10), 5)."α".repeat(10)is 20 bytes / 10 chars.start = 20 - 5 = 15, which falls inside the 8thα(occupying bytes 14..16) — not a char boundary.
- End-to-end repro path: call
chunk_document(title, body, config)with abodycontaining multiple paragraphs of multibyte text whose cumulative length exceedsconfig.chunk_size, so a mid-document flush triggerstail_overlapon accumulated multibyte content.
Expected: Chunking multibyte text produces valid UTF-8 chunks with no panic, regardless of where the byte-length budget falls relative to character boundaries.
Observed: Panic: byte index 15 is not a char boundary; it is inside 'α' (bytes 14..16) of the string.
Evidence:
- Code:
crates/embeddings/src/chunker.rs:149-158(tail_overlap), specificallytext[start..]/text[text.len() - n..]on raw byte indices with nochar_indices/is_char_boundaryguard. - Existing test
unicode_body_within_single_chunk_is_preserved(crates/embeddings/src/chunker.rs:385-386) only covers the single-chunk (no-flush) path and does not exercisetail_overlap, so it does not catch this.
Suspected area: crates/embeddings/src/chunker.rs::tail_overlap — needs a char-boundary-safe tail slice (e.g. iterate char_indices() from the end, or use floor_char_boundary once stable / a manual boundary search).
Findings consolidated: This ticket bundles the primary panic with three secondary quality issues in the same function/module, all surfaced by the same review pass. They do not share a line-level root cause with the panic — bundling them here is for filing efficiency, not because they're the same bug. If any is worth independent tracking, split it out:
- No separator at the overlap seam.
current = format!("{overlap_text}{para}")(line ~95) andtext.push_str(&word_chunk)(line ~73) concatenate directly, merging the last word of the overlap with the first word of the next paragraph/word-chunk with no space or newline. - Computed overlap silently disappears after an oversized-paragraph flush. The
elsebranch (lines ~96-101) that handles the non-oversized case doesn't carryoverlap_textforward the way the oversized-paragraph branch does — chunks immediately following an oversized-paragraph split lose their intended overlap. - Doc/code drift.
ChunkerConfig.chunk_sizedoc comment (line 14) says "characters," but every length check in the function (para.len(),current.len(),text[start..]) is a byte count viastr::len().
Scope expansion suggested: A dedicated boundary-exploration pass on the chunker with a matrix of scripts (CJK, RTL/Arabic, emoji with combining/ZWJ sequences) would likely surface more of the same class of bug.
Activity
jjbaumert commentedon Jul 10, 2026
Fixed the primary defect in a022e48:
tail_overlapnow advances to the next char boundary before slicing, so multibyte documents chunk without panicking (regression tests added TDD-style). The bytes-vs-characters doc drift (bundled item 3) is corrected in the same commit.Bundled items 1–2 (missing separator at the overlap seam, overlap dropped after oversized-paragraph flushes) change chunk output that existing tests pin, so per the bundling note they're being split into their own issue rather than piggybacked here.
jjbaumert commentedon Jul 10, 2026
The split-out seam-quality items are now tracked in #19.
test,fix: apply review findings on the #8/#9 fix commits
fix(embeddings): word-boundary chunk seams; overlap survives word-splits
fix(mermaid): gitGraph label anchoring + xychart series palette (gall…