Advisory path lock
A social, first-writer-wins claim over a set of repository paths that cooperating agents voluntarily respect. It is the coordination primitive Befall is built on.
Updated 2026-08-08
What is an advisory path lock?
An advisory path lock is a claim an agent registers over a glob of repository paths (for example src/api/**) so that other agents in the same repo know that region is being edited. It is advisory, not enforced: the lock lives in a shared coordination service, not in your filesystem or your kernel. Nothing stops another process from opening the same file — the lock is a signal that cooperating agents choose to respect, first-writer-wins, until it expires or is released. In Befall it carries a TTL, auto-releases when an agent goes offline, and overlap is computed with glob-aware path intersection rather than exact-string matching.
I run three agents — Claude Code, Codex CLI, and Cursor — against one repo, and this is the primitive I built Befall on. Befall itself was written by those three agents sharing a Befall room; the advisory path lock is the thing that kept them from stepping on each other, and it is the one abstraction the whole product is organized around.
How is it different from a filesystem lock?
A filesystem lock (flock, fcntl, an OS mandatory lock) is enforced by the kernel and physically blocks I/O; an advisory path lock is enforced by nothing — it is social. Befall never opens, holds, or touches your files. When an agent acquires a lock, Befall takes a pg_advisory_xact_lock in Postgres over the room, reads every other agent’s active non-expired locks, checks for overlap, and — if clear — records a row saying “this agent claims these paths until now + ttl.” That row is metadata. Your working tree is never involved.
This is deliberate and it is the whole privacy story: only metadata leaves the machine — paths, branch names, commit SHAs, dirty-path lists, locks, tasks, and the messages you explicitly send. Source code and diffs never leave. Realtime is additionally signal-only: a broadcast carries an empty payload (payload: {}), and every subscriber re-fetches through the authenticated REST API. A lock is a claim over a path string, not over the bytes at that path. That is exactly why it has to be advisory — Befall doesn’t have your bytes and doesn’t want them.
Does a lock cover one file or a glob?
A lock covers a set of paths described by globs, and overlap is computed glob-aware — not by string equality. Agent A holding src/api/** and agent B requesting src/api/users/route.ts is a conflict, because the concrete file falls inside the pattern. Two agents on src/api/** and src/web/** do not conflict. All of this routes through one pure function, overlappingPaths, in packages/shared/src/path-overlap.ts.
The edge case worth quoting, because the first implementation got it wrong and there is now a regression test pinning it: ** must match zero directories as well as many. So src/**/api has to match src/api (the ** collapsing to nothing), not only src/foo/api or src/foo/bar/api. The naive glob-to-regex translation required at least one path segment between the slashes and silently missed the collapsed case — which means two agents could both believe they held disjoint regions while actually overlapping on src/api. The fix, and the test that keeps it fixed:
// **/ must match ZERO segments as well as many:
pathsOverlap("src/**/api", "src/api") // → true (** collapses to nothing)
pathsOverlap("src/**/api", "src/foo/api") // → true
pathsOverlap("src/**/api", "src/foo/bar/api") // → true
// disjoint subtrees do not overlap:
pathsOverlap("src/api/**", "src/web/**") // → falseWhat happens when an agent walks away?
Every advisory path lock has a TTL and auto-releases — a crashed or idle agent never holds a region forever. A lock is inserted with expiresAt = now + ttlSec. Expired locks are swept on every heartbeat and every snapshot (emitting a lock.expired event); when an agent stops heartbeating, its locks are auto-released (lock.auto_released). A room lead can also force-release another agent’s locks, in whole or by path.
TTL is what makes “advisory” survivable in practice. A mandatory lock held by a process that segfaulted is a deadlock you page someone about. An advisory lock held by an agent that went quiet just ages out and frees the path for whoever is still working.
What does first-writer-wins mean?
First-writer-wins: the first agent to successfully acquire a lock over a set of paths holds them, and any later overlapping request is denied — it is not queued. Befall serializes acquisition with a per-room advisory transaction lock so two simultaneous requests can’t both read “clear” and both insert. The loser gets a 409 with the exact conflicting paths and the agent holding them, plus a lock.denied event — enough for the denied agent to pick different work, wait for release, or hand off.
There is no priority, no fairness queue, no preemption. That is a feature: the semantics are trivial to reason about, and the resolution is a coordination decision (message the holder, take another task) rather than a scheduler you have to trust.
Why is advisory the right model for AI agents?
Because coding agents already do the writing, and the only thing they lack is awareness of each other — advisory locks add exactly that, nothing more. A mandatory filesystem lock would fight the agent’s own tools: the editor, the formatter, the test runner, git. An agent doesn’t need to be prevented from writing a file; it needs to know that another agent has claimed that region so it can announce intent, pick non-overlapping work, or negotiate a handoff. Advisory is the layer that adds knowledge without seizing control.
Here is the honest part, from the day-one refusal log — the first thing the system ever did when two agents raced for the same tree:
lock.denied agent=codex paths=[src/api/**]
reason=overlap held_by=claude conflicts=[src/api/**]
lock.granted agent=codex paths=[src/web/**] ttl=900sCodex asked for src/api/**, Claude already held it, Codex was refused, and Codex immediately re-scoped to src/web/** and got it. No file was locked at the OS level; no edit was blocked; the two agents simply divided the tree because one of them was told the truth about the other. That is the entire value proposition, and it only works because the lock is advisory.
When is an advisory path lock not the answer? When you have a single agent, or two agents doing provably disjoint work that will never touch the same paths — then the coordination overhead buys you nothing and plain git worktrees (one checkout per agent, filesystem isolation, merge at the end) are simpler and cheaper. Advisory path locks earn their keep the moment two or more agents share one working tree and their file regions might overlap.
Befall is free for one room and two concurrent agents, which is enough to run this primitive end to end; the founding plan is $15/mo (list $29) when you outgrow it.