I wanted two independent agent instances (different machines, different runtime environments) to stay in sync on shared intent. Not real-time chat. Slower, operational collaboration:
- keep sessions local
- exchange actionable notes
- share a durable profile for the user/operator context
- sync skills and preferences across both
- avoid building a full message platform
Lowest possible surface area. That was the whole goal.
Yea, if you follow AI tooling, you might be thinking: why not ACP? Because ACP does not solve cross-machine coordination or durable shared state. ACP is local editor-agent transport only.
My first attempt was the obvious one: a shared filesystem bus.
bus/
inbox-a/
inbox-b/
archive-a/
archive-b/
PROTOCOL.txt
The protocol was minimal on purpose:
- one message file per event
- sender writes into peer inbox
- unread = in inbox
- processed = moved to archive
- messages are JSON with type, IDs, and payload
- runtime state remains local to each node
Looked solid enough for a first pass.
Why the shared mount failed
It didn’t explode. That’s exactly what made it hard to debug.
The shared storage was remote and mounted. From a shell prompt it looked normal, but it was not a local FS in the consistency sense. Directory listings were cached.
I saw cases like:
- write occurred on machine A
- machine B’s listing still showed empty inbox
- remote checks showed file existed
- cache clear did not immediately fix visibility
The mount configuration exposed a dir-cache-time of about 30 minutes.
At a 12-hour communication cadence, a 30-minute cache window felt long but manageable on paper. In reality it was exactly why the “shared state” could not be trusted.
Transporting bytes was easy. Confidence was the hard part:
- Did the recipient actually see the message?
- If not, is the sender still waiting?
- Is the listing stale, or did the write fail?
A bus that can lie with delay is worse than no bus at all.
And speed was never the real issue. Every stale read creates a second-order failure: humans spending effort reconciling state that should have been deterministic. That overhead kills the value of automation. I wanted “does it exist?” and “can I act on it?” to be the same question.
Why not “just use a real NFS backend?”
The obvious fallback was to replace the object-backed mount with a heavier file sharing setup:
- EFS / NFS share
- private network path
- Tailscale/VPN rules
- mount lifecycle management
That would probably improve consistency, but it moves the problem to infra babysitting. For a low-frequency collaboration loop, that was the wrong trade.
I needed correctness with less operational overhead.
The design change: Git as the coordination layer
I switched from “live mount as truth” to “explicitly synced artifacts as truth”.
The new model:
- one shared repo for durable bus state
- explicit
git pullbefore read - explicit
git commit && pushafter write - message lifecycle in files, but lifecycle visibility is commit-based
- explicit job boundaries so no local process assumes immediate remote truth
The protocol became deterministic:
git pull- read inbox
- move handled message to archive
git add -A && git commit && git push- write outbound messages
git add -A && git commit && git push
No implicit state. No unverified visibility assumptions.
Repo layout that stayed honest
I kept the bus repo intentionally small:
bus-repo/
inbox-a/
inbox-b/
archive-a/
archive-b/
user-sync/
USER.md
README.md
Messages carried intent, not code payloads:
- pull skills
- ack
- profile updated
- heartbeat
- state transition markers
That kept the bus from turning into a second replication system.
Separate planes: skills vs messages
I separated the concerns formally:
- skills repo: source for reusable procedures and skill definitions
- bus repo: coordination artifacts + profile state
Keeping them separate avoided cross-contamination and gave each repo a clear failure model.
The USER.md merge
USER.md wasn’t just text; it represented preferences and operator memory. Line-based merging was the wrong abstraction for it.
So for this file, I moved to in-model semantic merge:
- pull shared
USER.md - read local
USER.md - combine durable facts
- de-duplicate and keep signal over noise
- write back canonical merged version
- commit if changed
If both agents had written conflicting style, a human could still read what each represented. The system then resolved by semantic intent, not blind textual merge.
Automation: keep it predictable
Once transport became explicit, the automation layer was straightforward:
- scheduled skill sync from local sources
- filtered push of user-authored skills only
- periodic bus sync job
- immediate writes only for important state changes
The loop got boring, which is exactly what I wanted. Reliable, low drama.
Mounts vs Git
It wasn’t speed, and it definitely wasn’t elegance. The failure model became auditable.
With mounts, failures were ambiguous:
- is the file written?
- is the listing stale?
- is the remote copy the real truth?
With Git:
- did we pull?
- did we commit?
- did we push?
- did the other side fetch?
Each state transition has an explicit artifact and a timestamped point in history. That’s what made trust cheap.
I’m not arguing this is the best bus for everything. If you truly need low-latency collaborative editing, you still need a different substrate.
But for durable coordination across machines with sparse interaction, this model gives you stronger invariants than most “shared directory” approaches:
- deterministic visibility
- fewer hidden assumptions
- simpler failure triage
- easier operational recovery
What I do now
For low-frequency agent coordination, explicit synchronization usually beats pretend real-time shared folders.
If your transport can silently go stale, it isn’t a transport for control.
Git is slower. Its state transitions are explicit, so a stale directory listing cannot pass as current truth.