Optimistic concurrency
Two workers in the same cron interval both try to edit the same invoice. One reads status: "sent" and patches it to paid. The other reads status: "sent" and patches it to void. Both write. Last writer wins — silently.
That's fine for blog posts. It's not fine for accounts receivable.
R2 has the right primitive
Cloudflare R2 exposes conditional puts via onlyIf.etagMatches. Every R2ObjectBody carries a strong ETag. If you remember the ETag from your read and pass it on your write, the put succeeds only if the underlying object still has that exact ETag.
That's optimistic concurrency control. The lock isn't held; the precondition is checked at commit time. Losers explode loudly instead of silently overwriting winners.
The interface extension
The base StorageBackend has read and write. The optimistic flavor adds two optional methods:
interface VersionedRead {
content: string;
version: string; // opaque
}
interface StorageBackend {
// base...
readWithVersion?(path: string): Promise<VersionedRead | null>;
writeIfVersion?(path: string, content: string, expected: string): Promise<boolean>;
}
The R2 backend implements both. The Node backend doesn't — it runs single-process, the precondition is trivially satisfied, and falling back to plain read-then-write is correct there.
R2 implementation
async readWithVersion(path: string): Promise<VersionedRead | null> {
const obj = await this.bucket.get(this.treePrefix + path);
if (!obj) return null;
return { content: await obj.text(), version: obj.etag ?? "" };
}
async writeIfVersion(path: string, content: string, expected: string): Promise<boolean> {
if (!expected) return false;
const result = await this.bucket.put(this.treePrefix + path, content, {
onlyIf: { etagMatches: expected },
});
return result !== null;
}
bucket.put returns null when the precondition fails. We map that to false. No exceptions, no parsing — R2 does the work.
The retry loop
GitStore.edit becomes optimistic when the backend supports it:
for (let attempt = 0; attempt <= EDIT_RETRIES; attempt++) {
const current = await backend.readWithVersion(path);
if (!current) throw new Error("not found");
const existing = parseRecord(spec, current.content);
const next = this.applyPatch(spec, id, tenantId, existing, patch);
const wrote = await backend.writeIfVersion(
path, serializeRecord(spec, next), current.version,
);
if (wrote) {
await backend.commit(`edit ${spec.name} ${id}`, [path]);
return next;
}
}
throw new ConflictError(spec.name, id);
One retry. If the first write loses to a concurrent edit, we re-read (getting the freshly-written content + new ETag), re-apply the patch on top of that fresh state, and try again. That's important: the second attempt is patching the CURRENT state, not the original — so an unrelated concurrent change to a different field doesn't cause the second attempt to clobber it either.
If both attempts lose, ConflictError propagates to the tool layer, which surfaces it to the agent with a clear message: "Invoice 8f3b2e1a was modified concurrently — retry your edit."
Why one retry, not many
Retrying forever turns a contention problem into a livelock. One retry handles the common case (two writers, one wins, the other re-applies on top). The second loss usually means the data has changed in a way that invalidates the original intent — the right move is to surface that to the caller rather than blindly re-apply.
The tests
The conflict test mocks R2 with proper ETag semantics:
async put(key, value, options) {
const expected = options?.onlyIf?.etagMatches;
if (expected !== undefined) {
const existing = store.get(key);
if (!existing || existing.etag !== expected) return null;
}
store.set(key, { body: value, etag: bumpEtag() });
return { key };
}
One test proves that two interleaved edits result in ConflictError for the loser. The other proves that a single retry succeeds when only the first attempt races. Both pass against an in-memory bucket; the same code paths run live against real R2 in production.