Stop the Edit Chaos: How Hashes Solve Agent Conflicts
I’ve watched agents burn through context windows doing the same dance over and over: read a file, propose a change, get “string not found,” tweak whitespace, try again, accidentally edit the wrong occurrence, read the file again, try again. Multiply that by a five-file refactor and you’re paying for six round trips when two should have been enough.
The fix isn’t “smarter models.” It’s a boring database trick we’ve had for decades: optimistic concurrency control, using a content hash as the revision token.
The failure mode nobody talks about
Most agent edit tools work like glorified sed: you give an old_string and a new_string, and the runtime searches the file for a literal match. Sometimes that’s all you need. Often it isn’t.
Here’s what goes wrong in practice:
- Whitespace drift. The model copied four spaces; the file uses tabs. Or it included a trailing newline you didn’t. The match fails even though the edit is conceptually correct.
- Ambiguous context.
return errappears twelve times. The agent grabbed the wrong one, or the tool applied the first match and silently changed the wrong function. - Stale reads. The agent read
auth.tsat the start of the turn. You fixed a typo in another tab. The agent’s patch still “matches” — but it’s patching yesterday’s file against today’s reality. - All-or-nothing batches. The agent queues edits to five files. Four are fine. One changed. The whole batch fails, or worse, three land and two corrupt something you didn’t notice until CI explodes.
Each of these sends the agent back for another read–think–write cycle. That’s tokens, latency, and opportunity for the model to lose the plot.
String matching alone has no opinion about whether the file you’re editing is the file you think you’re editing.
How hash-based edits work
The pattern is simple:
- Read returns the file content and a revision: a hash (in Konstruct, SHA-256 of the full file bytes) that uniquely identifies that exact version of the file.
- Write includes that hash as
base_revision, plus the intended change (typically anold_string/new_stringpatch). - The runtime checks the hash before applying the patch. If the file on disk no longer matches, the write fails immediately with something like
STALE_REVISION— not “string not found,” not a partial apply, not a shrug.
The hash is the lock. The patch is the intent. You need both.
read_code("src/auth/session.ts")
→ content: "..."
→ revision: "a3f8c2..."
write_code({
path: "src/auth/session.ts",
base_revision: "a3f8c2...",
old_string: "const TTL = 3600;",
new_string: "const TTL = 7200;"
})
→ ok
If someone — human or another agent — changed session.ts between the read and the write, the hash won’t match. The edit is rejected before anyone tries to splice strings into the wrong version.
That’s optimistic locking: assume conflicts are rare, detect them cheaply, fail fast when they happen.
A concrete example: multi-file refactor
Say an agent is renaming an option across a small feature: types, handler, and two call sites. A reasonable session looks like this:
batch read:
types.ts → revision R1
handler.ts → revision R2
api.ts → revision R3
worker.ts → revision R4
batch write (4 patches, each with its base_revision):
types.ts @ R1 ✓
handler.ts @ R2 ✓
api.ts @ R3 ✗ STALE_REVISION (you edited a comment while it worked)
worker.ts @ R4 ✓
Three edits landed. One failed for a precise, actionable reason: api.ts changed since the read. The agent doesn’t re-read everything. It re-reads api.ts, confirms the rename still makes sense against the current content, and retries that single patch with a fresh revision.
Compare that to string-matching without revisions:
- All four patches might “match” even though
api.tsisn’t what the agent remembers. - Or the batch aborts entirely and the agent re-reads four files and re-reasons about four edits.
- Or two patches apply and two fail with unhelpful “not found” errors, and the agent guesses which ones actually worked.
Hash gating turns “something went wrong somewhere” into “this file, this revision, right now.”
Partial success is a feature, not a bug
Agents should be able to do more work per turn without betting the farm on perfect staleness assumptions.
Batch writes with per-file revision checks give you:
- Surgical retries. Only stale files need another read. Successful edits stay done.
- Stop-on-conflict semantics. The first
STALE_REVISIONcan halt the rest of the batch so the agent refreshes context before touching dependent files — without undoing work that already succeeded. - Optional atomic batches when you do want all-or-nothing (create a new module and its test together, say).
This is the difference between a transaction that rolls back because one row changed and a transaction that tells you which row changed so you can fix one UPDATE instead of replaying the whole script.
For agents, that directly translates to fewer round trips. Less re-reading. Less re-planning. Less “let me start over from the top” after a harmless concurrent edit.
Why this is less error-prone than string matching alone
To be clear: hash-based edits don’t eliminate patches. The agent still specifies what to change. The hash specifies which version it’s allowed to change.
That combination matters:
| String match only | Hash + patch | |
|---|---|---|
| Detects concurrent edits | No | Yes, before apply |
| Failure signal | ”not found” (ambiguous) | STALE_REVISION (specific) |
| Multi-file partial progress | Fragile | Supported by design |
| Retry scope | Often entire task | Usually one file |
String matching is brittle because finding text is not the same as having authority to change the file. Line numbers are worse — they’re stale the moment anyone adds a line above. Full-file replacement avoids match ambiguity but creates merge nightmares and huge diffs. Content hashes give you a stable handle on “this exact bytes-on-disk snapshot” without pretending the file won’t move under you.
Same input, same output — for edits too
I wrote elsewhere about giving agents the kind of structured context we used to give interns: maps, guardrails, and ways to verify work. Hash-based edits are the same philosophy applied to the write path.
Reads and writes become idempotent in intent: if the file hasn’t changed, the revision is stable and the patch applies cleanly. If it has changed, you find out immediately — not after three dependent files were updated against a ghost.
Agents already struggle with attention and context limits. Making them replay entire multi-file edits because one string didn’t match, or because they lacked a cheap staleness check, is self-inflicted pain. Optimistic locking is boring infrastructure. It’s also exactly the sort of thing that lets agents ship real diffs with fewer trips back to the well.
If you’re building agent tooling — or evaluating it — ask one question: when a write fails, does it tell you the file changed, or does it make you guess? That answer predicts how many round trips your agents will need, and how often they’ll “fix” the wrong thing confidently.
Konstruct implements this on every read_code / write_code pair. I’d love to see it become baseline everywhere agents touch source control. Your future self (and your CI bill) will thank you.