#5706·Aider

A valid multi-file edit is silently discarded if the response merely mentions an unrelated existing file (whole/editor-whole formats, non-interactive --yes-always)

Author: barban-devCreated Sep 13, 2026Updated Sep 13, 2026

Issue

Environment

  • aider 0.86.2
  • Python 3.12.11
  • Reproduced with a local OpenAI-compatible model (llama.cpp server) as the editor model in --architect mode, --editor-edit-format editor-whole, --yes-always, --no-show-model-warnings. Also reasoned through and confirmed to apply to the whole edit format directly (not just the --architect/editor-whole combination).

Summary

When a model's response is otherwise a perfectly valid whole/editor-whole edit, but the response text also happens to contain, anywhere, a string that matches the relative path or unique basename of some other file in the repo, check_for_file_mentions() treats that as an implicit request to add the file to the chat. Under --yes-always this is auto-confirmed, and the entire current response is discarded (via reflected_message) before apply_updates() is ever called — including the edits that had nothing to do with the mentioned file. In the worst case (many mentions at once) this triggers a token-limit crash on the next turn and the session ends having applied nothing at all, including the parts of the response that were completely correct.

This is not a model-quality problem: the model can behave perfectly and still lose its own valid edit, because the file-mention detection is a naive whole-text-token scan with no way to distinguish "this filename appears because it's part of the normal, unchanged content of a file I'm dutifully reproducing in full" from "I am asking for this file's content."

Minimal reproduction (no LLM required — pure text-matching bug)

The core issue is fully deterministic and reproducible without ever calling a model, by exercising get_file_mentions() directly against a toy repo:

from aider.coders.base_coder import Coder
from aider.io import InputOutput
from aider.models import Model

# any repo with at least: Makefile, src/foo.c, include/bar.h, include/sub/baz.h
io = InputOutput(yes=True)
model = Model("gpt-4o")  # never actually called
coder = Coder.create(main_model=model, io=io, fnames=[], use_git=True)

# (1) A line that would appear, unchanged, in a Makefile rule for a
#     *different* target than the one being edited:
print(coder.get_file_mentions("cc -o out src/foo.c\n"))
# -> {'src/foo.c'}   (matches: exact repo-root-relative path)

# (2) A flat #include of a top-level header, in a .c file whose *own* edit
#     has nothing to do with bar.h:
print(coder.get_file_mentions('#include "bar.h"\n'))
# -> {'include/bar.h'}   (matches: unique basename)

# (3) The same header, but included via a subdirectory-relative path
#     (relative to the compiler's -I search path, not the repo root):
print(coder.get_file_mentions('#include "sub/baz.h"\n'))
# -> set()   (neither strategy matches - this one "accidentally" escapes)

Case (1) and (2) both register a "mention" purely because the string happens to be present, with zero regard for why it's present. In a real editing session, (1) and (2) are exactly what happens whenever a model reproduces, in full, a file that has pre-existing references to other files it was never asked to touch (a Makefile with several build targets, a source file with several #includes of top-level headers, a CMakeLists.txt listing other sources, an __init__.py re-exporting other modules, etc.).

Real-world consequence

  1. User asks for 3 things: edit include/bar.h, create a new test file, add a target to Makefile.
  2. The editor model does all 3 correctly. Because it's rewriting the whole Makefile (required by whole/editor-whole), the unchanged parts of the Makefile still contain build-dependency lines naming several other pre-existing source files.
  3. check_for_file_mentions() on the model's own response (base_coder.py, check_for_file_mentions at line 1761, called from send_message's tail around line 1561-1566) picks up every one of those pre-existing filenames.
  4. Under --yes-always, confirm_ask("Add file to the chat?", ...) (io.py, if self.yes is True: res = "y" around line 866/940) auto-confirms adding all of them, with no cap.
  5. Back in run_one (base_coder.py), reflected_message is set and the function returns before apply_updates() is reached — the correct edit for all 3 originally-requested files is thrown away, and a new turn starts with all those extra files now injected as full content.
  6. If the model responds to that turn by (reasonably, given its system prompt: wholefile_prompts.py's system_reminder literally says "Output a copy of each file that needs changes") producing content for some of those newly-injected files too, the cycle can repeat and blow through the model's context window well before max_reflections (default 3, base_coder.py:101) is ever reached. The session then ends having applied nothing: not the newly-requested edits, not the incidentally-fetched files, nothing.

We verified this end-to-end with a real (small, local) model: a 3-file task that the model completed correctly on its first attempt still resulted in zero applied edits and zero commits, twice, purely because of this mechanism.

Why --yes-always makes it worse, but doesn't cause it

Note that check_for_file_mentions() only runs on the editor's own generated reply in the --architect/auto_accept_architect flow, because ArchitectCoder.reply_completed() calls editor_coder.run(with_message=content, preproc=False) (architect_coder.py:44) — preproc=False skips scanning the incoming instruction for mentions. So the only place this fires is on the editor's own output, and it is also the only mechanism by which the editor model can legitimately obtain the content of a pre-existing file it needs to edit but wasn't given up front. This means a blanket "never auto-confirm" patch (e.g. forcing explicit_yes_required=True on that one confirm_ask call) is not a safe fix — it would break the common, legitimate case (editor mentions a file it genuinely needs) along with the pathological one.

Suggested directions

  1. Apply first, reflect second. Reorder so apply_updates() runs on the current response before check_for_file_mentions() can short-circuit it. A response that contains valid, applicable edits should never be discarded wholesale just because it also mentions an unrelated file elsewhere in its text. Any genuinely-needed file could still be fetched for a following turn, without cost to the edits that already succeeded. This seems like the smallest, safest change and would have prevented 100% of what we observed.
  2. Cap the blast radius per turn. If a single response newly mentions more than some small N files (e.g. 2-3) at once, treat it as noise rather than genuine intent and don't add any of them / don't reflect. A real, deliberate "I need file X" request is almost always one file, not a dozen.
  3. An explicit, unambiguous protocol for the editor to request a file's content (e.g. a distinct marker like FILE-REQUEST(path)) instead of inferring intent from incidental text matching. This is the most robust fix but also the most invasive (changes the contract with every edit-format's system prompt).
  4. At minimum, document this failure mode for anyone running aider non-interactively (--yes-always, CI/agentic pipelines): whole-file reproduction of any file that itself references other project files (build files, __init__.py, headers with flat includes, etc.) is currently unsafe and can silently discard otherwise-correct multi-file edits.

Happy to share the full verbose logs of his issue if usefu and hope this info could help!

Version and model info

No response