Route on the credit budget, not just the rate-limit windows - #162
Conversation
The credit budget (spend_control) is a third exhaustion axis on its own reset clock, independent of the 5h/7d rate-limit windows. Admission now skips an account whose budget is reached, and cold-session placement weights the budget's remaining capacity alongside the windows so a nearly-spent budget deprioritises the account. Both fail open on a missing or lapsed reset, and an account with no spend control is unaffected.
Covers: a spent budget is skipped by admission; a nearly-spent budget is deprioritised in cold placement; a no-spend-control account routes by its rate-limit windows alone; and the last-candidate preservation still holds when every budget is spent.
decideStickyBreak judged only rate-limit windows, so a session pinned to an account whose credit budget was spent stayed pinned and kept being refused — the same blind spot admission and cold placement now close. Extract spendControlExhaustedResetAt as the single definition of a spent budget and use it in both exhaustedQuotaResetAt and decideStickyBreak, so admission and migration can never disagree. Ordering is unchanged: the unknown and stale guards still run first, and a stale, malformed, or missing reading retains the pin.
Covers: a reached budget migrates the pin with the exhausted reason shape; a stale, malformed, missing, or lapsed reading retains it; the reached boolean is trusted over a spent-looking percentage; a no-spend-control account decides exactly as today; and admission and migration agree on the same spent state.
|
Merged as Including migration was the right call and the argument for it is the one I would have made: half a wired axis is worse than none, because a cold session refusing to start on an account a warm session stays pinned to reads as a routing bug rather than as a budget. A follow-up would have shipped that disagreement for however long the follow-up took. I verified the two properties that could cause an outage rather than the ones that prove the feature, since those are where the cost is asymmetric. Filtering never removes the last path. Disabling the guard reddens two, including Fail-open on a lapsed reset. Making a past reset count as exhausted reddens both the admission and the migration test. Worth noting those are two independent tests over one definition — which is the payoff of Using the credit's own One thing I would still like eventually, though not here: |
The follow-up deferred from #160. That PR made the credit budget visible; nothing acted on it.
The blind spot
spend_control.individual_limitis a third exhaustion axis on its own clock. The account I probed sat at 0% on its 5-hour window and 51% on its 7-day window while 20% through a credit budget resetting a month out. Admission and placement both weighed rate-limit windows only, so:Both present as unexplained failures on an account the sidebar shows as healthy.
What this wires
Admission —
exhaustedQuotaResetAtnow treats a reached credit budget with a future reset as exhausted, on the same fail-open contract as windows: a missing or lapsed reset never blocks.Cold placement —
candidateWeightfolds the credit reading into the existing min-over-axes viasustainableWindowWeight, using the credit's ownresetsAtrather than assuming a 5h/7d duration. That matters: this window resets on a month clock, so borrowing a window duration would compute pressure against the wrong denominator.Warm-pin migration —
decideStickyBreaktreats a reached credit budget as confirmed exhaustion.That third piece was not in the original scope, and the implementer flagged it as a known gap rather than leaving it silent. It belongs here rather than in a follow-up: wiring admission and placement but not migration produces an inconsistent contract, where a cold session refuses to start on a spend-exhausted account while a warm session stays pinned to it and keeps being refused. A half-wired axis is worse than an unwired one, because the halves disagree and the disagreement reads as a routing bug.
decideStickyBreakalready migrates on 401/403, but a spent budget presents as neither, so the pin would have survived until the rate-limit window independently exhausted.One definition of "spent"
Admission and migration both call a single exported
spendControlExhaustedResetAt. Two definitions of exhaustion would be the same defect one layer down, so a test asserts they agree on the same quota object:isQuotaExhausted(spent)is true anddecideStickyBreak(spent)migrates.spend_control.reachedis authoritative rather than inferring from percentages — a test pins that the boolean wins over a spent-looking percentage.Invariants held
applyAdmissionQuotaSafetystill returns the current selection when nothing is retained. A test reddens if that branch is disabled, so every-account-spent degrades to today's behaviour rather than to zero candidates.decideStickyBreakis unchanged: unknown guard first, stale before killswitch (the comment there explains why a stale snapshot must not judge an account the killswitch would consider below floor), windows, then credit.Verification
I re-ran the migration mutation by hand against the merged branch to confirm it is not self-satisfying.
Not modelled
quotaSnapshotPassesPolicy— the threshold-based minimum-remaining filter behindgetUsableFallbackAccounts— still judges windows only. Credit has no configured threshold, so that is a separate policy axis rather than exhaustion, and wiring it would mean inventing a default the operator never set.Gates from the repo root: build, format:check (157 files), lint (157), types, test — 147 core + 1391 OpenCode (+19) + 14 Pi.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Makes routing act on the credit budget (
spend_control), which was previously surfaced but never enforced. Admission, cold placement, and warm-pin migration now treat a reached budget as a third exhaustion axis on its own reset clock.spendControlExhaustedResetAtsignal, so admission and migration can never disagree on what "spent" means.reachedboolean is authoritative; a spent-looking percentage alone never exhausts.Written for commit c4f2b71. Summary will update on new commits.