claude-multiacc 2.0.5 → 2.0.6

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.
@@ -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.6",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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()