TLDR: When two agents write the same file concurrently, the Edit tool's snapshot guard is the wrong weapon. Use an anchored, idempotent write instead — and always dedup on what cannot change, not on what the LLM happened to generate this time.

The Setup

Apollo (my AI assistant) has two things that save memory in the background: a manual apollo-save routine I kick off at the end of a session, and an iMessage daemon that scans messages and stages its own learnings into _inbox.md (my memory staging file).

For a long time those two never stepped on each other.

Then I had a busy day on 2026-06-16 and ran both at once.

What Broke

Three things happened in rapid sequence — all of them quiet.

First, every attempt to update MEMORY.md threw File has been modified since read. Not once. Repeatedly. The other saver had written the file between Apollo's Read and its Edit.

Second, the staged _inbox.md lines I was about to promote? Gone. The daemon had already promoted and cleared them.

Third — and this one would have scared me if I hadn't caught it — git add found nothing dirty. My new files were already in commit 550aaf5. The daemon's add-all had swept them in.

So: no error. No data loss. Just the illusion that nothing had been saved.

What I Tried First (That Didn't Work)

My first instinct was to make the save routine more careful — read later, edit faster.

Useless. The race window is nanoseconds. Any read-then-edit pattern on a shared file is broken by design when two writers are running.

The same mistake showed up in my scanner's dedup logic. I was generating a stable task ID from the message subject. Seemed reasonable — until Sonnet hallucinated a different synthetic ID every scan for the exact same Slack message:

  • <slack-user-id>-a-client-2026-04-22T13:02:00Z
  • CE_slack_a-client_<slack-user-id>_Apr22
  • slack_ce_a-client_20260422T130200Z

One message from a client contact. Five duplicate tasks across five hourly scans. Subject-hash dedup: completely useless when the key itself isn't stable.

The Fix That Actually Worked

Two fixes. Same principle.

For shared-file writes: switch from Edit tool to an atomic Python read-modify-write, keyed on an anchor string, with an idempotency guard:

if pointer not in s:
    # insert

Order-independent. Self-deduping. Whichever saver runs second reads the first one's write and skips. No collision, no clobber.

For scanner dedup: throw out subject-hash entirely. Switch to generate_content_dedup_id(source, sender, original_content) — hash only the fields that cannot change. The real Slack ID is {channel}:{ts}. The sender doesn't hallucinate. The raw content doesn't shift. Hash those.

The rule underneath both fixes is the same: key on immutable fields, never on what the LLM generated this session.

Why This Matters to Me

The symptom that scared me most was git add finding nothing dirty.

It looked like failure. It was actually success — the other saver had committed my work for me.

That reframe changed how I build these systems now. "Nothing to commit" in a concurrent environment is a race outcome, not an error. Verify your files landed in someone's commit. Then move on.

The dedup lesson is the same: stop trusting the generated representation. Trust the source.

P.S. If you're building any kind of multi-agent memory system, the Edit tool is not your friend on shared files. It was built for single-writer scenarios. The moment you have two savers — even occasionally — you need anchored idempotent writes or you'll debug phantom data loss for hours.