DEV Community

Andre
Andre

Posted on • Originally published at olund.dev

Running parallel agent sessions without them stepping on each other

Originally published at olund.dev.

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:

  1. Corruption: two sessions write the same state file and one clobbers the other.
  2. 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)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)