Documentation
¶
Overview ¶
Package lease persists an EXPLICIT, OWNED engagement seat for satelle work items (sty_8426b9c0). Engagement is no longer only derived from committed status: a lease is acquired at the START of an engaging transition (before gate+dispatch) so concurrent engages and edit/commit gates see the seat immediately. The DOT shape still defines WHICH states are engaging; the lease records WHO holds the seat NOW.
Atomicity: store.Open uses MaxOpenConns(1) + _txlock=immediate, so a check-and-insert inside one BEGIN IMMEDIATE transaction is single-flight across concurrent satelle processes.
Index ¶
- Constants
- Variables
- func EffectiveActivity(l Lease, now time.Time) (label string, index, total int, elapsed time.Duration, ok bool)
- func EffectiveInFlight(l Lease, now time.Time) bool
- func IsStale(l Lease, now time.Time) bool
- func Migrate(db *sql.DB) error
- func ResolveOwner() string
- type AcquireOpts
- type Lease
- type Outcome
- type Store
- func (s *Store) Acquire(ctx context.Context, itemID, kind, owner, state string, occupiesStorySeat bool) (Lease, Outcome, *Lease, error)
- func (s *Store) AcquireWith(ctx context.Context, opts AcquireOpts) (Lease, Outcome, *Lease, error)
- func (s *Store) AnyActive(ctx context.Context) (bool, error)
- func (s *Store) ClearInFlight(ctx context.Context, itemID string) error
- func (s *Store) Confirm(ctx context.Context, itemID, committedState string) error
- func (s *Store) ForceRelease(ctx context.Context, itemID string) error
- func (s *Store) Get(ctx context.Context, itemID string) (Lease, error)
- func (s *Store) Heartbeat(ctx context.Context, itemID, owner string) error
- func (s *Store) List(ctx context.Context) ([]Lease, error)
- func (s *Store) Reap(ctx context.Context) ([]Lease, error)
- func (s *Store) Release(ctx context.Context, itemID, owner string) error
- func (s *Store) RequestStop(ctx context.Context, itemID, requester, reason string) error
- func (s *Store) SetActivity(ctx context.Context, itemID, label string, index, total int) error
- func (s *Store) SetHeartbeat(ctx context.Context, itemID string, at time.Time) error
- func (s *Store) SetInFlightAt(ctx context.Context, itemID string, at time.Time) error
Constants ¶
const HeartbeatTTL = 30 * time.Minute
HeartbeatTTL is how long a lease may sit without a heartbeat before a colliding acquire treats it as stale (process death). Must exceed a long synchronous dispatch (reviewer + planner can run many minutes).
const InFlightTTL = HeartbeatTTL
InFlightTTL is a backstop for in_flight aging when the transitioning pid is still alive (or unknown). The primary self-heal for a dead mid-transition process is InFlightPid + pidAlive (sty_bf797fa9 AC3) — not this TTL. Must exceed a long multi-gate dispatch (same order as HeartbeatTTL).
Variables ¶
var ErrNotFound = errors.New("lease: not found")
ErrNotFound is returned when Get misses.
var ErrNotOwner = errors.New("lease: not owner")
ErrNotOwner is returned when Release is attempted by a non-owner.
var ErrTreeConflict = errors.New("one engagement per working tree")
ErrTreeConflict is the sentinel a caller wraps when it turns OutcomeTreeConflict into a refusal, so the reason survives as errors.Is rather than as message text.
var PidAlive = defaultPidAlive
pidAlive reports whether pid is a live process on this host (best-effort). Overridable in tests via PidAlive.
Functions ¶
func EffectiveActivity ¶ added in v0.0.408
func EffectiveActivity(l Lease, now time.Time) (label string, index, total int, elapsed time.Duration, ok bool)
EffectiveActivity returns the queryable progress for a live in-flight lease. ok is false when the transition is not effectively in flight or no activity has been stamped (sty_598a8e1b). Domain calculation lives in lease — same rule as EffectiveInFlight.
func EffectiveInFlight ¶ added in v0.0.403
EffectiveInFlight reports whether the lease is mid-transition for gate purposes. Consumers of "is a transition running right now" must use this rather than raw InFlight (architecture: domain calculation lives in lease).
Order of checks:
- raw InFlight false → not live
- InFlightPid > 0 and dead on this host → not live (primary AC3 self-heal: the process that owned the transition is gone; Owner stays pid-less)
- zero InFlightAt → not live (residue after upgrade without backfill)
- InFlightAt older than InFlightTTL → not live (backstop when pid unknown or still alive but stuck)
func IsStale ¶ added in v0.0.215
IsStale reports whether a lease may be stolen or ignored for engagement: the heartbeat is older than HeartbeatTTL, or the owner embeds a dead local pid. Exported so gate/seat surfaces share one definition with Acquire steal paths.
func ResolveOwner ¶
func ResolveOwner() string
ResolveOwner returns the process owner identity for acquire/release. SATELLE_OWNER (trimmed) wins for multi-agent isolation. The default is local@hostname (no pid) so sequential CLI invocations on one machine share ownership — a pid-scoped default would break park/done release after the engage process exits (sty_8426b9c0). Process death is detected via HeartbeatTTL (and optional local@host:pid forms if an operator sets that).
Types ¶
type AcquireOpts ¶ added in v0.0.427
type AcquireOpts struct {
ItemID string
Kind string
Owner string
State string
// StorySeat gates the story-seat rule. Tasks take a lease (so "is anything
// engaged" sees them) but never occupy the story seat.
StorySeat bool
// SeatKey is the arbitration key. Callers pass the item's own id for single
// occupancy, or a shared key (e.g. a parent id) to admit co-holders. Empty
// is treated as a singleton keyed by ItemID.
SeatKey string
// Worktree is the git toplevel this engagement is anchored to. Empty opts
// out of tree arbitration (git unavailable / non-repo caller).
Worktree string
}
AcquireOpts are the arbitration inputs for one seat claim. The lease package is MECHANISM: it applies the key and tree rules it is handed and never learns the name of the concurrency mode that chose them (configuration over code).
type Lease ¶
type Lease struct {
ItemID string
Kind string
StorySeat bool
// SeatKey is the ARBITRATION key this lease claimed the story seat under
// (sty_c098dc2d). Two live seat holders coexist only when their keys are
// equal and non-empty. The caller chooses the key policy — the item's own id
// (single occupancy) or its parent id (siblings of one epic). Empty on
// pre-migration rows, which therefore behave as singletons.
SeatKey string
// Worktree is the git working tree this engagement was anchored to (git
// toplevel at acquire). Two live seat leases never share a non-empty tree —
// they would attribute each other's edits. Empty when git was unavailable or
// on pre-migration rows; empty never participates in tree arbitration.
Worktree string
Owner string
State string
AcquiredAt time.Time
HeartbeatAt time.Time
StopRequestedBy string
StopReason string
InFlight bool // transition in progress (gate+dispatch not yet committed)
// InFlightAt is when in_flight was last set to 1. Zero when not in flight
// or on pre-migration rows. Used by EffectiveInFlight (not HeartbeatAt —
// hooks refresh heartbeat while the seat is held).
InFlightAt time.Time
// InFlightPid is the OS pid of the process that marked in_flight (the
// transitioning CLI). 0 when unknown / not in flight. Distinct from Owner
// (which is deliberately pid-less local@host so sequential CLI invocations
// share the seat). When non-zero and dead on this host, EffectiveInFlight
// is false immediately — the process that owned the transition is gone.
InFlightPid int
// Activity* describe the current gate/phase while in_flight (sty_598a8e1b).
// Empty when not in flight or when progress has not been stamped yet.
ActivityLabel string
ActivityIndex int
ActivityTotal int
ActivityAt time.Time
}
Lease is one engagement seat row.
func PickForWorktree ¶ added in v0.0.427
PickForWorktree selects the lease anchored to worktree. This is the domain answer to "which of these leases is THIS session's" — needed because the default owner is pid-less local@host (see ResolveOwner), so two sibling leases held from one host are indistinguishable by owner alone. A heartbeat routed by owner would refresh whichever row came first and let the actually worked sibling go stale at TTL (sty_c098dc2d).
ok is false for an empty worktree or no match — callers then fall back to their own rule rather than guessing.
type Outcome ¶
type Outcome int
Outcome of Acquire.
const ( // OutcomeAcquired — this call inserted a new lease. OutcomeAcquired Outcome = iota // OutcomeAlreadyHeld — same owner holds a settled lease; sequential step may continue. OutcomeAlreadyHeld // OutcomeInFlight — same owner already has a transition in flight (AC3 no-op). OutcomeInFlight // OutcomeStolen — a stale holder was replaced by this call. OutcomeStolen // OutcomeConflict — another live owner holds this item or the story seat. OutcomeConflict // OutcomeTreeConflict — another live seat lease is anchored to the same // git working tree. Refused regardless of seat key: two engagements sharing // one tree would attribute each other's edits (sty_c098dc2d). OutcomeTreeConflict )
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps engagement_lease operations against a shared sqlite handle.
func (*Store) Acquire ¶
func (s *Store) Acquire(ctx context.Context, itemID, kind, owner, state string, occupiesStorySeat bool) (Lease, Outcome, *Lease, error)
Acquire claims the seat for itemID under single occupancy — the seat key is the item's own id, so any other live seat holder conflicts. This is the unkeyed, tree-unanchored form; AcquireWith is the full surface.
func (*Store) AcquireWith ¶ added in v0.0.427
AcquireWith claims the seat for opts.ItemID. Returns the resulting lease, the outcome, and — on OutcomeConflict / OutcomeTreeConflict — the blocking holder. check-and-insert runs in ONE immediate transaction.
Arbitration for a NEW story-seat claim, in order:
- KEY — a live seat lease conflicts unless its key equals this claim's key and both are non-empty. Single occupancy is simply "every key is the item's own id"; there is no mode branch here.
- TREE — among claims the key ADMITS, a live seat lease on the same non-empty worktree refuses (OutcomeTreeConflict): two engagements sharing one tree would attribute each other's edits.
Key before tree because the two refusals give different advice, and only the key answer survives moving: a claim the key rejects is not helped by a fresh working tree, so telling it to make one would be wrong. It also means single occupancy never reaches the tree branch, leaving its refusal exactly as it was.
Stale holders are stolen (deleted) as they are encountered, exactly as before.
func (*Store) AnyActive ¶
AnyActive reports whether any engagement lease row exists. Callers that gate edit/commit must prefer List + a performing-state predicate (see evaluateSeat in the hook package): a bare row is not necessarily live engagement (sty_1738f973 — orphaned / stale / non-performing rows must not open the gate).
func (*Store) ClearInFlight ¶
ClearInFlight drops the in_flight flag without changing state — used when a sequential step aborts (gate reject) so a retry can re-acquire the step.
func (*Store) Confirm ¶
Confirm settles an in-flight transition: records committed engaging state and clears in_flight. Called after store.Update succeeds for an engaging target.
func (*Store) ForceRelease ¶
ForceRelease deletes the lease for itemID regardless of owner. Used when a workflow transition into terminal/park commits — the DOT exit is the authority that the seat is free (sequential CLI pids must not leave a stuck seat because the engage process already exited).
func (*Store) Heartbeat ¶
Heartbeat refreshes heartbeat_at for the owner's lease (optional long-dispatch).
func (*Store) List ¶ added in v0.0.215
List returns every engagement lease row (including stale / in-flight). The lease package is pure mechanism — callers decide which rows count as live engagement (staleness via IsStale; performing-state via workflow shape).
func (*Store) Reap ¶ added in v0.0.215
Reap deletes rows whose heartbeat/pid marks them stale. Returns the reaped leases. Opportunistic housekeeping for seat-list honesty; gate correctness does not depend on Reap — IsStale already excludes those rows from engagement.
func (*Store) Release ¶
Release frees the seat for itemID. Only the owner may release mid-flight (AC4/AC5). For workflow exit (terminal/park) use ForceRelease.
func (*Store) RequestStop ¶
RequestStop annotates the holder row with a stop request. Never deletes. Any principal may request; arbitration happens at the next step edge (AC5).
func (*Store) SetActivity ¶ added in v0.0.408
SetActivity stamps the current gate/phase on an in-flight lease so seat can report progress without the dispatching terminal (sty_598a8e1b). Best-effort: callers may ignore the error — observability must not fail a transition.
func (*Store) SetHeartbeat ¶ added in v0.0.215
SetHeartbeat overwrites heartbeat_at for itemID. Used by regression tests and seat diagnostics to simulate a frozen heartbeat after process death (sty_1738f973 AC5 kill-simulation). Not an operator happy-path verb.