You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
StrReplaceFile decodes the whole file with errors="replace", edits the string, and writes the whole string back. Any byte in the file that isn't valid UTF-8 — anywhere, including far from the edit — is replaced by U+FFFD and written to disk as EF BF BD. The file's length and contents change outside the requested edit, permanently, and the approval diff can't show it because the diff is computed from the already-lossy string.
This is the same root cause as #2191 / #1952 (whole-file round-trip silently rewriting content the edit never touched), but for undecodable bytes rather than line endings.
Where
src/kimi_cli/tools/file/replace.py
# line 132content=awaitp.read_text(errors="replace")
...
# line 170awaitp.write_text(content, errors="replace")
Reproduction
A file with a single invalid byte, unrelated to the edit:
The \xff is gone and the file grew by two bytes, on an edit that only asked to touch alpha.
One thing worth flagging before anyone fixes this
Dropping errors="replace" from the write at line 170 does not fix it. I checked — U+FFFD is a perfectly valid character to encode, so it still writes EF BF BD:
The loss happens at the read. That said, line 170 does look inconsistent with the project's own rule in tests_ai/test_encoding_error_handling.md — "Writing files and encoding Python strings to bytes do not require errors="replace"" — and with the sibling tool, write.py:158, which writes without it. Worth tidying either way, just not as the fix.
write.py can't hit this bug: it reads with errors="replace" only to build a display diff, and writes params.content (fresh model-supplied text). StrReplaceFile is the only file tool that reads, edits, and writes back the same content.
Possible directions
I didn't open a PR because the sensible fix is a design decision that's yours to make, and each option has a real cost:
surrogateescape on both ends — byte-exact round-trip (verified: 25 → 25 bytes, \xff preserved). But lone surrogates raise UnicodeEncodeError when the string is later encoded, which this codebase has already been bitten by once in UnicodeEncodeError - Surrogates Not Allowed When Writing History #420. The edited content flows into build_diff_blocks and the approval display, so that risk is live here. It would also need errors widening in kaos.path, where it's typed Literal["strict", "ignore", "replace"].
Refuse the edit — read strict, and return a ToolError when the file isn't valid UTF-8. Small, can't crash, and arguably right for a text-editing tool. Cost: it declines edits the tool currently performs (badly).
Summary
StrReplaceFiledecodes the whole file witherrors="replace", edits the string, and writes the whole string back. Any byte in the file that isn't valid UTF-8 — anywhere, including far from the edit — is replaced by U+FFFD and written to disk asEF BF BD. The file's length and contents change outside the requested edit, permanently, and the approval diff can't show it because the diff is computed from the already-lossy string.This is the same root cause as #2191 / #1952 (whole-file round-trip silently rewriting content the edit never touched), but for undecodable bytes rather than line endings.
Where
src/kimi_cli/tools/file/replace.pyReproduction
A file with a single invalid byte, unrelated to the edit:
The
\xffis gone and the file grew by two bytes, on an edit that only asked to touchalpha.One thing worth flagging before anyone fixes this
Dropping
errors="replace"from the write at line 170 does not fix it. I checked — U+FFFD is a perfectly valid character to encode, so it still writesEF BF BD:The loss happens at the read. That said, line 170 does look inconsistent with the project's own rule in
tests_ai/test_encoding_error_handling.md— "Writing files and encoding Python strings to bytes do not requireerrors="replace"" — and with the sibling tool,write.py:158, which writes without it. Worth tidying either way, just not as the fix.write.pycan't hit this bug: it reads witherrors="replace"only to build a display diff, and writesparams.content(fresh model-supplied text).StrReplaceFileis the only file tool that reads, edits, and writes back the same content.Possible directions
I didn't open a PR because the sensible fix is a design decision that's yours to make, and each option has a real cost:
surrogateescapeon both ends — byte-exact round-trip (verified: 25 → 25 bytes,\xffpreserved). But lone surrogates raiseUnicodeEncodeErrorwhen the string is later encoded, which this codebase has already been bitten by once in UnicodeEncodeError - Surrogates Not Allowed When Writing History #420. The edited content flows intobuild_diff_blocksand the approval display, so that risk is live here. It would also neederrorswidening inkaos.path, where it's typedLiteral["strict", "ignore", "replace"].Refuse the edit — read strict, and return a
ToolErrorwhen the file isn't valid UTF-8. Small, can't crash, and arguably right for a text-editing tool. Cost: it declines edits the tool currently performs (badly).Splice at the byte level — apply the replacement to the original bytes and only rewrite the changed span. Most correct, most work, and would also fix the CRLF class in [Windows] StrReplaceFile silently converts entire file from CRLF to LF, forcing Agent to abandon native tools for Python workarounds #2191 / fix(file): preserve CRLF during file edits #1953.
Happy to send a PR for whichever direction you'd prefer.