Expand description
Cross-process session-ownership leases — at most one live process
may drive turns for a given agent instance_id at a time.
Sibling of super::store::Registry but deliberately a separate
file tree (<registry_root>/leases/<instance_id>.{lease.json,lock,epoch}),
not new fields on super::schema::NamedAgentRecordV1:
Registry::upsert is a blind read-modify-write (cross-process
safety comes only from atomic rename, no compare-and-swap — see
that module’s doc comment), which is fine for the registry’s own
last-writer-wins semantics but wrong for a lock, where “I thought
nobody held it” must be an atomic check.
claim/renew/release/verify each hold a real, blocking,
per-instance_id OS advisory lock (flock on Unix, LockFileEx on
Windows — same primitive family already used for single-instance
enforcement in backend::base::MuxLock and
agentmux-launcher::second_instance, just scoped per-lease instead of
globally) for the short duration of the call, not across calls. An
earlier draft tried to get away with pure atomic-rename/create-if-not-
exists tricks and no real lock; it had two real, independently-
discovered races (a create-then-write window a concurrent claimer could
observe as “corrupt” and evict, and a check-then-write TOCTOU in renew
that could clobber a new owner) — a real critical section is simpler
AND correct, where the lock-free version was neither.
Since SPEC_AGENT_SINGLE_LIVE_INSTANCE_2026_09_24.md Phase 1 the key is
the agent UID and interactive (persistent) panes hold a lease for as long
as their CLI process lives, not just the subprocess controller per turn.
That spec’s review added three rules this module enforces:
- Fail closed on an unreadable lease. A lease file that doesn’t parse
proves nothing about its holder. It stays held until nobody has
rewritten it for a full TTL (a live holder renews — rewrites — it every
RENEW_INTERVAL_MS), rather than being evicted on sight. - Epochs survive release. Every change of owner takes the next value
of a per-key counter (
<instance_id>.epoch) that, like the lock file, is never deleted — so a release followed by a new claim can never reuse an older holder’s epoch. - A dead holder is reclaimed at once. A lease records its holder’s host, pid and process start time; on the same host a pid that is gone, or now belongs to a different process, frees the lease immediately instead of after the TTL.
Root cause + design context:
docs/retro/RETRO_DEV_BUILD_SHARED_AGENT_SESSION_COLLISION_2026_07_29.md,
docs/analysis/ANALYSIS_MULTI_AGENT_SESSION_AND_WORKDIR_ISOLATION_2026-07-29.md,
docs/specs/SPEC_AGENT_SINGLE_LIVE_INSTANCE_2026_09_24.md.
Structs§
- Claimant
Info - What a claimant records about itself beyond its boot id and block — shown to whoever it later refuses. Empty strings are allowed (tests, callers that don’t know); they render as “unknown”.
- Critical
Section 🔒 - A held per-
instance_idOS advisory lock. Blocking (waits rather than failing immediately) — critical sections here are a handful of small file ops, so worst-case wait time is negligible. Released automatically on drop: closing the underlying handle releases bothflock(Unix) andLockFileEx(Windows) advisory locks. - Lease
- A held lease. Cheap to
Clone(a fewStrings + aPathBuf) — the renew task, the pre-turn fence check and the eventual release each need their own owned copy. - Lease
File 🔒 - Lease
Holder - Who holds a lease, as recorded in its file. Fields added by the single-live-instance spec are empty/zero on a lease written by an older build; readers must treat those as “unknown”, never as a value.
- Lease
Store
Enums§
- Lease
Error - OnDisk 🔒
- The lease file as found on disk.
Constants§
- LEASE_
TTL_ MS - 3x the renew interval — tolerates two missed ticks (disk hiccup, GC pause) before a lease is considered abandoned and reclaimable.
- RENEW_
INTERVAL_ MS - How often a held lease must be renewed. Drives its own dedicated renewal
task (
subprocess/host_spawn.rsper turn;backend::agent_admissionfor a persistent pane’s process) on this interval — not piggybacked on anything else.
Functions§
- holder_
is_ 🔒dead - True only when the holder is provably gone: it recorded this host, and its pid is not running or now belongs to a process that started at a different time. Anything we can’t prove (a lease from another host — the shared root on a network drive — or one written before hosts were recorded) is treated as alive.
- is_
this_ 🔒claim - Is
filethe leaseleasewas granted — same owner, same block, same claim? An emptyclaim_id(older build) matches. - lease_
bytes 🔒 - lock_
exclusive 🔒 - process_
start_ 🔒s - Start time (seconds since the Unix epoch) of process
pidon this host,Noneif no such process is running. - same_
block 🔒 - Does
filebelong toblock_id? A lease written before block ids were compared (empty) matches any block of its owner. - this_
host 🔒 - This host’s name as recorded in a lease — lowercased and trimmed so two processes on one host always agree.
- try_
lock_ 🔒exclusive - Non-blocking
lock_exclusive:Ok(true)when the lock was taken,Ok(false)when another holder has it. For waiters that must stay cancellable (poll this instead of blocking inlock_exclusive).