Documentation
¶
Overview ¶
Package mrqueue provides merge request queue storage and events.
Package mrqueue provides merge request queue storage. MRs are stored locally in .beads/mq/ and deleted after merge. This avoids sync overhead for transient MR state.
Package mrqueue provides merge request queue storage and priority scoring.
MQ Priority Objective Function ¶
The merge queue uses a priority scoring function to determine processing order. Higher scores mean higher priority (process first).
## Scoring Formula
score = BaseScore
+ ConvoyAgeWeight * hoursOld(convoy) // Prevent starvation
+ PriorityWeight * (4 - priority) // P0 > P4
- min(RetryPenalty * retryCount, MaxRetryPenalty) // Prevent thrashing
+ MRAgeWeight * hoursOld(MR) // FIFO tiebreaker
## Default Weights
BaseScore: 1000.0 (keeps all scores positive) ConvoyAgeWeight: 10.0 (10 pts/hour = 240 pts/day) PriorityWeight: 100.0 (P0=+400, P4=+0) RetryPenalty: 50.0 (each retry loses 50 pts) MRAgeWeight: 1.0 (1 pt/hour, minor FIFO factor) MaxRetryPenalty: 300.0 (caps at 6 retries worth)
## Design Principles
1. Deterministic: same inputs always produce same score (uses explicit Now param)
- Convoy Starvation Prevention: older convoys escalate in priority. A 48-hour old P4 convoy will beat a fresh P0 standalone issue (+480 vs +400).
3. Priority Respect: within similar convoy ages, P0 issues beat P4 issues.
- Thrashing Prevention: MRs that repeatedly fail with conflicts get deprioritized, giving the repo state time to stabilize.
5. FIFO Fairness: within same convoy/priority/retry state, older MRs go first.
## Example Scores
Fresh P0, no convoy: 1400 (1000 + 400) Fresh P4, no convoy: 1000 (1000 + 0) Fresh P2, 24h convoy: 1440 (1000 + 200 + 240) Fresh P4, 48h convoy: 1480 (1000 + 0 + 480) P2, 24h convoy, 3 retries: 1290 (1000 + 200 + 240 - 150) P0, no convoy, 6+ retries (capped): 1100 (1000 + 400 - 300)
## Tuning
All weights are configurable via ScoreConfig. The defaults are designed so:
- A 48-hour convoy beats any standalone priority (starvation prevention)
- Priority differences dominate within same convoy
- Retry penalty is significant but capped (eventual progress guaranteed)
Index ¶
- Constants
- Variables
- func ScoreMR(input ScoreInput, config ScoreConfig) float64
- func ScoreMRWithDefaults(input ScoreInput) float64
- type BeadStatusChecker
- type Event
- type EventLogger
- func (l *EventLogger) LogEvent(event Event) error
- func (l *EventLogger) LogMergeFailed(mr *MR, reason string) error
- func (l *EventLogger) LogMergeSkipped(mr *MR, reason string) error
- func (l *EventLogger) LogMergeStarted(mr *MR) error
- func (l *EventLogger) LogMerged(mr *MR, mergeCommit string) error
- func (l *EventLogger) LogPath() string
- type EventType
- type MR
- type Queue
- func (q *Queue) Claim(id, workerID string) error
- func (q *Queue) ClearBlockedBy(mrID string) error
- func (q *Queue) Count() int
- func (q *Queue) Dir() string
- func (q *Queue) EnsureDir() error
- func (q *Queue) Get(id string) (*MR, error)
- func (q *Queue) List() ([]*MR, error)
- func (q *Queue) ListBlocked(checkStatus BeadStatusChecker) ([]*MR, error)
- func (q *Queue) ListByScore() ([]*MR, error)
- func (q *Queue) ListClaimedBy(workerID string) ([]*MR, error)
- func (q *Queue) ListReady(checkStatus BeadStatusChecker) ([]*MR, error)
- func (q *Queue) ListUnclaimed() ([]*MR, error)
- func (q *Queue) Release(id string) error
- func (q *Queue) Remove(id string) error
- func (q *Queue) SetBlockedBy(mrID, taskID string) error
- func (q *Queue) Submit(mr *MR) error
- type ScoreConfig
- type ScoreInput
Constants ¶
const ClaimStaleTimeout = 10 * time.Minute
ClaimStaleTimeout is how long before a claimed MR is considered stale. If a worker claims an MR but doesn't process it within this time, another worker can reclaim it.
Variables ¶
var ( ErrNotFound = fmt.Errorf("merge request not found") ErrAlreadyClaimed = fmt.Errorf("merge request already claimed by another worker") )
Common errors for claiming
Functions ¶
func ScoreMR ¶
func ScoreMR(input ScoreInput, config ScoreConfig) float64
ScoreMR calculates the priority score for a merge request. Higher scores mean higher priority (process first).
The scoring formula:
score = BaseScore
+ ConvoyAgeWeight * hoursOld(convoy) // Prevent convoy starvation
+ PriorityWeight * (4 - priority) // P0=+400, P4=+0
- min(RetryPenalty * retryCount, MaxRetryPenalty) // Prevent thrashing
+ MRAgeWeight * hoursOld(MR) // FIFO tiebreaker
Design principles:
- Deterministic: same inputs always produce same score
- Convoy starvation prevention: older convoys escalate in priority
- Priority respect: P0 bugs beat P4 backlog items
- Thrashing prevention: repeated failures get deprioritized
- FIFO fairness: within same convoy/priority, older MRs go first
func ScoreMRWithDefaults ¶
func ScoreMRWithDefaults(input ScoreInput) float64
ScoreMRWithDefaults is a convenience wrapper using default config.
Types ¶
type BeadStatusChecker ¶
BeadStatusChecker is a function type that checks if a bead is open. Returns true if the bead is open (not closed), false if closed or not found.
type Event ¶
type Event struct {
Timestamp time.Time `json:"timestamp"`
Type EventType `json:"type"`
MRID string `json:"mr_id"`
Branch string `json:"branch"`
Target string `json:"target"`
Worker string `json:"worker,omitempty"`
SourceIssue string `json:"source_issue,omitempty"`
Rig string `json:"rig,omitempty"`
MergeCommit string `json:"merge_commit,omitempty"` // For merged events
Reason string `json:"reason,omitempty"` // For failed/skipped events
}
Event represents a single MQ lifecycle event.
type EventLogger ¶
type EventLogger struct {
// contains filtered or unexported fields
}
EventLogger handles writing MQ events to the event log.
func NewEventLogger ¶
func NewEventLogger(beadsDir string) *EventLogger
NewEventLogger creates a new EventLogger for the given beads directory.
func NewEventLoggerFromRig ¶
func NewEventLoggerFromRig(rigPath string) *EventLogger
NewEventLoggerFromRig creates an EventLogger for the given rig path.
func (*EventLogger) LogEvent ¶
func (l *EventLogger) LogEvent(event Event) error
LogEvent writes an event to the MQ event log.
func (*EventLogger) LogMergeFailed ¶
func (l *EventLogger) LogMergeFailed(mr *MR, reason string) error
LogMergeFailed logs a merge_failed event.
func (*EventLogger) LogMergeSkipped ¶
func (l *EventLogger) LogMergeSkipped(mr *MR, reason string) error
LogMergeSkipped logs a merge_skipped event.
func (*EventLogger) LogMergeStarted ¶
func (l *EventLogger) LogMergeStarted(mr *MR) error
LogMergeStarted logs a merge_started event.
func (*EventLogger) LogMerged ¶
func (l *EventLogger) LogMerged(mr *MR, mergeCommit string) error
LogMerged logs a merged event.
func (*EventLogger) LogPath ¶
func (l *EventLogger) LogPath() string
LogPath returns the path to the event log file.
type EventType ¶
type EventType string
EventType represents the type of MQ lifecycle event.
const ( // EventMergeStarted indicates refinery began processing an MR. EventMergeStarted EventType = "merge_started" // EventMerged indicates an MR was successfully merged. EventMerged EventType = "merged" // EventMergeFailed indicates a merge failed (conflict, tests, etc.). EventMergeFailed EventType = "merge_failed" // EventMergeSkipped indicates an MR was skipped (already merged, etc.). EventMergeSkipped EventType = "merge_skipped" )
type MR ¶
type MR struct {
ID string `json:"id"`
Branch string `json:"branch"` // Source branch (e.g., "polecat/nux")
Target string `json:"target"` // Target branch (e.g., "main")
SourceIssue string `json:"source_issue"` // The work item being merged
Worker string `json:"worker"` // Who did the work
Rig string `json:"rig"` // Which rig
Title string `json:"title"` // MR title
Priority int `json:"priority"` // Priority (lower = higher priority)
CreatedAt time.Time `json:"created_at"`
AgentBead string `json:"agent_bead,omitempty"` // Agent bead ID that created this MR (for traceability)
// Priority scoring fields
RetryCount int `json:"retry_count,omitempty"` // Conflict retry count for priority penalty
ConvoyID string `json:"convoy_id,omitempty"` // Parent convoy ID if part of a convoy
ConvoyCreatedAt *time.Time `json:"convoy_created_at,omitempty"` // Convoy creation time for starvation prevention
// Claiming fields for parallel refinery workers
ClaimedBy string `json:"claimed_by,omitempty"` // Worker ID that claimed this MR
ClaimedAt *time.Time `json:"claimed_at,omitempty"` // When the MR was claimed
// Blocking fields for non-blocking delegation
BlockedBy string `json:"blocked_by,omitempty"` // Task ID that blocks this MR (e.g., conflict resolution task)
}
MR represents a merge request in the queue.
func (*MR) IsBlocked ¶
func (mr *MR) IsBlocked(checkStatus func(beadID string) (isOpen bool, err error)) (bool, string, error)
IsBlocked checks if an MR is blocked by a task that is still open. If blocked, returns true and the blocking task ID. checkStatus is a function that checks if a bead is still open.
type Queue ¶
type Queue struct {
// contains filtered or unexported fields
}
Queue manages the MR storage.
func NewFromWorkdir ¶
NewFromWorkdir creates a queue by finding the rig root from a working directory.
func (*Queue) Claim ¶
Claim attempts to claim an MR for processing by a specific worker. Returns nil if successful, ErrAlreadyClaimed if another worker has it, or ErrNotFound if the MR doesn't exist. Uses atomic file operations to prevent race conditions.
func (*Queue) ClearBlockedBy ¶
ClearBlockedBy removes the blocking task from an MR.
func (*Queue) List ¶
List returns all pending MRs, sorted by priority then creation time. Deprecated: Use ListByScore for priority-aware ordering.
func (*Queue) ListBlocked ¶
func (q *Queue) ListBlocked(checkStatus BeadStatusChecker) ([]*MR, error)
ListBlocked returns MRs that are blocked by open tasks. Useful for reporting/monitoring.
func (*Queue) ListByScore ¶
ListByScore returns all pending MRs sorted by priority score (highest first). Uses the ScoreMR function which considers:
- Convoy age (prevents starvation)
- Issue priority (P0-P4)
- Retry count (prevents thrashing)
- MR age (FIFO tiebreaker)
func (*Queue) ListClaimedBy ¶
ListClaimedBy returns MRs claimed by a specific worker.
func (*Queue) ListReady ¶
func (q *Queue) ListReady(checkStatus BeadStatusChecker) ([]*MR, error)
ListReady returns MRs that are ready for processing: - Not claimed by another worker (or claim is stale) - Not blocked by an open task Sorted by priority score (highest first). The checkStatus function is used to check if blocking tasks are still open.
func (*Queue) ListUnclaimed ¶
ListUnclaimed returns MRs that are not claimed or have stale claims. Sorted by priority then creation time.
func (*Queue) Release ¶
Release releases a claimed MR back to the queue. Called when processing fails and the MR should be retried.
func (*Queue) SetBlockedBy ¶
SetBlockedBy marks an MR as blocked by a task (e.g., conflict resolution). When the blocking task closes, the MR becomes ready for processing again.
type ScoreConfig ¶
type ScoreConfig struct {
// BaseScore is the starting score before applying factors.
// Default: 1000 (keeps all scores positive)
BaseScore float64
// ConvoyAgeWeight is points added per hour of convoy age.
// Older convoys get priority to prevent starvation.
// Default: 10.0 (10 pts/hour = 240 pts/day)
ConvoyAgeWeight float64
// PriorityWeight is multiplied by (4 - priority) so P0 gets most points.
// P0 adds 4*weight, P1 adds 3*weight, ..., P4 adds 0*weight.
// Default: 100.0 (P0 gets +400, P4 gets +0)
PriorityWeight float64
// RetryPenalty is subtracted per retry attempt to prevent thrashing.
// MRs that keep failing get deprioritized, giving repo state time to stabilize.
// Default: 50.0 (each retry loses 50 pts)
RetryPenalty float64
// MRAgeWeight is points added per hour since MR submission.
// Minor factor for FIFO ordering within same priority/convoy.
// Default: 1.0 (1 pt/hour)
MRAgeWeight float64
// MaxRetryPenalty caps the total retry penalty to prevent permanent deprioritization.
// Default: 300.0 (after 6 retries, penalty is capped)
MaxRetryPenalty float64
}
ScoreConfig contains tunable weights for MR priority scoring. All weights are designed so higher scores = higher priority (process first).
func DefaultScoreConfig ¶
func DefaultScoreConfig() ScoreConfig
DefaultScoreConfig returns sensible defaults for MR scoring.
type ScoreInput ¶
type ScoreInput struct {
// Priority is the issue priority (0=P0/critical, 4=P4/backlog).
Priority int
// MRCreatedAt is when the MR was submitted to the queue.
MRCreatedAt time.Time
// ConvoyCreatedAt is when the convoy was created.
// Nil if MR is not part of a convoy (standalone work).
ConvoyCreatedAt *time.Time
// RetryCount is how many times this MR has been retried after conflicts.
// 0 = first attempt.
RetryCount int
// Now is the current time (for deterministic testing).
// If zero, time.Now() is used.
Now time.Time
}
ScoreInput contains the data needed to score an MR. This struct decouples scoring from the MR struct, allowing the caller to provide convoy age from external lookups.