claude-multiacc 2.0.49 → 2.0.50

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.
Files changed (34) hide show
  1. package/docs/UNIFIED_SELECTOR.md +4 -3
  2. package/docs/VERIFICATION.md +1 -1
  3. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  4. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  5. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  6. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  7. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  8. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  9. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  16. package/lib/selector_policy.py +3 -12
  17. package/package.json +3 -2
  18. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  19. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  20. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  21. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  22. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  23. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  24. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  25. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  26. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  27. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  28. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  29. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  30. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  31. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  32. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  33. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  34. package/tests/test_selector_spread.py +65 -0
@@ -57,9 +57,10 @@ weekly limits").
57
57
  is what makes a burst of parallel launches fan out instead of stacking on one
58
58
  account, and it is deterministic: the same request always yields the same winner.
59
59
 
60
- With no usable quota anywhere the band cannot apply (`band_floor` is `null`) and the
61
- plain ordering stands: known quota ahead of unknown, then highest effective headroom,
62
- then least-recently-selected.
60
+ With no usable quota anywhere the band cannot apply (`band_floor` is `null`). Every
61
+ eligible account is then a peer: least-recently-selected wins, followed by the same
62
+ request-key spread. Equal histories must not concentrate launches on the first provider,
63
+ account or runner identity merely because telemetry is unknown.
63
64
 
64
65
  Success returns the concrete engine/account/runner generation, normalized score basis
65
66
  (which still reports `effective_headroom` = min(weekly, session) for observability),
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```bash
6
6
  tests/run-tests.sh # sandboxed compatibility tests, no quota
7
- python3 tests/test_selector.py # unified selector contract
7
+ python3 -m unittest discover -s tests -p 'test_selector*.py' # unified selector contract and distribution
8
8
  python3 -m unittest discover -s tests -p 'test_autoresume*.py' # watchers and fallback probes (also in run-tests.sh)
9
9
  npm run test:commands # packed npm commands; run npm install --ignore-scripts first
10
10
  claude-accounts verify # real matrix: `claude -p "reply OK"` per authed account
@@ -119,14 +119,6 @@ def _apply_policy(request: dict, rows: list[dict]) -> tuple[tuple, int]:
119
119
  return producer, alternatives
120
120
 
121
121
 
122
- def _winner_key(row: dict) -> tuple:
123
- descending_headroom = Decimal(row["effective_headroom"] or "-1").copy_negate()
124
- return (
125
- not row["quota_known"], descending_headroom,
126
- row["last_selected_at"] is not None, row["last_selected_at"] or "",
127
- row["engine"], row["account_id"], row["runner_id"], row["runner_generation"])
128
-
129
-
130
122
  def _session_gate(rows: list[dict], gate: Decimal | None) -> list[dict]:
131
123
  """The FIRST cut: accounts whose 5h session bucket still has room.
132
124
 
@@ -229,13 +221,12 @@ def select(request: object) -> dict:
229
221
  ranked = healthy or known
230
222
  floor = _band_floor(ranked, band) if band is not None else None
231
223
  if floor is None:
232
- # No usable quota anywhere: nothing to band, so keep the plain ordering
233
- # (which already ranks unknown-quota rows last and rotates on ties).
224
+ # No usable quota anywhere: every eligible row is an unknown peer.
225
+ # Keep LRU and the request spread instead of preferring a fixed identity.
234
226
  banded = eligible
235
- winner = min(banded, key=_winner_key)
236
227
  else:
237
228
  banded = [row for row in ranked if Decimal(row["weekly_remaining"]) >= floor]
238
- winner = min(banded, key=_band_key(request["reservation_key"]))
229
+ winner = min(banded, key=_band_key(request["reservation_key"]))
239
230
  rows.sort(key=_snapshot_key)
240
231
  snapshot = canonical_sha256(rows)
241
232
  chosen = dict(zip(("runner_id", "runner_generation", "engine", "account_id"), _row_identity(winner)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.49",
3
+ "version": "2.0.50",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,7 +56,8 @@
56
56
  "scripts": {
57
57
  "postinstall": "node scripts/postinstall.mjs",
58
58
  "test": "npm run test:core && npm run test:seeding && npm run test:commands",
59
- "test:core": "bash tests/run-tests.sh && python3 tests/test_selector.py && python3 tests/test_shim_path.py",
59
+ "test:core": "bash tests/run-tests.sh && npm run test:selector && python3 tests/test_shim_path.py",
60
+ "test:selector": "python3 -m unittest discover -s tests -p 'test_selector*.py'",
60
61
  "test:seeding": "python3 tests/test_account_seeding.py && python3 tests/test_mcp_registry.py && npm run test:codex-settings",
61
62
  "test:codex-settings": "python3 tests/test_codex_config_edit.py && python3 tests/test_codex_settings.py",
62
63
  "test:commands": "python3 tests/test_packaged_commands.py"
@@ -0,0 +1,65 @@
1
+ """Unknown quota must not turn a launch burst into a fixed-identity preference."""
2
+
3
+ from collections import Counter
4
+ import unittest
5
+
6
+ from test_selector import _candidate, _request
7
+ from selector_policy import select
8
+
9
+
10
+ class UnknownQuotaSpreadTests(unittest.TestCase):
11
+ def test_unknown_and_partial_quota_spread_across_accounts_and_providers(self):
12
+ for weekly, session in ((None, None), (0, None), (None, 0)):
13
+ with self.subTest(weekly=weekly, session=session):
14
+ pool = [_candidate(engine=engine, account_id=f"acct-{index:02d}",
15
+ weekly_pct=weekly, session_pct=session)
16
+ for engine in ("claude", "codex") for index in range(4)]
17
+ counts = Counter()
18
+ for index in range(120):
19
+ response = select(_request(pool, reservation_key=f"burst-{index}"))
20
+ self.assertTrue(response["ok"])
21
+ self.assertIsNone(response["band_floor"])
22
+ self.assertEqual(response["session_ok_count"], 0)
23
+ counts[(response["engine"], response["account_id"])] += 1
24
+ self.assertEqual(len(counts), len(pool), counts)
25
+ self.assertLess(max(counts.values()), 40, counts)
26
+
27
+ def test_equal_history_spreads_same_account_id_across_runner_identities(self):
28
+ pool = [_candidate(runner_id=index, engine="codex", account_id="acct-01",
29
+ weekly_pct=None, session_pct=None,
30
+ reservation_history={"active_expires_at": None,
31
+ "last_selected_at": "2026-08-25T08:00:00Z"})
32
+ for index in range(1, 9)]
33
+ counts = Counter(select(_request(pool, reservation_key=f"burst-{index}"))["runner_id"]
34
+ for index in range(120))
35
+ self.assertEqual(len(counts), len(pool), counts)
36
+ self.assertLess(max(counts.values()), 40, counts)
37
+
38
+ def test_unknown_quota_preserves_lru_and_active_reservation_exclusion(self):
39
+ pool = [_candidate(account_id=name, weekly_pct=None, session_pct=None,
40
+ reservation_history={"active_expires_at": expiry,
41
+ "last_selected_at": last})
42
+ for name, expiry, last in (
43
+ ("reserved", "2026-08-25T09:10:00Z", None),
44
+ ("unused", None, None),
45
+ ("old", None, "2026-08-25T08:00:00Z"),
46
+ ("recent", None, "2026-08-25T08:50:00Z"))]
47
+ for index in range(20):
48
+ response = select(_request(pool, reservation_key=f"burst-{index}"))
49
+ self.assertEqual(response["account_id"], "unused")
50
+ self.assertEqual(response["eligible_count"], 3)
51
+ for index in range(20):
52
+ response = select(_request(pool[2:], reservation_key=f"burst-{index}"))
53
+ self.assertEqual(response["account_id"], "old")
54
+
55
+ def test_unknown_quota_remains_deterministic_and_input_order_independent(self):
56
+ pool = [_candidate(account_id=f"acct-{index:02d}", weekly_pct=None, session_pct=None)
57
+ for index in range(8)]
58
+ response = select(_request(pool, reservation_key="stable"))
59
+ self.assertEqual(response, select(_request(pool, reservation_key="stable")))
60
+ reversed_response = select(_request(list(reversed(pool)), reservation_key="stable"))
61
+ self.assertEqual(response["account_id"], reversed_response["account_id"])
62
+
63
+
64
+ if __name__ == "__main__":
65
+ unittest.main()