Skip to main content

Module leases

Module leases 

Source
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§

ClaimantInfo
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”.
CriticalSection 🔒
A held per-instance_id OS 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 both flock (Unix) and LockFileEx (Windows) advisory locks.
Lease
A held lease. Cheap to Clone (a few Strings + a PathBuf) — the renew task, the pre-turn fence check and the eventual release each need their own owned copy.
LeaseFile 🔒
LeaseHolder
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.
LeaseStore

Enums§

LeaseError
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.rs per turn; backend::agent_admission for 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 file the lease lease was granted — same owner, same block, same claim? An empty claim_id (older build) matches.
lease_bytes 🔒
lock_exclusive 🔒
process_start_s 🔒
Start time (seconds since the Unix epoch) of process pid on this host, None if no such process is running.
same_block 🔒
Does file belong to block_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 in lock_exclusive).