fix(vault): stop vault_read rejecting non-ASCII text at the sniff boundary - #1501
Conversation
…ndary The text-only gate sniffs the first 8192 bytes of a document and rejects the read when they are not valid UTF-8. The window was cut at a plain byte offset, so on non-ASCII text it regularly landed inside a multi-byte rune and left a dangling prefix. The gate then reported the document as binary even though the file was intact — measured on Vietnamese documents, about one in four files above 8 KB was blocked, depending on where the accented characters happen to fall. Trim up to 3 bytes off the window when, and only when, the content was actually cut there, then judge. A file that fits inside the window is left alone, so genuinely broken trailing bytes are still caught, and binary content stays blocked because its invalid bytes are spread over the whole window. readCapped had the same byte-offset cut at max_bytes, which put a broken character at the end of every truncated non-ASCII document.
clark-cant
left a comment
There was a problem hiding this comment.
Review: PR #1501 — fix(vault): stop vault_read rejecting non-ASCII text at the sniff boundary
Summary: Fixes a UTF-8 boundary bug where vault_read falsely rejects valid non-ASCII documents as binary. The sniff window cuts at a fixed byte offset (8192), which can split a multi-byte rune — utf8.Valid then reports the fragment as invalid. Same bug in readCapped produces broken trailing characters.
Risk level: Low — targeted 2-file fix (+144/-3), no API changes.
Mandatory gates:
- Duplicate / prior implementation: ✅ clear — no prior PR/issue/commit addresses this specific boundary bug
- Project standards: ✅ clean Go, follows existing vault_read patterns
- Strategic necessity: ✅ clear value — fixes broken vault_read for all non-ASCII (Vietnamese, Chinese, etc.) documents >8KB
Findings:
- No critical or important issues
trimPartialTrailingRune()is correct: trims up toutf8.UTFMax-1(3) bytes from the tail, tight upper bound for any split rune- Sniff guard
len(sniff) < len(content)is the right condition — only trim when the window was actually truncated, preserving detection of genuinely broken short files readCappedfix is symmetric and correct- Tests are excellent: 4 new test cases covering 2-byte/3-byte rune splits at boundary, binary content still rejected, short broken files still caught, max_bytes cut inside rune produces no U+FFFD
- PR body documents the root cause with evidence (120-offset sweep showing ~1/4 documents affected)
Verdict: Approve — Clean, well-tested fix for a real bug affecting non-ASCII vault documents.
Posted by github-maintain at 2026-08-03T17:28:15Z
clark-cant
left a comment
There was a problem hiding this comment.
Review: PR #1501 — fix(vault): stop vault_read rejecting non-ASCII text at the sniff boundary
Summary: Fixes a UTF-8 boundary bug where vault_read falsely rejects valid non-ASCII documents as binary. The sniff window cuts at a fixed byte offset (8192), which can split a multi-byte rune — utf8.Valid then reports the fragment as invalid. Same bug in readCapped produces broken trailing characters.
Risk level: Low — targeted 2-file fix (+144/-3), no API changes.
Mandatory gates:
- Duplicate / prior implementation: ✅ clear — no prior PR/issue/commit addresses this specific boundary bug
- Project standards: ✅ clean Go, follows existing vault_read patterns
- Strategic necessity: ✅ clear value — fixes broken vault_read for all non-ASCII (Vietnamese, Chinese, etc.) documents >8KB
Findings:
- No critical or important issues
trimPartialTrailingRune()is correct: trims up toutf8.UTFMax-1(3) bytes from the tail, tight upper bound for any split rune- Sniff guard
len(sniff) < len(content)is the right condition — only trim when the window was actually truncated, preserving detection of genuinely broken short files readCappedfix is symmetric and correct- Tests are excellent: 4 new test cases covering 2-byte/3-byte rune splits at boundary, binary content still rejected, short broken files still caught, max_bytes cut inside rune produces no U+FFFD
- PR body documents the root cause with evidence (120-offset sweep showing ~1/4 documents affected)
Verdict: Approve — Clean, well-tested fix for a real bug affecting non-ASCII vault documents.
Posted by github-maintain at 2026-08-03T17:28:15Z
What
vault_readrejects valid UTF-8 documents as binary when they contain non-ASCII text.Why
The text-only gate has three layers; layer 3 sniffs the first
vaultReadUTF8SniffBytes(8192) bytes and blocks the read when they are not valid UTF-8. That layer is needed — a PNG renamednotes.mdwithdoc_type = notepasses layers 1 and 2 — but the window was cut at a plain byte offset:UTF-8 is variable width. Accented Vietnamese characters take 2–3 bytes, so byte 8192 regularly falls inside a character and the window ends with a dangling lead byte (
C3with itsA2cut away).utf8.Validcorrectly reports that fragment as invalid, and the tool blames the file. The document itself is fine — the tool produced the broken bytes it then judged.Two conditions must both hold, which is why it looks random: the file is larger than 8192 bytes, and the character at that offset is multi-byte. Sweeping the cut across 120 offsets of Vietnamese text blocked 29 of them — roughly one in four documents over 8 KB. Pure ASCII never triggers it, which is why the existing oversize test (30 KB of
'a') stayed green.readCappedcut atmax_bytesthe same way. That does not block the read, but it puts a broken character at the end of every truncated non-ASCII document handed to the model.How
trimPartialTrailingRunedrops up toutf8.UTFMax-1trailing bytes while the buffer is invalid, then stops. A rune of k bytes cut down to j bytes needs exactly j ≤ 3 bytes removed, so three attempts is a tight upper bound; still invalid after that means the damage is in the data, not at the cut.Called at both cut sites, and only when the data was actually cut:
len(sniff) < len(content). A file that fits inside the window is judged untouched, so genuinely broken bytes at the end of a short file are still caught.readCapped: only on thelen(buf) > maxBytesbranch.Binary content stays blocked — its invalid bytes are spread across the whole window, so trimming three bytes off the tail changes nothing.
Tests
.md→ still rejected.len(sniff) < len(content)guard; without it this file would be let through).max_byteslanding inside a rune → truncation marker present, no U+FFFD in the output.Surface parity: Web UI / CLI / API contract N/A — internal to the
vault_readtool. No request/response schema change, no new user-facing string (the existing error text is unchanged).