ADR-075: Solver-Fair Queue Dispatch Order

Field Value
Decision ID ADR-075
Initiative unsorry — volunteer-scale orchestration / queue fairness
Proposed By unsorry maintainers
Date 2026-06-20
Status Accepted

Context

The queued-proof dispatcher (dispatch_queue in swarm/agent.sh, ADR-058) turns queued/prove/<goal>/<agent-id>-<hex> branches into governed, auto-merge PRs. It iterates git for-each-ref refs/remotes/origin/queued/prove, which returns refs lexically by name — i.e. by goal. The submission governor (ADR-058) admits only a few branches per pass and Gate A is the throughput ceiling (~1 h/proof, ≤20 in flight), so dispatch effectively walks the goal alphabet a slice at a time. There is no per-solver fairness in that walk.

The consequence is contributor starvation by lexical position. Observed in practice: @ohdearquant and @chat-bit-01 between them hold ~1,882 g…-prefixed machine-named goals (geud-*, gzmod-*) that sort to the very front; @ruvnet’s 135 re-routed proofs (the #1904 “ruv sledgehammer” recovery) cluster in h…/s… (103 of 135 start with s) and sat at dispatch ranks 1850–2002 of 2003 — 0 in the first 600, 0 in-flight, 0 landed — while @ohdearquant’s drained steadily. @ruvnet’s backlog was verified distinct, unproved, dup-free and self-contained: it was not blocked on admission, dedup, or a build problem, only on where its goal names fall in the alphabet.

The ADR-054 open-PR quota caps the top of the queue per author (≤20 simultaneous open prove PRs) but says nothing about drain order, so a late-sorting or lower-volume contributor starves indefinitely behind a high-volume early-alphabet one.

WH(Y) Decision Statement

In the context of a governed dispatcher that opens only a few queued proof PRs per pass and walks branches in lexical (goal-name) order,

facing the choice between leaving dispatch lexical (a high-volume, early-alphabet contributor starves everyone whose goals sort later — the observed @ruvnet case), keying fairness on the branch commit author (wrong: a re-routed branch is authored by the operator who ran reroute_stranded, not by the solver, so author-keying would misattribute @ruvnet’s 132 re-routes to the operator), or resolving solver provenance per branch inside the dispatch loop (correct but reads every branch’s index file each pass — too slow for ~2,900 branches),

we decided for a per-solver round-robin ordering. fair_dispatch_order groups the queued branches by solver — read from the authoritative queue board (docs/queue.json on origin/main, which already resolves the solver≜ / git-author provenance, ADR-066) — and emits one branch per solver per round (max-min fairness), with two graceful fallbacks: a branch not yet on the board (pushed since the last board refresh) is bucketed by its agent-id token, and an unreadable board degrades to the prior lexical order. Dedup (ADR-064/071), the governor (ADR-058) and Gate A are unchanged — this only reorders the candidate list,

and neglected a per-branch provenance resolve in the loop (too slow), an author-keyed grouping (misattributes re-routes), and a new persistent fair-queue scheduler (an operational dependency ADR-004 deliberately avoids — the board already carries the solver mapping, so no new state is introduced),

to achieve steady drain for every active solver: a small backlog (@ruvnet’s 135) clears promptly instead of waiting behind ~1,900 earlier-sorting goals, while a large backlog drains at the same per-round rate rather than monopolising every slot,

accepting that fairness is best-effort ordering only — it depends on a generated artifact that can lag (a just-pushed branch is bucketed by agent-id token until the next board refresh), the agent-id token is a coarse key that is gameable in principle (bounded in practice by the ADR-054 per-author open-PR cap), and round-robin equalises per-round throughput across solvers rather than weighting by backlog size — a deliberate anti-starvation choice, not proportional sharing.

Consequences