Running parallel agent sessions without them stepping on each other
I usually have several coding-agent sessions running at once. Sometimes on different projects, often on the same repo: one session deep in a refactor, one writing docs, one investigating a bug. Nothing about an agent harness makes this safe by default. The two failure modes show up fast:
- Corruption: two sessions write the same state file and one clobbers the other.
- Collision: two sessions, blind to each other, pick up the same work or edit the same files, and you discover it at diff time.
These feel like one problem ("agents conflict") but they are structurally different, and in my memory system they got structurally different fixes. The corruption fix has no runtime component at all. The collision fix has no locks. This post is about both, and about the things I deliberately did not build.
Corruption: solved by construction, not coordination
Each of my agent sessions keeps working memory on disk: a scratchpad, a decision log, open questions. The naive layout - one set of files per project branch - dies immediately with two live sessions, and the classic answer is locking or serialized writes through a daemon.
The layout that needs neither: every session writes only to its own directory, keyed by its session id. Subagents inherit the id with a suffix, so even a session's own workers cannot collide with their parent. No two writers ever touch the same file, not because a lock stops them, but because no shared file exists. At session start, a materialize step folds all sessions' entries into one read-only merged view, so a new session still reads everything that happened on the branch - stamped as historical reference, because a snapshot is stale by definition.
There is exactly one exception, and it is load-bearing: a single
current-task.md per branch, shared by every session, last writer wins.
That file answers "what is this branch about right now", which is only
useful because it is shared. The design rule that fell out: share nothing
by default, and when you do share, share one small file whose whole point is
being the single meeting place.
This solved corruption completely. It did nothing for collision - a session writing safely in its own directory can still cheerfully redo work another session finished an hour ago.
Collision: sessions need to see each other
The actual pain, once corruption was gone, was launching a second session into a repo blind. It did not know a first session existed, let alone what files it was touching. My fix is a presence layer: a machine-local registry of live sessions, maintained entirely by lifecycle hooks that were already firing on session start, tool use, and session end.
Each record carries the branch, the files the session has touched, an optional one-line intent, and a freshness timestamp. When a new session starts, it gets a block injected into its context:
3 other session(s) live on this branch right now - avoid editing the same files:
- 6679ade6 - touching channel.md, production-readiness.md (active 5m ago)
- 30b9890c - touching audit.rs, audit.test.ts (active 52m ago)
- c5e09075 - touching review-queue.md, CONTEXT.md (active 1h ago)
That is most of the feature. The dominant collision case was never two sessions racing for a file at the same millisecond; it was me starting session B without remembering what session A was doing. Awareness at launch time, plus an on-demand "who else is here" query the agent can run mid-session, covers almost all of it.
The part that took actual design care is trust. A presence registry is only useful if the agent can believe it: phantom peers - records of sessions that died without cleaning up - turn the block into noise the agent learns to ignore, and then the feature is worse than nothing. Session-end hooks are unreliable by nature (crashes, hard kills, dead batteries), so liveness is checked at read time against the OS: does the recorded pid exist, and does its process start-time match what the record captured? The second check matters more than it looks. Pids get reused; without the start-time match, a recycled pid makes a dead session read as alive forever, which is precisely the failure the layer exists to prevent.
No locks, on purpose
The instinctive design here is mutual exclusion: lock files a session is editing, deny the other session access. I rejected it, and the reasoning generalizes:
- The problem was awareness, not exclusion. Sessions were not fighting over files; they were ignorant of each other. Informing them fixes the actual failure. Blocking them fixes a different, mostly hypothetical one.
- A lock is hostile to the human in the loop. These are my own parallel sessions. A hard lock means my repo tells me no when I ask a second session to touch a file the first one grazed an hour ago.
- Advisory degrades gracefully; locks degrade catastrophically. A stale presence record wastes a warning line. A stale lock blocks work until someone hunts it down.
So the whole layer is advisory. It informs, it never blocks. Weeks in, I have not once wished for the lock.
I also rejected the heavier alternative: a durable task queue where every session formally claims a task before touching anything. Queues answer "what is the status of this work" - a real question, but a different one. For live collision avoidance a claimed task is simultaneously too coarse ("someone is on the refactor" does not tell you which file they are editing right now) and too much ceremony (every session pays a claim step to defend against a rare event). Presence is finer and free: the hooks fire anyway.
The human side: which session needs me
Coordination between sessions is half the story. The other half is coordinating me. With four sessions across three projects, the expensive question stops being "are the agents colliding" and becomes "which one is waiting on my input while I stare at a different terminal".
Presence records carry a state field for this: working or waiting. Turn
boundaries drive it - submitting a prompt marks the session working,
the agent finishing its turn marks it waiting. The subtlety is that tool
activity alone cannot tell you this: a session that is thinking, or reading
files, looks idle by every activity metric while genuinely busy. Only the
turn boundary is truthful. A workbench view renders the sessions per
project with a marker on whoever is waiting, and a backgrounded session
going idle fires a notification.
The effect on throughput is larger than the collision fix. Parallel sessions were already safe; this made them worth it, because agent idle time - finished, waiting, unnoticed - was where the parallelism actually leaked.
What transfers
The specifics are mine, but the shapes are portable to any multi-session agent setup:
- Partition state by writer; make shared files a deliberate exception. Corruption problems you solve by construction do not come back. Locks you add have to be right forever.
- Inject awareness at start time. The dominant collision is launching blind, not racing. A cheap "who is here, touching what" block at session start beats sophisticated conflict detection you build later.
- A registry nobody trusts is worse than none. Whatever your liveness story is, phantom entries are the death of an advisory system - the reader learns to ignore it. Verify liveness at read time, against a source that survives crashes.
- Prefer advisory over blocking until proven otherwise. Especially when every session ultimately answers to one human.
- Track waiting, not just working. If you run parallel sessions, the bottleneck quietly becomes your own attention. Surface it.
None of this needed a coordinator process, a message bus, or a database. It is a directory convention, a handful of JSON files in a cache directory, and hooks that were already firing. The boring infrastructure was the feature.