# Recovering an agent’s half-finished edits

Author: Chris Chabot
Canonical URL: https://chabot.dev/blog/recovering-an-agents-half-finished-edits/
Published: 2026-11-03

How expected and intended digests let a restarted coding agent tell whether each file edit landed, is still owed or was overtaken, and when it must refuse to write.

In the summer of 2026 our coding agent could change up to 16 files in a single tool call. Between deciding on those edits and reporting the result it had to plan the writes, record its intentions, replace each file, record what happened and hand that account back to the model, and the process could die between any two of those steps. The next run would then inherit a conversation ending in a tool call with no result and a workspace in which some of the files might have changed. Running the edits again is the obvious recovery and also the dangerous one, because it assumes that nothing else has touched those files in the meantime.

What the next run needs is a way to tell, file by file, whether an edit happened, did not happen, or has been overtaken by something else. A log cannot answer that on its own. A journal and a file system cannot commit together, so whichever you write first, a crash can fall between the two: record success after the rename and you can lose the record of a finished write, record it before and you can keep the record of a write that never landed. The design I settled on stopped asking the journal for the outcome. The journal held the question, and the file answered it.

Before the writer touched anything, it ran the whole batch against an in-memory file system. An edit that could not apply failed at this stage, whichever path it was on, and the failure was saved as a receipt with no file changed. If the plan held, the writer stored a prepared receipt for every path with two digests, one for the bytes it expected to find and one for the bytes it intended to leave, and it waited for the store to acknowledge those receipts before the first rename. If that flush failed, the batch was discarded. This made the journal a hard dependency of editing, so an outage in the session store stopped the agent from changing files. That is a real cost and the right one here, since an edit nobody can account for after a restart is exactly what the writer existed to prevent.

Paths then committed in the order the model had declared them, each under a per-path lock, so that two edits through that writer could not both verify the same old contents and have the second erase the first. Each write was a small protocol of its own. The writer compared the live file with the receipt. If the file already held the intended bytes, the path was done and nothing was written. If it held anything other than the expected bytes, the write was refused. Otherwise the new contents went to a uniquely tagged staging file beside the target with the original permissions, were verified, were renamed into place and were verified again, and any residue was cleaned up. The commit record was flushed before the model saw the result, so the model was never told about a success the journal did not know about. When that flush failed the path was marked as needing reconciliation, when the transport failed the path stayed pending, and in both cases the later paths stayed pending too.

Restart is where the two digests pay off. Resuming a batch meant reading each unsettled receipt and then reading the file:

| The live file | What that means | What resume did |
| --- | --- | --- |
| Matches the intended digest | The intended contents are present | Promoted the receipt to committed and wrote nothing |
| Matches the expected digest | The file still has the expected contents | Performed it |
| Matches neither | The file holds an unexpected version | Reported a conflict, kept the receipt and wrote nothing |
| Cannot be read | Nothing can be concluded | Stayed pending as a failed write, not treated as absent |

The third row is why this was worth building instead of a simple retry. A colleague may have fixed the same bug by hand, a formatter may have rewritten the file, or the disk may have corrupted it. A digest shows that the bytes differ and says nothing about why, so the writer could not tell a colleague's fix from corruption and did not try. It would be easy, and would even feel responsible, to have the recovery path restore what the agent intended. But that treats the agent's plan as more authoritative than the file, when the plan is the one thing in the room known to be out of date. So the third state stayed unresolved evidence: the writer failed closed, the receipt was retained, and whoever picked up the conflict had something to reason from. The same suspicion extended to the path itself, since a symlink swapped in after planning can point the same name somewhere else, and the writer rejected that change as well.

The per-path lock serialized this writer's edits; an external editor did not take that lock. The last check of the target's contents came before staging, so a change made during staging and verification could still be overwritten by rename. Recovery refused an unexpected version, but preventing that live race required coordination beyond this writer's lock.

Each rename published one file atomically, so a crash could leave the first file changed and the second untouched. The receipts told the model which paths had committed and which had not been attempted, with an instruction to resume using the same batch ID. A path already at its intended contents needed only its receipt settled; the writer did not rename it again.

Write-ahead intent and atomic rename are old ideas; applying them to agent edits meant deciding where the writer should refuse. Models resume with great confidence, which is a good reason not to make them responsible for working out what happened.
