Repository navigation
fix(core): preserve mixed line endings in edit tool - #53166
Merged
Merged
Conversation
Match oldString against the LF view the read tool shows the model and map offsets back to the source, instead of rewriting every newline to CRLF as soon as the file contains a single CRLF. Replacement text takes the dominant line ending of the matched region, falling back to the file's dominant ending. The tool description now documents line-ending handling and the lenient fallback matching tiers.
… edits Lines a replacement keeps or changes in place reuse the matched region's own endings, so context inside a multi-line match no longer takes the region's dominant ending in mixed files. Edits whose oldString and newString differ only in \r apply to the exact bytes, which restores explicit CRLF/LF conversion. An edit that would leave the file unchanged now fails instead of reporting success (#45927).
neriousy
marked this pull request as draft
October 5, 2026 10:55
neriousy
marked this pull request as ready for review
October 5, 2026 11:54
neriousy
marked this pull request as draft
October 5, 2026 11:59
neriousy
marked this pull request as ready for review
October 12, 2026 13:57
Contributor
There was a problem hiding this comment.
Looks good. I reproduced both reported cases on the base with the simulated model: the lone-LF region in head\r\nAAA OLD BBB\ntail\n gained a CRLF, and a mostly-CRLF file's lone LF line was rewritten to CRLF. With this PR, both edits keep every original line ending byte-for-byte. tool-edit.test.ts passes (20; the 4 new tests fail on the base), and packages/core type-checks.
Public API is unchanged: only the edit tool's description text and internal matching and replacement behaviour change.
No blocking concerns. The new tests are focused and deterministic.
teknium1
added a commit
to jonpol01/hermes-agent
that referenced
this pull request
Oct 16, 2026
…rence Port from anomalyco/opencode#53166 (preserve mixed line endings in edit tool): their fix class, replayed against hermes, reproduced the same bug on both patch modes. The fix itself is @jonpol01's NousResearch#125519 (cherry-picked above); this records the new contract on write_file's docstring.
12 of 19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pr-53166.mp4
Fixes #53149
Fixes #53151
Refs #45927
Note
Not ready to merge yet. Eval results are below.
Problem
The edit tool picked one line ending for the whole file:
source.includes("\r\n") ? "\r\n" : "\n". If a file contained a single CRLF, every\nin botholdStringandnewStringwas rewritten to CRLF. In mixed-ending files this:oldStringvalues fail with the misleading "must match exactly, including whitespace and indentation" error.The read tool strips
\r, so the model never sees CRLF and had no way to work around it.Fix
oldStringandnewStringdiffer only in\r, the edit is treated as a line-ending change and applied to the exact bytes. Explicit CRLF↔LF conversion still works.\n, that existing endings are preserved, how to change endings explicitly, and the lenient fallback tiers that were previously undocumented.Testing
head\r\nAAA OLD BBB\ntail\n), a mostly-CRLF file whoseoldStringcrosses a lone LF line, unchanged context inside a mixed-ending match, explicit CRLF→LF conversion, and the no-op failure.bun test test/tool-edit.test.tsinpackages/core: 20 pass.bun run check: passes.Eval results
The runs come from two dumber evals:
line-endingsandline-endings-realistic. 20 models (DeepSeek, Grok, Muse, Gemini, GLM, MiniMax, Claude, Kimi, Qwen; GPT is left out because it getspatchinstead ofedit) × cases × 3 repetitions. Trials only getread,glob,grepandedit, so every byte on disk went through the edit tool. Baseline is0bef0ea(v2), v1 is64eb0f6(this PR's first commit), v2 isd38a53b(this PR's current head). The variants ran at the same time. The baseline and v1 were each run twice to measure noise.line-endings-realistic: multi-file feature work in a mixed C#/TS/Python/Rust repo (360 trials per run)The repo has CRLF, LF and mixed-ending files. No prompt mentions line endings. Pass = every requested change is present and no line ending was damaged.
Pass rate per case (out of 60):
v1 still damaged line endings: unchanged context inside a multi-line match took the region's dominant ending. v2's per-line alignment removes all of it. The remaining v2 failures are missing requirements (model mistakes) plus one Gemini
MALFORMED_FUNCTION_CALL, not line endings. Fewer failed edits also mean fewer retries, which is where the drops in time (about −20%), tokens (about −28%) and cost (about −16%) come from.line-endings: single-file edits with known bytes (780 trials per run)mixed-single-crlf(#53149)crlf-convert-to-lf11 of the 13 cases are at or near 100% in every variant.
\nwas normalized,crlf-convert-to-lfcould no longer be done withedit, and the tool reported success on a no-op. v2 fixes both. The 9 remaining "unchanged" trials are models that changed the file and then reverted it themselves; the tool now rejects true no-ops.crlf-convert-to-lfwith onlyeditstill passes about half the time, and about a third of trials hit the 300s timeout. Many models (Claude included) write\ras a literal backslash escape instead of a real carriage return, so theiroldStringnever matches. With a shell available this case is trivial.Full tool set (shell, write, subagents, …)
Same evals with OpenCode's full tool set, baseline
0bef0eavs v2d38a53b, run at the same time. TheLINE_ENDINGS_TOOLS=allswitch and the raw runs are in anomalyco/evals#15.line-endings-realisticpass rateline-endingspass ratemixed-single-crlfline-endingsmean trial time / tokens / costline-endings-realisticmean trial time / tokens / costRealistic pass rate per case (out of 60): add-delete 5 → 55, add-discount 4 → 56, category-two-turns 2 → 52, move-item-value 44 → 60, rename-sku 11 → 47, seed-and-docs 57 → 60.
None of the 17 damaged v2 realistic trials was damaged by
edit:writeto rewrite a whole CRLF file with LF (DeepSeek V4 Pro accounts for 11 of them, MiniMax M3 for 1). The same trials are damaged on the baseline too.shell(text-mode read/write turns CRLF into LF)..csfile to CRLF withperl, as.gitattributesasks. The scorer counts that as damage, but it is a reasonable choice.The realistic time difference comes from provider stalls that hit the 600s timeout: MiniMax M3 and Gemini 3.8 Flash account for 9 timeouts vs 7 on the baseline, with idle sessions and almost no edits. Median trial time is 52s vs 50s, and tokens and cost are flat.
Not covered yet
writedoes not preserve line endings when it overwrites an existing file. It is now the main remaining source of damage with the full tool set. A follow-up could keep the overwritten file's ending.patch(used for GPT models) does not touch line endings and is not covered by these evals.edit, explicit CRLF→LF conversion still fails about half the time because models write\ras a literal escape.Added by the opencode bot (AI-generated).
Fixes #45927
Fixes #45880
Related: #54605, #45926