July 28, 2026
Every File Operation Gets Exactly One Owner
Splitting one overloaded file tool into four single-purpose ones surfaced three separate bugs in edit_file — it couldn't delete text, couldn't preserve whitespace, and couldn't tell a stale edit from a fresh one.
Went looking for one bug and found three. That’s today.
Started with the old do-everything file tool — one entry point that could create a file or a folder depending on whether the path handed to it ended in a trailing slash. A weak model got that guess wrong constantly: ask for a folder, get an empty file instead, no error. Split it into four tools that each do one job — write a file, make a directory, delete a path, set permissions. The guessing goes away because there’s no longer a shared entry point to guess from. One verb per tool. The overlap didn’t get documented around, it got deleted.
While I was in there I went back through edit_file, the tool I shipped a few days ago to replace bulk find-and-replace. Four days live, and it was broken in three separate ways nobody had hit yet.
First: no read-before-modify guard. Read a file, edit it, edit it again — the second edit anchors on text the first edit already changed, comes back “not found,” and the model reads that as “try again” instead of “you’re looking at stale text.” It retries the same failing call until the runaway guard kills the turn, and you’re left with a half-edited file and no idea why. Fixed it properly this time: the model’s last read of a file has to be more recent than the last successful edit to it.
Second, and dumber: the dispatch layer trims whitespace off every string parameter before a tool sees it. Fine for most tools. Not fine for a search-and-replace pair, where the trailing newline you’re trying to delete or the leading spaces you’re trying to fix ARE the payload. edit_file couldn’t delete a whole line — the anchor lost its newline and left a blank one behind. Couldn’t fix indentation, either. And write_file was silently dropping the trailing newline off every file it wrote. Gave those specific parameters an exemption from the global trim.
Third: deletion is replace: "". Two layers checked “is this parameter present” with a truthiness test, and an empty string fails that test the same way None does. So the tool told the model “missing parameter” for a parameter it had, in fact, been sent — which just sends the model back to resend the exact same call. Structurally could not delete text. Added a trait for params allowed to be empty but still required to be present.
None of these three would’ve shown up in a demo. They only show up when something uses the tool the way a real edit works — read, then a second read-dependent edit, then a deletion, then a whitespace-sensitive fix. Four days of that not happening is a coincidence I don’t want to rely on again.
Smaller stuff rode along: the tool-loading menu now groups by category instead of dumping 28 tools in one comma-joined line, and hides tools the model’s already loaded. A crashed turn used to sit there rendering “thinking…” forever because the UI never re-fetched on the terminal frame — now it shows the crash note it was supposed to show. And the TTS/embedding memory footprint dropped from 3.4GB steady-state to under 800MB once I found the allocator that was never handing pages back to the OS.
Next: watching whether the file split and the edit_file fixes actually hold up outside today’s testing, the way yesterday’s fix is still being watched for the same reason.
-
Split the old three-in-one file tool into four single-purpose ones — write, make directory, delete, set permissions — closing the trailing-slash guess that could turn a folder request into an empty file
-
Fixed
edit_fileto refuse a stale or blind edit instead of looping on “not found” until the runaway guard killed the turn -
Fixed the dispatch layer trimming whitespace off edit_file’s search/replace text and write_file’s contents, which made it impossible to delete a line, fix indentation, or preserve a trailing newline
-
Fixed deletion (
replace: "") being structurally rejected as a missing parameter by two separate validation layers -
Grouped the tool-loading menu by category and hid already-loaded tools; fixed crashed turns rendering “thinking…” forever; cut TTS/embedding memory footprint from 3.4GB steady-state to under 800MB