Documentation
¶
Overview ¶
Package landinglane records exclusive ownership of one (repository, target) landing lane so two live sessions never drive `wb pr land` or `wb worktree merge` toward the same target branch at once.
On 2026-09-07 two live sessions landed on sneat-dev/wb main concurrently: main advanced under a published candidate four times in one session, costing a re-prepare or stranding a receipt each time. The working agreement is one landing owner per (repository, target branch); this package is the mechanical enforcement of that agreement.
A lane record is a durable claim, not a running lock: it survives the command that acquired it so a later `wb pr land` / `wb worktree merge` invocation from the same session (resume, retry, a second batch) is recognised as the same owner, while a different live session is refused and named. Mutual exclusion during the read-modify-write itself is a short-lived flock scoped to one Acquire/Release/Heartbeat call, not the whole command.
Index ¶
- Constants
- Variables
- func Heartbeat(home, repository, target, wbSessionID string, now func() time.Time) error
- func LaneID(repository, target string) string
- func Release(home, repository, target, wbSessionID string) error
- type AcquireRequest
- type ConflictError
- type CorruptRecordError
- type Owner
- type Record
Constants ¶
const DefaultStaleAfter = 30 * time.Minute
DefaultStaleAfter is how long a lane owner's heartbeat may go unrefreshed before it is treated as abandoned and taken over automatically. It is deliberately generous: `wb pr land` and `wb worktree merge` can spend real minutes waiting on CI, and a lane must not flap ownership under a slow but live run.
const DirName = "lanes"
DirName is the directory under WB's home that holds lane ownership records.
const SchemaVersion = 1
SchemaVersion guards forward-incompatible record shape changes.
Variables ¶
var ErrTakeoverReasonRequired = errors.New("--take-over-lane requires --lane-reason <text>")
TakeoverReasonRequiredError reports that --take-over-lane was requested without the --lane-reason it must carry into the record and the receipt.
Functions ¶
func Heartbeat ¶
Heartbeat refreshes the current owner's heartbeat timestamp while a landing command keeps running. It is a no-op, not an error, when the caller no longer owns the lane (another session already took it over, or it was already released) so a background ticker never needs to distinguish "lost the lane" from "nothing to do".
func LaneID ¶
LaneID deterministically names the (repository, target) landing lane. It mirrors the readable-plus-hash shape used elsewhere in WB (see worktreeMergeLaneID in internal/orchestrate) so a lane id is stable, filesystem-safe, and recognisable at a glance without importing the orchestrate package, which itself depends on this one.
func Release ¶
Release retires this session's ownership of the lane. It is a no-op when the lane is not currently held by wbSessionID, matching the working agreement that a command exiting without an active receipt (or reaching a terminal receipt status) frees the lane rather than leaving a stale claim behind for the stale-timeout to eventually clear.
Types ¶
type AcquireRequest ¶
type AcquireRequest struct {
Repository string
Target string
Self Owner
// SessionDir is the WB session registry directory used to evaluate the
// current owner's liveness (see internal/session). Required unless
// IsOwnerLive is supplied directly, e.g. by a test.
SessionDir string
// IsOwnerLive overrides the default session-registry liveness check.
// Tests inject this to avoid depending on real processes; production
// callers should leave it nil.
IsOwnerLive func(Owner) bool
StaleAfter time.Duration
Now func() time.Time
TakeOver bool
TakeoverReason string
}
AcquireRequest describes one attempt to acquire or refresh a landing lane.
type ConflictError ¶
type ConflictError struct {
Record Record
}
ConflictError reports that a different live session holds the lane. Error names the owner, its command and receipt, and the only sanctioned overrides, so a refusal is actionable without a second lookup.
func (*ConflictError) Error ¶
func (e *ConflictError) Error() string
type CorruptRecordError ¶
CorruptRecordError reports that a lane record exists but could not be parsed. Acquire fails closed on this: an unreadable record must never be treated as an absent one, because that would grant the lane with no conflict check at all — the one case a corrupt file is least allowed to cause. Only an explicit --take-over-lane --lane-reason override replaces it.
func (*CorruptRecordError) Error ¶
func (e *CorruptRecordError) Error() string
func (*CorruptRecordError) Unwrap ¶
func (e *CorruptRecordError) Unwrap() error
type Owner ¶
type Owner struct {
WBSessionID string `json:"wb_session_id"`
PID int `json:"pid"`
Runtime string `json:"runtime,omitempty"`
Model string `json:"model,omitempty"`
// Command names the verb holding the lane, e.g. "wb pr land" or
// "wb worktree merge land", so a refusal can say what the owner is doing.
Command string `json:"command,omitempty"`
// ReceiptPath is the local receipt this owner is driving, when one
// exists yet (a `wb pr land` run before its first receipt write has
// none).
ReceiptPath string `json:"receipt_path,omitempty"`
AcquiredAt time.Time `json:"acquired_at"`
HeartbeatAt time.Time `json:"heartbeat_at"`
}
Owner identifies the WB session and command driving a landing lane.
type Record ¶
type Record struct {
SchemaVersion int `json:"schema_version"`
Lane string `json:"lane"`
Repository string `json:"repository"`
Target string `json:"target"`
Owner Owner `json:"owner"`
// PriorOwner and the takeover fields are set only on the acquisition
// that took the lane from a different owner, so a reader can see why
// ownership moved without replaying history.
PriorOwner *Owner `json:"prior_owner,omitempty"`
TakenOver bool `json:"taken_over,omitempty"`
TakeoverReason string `json:"takeover_reason,omitempty"`
// TakeoverNote explains an automatic (stale-owner) takeover; it is
// distinct from TakeoverReason, which only an explicit --take-over-lane
// --reason override sets.
TakeoverNote string `json:"takeover_note,omitempty"`
}
Record is the durable state of one (repository, target) landing lane.