DEV Community

Cover image for Nine agents in one worktree and each one knew only the branch name
Chad Priest
Chad Priest

Posted on Originally published at blog.vodou.ai

Nine agents in one worktree and each one knew only the branch name

Every agent harness lets you run more than one session against the same repo. Almost none of them tell a session that another one exists. Run a few Claude Code windows, a Cursor tab, and a headless job on one checkout and each of them starts with the same picture: the branch, a diff count, and nothing about who produced the diff. They share .git/index. They do not share a sentence.

I measured mine on August 26. Nine live claude processes, one worktree, one index, 91 dirty files. The bootstrap a new session received said which branch it was on and how many files were dirty. That was the entire situational awareness. Two of those sessions had already swept each other's edits into commits, once with a mod declaration pointing at a file that was not yet committed, so HEAD did not compile from a fresh clone. The information that would have prevented both incidents existed in the process table and the transcripts. Nothing carried it between sessions.

Nine claude processes, 91 dirty files, and one line of context: the branch

What I built is small. The daemon that already answers the prompt hook now keeps one row per live session in a table called agent_sessions (the public migration is migrations/087_agent_sessions.sql). Every turn, the hook sends the prompt plus the files this session has touched, and the daemon upserts the row. On the way back out, the daemon computes the intersection of this session's touched set with every other live session's set. If the intersection is empty it says nothing. If it is not, it emits at most two lines: which host the peer is (Claude Code, Cursor, and so on) and which shared files it has touched.

Prompt hook sends files touched to the daemon, which upserts a session row, intersects it with peers, and returns at most two lines only when the sets overlap

One upsert, one select, silence by default

Silence is the designed common case, and that was a deliberate call. The obvious version lists every peer every turn. At nine sessions that is nine lines of noise on every prompt, and a model that sees nine lines of noise on every prompt learns to skip the block. It then skips it the one time it matters. A block that appears only when the sets overlap is read because it is rare.

The live gate was two sessions on one hot file. Session A alone got no block, which is correct. Session B, on the same file, was told about A and its host. A on its next turn was told about B. Round trips were 481 and 476 milliseconds, and nearly all of that is the memory search the hook was already doing. The upsert and the select are one indexed statement each.

The first version keyed on a pid, and the second wrote to a database with no such table

Two things were wrong before it worked.

The plan I wrote for this reasoned about sessions by process id, because the daemon already tracks pids for its socket clients. I started there. It minted a new session every turn. The hook is a short-lived process: it starts, sends the prompt, reads the reply, exits. Its pid is different on every turn, so every turn looked like a brand-new peer and the "live sessions" table filled with ghosts. The stable identity was the transcript path. It is unique per session, it outlives every hook invocation, and the daemon could already infer the host from its shape. I threw away the pid keying and kept the transcript.

The second failure was worse because I had written it down before and did it anyway. The daemon holds two SQLite databases: one for memory, one for core state. Each has its own migration chain. I put migration 087 in the core chain, then wrote the upsert through the daemon's primary handle, which is the memory database. The first swap to the new binary logged this on every prompt:

[session-contract] upsert_agent_session failed: no such table: agent_sessions
Enter fullscreen mode Exit fullscreen mode

The core database plainly had the table at schema 87. The write was going to a file that had never heard of it. My own coordination plan has a section called "two stores, no map" that describes exactly this trap. I reproduced it one file over. The fix moved both the writer and the reader onto the lightweight core handle that the intent-signal layer already held. If that handle is absent, because the file is missing or the open failed, the session is simply not recorded and the block is empty. The feature degrades to its own common case.

Pid-keyed design minted ghost sessions, transcript-keyed fix, write went to memory database, moved to core handle, live gate passed

One more thing from the same daemon that week, because it changes how you should read "the daemon says nothing." On August 29 I found that 17 of its periodic loops used a timer whose default replays every tick missed while the loop body was busy. One extraction cycle ran about 27 minutes, and the 60-second timer then fired 30 cycles inside a quarter of a second. Since each loop is its own task, a long stall released every backlog at once, and one slow cycle became a load spike. A peer-awareness block that is silent by design is only trustworthy if the process producing it is healthy, and that process had a stampede mode nobody had noticed.

The property: identity outlives the reporter, and the signal is conditional on intersection

Stated so you can check it against your own code rather than nod at it.

A session identity must be derived from an artifact that outlives the process reporting it. If your hook, wrapper, or sidecar exits between turns, any key based on its pid, its socket, or its start time will fracture one session into many. The check is mechanical: find the key, find the process that supplies it, and ask whether that process survives from one turn to the next.

A peer-awareness signal must be conditional on the intersection of write sets, not on the existence of peers. "There are eight other sessions" is true and useless. "Session cursor touched src/llm.ts and so did you" is actionable. If your block fires whenever the peer count is above zero, it will be skimmed.

And when a process holds more than one store, the code that writes a table must obtain its handle from the same place that owns that table's migration. Two migration chains and one ambiguous handle is a latent "no such table" that only shows up in production, on the file that did not get the migration.

Five minutes on your own stack: count the sessions, then ask one what it knows

You need nothing from Vodou for this. First, count concurrent agent processes on one checkout:

ps -axo pid,lstart,command | grep -E '[c]laude|[c]ursor|[a]ider|[c]odex' | wc -l
git status --porcelain | wc -l
Enter fullscreen mode Exit fullscreen mode

If the first number is above one and the second is above zero, you have the precondition. Now ask what any one session is told about the others. Open the context your harness injects at prompt time (a hook's stdout, a system prompt file, a bootstrap block) and grep it for another session's host name or a file path from git status. Passing looks like a line that names a peer and a shared path. Failing looks like a branch name and a count, or nothing at all.

If you already keep a session table, run the intersection query the block should be built on:

SELECT p.session_id, p.host, f.path
FROM session_files f
JOIN sessions p ON p.session_id = f.session_id
WHERE f.path IN (SELECT path FROM session_files WHERE session_id = :me)
  AND p.session_id <> :me
  AND p.last_seen > datetime('now', '-30 minutes');
Enter fullscreen mode Exit fullscreen mode

Passing: zero rows when you are alone on your files, and rows only for peers who share a path. Failing: rows for every peer regardless of path, which means your block is unconditional, or a no such table error, which means the table and the handle disagree about which file it lives in. For that last one:

for db in *.db; do echo "$db: $(sqlite3 "$db" "select count(*) from sqlite_master where name='sessions'")"; done
Enter fullscreen mode Exit fullscreen mode

One file should say 1. If two say 1, or the one your writer opens says 0, you have found the two-stores-no-map bug before it finds you.

The locking tools solve the write; this solves the read

Most of the published work on this problem reaches for locks. succubus gives every agent a persistent name and a shared daemon with file claims. Hivemind states the design goal plainly: the CRDT is just transport, and the value is claiming a region before writing to it, with agent-versus-agent conflicts hard-blocked. CoordinationHub and ACDP do explicit document and region locking over a server, and sasp-mcp broadcasts edit intents over Yjs.

Those are all reasonable, and I did not build one. The incidents I had were not two agents writing one function at the same instant. They were one agent staging a file another agent had half-edited, an hour apart, with git as the only shared surface. A lock would not have fired, because the first editor had already released it. What was missing was a sentence at prompt time saying the file was in someone else's working set. Awareness at the read is cheaper than a lock at the write, and it does not refuse a human's session because a bot got there first. The memory literature does not cover it either: the four-tier pattern spec consolidates on session end and has no tier for "who else is in this repository right now," which is state that is only true for the next few minutes and worthless after.

Still live: the touched set is a union capped at 60, so a long session claims files it finished with hours ago

The touched set is unioned across turns and capped at 60 paths. A session that has been running all day and read forty files it no longer cares about still intersects with everyone who opens any of them. The block is honest about what was touched but says nothing about whether the touch is still warm. I have not measured how often that produces a line that should have been silence, and until I do, the cap is a guess, not a number.


Source: Nine agents in one worktree and each one knew only the branch name by Chad Priest, from Building Vodou in Public.

Top comments (0)