fix(file-ops): byte-layer binary detection — stop flagging boundary-cut UTF-8 as binary (#80308 class) - #80440
Conversation
…ort-lossy text Fixes the read_file half of NousResearch#80308 and the class behind NousResearch#80261, NousResearch#80250, NousResearch#80188, NousResearch#80349, NousResearch#79834, NousResearch#79534, NousResearch#79408. The binary sniff sampled files via 'head -c 1000' through the terminal transport, which decodes stdout with errors="replace". A multibyte character cut at byte 1000 therefore arrived as U+FFFD, and _is_likely_binary treated any U+FFFD as binary — flagging valid CJK and emoji text as unreadable. At the text layer a stored replacement char and a transport-manufactured one are indistinguishable, which is why per-callsite adjustments kept leaving siblings open. Sample as 'head -c 1000 | base64' so raw bytes survive the transport (fail-open to the legacy heuristic when the transport cannot produce clean base64), then classify bytes: NUL => binary; valid UTF-8 allowing one incomplete multibyte sequence at the sample end => text; mid-stream invalid UTF-8 (latin-1, true binaries) => read-only, preserving the anti-mojibake guarantee the old check existed for. Files legitimately containing U+FFFD become readable.
|
Merged in #81961 — your byte-layer detection (base64 sample transport + |
|
Thank you — and credit where due: the anti-mojibake case only survived because the original U+FFFD check's comment explained why it existed, so the byte-layer contract was designed to keep its guarantee rather than delete it. The seven fragment PRs mapped the class member by member first; this was a team effort across ~8 people who each caught a real case. |
Summary
Boundary fix for the read_file binary-misclassification class reported in #80308 (Bug 1; Bug 2 is Windows path translation, out of scope here). Binary detection now happens at the byte layer, where "is this UTF-8?" is a well-defined question — instead of on text that the terminal transport has already lossily decoded.
Root cause (the class, not the instance)
read_filesamples viahead -c 1000through the terminal transport, which decodes stdout witherrors="replace". A multibyte character cut at byte 1000 arrives as U+FFFD, and_is_likely_binary(tools/file_operations.py:906) treats any U+FFFD as binary — so valid CJK/emoji text reads as "Binary file". At the text layer, a stored U+FFFD (legit — logs of lossy output) and a transport-manufactured one are indistinguishable, which is why each open fragment fix — #80261 (@luntion), #80250 (@0809android), #80188 (@shihuaiya), #80349 (@JonthanaHanh), #79834 (@cgordoncarroll), #79534 (@diesdaas), #79408 (@LShang001) — caught a real member of the class while siblings stayed open. Same pattern #80258 closed for the lifecycle-guard class.Changes
_sample_file_bytes(): sample ashead -c 1000 … | base64so raw bytes survive the transport; fail-open to the legacy text heuristic when the transport can't produce clean base64 (nonzero exit, non-base64 output)._is_likely_binary_bytes(): NUL ⇒ binary; valid UTF-8 allowing one incomplete multibyte sequence at the sample's end (an artifact of the byte-boundary cut, UTF-8 sequences ≤ 4 bytes) ⇒ text; mid-stream invalid UTF-8 (latin-1, true binaries) ⇒ read-only — preserving the anti-mojibake guarantee the old U+FFFD check existed for: a read→edit→write round-trip must never rewrite undecodable bytes with replacement characters.read_file,read_file_raw) use the byte layer; extension fast-path unchanged; legacy heuristic kept intact as the fallback.Validation
TestByteLayerBinaryDetection(16 tests, all fail on main): CJK cut at byte 1000, pure-CJK sample, emoji cut at boundary, UTF-8 BOM, stored-U+FFFD log file, NUL/ELF binaries, latin-1 stays read-only, empty/short samples, invalid-prefix truncation, base64 transport round-trip, non-base64 and nonzero-exit fallbacks, end-to-endread_filereturning CJK content, end-to-end NUL binary still blocked.scripts/run_tests.sh tests/tools/test_file_operations.py: 75 passed. Neighbor suites (test_file_operations_edge_cases,test_terminal_task_cwd): green. Pre-existing failures on clean main (test_image_source,test_vision_tools,test_url_safety,test_website_policy, 3 MCP collection errors) are unchanged by this diff — verified by stash-runs.test_read_file_uses_bash_safe_windows_pathsnow expects the| base64sample command (mock answers with base64 payload).Scope notes
search_filesreturning 0 results ([Bug]: read_file incorrectly flags valid UTF-8 CJK files as binary, and search_files is completely broken (returns 0 for all queries) #80308 Bug 2) is Windows/MSYS2 path translation per the reporter's own diagnosis, separately addressed by fix(search): stop the Windows path translator from rewriting search regexes #80407/fix(tools): stop search_files failing on absolute Windows paths under Git Bash #80314 — intentionally not touched here.base64on PATH degrade to today's behavior exactly (fail-open), so no platform can get worse.