claude-multiacc 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1273,6 +1273,7 @@ cmd_limits() {
1273
1273
  import hashlib, json, os, sys, time, urllib.request
1274
1274
  sys.path = [sys.argv[6]] + [p for p in sys.path if p not in ('', '.')]
1275
1275
  import keychain # noqa: E402 (macOS Keychain-held logins; a no-op elsewhere)
1276
+ from audit import creds_doc_state # noqa: E402 (one rule for "can this credential work")
1276
1277
 
1277
1278
  root, threshold, quiet, url, force = sys.argv[1], int(sys.argv[2]), sys.argv[3] == '1', sys.argv[4], sys.argv[5] == '1'
1278
1279
  now = time.time()
@@ -1655,6 +1656,18 @@ for acct in manifest.get('accounts', []):
1655
1656
  locked = store == 'locked'
1656
1657
  if locked:
1657
1658
  store = None
1659
+ # "This account has an OAuth login that could still work." A Keychain-held one
1660
+ # only the Mac's own launchd probe can open counts: it is unusable HERE, not
1661
+ # broken. A provably DEAD grant (no refresh token, or a refresh token that has
1662
+ # itself expired) does not count — for that account the setup token really is
1663
+ # the only bearer left, and it must still get its one try.
1664
+ oauth_alive = locked
1665
+ if store is not None:
1666
+ try:
1667
+ oauth_alive = creds_doc_state(store.read(), now)[0] == 'ok'
1668
+ except Exception:
1669
+ oauth_alive = True # unreadable is not proof of death: do not spend the token
1670
+ has_oauth = oauth_alive
1658
1671
  if store is not None:
1659
1672
  try:
1660
1673
  c = store.read().get('claudeAiOauth', {})
@@ -1672,7 +1685,19 @@ for acct in manifest.get('accounts', []):
1672
1685
  tok = None
1673
1686
  if tok:
1674
1687
  bearer, source = tok, 'oauth'
1675
- if not bearer and os.path.isfile(tpath):
1688
+ # A setup token is a LAST RESORT and only for an account that has no OAuth
1689
+ # credential at all. Where one exists but could not be used this pass — its
1690
+ # access token expired moments ago (refresh_oauth waits REFRESH_MIN_EXPIRED to
1691
+ # prove no live session owns it), a refresh backoff, or a Keychain this session
1692
+ # cannot open — spending the token is guaranteed to earn a 403 it can never not
1693
+ # earn, and that 403 used to park the whole ACCOUNT for six hours. Telemetry then
1694
+ # froze for an account whose OAuth would have worked on the very next pass, and
1695
+ # the shim ranked the pool on hour-old readings. Waiting for the next pass costs
1696
+ # minutes; the token costs six hours and answers nothing.
1697
+ if not bearer and has_oauth:
1698
+ say(f'{aid}: oauth credential not usable this pass; NOT spending the setup '
1699
+ f'token on a usage endpoint that always refuses it — retrying next pass')
1700
+ elif not bearer and os.path.isfile(tpath):
1676
1701
  # Once this endpoint has refused THIS token file for lacking a scope, asking
1677
1702
  # again is guaranteed to fail and only spends the account's hourly budget —
1678
1703
  # which is how a permanent authorization problem disguised itself as a rate
@@ -1717,6 +1742,14 @@ for acct in manifest.get('accounts', []):
1717
1742
  except Exception:
1718
1743
  pass
1719
1744
 
1745
+ def save_prev():
1746
+ """Persist the account's telemetry state as-is (no backoff, no invented
1747
+ freshness) — used when what failed says nothing about the ACCOUNT."""
1748
+ tmp = lpath + '.tmp'
1749
+ with open(tmp, 'w') as f:
1750
+ json.dump(prev, f, indent=1)
1751
+ os.replace(tmp, lpath)
1752
+
1720
1753
  def park(wait, note):
1721
1754
  """Record a failed fetch WITHOUT inventing freshness: fetched_at is left
1722
1755
  exactly as it was, so a parked account still reads as stale everywhere."""
@@ -1724,10 +1757,7 @@ for acct in manifest.get('accounts', []):
1724
1757
  prev['backoff'] = wait
1725
1758
  prev['last_error'] = note
1726
1759
  prev['last_error_at'] = int(now)
1727
- tmp = lpath + '.tmp'
1728
- with open(tmp, 'w') as f:
1729
- json.dump(prev, f, indent=1)
1730
- os.replace(tmp, lpath)
1760
+ save_prev()
1731
1761
 
1732
1762
  if e.code == 429:
1733
1763
  # Respect Retry-After; otherwise exponential backoff capped at 30 min.
@@ -1765,8 +1795,16 @@ for acct in manifest.get('accounts', []):
1765
1795
  tdigest = token_digest(tpath)
1766
1796
  if tdigest:
1767
1797
  prev['token_scope_denied'] = tdigest
1768
- park(wait, f'HTTP {e.code} (source={source})'
1769
- + (' permanent, server said do not retry' if denied else ''))
1798
+ if scope_denied and source == 'token':
1799
+ # A refusal of the TOKEN is a fact about that credential, not about
1800
+ # the account: parking the account here also blocked the OAuth path,
1801
+ # so an account that merely missed one refresh went dark for six
1802
+ # hours. The digest above already stops this token being spent again;
1803
+ # leave the account free to try its OAuth login next pass.
1804
+ save_prev()
1805
+ else:
1806
+ park(wait, f'HTTP {e.code} (source={source})'
1807
+ + (' — permanent, server said do not retry' if denied else ''))
1770
1808
  if scope_denied and source == 'token':
1771
1809
  # The exact shape of this outage: a setup token is minted WITHOUT the
1772
1810
  # user:profile scope the usage endpoint requires, so an account whose
@@ -1775,7 +1813,8 @@ for acct in manifest.get('accounts', []):
1775
1813
  say(f'{aid}: usage endpoint refuses the setup token (HTTP {e.code} — a '
1776
1814
  f'setup token has no user:profile scope). Telemetry is DEAD for this '
1777
1815
  f'account until it has an OAuth login here: claude-accounts login {aid}. '
1778
- f'Backing off {wait}s.')
1816
+ f'This token will not be offered again; the ACCOUNT is not parked, so '
1817
+ f'an OAuth login works the moment it lands.')
1779
1818
  elif denied:
1780
1819
  say(f'{aid}: usage fetch refused for good (HTTP {e.code}, source={source}); '
1781
1820
  f'backing off {wait}s — re-login needed: claude-accounts login {aid}')
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
  from decimal import Decimal, localcontext
7
7
 
8
8
  from selector_primitives import (
9
+ DEFAULT_HEADROOM_BAND,
9
10
  SCHEMA,
10
11
  SELECTOR_VERSION,
11
12
  canonical_sha256,
@@ -125,6 +126,47 @@ def _winner_key(row: dict) -> tuple:
125
126
  row["engine"], row["account_id"], row["runner_id"], row["runner_generation"])
126
127
 
127
128
 
129
+ def _band_floor(rows: list[dict], band: Decimal) -> Decimal | None:
130
+ """The lowest headroom still counted as "as good as the best" this round.
131
+
132
+ Ranking strictly by headroom hands every task to whichever account is on top,
133
+ which is precisely how one account's limit gets burned to zero while three
134
+ others idle: the runner-up only ever wins after the leader has been spent
135
+ below it. Accounts within ``band`` points of the leader are treated as equally
136
+ good, and the tie-break below spreads work across them.
137
+ """
138
+ known = [Decimal(row["effective_headroom"]) for row in rows
139
+ if row["quota_known"] and row["effective_headroom"] is not None]
140
+ return max(known) - band if known else None
141
+
142
+
143
+ def _spread(reservation_key: str, row: dict) -> str:
144
+ """A stable pseudo-random ordinal for this row IN THIS REQUEST.
145
+
146
+ Real randomness cannot live here — the response carries a reproducible
147
+ digest of its own inputs, and two runs of the same request must agree. A
148
+ digest over (reservation key, identity) gives every task its own order over
149
+ the band while staying a pure function of the request, so a burst of
150
+ parallel launches spreads instead of stacking on one account.
151
+ """
152
+ return canonical_sha256({"reservation_key": reservation_key,
153
+ "identity": list(_row_identity(row))})
154
+
155
+
156
+ def _band_key(reservation_key: str):
157
+ """Order INSIDE the band: least-recently-used first, then the spread digest.
158
+
159
+ Headroom deliberately does not appear — inside the band it is what we are
160
+ choosing to ignore. LRU is what makes this rotate rather than merely jitter:
161
+ with three banded accounts, three sequential tasks touch all three.
162
+ """
163
+ def key(row: dict) -> tuple:
164
+ return (row["last_selected_at"] is not None, row["last_selected_at"] or "",
165
+ _spread(reservation_key, row),
166
+ row["engine"], row["account_id"], row["runner_id"], row["runner_generation"])
167
+ return key
168
+
169
+
128
170
  def _snapshot_key(row: dict) -> tuple:
129
171
  if row["identity_valid"]:
130
172
  return 0, row["runner_id"], row["runner_generation"], row["engine"], row["account_id"]
@@ -154,7 +196,17 @@ def select(request: object) -> dict:
154
196
  eligible = [row for row in rows if row["eligible"]]
155
197
  if not eligible:
156
198
  return _error("no_candidate", {"eligible_count": 0})
157
- winner = min(eligible, key=_winner_key)
199
+ band, band_text = decimal_value(request.get("headroom_band", DEFAULT_HEADROOM_BAND))
200
+ floor = _band_floor(eligible, band) if band is not None else None
201
+ if floor is None:
202
+ # No usable quota anywhere: nothing to band, so keep the plain ordering
203
+ # (which already ranks unknown-quota rows last and rotates on ties).
204
+ banded = eligible
205
+ winner = min(banded, key=_winner_key)
206
+ else:
207
+ banded = [row for row in eligible if row["quota_known"]
208
+ and Decimal(row["effective_headroom"]) >= floor]
209
+ winner = min(banded, key=_band_key(request["reservation_key"]))
158
210
  rows.sort(key=_snapshot_key)
159
211
  snapshot = canonical_sha256(rows)
160
212
  chosen = dict(zip(("runner_id", "runner_generation", "engine", "account_id"), _row_identity(winner)))
@@ -162,6 +214,8 @@ def select(request: object) -> dict:
162
214
  "schema": SCHEMA, "selector_version": SELECTOR_VERSION,
163
215
  "database_now": canonical_now, "policy": request["policy"],
164
216
  "required_engine": request.get("required_engine"), "reservation_key": request["reservation_key"],
217
+ # The band changes which account wins, so it belongs in the proof.
218
+ "headroom_band": band_text,
165
219
  "candidate_snapshot_digest": snapshot, "selected_identity": chosen}
166
220
  score_fields = ("quota_known", "weekly_pct", "session_pct", "weekly_remaining",
167
221
  "session_remaining", "effective_headroom", "last_selected_at")
@@ -169,6 +223,11 @@ def select(request: object) -> dict:
169
223
  "score_basis": {name: winner[name] for name in score_fields},
170
224
  "eligible_count": len(eligible),
171
225
  "eligible_alternative_count": alternatives,
226
+ # Observability: which accounts were considered interchangeable,
227
+ # so "why did it not pick the emptiest one" has an answer.
228
+ "headroom_band": band_text,
229
+ "band_floor": format(floor, "f") if floor is not None else None,
230
+ "band_count": len(banded),
172
231
  "candidate_snapshot_digest": snapshot,
173
232
  "selection_digest": canonical_sha256(digest_input)}
174
233
  if request["policy"] == "reviewer" and not alternatives and _row_identity(winner) == producer:
@@ -11,6 +11,13 @@ from datetime import datetime, timezone
11
11
  from decimal import Decimal, InvalidOperation
12
12
 
13
13
  SCHEMA = "claude-multiacc/pool-selection.v2"
14
+ # NOT bumped for the headroom band. This string is a BUILD FENCE, not a changelog:
15
+ # a queued task records the version that reserved its account, and the claim function
16
+ # (agent-sdk 0005_delivery_claim_functions.sql) only lets a runner advertising the same
17
+ # string claim it — while the runner's value is a hardcoded constant shipped in its
18
+ # bundle. With Macs routinely several builds behind, bumping this stops every lagging
19
+ # Mac from claiming any new work until it updates. The band is additive and optional:
20
+ # the request shape, the response shape and every consumer are unchanged.
14
21
  SELECTOR_VERSION = "2.0.1"
15
22
  TIME_RE = re.compile(
16
23
  r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-5][0-9]"
@@ -25,6 +32,10 @@ CANDIDATE_KEYS = IDENTITY_KEYS | {
25
32
  REQUEST_KEYS = {
26
33
  "schema", "database_now", "policy", "required_engine", "producer_identity",
27
34
  "excluded_identities", "candidates", "reservation_key"}
35
+ # Optional because a caller pinned to an older panel build still sends exactly
36
+ # REQUEST_KEYS; absent, the policy applies its own default band.
37
+ OPTIONAL_REQUEST_KEYS = {"headroom_band"}
38
+ DEFAULT_HEADROOM_BAND = "30"
28
39
  POLICIES = {
29
40
  "default_claude", "default_codex", "default_both", "explicit", "producer_retry", "reviewer"}
30
41
 
@@ -166,8 +177,16 @@ def request_error(request: object) -> tuple[str, dict] | None:
166
177
  return "invalid_request", {"field": "schema"}
167
178
  if request.get("schema") != SCHEMA:
168
179
  return "unsupported_schema", {"field": "schema"}
169
- if set(request) != REQUEST_KEYS:
180
+ keys = set(request)
181
+ if not REQUEST_KEYS <= keys or keys - REQUEST_KEYS - OPTIONAL_REQUEST_KEYS:
170
182
  return "invalid_request", {"field": "$"}
183
+ if "headroom_band" in request:
184
+ band, _text = decimal_value(request["headroom_band"])
185
+ # decimal_value clamps to [0,100] and rejects non-numeric shapes; a band
186
+ # the caller cannot express exactly must fail loudly rather than silently
187
+ # widen selection to the whole pool.
188
+ if band is None or isinstance(request["headroom_band"], bool):
189
+ return "invalid_request", {"field": "headroom_band"}
171
190
  try:
172
191
  timestamp(request["database_now"])
173
192
  except ValueError:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1791,17 +1791,21 @@ EOF
1791
1791
  claude-accounts limits --force 2>&1)"
1792
1792
  check "a scope-denied 403 names the missing scope" "user:profile" "$out"
1793
1793
  check "a scope-denied 403 names the ceremony that fixes it" "claude-accounts login acct-01" "$out"
1794
- check "a scope-denied 403 backs off instead of retrying" "Backing off" "$out"
1794
+ # The CREDENTIAL is refused for good (the digest below stops it being offered
1795
+ # again); the ACCOUNT is deliberately NOT parked, because parking it also blocked
1796
+ # the OAuth path and froze telemetry for hours on accounts whose login was fine.
1797
+ check "a scope-denied 403 retires the token, not the account" \
1798
+ "This token will not be offered again" "$out"
1795
1799
  python3 - "$SD/acct-01/limits.json" "$now" <<'EOF'
1796
1800
  import json, sys
1797
1801
  lim = json.load(open(sys.argv[1]))
1798
1802
  now = int(sys.argv[2])
1799
- assert lim['retry_after'] > now + 3600, lim # parked for hours, not minutes
1803
+ assert lim.get('token_scope_denied'), lim # THIS token is retired
1804
+ assert not lim.get('retry_after'), lim # ...but the account is not parked
1800
1805
  assert lim['fetched_at'] == now - 950000, lim # a FAILURE never invents freshness
1801
- assert 'HTTP 403' in lim['last_error'], lim
1802
1806
  EOF
1803
- [ $? -eq 0 ] && t_ok "a refused fetch records a long backoff and keeps its stale fetched_at" \
1804
- || t_fail "403 backoff state" "see $SD/acct-01/limits.json"
1807
+ [ $? -eq 0 ] && t_ok "a refused token is retired without parking the account, keeping its stale fetched_at" \
1808
+ || t_fail "403 credential-scoped state" "see $SD/acct-01/limits.json"
1805
1809
 
1806
1810
  # The whole point: the next scheduled pass must NOT spend another request. Before the
1807
1811
  # fix this retried every five minutes, from every machine, forever.
@@ -1809,9 +1813,12 @@ EOF
1809
1813
  out="$(CLAUDE_ACCOUNTS_ROOT="$SD" CLAUDE_MULTIACC_USAGE_URL="http://127.0.0.1:$uport/scope-denied" \
1810
1814
  claude-accounts limits 2>&1)"
1811
1815
  after="$(wc -l < "$uhits")"
1812
- [ "$before" = "$after" ] && t_ok "a parked account is not re-fetched on the next pass" \
1816
+ [ "$before" = "$after" ] && t_ok "a retired token is not re-offered on the next pass" \
1813
1817
  || t_fail "403 retry storm" "endpoint hit again ($before -> $after requests)"
1814
- check "the parked account says why it is waiting" "backing off after HTTP 403" "$out"
1818
+ # It is the CREDENTIAL that is spent, so the message names the ceremony that
1819
+ # replaces it rather than a clock the operator would otherwise sit and watch.
1820
+ check "and says what would fix it, not how long to wait" \
1821
+ "telemetry stays dark until: claude-accounts login acct-01" "$out"
1815
1822
 
1816
1823
  # Any other non-2xx backs off too — a 5xx retried every pass is the same storm.
1817
1824
  rm -f "$SD/acct-01/limits.json"
@@ -1990,6 +1997,57 @@ EOF
1990
1997
  [ "$before" = "$after" ] && t_ok "a token already refused for scope is not offered again" \
1991
1998
  || t_fail "token re-offered" "endpoint hit again ($before -> $after)"
1992
1999
  check "and the message says what would fix it" "claude-accounts login acct-01" "$out"
2000
+ # ---- an OAuth account NEVER spends its setup token here, and a token refusal
2001
+ # ---- never parks the ACCOUNT ------------------------------------------------
2002
+ # Live symptom (my-mini, 2026-08-28): acct-02/acct-05 sat 1-5 HOURS stale with
2003
+ # "backing off after HTTP 403 (source=token) — permanent" while their OAuth
2004
+ # credentials were fine. The chain: the access token had expired minutes ago, so
2005
+ # refresh_oauth declined (REFRESH_MIN_EXPIRED proves no live session owns it),
2006
+ # the probe fell through to the setup token, the endpoint refused it for scope,
2007
+ # and that parked the whole ACCOUNT for six hours — blocking the OAuth path that
2008
+ # would have worked on the very next pass. The shim then ranked the pool on
2009
+ # hour-old readings and said so ("usage telemetry is 1h old").
2010
+ SD2="$WORK/token-poison"
2011
+ mkdir -p "$SD2/acct-01" "$SD2/tmp"
2012
+ : > "$SD2/.limits-kick"
2013
+ cat > "$SD2/accounts.json" <<EOF
2014
+ { "version": 1, "server": "root@203.0.113.1", "server_root": "/root/.claude-accounts",
2015
+ "server_repo": "/root/claude-multiacc", "threshold": 90,
2016
+ "accounts": [ {"id": "acct-01", "email": "poison@test", "home": "mac",
2017
+ "added_at": "2026-08-28T00:00:00Z"} ] }
2018
+ EOF
2019
+ # An OAuth credential whose ACCESS token expired a moment ago: too recent for
2020
+ # refresh_oauth to touch, so this pass has no bearer it may use.
2021
+ printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-justexpired","refreshToken":"r","expiresAt":%s000,"refreshTokenExpiresAt":9999999999999}}' \
2022
+ "$((now - 30))" > "$SD2/acct-01/.credentials.json"
2023
+ printf 'sk-ant-oat01-PORTABLE0001\n' > "$SD2/acct-01/server.token"
2024
+ before="$(wc -l < "$uhits")"
2025
+ out="$(CLAUDE_ACCOUNTS_ROOT="$SD2" CLAUDE_MULTIACC_USAGE_URL="http://127.0.0.1:$uport/scope-denied" \
2026
+ claude-accounts limits 2>&1)"
2027
+ after="$(wc -l < "$uhits")"
2028
+ [ "$before" = "$after" ] \
2029
+ && t_ok "an OAuth account never spends its setup token on the usage endpoint" \
2030
+ || t_fail "token spent" "the endpoint was called ($before -> $after) with a token that can only 403"
2031
+ check "and it says it will simply retry" "retrying next pass" "$out"
2032
+ [ ! -f "$SD2/acct-01/limits.json" ] \
2033
+ && t_ok "no six-hour park is written for an account that merely missed a refresh" \
2034
+ || t_fail "account parked" "$(cat "$SD2/acct-01/limits.json")"
2035
+
2036
+ # A token-only account still tries once (that is the only way to learn), but the
2037
+ # refusal must park the CREDENTIAL, not the account: the digest is remembered and
2038
+ # no retry_after is written, so a later OAuth login is free to work immediately.
2039
+ rm -f "$SD2/acct-01/.credentials.json" "$SD2/acct-01/limits.json"
2040
+ CLAUDE_ACCOUNTS_ROOT="$SD2" CLAUDE_MULTIACC_USAGE_URL="http://127.0.0.1:$uport/scope-denied" \
2041
+ claude-accounts limits >/dev/null 2>&1
2042
+ python3 - "$SD2/acct-01/limits.json" <<'EOF'
2043
+ import json, sys
2044
+ d = json.load(open(sys.argv[1]))
2045
+ assert d.get('token_scope_denied'), d
2046
+ assert not d.get('retry_after'), f"the account was parked by a credential refusal: {d}"
2047
+ EOF
2048
+ [ $? -eq 0 ] && t_ok "a token scope refusal parks the credential, never the account" \
2049
+ || t_fail "token refusal parked the account" "see $SD2/acct-01/limits.json"
2050
+
1993
2051
  # Re-minting the token is a new credential, so it earns a fresh try.
1994
2052
  printf 'sk-ant-oat01-REMINTED01\n' > "$SD/acct-01/server.token"
1995
2053
  before="$(wc -l < "$uhits")"
@@ -54,7 +54,7 @@ class SelectorTests(unittest.TestCase):
54
54
  "e8ef0b4bc7b95e76231ee6da79616d92bd9251c1c991845c8c0524fcbfb6593a")
55
55
  self.assertEqual(
56
56
  response["selection_digest"],
57
- "2c9607e3dc1fde3ddf4e4469fbe48cf0e6a3c8114ef0c6b5f3c82f8ed4f78e3e")
57
+ "8b68d69b31b2d2252a16f7b83ac46860987647aa360402caa815b2a1bd468def")
58
58
 
59
59
  def test_unicode_and_active_reservation_proof(self):
60
60
  busy = {"active_expires_at": "2026-08-25T09:00:00.000001Z", "last_selected_at": None}
@@ -68,7 +68,7 @@ class SelectorTests(unittest.TestCase):
68
68
  "29e8e7d1a611f38ee0be43622102b33875e7ff75ff9024aa43d16151575071d4")
69
69
  self.assertEqual(
70
70
  response["selection_digest"],
71
- "2f018ab0e308a8465898e75a867e18d8620c03b4a49109edc1d2d590bf0d1121")
71
+ "72e7b31a00496fff0f6281f4dacd656d3a2a1fc438038e2c20c49274e1f61068")
72
72
 
73
73
  def test_both_chooses_each_provider_and_survives_provider_loss(self):
74
74
  codex = select(_request([
@@ -207,8 +207,106 @@ class SelectorTests(unittest.TestCase):
207
207
  for item in bad))
208
208
  self.assertEqual(json.loads(decimal_response.stdout)["account_id"], "z-lower")
209
209
  self.assertEqual(version, "2.0.1")
210
+ # npm package versioning, unrelated to SELECTOR_VERSION: auto-version bumps the
211
+ # next patch above what is published.
210
212
  self.assertEqual(versions, ["2.0.0", "2.0.1"])
211
213
 
212
214
 
215
+ class HeadroomBandTests(unittest.TestCase):
216
+ """Accounts within `headroom_band` points of the leader are interchangeable.
217
+
218
+ Ranking strictly by headroom sends every task to whichever account is on top
219
+ until it is spent below the runner-up, which is how one account's weekly limit
220
+ was burned to zero while three others sat idle.
221
+ """
222
+
223
+ def _pool(self, *specs) -> list[dict]:
224
+ # spec: (account_id, weekly_pct, last_selected_at)
225
+ return [_candidate(account_id=name, weekly_pct=pct, session_pct=0,
226
+ reservation_history={"active_expires_at": None,
227
+ "last_selected_at": last})
228
+ for name, pct, last in specs]
229
+
230
+ def test_band_spreads_launches_instead_of_stacking_on_the_leader(self):
231
+ # headroom 100 / 80 / 70 / 60 — the first three are within 30 of the leader.
232
+ pool = self._pool(("a", 0, None), ("b", 20, None), ("c", 30, None), ("d", 40, None))
233
+ picked = {}
234
+ for index in range(120):
235
+ response = select(_request(pool, reservation_key=f"launch-{index}"))
236
+ self.assertTrue(response["ok"])
237
+ picked[response["account_id"]] = picked.get(response["account_id"], 0) + 1
238
+ self.assertEqual(response["band_floor"], "70")
239
+ self.assertEqual(response["band_count"], 3)
240
+ self.assertEqual(set(picked), {"a", "b", "c"},
241
+ "every banded account must take work; 'd' is out of band")
242
+ # No account may take more than half: the point is spreading the burn.
243
+ self.assertLess(max(picked.values()), 60, picked)
244
+ self.assertGreater(min(picked.values()), 20, picked)
245
+
246
+ def test_same_request_always_selects_the_same_account(self):
247
+ pool = self._pool(("a", 0, None), ("b", 20, None), ("c", 30, None))
248
+ first = select(_request(pool, reservation_key="stable"))
249
+ for _ in range(5):
250
+ repeat = select(_request(pool, reservation_key="stable"))
251
+ self.assertEqual(repeat["account_id"], first["account_id"])
252
+ self.assertEqual(repeat["selection_digest"], first["selection_digest"])
253
+
254
+ def test_least_recently_used_wins_inside_the_band(self):
255
+ # The emptiest account (a) was used most recently; the band rotates past it.
256
+ pool = self._pool(("a", 0, "2026-08-25T08:59:00.000000Z"),
257
+ ("b", 20, "2026-08-25T08:00:00.000000Z"),
258
+ ("c", 30, "2026-08-25T07:00:00.000000Z"))
259
+ self.assertEqual(select(_request(pool))["account_id"], "c")
260
+ # A never-used account outranks every used one.
261
+ pool = self._pool(("a", 0, "2026-08-25T08:59:00.000000Z"), ("b", 20, None))
262
+ self.assertEqual(select(_request(pool))["account_id"], "b")
263
+
264
+ def test_out_of_band_account_never_wins_however_idle(self):
265
+ pool = self._pool(("a", 0, "2026-08-25T08:59:59.000000Z"),
266
+ ("d", 40, "2020-01-01T00:00:00.000000Z"))
267
+ response = select(_request(pool))
268
+ self.assertEqual(response["account_id"], "a")
269
+ self.assertEqual(response["band_count"], 1)
270
+
271
+ def test_zero_band_restores_strict_most_headroom(self):
272
+ pool = self._pool(("a", 0, "2026-08-25T08:59:00.000000Z"), ("b", 20, None))
273
+ self.assertEqual(select(_request(pool, headroom_band=0))["account_id"], "a")
274
+
275
+ def test_band_is_configurable_and_validated(self):
276
+ pool = self._pool(("a", 0, None), ("d", 40, None))
277
+ wide = select(_request(pool, headroom_band="45"))
278
+ self.assertEqual(wide["band_floor"], "55")
279
+ self.assertEqual(wide["band_count"], 2)
280
+ for bad in ("abc", True, [], "1e5", None):
281
+ refusal = select(_request(pool, headroom_band=bad))
282
+ self.assertFalse(refusal["ok"], bad)
283
+ self.assertEqual(refusal["error_detail"], {"field": "headroom_band"}, bad)
284
+
285
+ def test_unknown_quota_never_enters_the_band(self):
286
+ # A row with no telemetry has no headroom to compare; it must not become
287
+ # "as good as the leader" just because the band is generous.
288
+ pool = self._pool(("a", 0, None))
289
+ pool.append(_candidate(account_id="blind", weekly_pct=None, session_pct=None,
290
+ reservation_history={"active_expires_at": None,
291
+ "last_selected_at": None}))
292
+ response = select(_request(pool))
293
+ self.assertEqual(response["account_id"], "a")
294
+ self.assertEqual(response["band_count"], 1)
295
+ # ...and with NO usable telemetry anywhere, the band cannot apply at all.
296
+ blind = [_candidate(account_id=name, weekly_pct=None, session_pct=None)
297
+ for name in ("x", "y")]
298
+ response = select(_request(blind))
299
+ self.assertTrue(response["ok"])
300
+ self.assertIsNone(response["band_floor"])
301
+
302
+ def test_band_participates_in_the_selection_proof(self):
303
+ pool = self._pool(("a", 0, None), ("b", 20, None))
304
+ narrow = select(_request(pool, headroom_band="5"))
305
+ wide = select(_request(pool, headroom_band="50"))
306
+ self.assertNotEqual(narrow["selection_digest"], wide["selection_digest"])
307
+ # An omitted band is the documented default, and says so in the response.
308
+ self.assertEqual(select(_request(pool))["headroom_band"], "30")
309
+
310
+
213
311
  if __name__ == "__main__":
214
312
  unittest.main()