lease

package
v0.0.437 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 8 Imported by: 0

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

View Source
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).

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

View Source
var ErrNotFound = errors.New("lease: not found")

ErrNotFound is returned when Get misses.

View Source
var ErrNotOwner = errors.New("lease: not owner")

ErrNotOwner is returned when Release is attempted by a non-owner.

View Source
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.

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

func EffectiveInFlight(l Lease, now time.Time) bool

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:

  1. raw InFlight false → not live
  2. 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)
  3. zero InFlightAt → not live (residue after upgrade without backfill)
  4. InFlightAt older than InFlightTTL → not live (backstop when pid unknown or still alive but stuck)

func IsStale added in v0.0.215

func IsStale(l Lease, now time.Time) bool

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 Migrate

func Migrate(db *sql.DB) error

Migrate creates the engagement_lease table. Idempotent.

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

func PickForWorktree(leases []Lease, worktree string) (Lease, bool)

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 New

func New(db *sql.DB) *Store

New returns a Store bound to db.

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

func (s *Store) AcquireWith(ctx context.Context, opts AcquireOpts) (Lease, Outcome, *Lease, error)

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:

  1. 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.
  2. 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

func (s *Store) AnyActive(ctx context.Context) (bool, error)

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

func (s *Store) ClearInFlight(ctx context.Context, itemID string) error

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

func (s *Store) Confirm(ctx context.Context, itemID, committedState string) error

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

func (s *Store) ForceRelease(ctx context.Context, itemID string) error

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) Get

func (s *Store) Get(ctx context.Context, itemID string) (Lease, error)

Get returns the lease for itemID or ErrNotFound.

func (*Store) Heartbeat

func (s *Store) Heartbeat(ctx context.Context, itemID, owner string) error

Heartbeat refreshes heartbeat_at for the owner's lease (optional long-dispatch).

func (*Store) List added in v0.0.215

func (s *Store) List(ctx context.Context) ([]Lease, error)

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

func (s *Store) Reap(ctx context.Context) ([]Lease, error)

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

func (s *Store) Release(ctx context.Context, itemID, owner string) error

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

func (s *Store) RequestStop(ctx context.Context, itemID, requester, reason string) error

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

func (s *Store) SetActivity(ctx context.Context, itemID, label string, index, total int) error

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

func (s *Store) SetHeartbeat(ctx context.Context, itemID string, at time.Time) error

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.

func (*Store) SetInFlightAt added in v0.0.403

func (s *Store) SetInFlightAt(ctx context.Context, itemID string, at time.Time) error

SetInFlightAt overwrites in_flight_at for itemID (and forces in_flight=1). Test/diagnostic seam for stuck-flag aging (sty_bf797fa9). Not an operator path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL