claude-multiacc 2.0.37 → 2.0.39
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.
- package/README.md +84 -6
- package/bin/claude +122 -6
- package/bin/claude-accounts +146 -9
- package/bin/codex +116 -7
- package/bin/codex-accounts +137 -5
- package/docs/ACCOUNT_OPERATIONS.md +34 -0
- package/docs/CODEX.md +29 -2
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/claude_reset.py +70 -35
- package/lib/codex_reset.py +31 -4
- package/lib/codex_reset_telemetry.py +4 -2
- package/lib/common.sh +19 -0
- package/lib/mcp_registry.py +1372 -0
- package/package.json +2 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +588 -0
- package/tests/test_account_seeding.py +40 -0
- package/tests/test_claude_reset.py +61 -11
- package/tests/test_codex_reset.py +43 -2
- package/tests/test_mcp_registry.py +597 -0
|
@@ -107,6 +107,46 @@ class AccountSeedingTests(unittest.TestCase):
|
|
|
107
107
|
self.assertTrue((root / "acct-01" / name).is_symlink())
|
|
108
108
|
self.assertFalse(os.path.lexists(root / "acct-99" / name))
|
|
109
109
|
|
|
110
|
+
def test_seeding_applies_the_pool_mcp_registry(self):
|
|
111
|
+
"""The registry reaches a fresh account at seed time; an account's own servers survive."""
|
|
112
|
+
registry = {"version": 1, "retired": [], "projects": {}, "mcpServers": {
|
|
113
|
+
"seeded": {"type": "stdio", "command": "echo", "args": ["hi"], "env": {"K": "v"}}}}
|
|
114
|
+
for provider, name in (("claude", ".claude.json"), ("codex", "config.toml")):
|
|
115
|
+
with self.subTest(provider=provider):
|
|
116
|
+
root = self.home / f".{provider}-accounts"
|
|
117
|
+
root.mkdir(exist_ok=True)
|
|
118
|
+
(root / "accounts.json").write_text(json.dumps({
|
|
119
|
+
"version": 1, "server": "none",
|
|
120
|
+
"accounts": [{"id": "acct-01", "email": "fixture@example.test", "home": "mac"}]}))
|
|
121
|
+
(root / "mcp-servers.json").write_text(json.dumps(registry))
|
|
122
|
+
account = root / "acct-01"
|
|
123
|
+
account.mkdir()
|
|
124
|
+
if provider == "claude":
|
|
125
|
+
(account / name).write_text(json.dumps(
|
|
126
|
+
{"mcpServers": {"own": {"type": "stdio", "command": "true", "args": [], "env": {}}}}))
|
|
127
|
+
else:
|
|
128
|
+
(account / name).write_text('model = "gpt-5"\n\n[mcp_servers.own]\ncommand = "true"\nargs = []\n')
|
|
129
|
+
for _ in range(2):
|
|
130
|
+
self.seed(provider, account)
|
|
131
|
+
text = (account / name).read_text()
|
|
132
|
+
if provider == "claude":
|
|
133
|
+
servers = json.loads(text)["mcpServers"]
|
|
134
|
+
self.assertEqual(servers["seeded"]["command"], "echo")
|
|
135
|
+
self.assertIn("own", servers)
|
|
136
|
+
else:
|
|
137
|
+
self.assertIn("[mcp_servers.seeded]", text)
|
|
138
|
+
self.assertIn('command = "echo"', text)
|
|
139
|
+
self.assertIn("[mcp_servers.own]", text)
|
|
140
|
+
self.assertIn('model = "gpt-5"', text)
|
|
141
|
+
self.assertTrue((account / ".mcp-applied").exists())
|
|
142
|
+
|
|
143
|
+
def test_seeding_without_a_registry_touches_no_config(self):
|
|
144
|
+
for provider, _, name in PROVIDERS:
|
|
145
|
+
with self.subTest(provider=provider):
|
|
146
|
+
account = self.seed(provider)
|
|
147
|
+
self.assertFalse((account / ".mcp-applied").exists())
|
|
148
|
+
self.assertFalse((self.home / f".{provider}-accounts" / "mcp-servers.json").exists())
|
|
149
|
+
|
|
110
150
|
|
|
111
151
|
if __name__ == "__main__":
|
|
112
152
|
unittest.main()
|
|
@@ -36,7 +36,7 @@ def iso(offset: int) -> str:
|
|
|
36
36
|
def usage_doc(*, session=0, weekly=50, fable=50, exhausted=(), resets_left=1,
|
|
37
37
|
use_requires_limit=False, eligible=True, reason=None, blocking=(),
|
|
38
38
|
clears=("five_hour", "seven_day", "seven_day_overage_included"),
|
|
39
|
-
usable=True, week_resets_in=3 * 86400) -> dict:
|
|
39
|
+
usable=True, week_resets_in=3 * 86400, session_resets_in=3600) -> dict:
|
|
40
40
|
"""A usage response in the live 2026-09-22 shape, cedar_ember block included."""
|
|
41
41
|
grants = [{
|
|
42
42
|
"id": GRANT, "label": "Claude Opus 5.5 launch: one usage-limit reset for Pro and Max",
|
|
@@ -50,7 +50,7 @@ def usage_doc(*, session=0, weekly=50, fable=50, exhausted=(), resets_left=1,
|
|
|
50
50
|
return {
|
|
51
51
|
"limits": [
|
|
52
52
|
{"kind": "session", "group": "session", "percent": session,
|
|
53
|
-
"resets_at": iso(
|
|
53
|
+
"resets_at": iso(session_resets_in) if session else None, "scope": None},
|
|
54
54
|
{"kind": "weekly_all", "group": "weekly", "percent": weekly,
|
|
55
55
|
"resets_at": iso(week_resets_in), "scope": None},
|
|
56
56
|
{"kind": "weekly_scoped", "group": "weekly", "percent": fable,
|
|
@@ -234,22 +234,65 @@ class ClaudeResetIntegrationTest(ClaudePoolSandbox, unittest.TestCase):
|
|
|
234
234
|
self.run_limits()
|
|
235
235
|
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
236
236
|
|
|
237
|
-
def
|
|
238
|
-
self.server.state["usage"] = usage_doc(session=
|
|
237
|
+
def test_does_not_redeem_below_the_pools_own_threshold(self) -> None:
|
|
238
|
+
self.server.state["usage"] = usage_doc(session=89, weekly=80, fable=89)
|
|
239
239
|
self.run_limits()
|
|
240
240
|
self.assertEqual(self.server.state["posts"], [])
|
|
241
241
|
self.assertFalse((self.account / ".usage-reset.json").exists())
|
|
242
242
|
self.assertEqual(self.limits_doc()["reset_credits_available"], 1)
|
|
243
243
|
|
|
244
|
-
def
|
|
245
|
-
# The
|
|
246
|
-
#
|
|
244
|
+
def test_a_week_parked_at_the_pools_threshold_is_reset(self) -> None:
|
|
245
|
+
# The live acct-15 shape (2026-09-22): weekly_all 93% parks the account for six
|
|
246
|
+
# days at the pool's 90% line; waiting for 95% left its reset unused.
|
|
247
|
+
self.server.state["usage"] = usage_doc(session=100, weekly=93, fable=64,
|
|
248
|
+
session_resets_in=5400)
|
|
249
|
+
self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
|
|
250
|
+
self.run_limits()
|
|
251
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
252
|
+
self.assertFalse((self.account / ".limited").exists())
|
|
253
|
+
|
|
254
|
+
def park(self, bucket: str, lifts_in: int, reason: str = "client-rate-limit") -> Path:
|
|
255
|
+
marker = self.account / ".limited"
|
|
256
|
+
marked = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 600))
|
|
257
|
+
marker.write_text(f"{int(time.time()) + lifts_in}\nbucket={bucket} percent=100 "
|
|
258
|
+
f"marked_at={marked} reason={reason}\n", encoding="utf-8")
|
|
259
|
+
return marker
|
|
260
|
+
|
|
261
|
+
def test_a_park_the_telemetry_cannot_see_is_reset(self) -> None:
|
|
262
|
+
# acct-13 (a client five-hour 429 while OAuth read 0% everywhere) and acct-19 (the
|
|
263
|
+
# panel's weekly park while OAuth read 24%): parked is parked.
|
|
264
|
+
for bucket, lifts_in in (("client:five_hour", 5400),
|
|
265
|
+
("weekly_all", 3 * 86400), ("panel:observed", 86400)):
|
|
266
|
+
with self.subTest(bucket=bucket):
|
|
267
|
+
self.server.state["posts"] = []
|
|
268
|
+
self.server.state["usage"] = usage_doc(session=0, weekly=19, fable=24)
|
|
269
|
+
self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
|
|
270
|
+
(self.account / ".usage-reset.json").unlink(missing_ok=True)
|
|
271
|
+
(self.account / "limits.json").unlink(missing_ok=True)
|
|
272
|
+
marker = self.park(bucket, lifts_in)
|
|
273
|
+
self.run_limits()
|
|
274
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
275
|
+
self.assertFalse(marker.exists())
|
|
276
|
+
|
|
277
|
+
def test_an_error_cooldown_is_not_a_usage_park(self) -> None:
|
|
278
|
+
self.park("session", 600, reason="error-cooldown")
|
|
279
|
+
self.server.state["usage"] = usage_doc(weekly=19, fable=24)
|
|
280
|
+
self.run_limits()
|
|
281
|
+
self.assertEqual(self.server.state["posts"], [])
|
|
282
|
+
|
|
283
|
+
def test_a_five_hour_wall_that_lifts_within_the_hour_keeps_its_reset(self) -> None:
|
|
247
284
|
self.server.state["usage"] = usage_doc(session=100, weekly=40, fable=40,
|
|
248
|
-
exhausted=["five_hour"])
|
|
285
|
+
exhausted=["five_hour"], session_resets_in=1800)
|
|
249
286
|
self.run_limits()
|
|
250
287
|
self.assertEqual(self.server.state["posts"], [])
|
|
288
|
+
self.assertIn("limit reset held", self.log())
|
|
251
289
|
self.assertTrue((self.account / ".limited").exists()) # still parked for the 5h
|
|
252
|
-
|
|
290
|
+
# ...but one that holds the account for longer is reset.
|
|
291
|
+
self.server.state["usage"] = usage_doc(session=100, weekly=40, fable=40,
|
|
292
|
+
exhausted=["five_hour"], session_resets_in=5400)
|
|
293
|
+
self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
|
|
294
|
+
self.run_limits()
|
|
295
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
253
296
|
|
|
254
297
|
def test_a_five_hour_wall_beside_a_spent_week_is_redeemed(self) -> None:
|
|
255
298
|
# The live acct-17 shape of 2026-09-22: session 100% AND Fable weekly at 95%.
|
|
@@ -260,9 +303,9 @@ class ClaudeResetIntegrationTest(ClaudePoolSandbox, unittest.TestCase):
|
|
|
260
303
|
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
261
304
|
self.assertFalse((self.account / ".limited").exists())
|
|
262
305
|
|
|
263
|
-
def
|
|
306
|
+
def test_a_week_that_refills_within_the_hour_keeps_its_reset(self) -> None:
|
|
264
307
|
self.server.state["usage"] = usage_doc(fable=100, exhausted=["seven_day_overage_included"],
|
|
265
|
-
week_resets_in=
|
|
308
|
+
week_resets_in=1800)
|
|
266
309
|
self.run_limits()
|
|
267
310
|
self.assertEqual(self.server.state["posts"], [])
|
|
268
311
|
self.assertIn("limit reset held", self.log())
|
|
@@ -272,6 +315,13 @@ class ClaudeResetIntegrationTest(ClaudePoolSandbox, unittest.TestCase):
|
|
|
272
315
|
self.run_limits(CLAUDE_MULTIACC_RESET_MIN_HORIZON="0")
|
|
273
316
|
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
274
317
|
|
|
318
|
+
def test_a_week_that_refills_in_eleven_hours_is_reset(self) -> None:
|
|
319
|
+
# acct-21 (2026-09-22): Fable at 95%, week back at 09:00 — out of service 11h.
|
|
320
|
+
self.server.state["usage"] = usage_doc(weekly=56, fable=95, week_resets_in=11 * 3600)
|
|
321
|
+
self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
|
|
322
|
+
self.run_limits()
|
|
323
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
324
|
+
|
|
275
325
|
def test_a_grant_that_needs_the_limit_waits_for_it(self) -> None:
|
|
276
326
|
self.server.state["usage"] = usage_doc(fable=97, use_requires_limit=True)
|
|
277
327
|
self.run_limits()
|
|
@@ -140,18 +140,59 @@ class CodexResetIntegrationTest(CodexPoolSandbox, unittest.TestCase):
|
|
|
140
140
|
self.run_limits()
|
|
141
141
|
self.assertEqual(len(self.server.state["posts"]), 2)
|
|
142
142
|
|
|
143
|
-
def
|
|
143
|
+
def test_does_not_redeem_below_the_pools_threshold(self) -> None:
|
|
144
144
|
now = int(time.time())
|
|
145
145
|
self.server.state["usage"] = {"rate_limit": {"allowed": True,
|
|
146
|
-
"primary_window": {"used_percent":
|
|
146
|
+
"primary_window": {"used_percent": 89, "limit_window_seconds": 18000,
|
|
147
147
|
"reset_at": now + 3600}},
|
|
148
148
|
"rate_limit_reset_credits": {"available_count": 1}}
|
|
149
|
+
self.server.state["credits"] = {"available_count": 1, "credits": []}
|
|
149
150
|
result = self.run_limits()
|
|
150
151
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
151
152
|
self.assertEqual(self.server.state["credit_gets"], 1)
|
|
152
153
|
self.assertEqual(self.server.state["posts"], [])
|
|
153
154
|
self.assertFalse((self.pool / "acct-01/.usage-reset.json").exists())
|
|
154
155
|
|
|
156
|
+
def test_an_account_parked_at_the_pools_threshold_uses_its_reset(self) -> None:
|
|
157
|
+
# Operator rule (2026-09-22): a PARKED account uses its reset. The pool parks at
|
|
158
|
+
# 90%; waiting for 95% left accounts at 90-94% idle for days with credits unused.
|
|
159
|
+
now = int(time.time())
|
|
160
|
+
self.server.state["usage"] = {"rate_limit": {"allowed": True,
|
|
161
|
+
"primary_window": {"used_percent": 92, "limit_window_seconds": 604800,
|
|
162
|
+
"reset_at": now + 4 * 86400}}}
|
|
163
|
+
self.server.state["credits"] = {"available_count": 1, "credits": []}
|
|
164
|
+
self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
|
|
165
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
166
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
167
|
+
self.assertFalse((self.pool / "acct-01/.limited").exists())
|
|
168
|
+
|
|
169
|
+
def test_a_client_reported_park_uses_its_reset(self) -> None:
|
|
170
|
+
# The rollout scan parked the account on a real 429 its telemetry does not show.
|
|
171
|
+
now = int(time.time())
|
|
172
|
+
marker = self.pool / "acct-01/.limited"
|
|
173
|
+
marker.write_text(f"{now + 3 * 86400}\nbucket=client:7d percent=100 "
|
|
174
|
+
f"marked_at=2020-01-01T00:00:00Z reason=client-rate-limit\n")
|
|
175
|
+
self.server.state["usage"] = {"rate_limit": {"allowed": True,
|
|
176
|
+
"primary_window": {"used_percent": 40, "limit_window_seconds": 604800,
|
|
177
|
+
"reset_at": now + 3 * 86400}}}
|
|
178
|
+
self.server.state["credits"] = {"available_count": 1, "credits": []}
|
|
179
|
+
self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
|
|
180
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
181
|
+
self.assertEqual(len(self.server.state["posts"]), 1)
|
|
182
|
+
self.assertFalse(marker.exists())
|
|
183
|
+
|
|
184
|
+
def test_a_park_that_lifts_within_the_hour_keeps_its_reset(self) -> None:
|
|
185
|
+
now = int(time.time())
|
|
186
|
+
(self.pool / "acct-01/.limited").write_text(
|
|
187
|
+
f"{now + 1200}\nbucket=client:5h percent=100 marked_at=2020-01-01T00:00:00Z "
|
|
188
|
+
f"reason=client-rate-limit\n")
|
|
189
|
+
self.server.state["usage"] = {"rate_limit": {"allowed": True,
|
|
190
|
+
"primary_window": {"used_percent": 40, "limit_window_seconds": 604800,
|
|
191
|
+
"reset_at": now + 3 * 86400}}}
|
|
192
|
+
self.server.state["credits"] = {"available_count": 1, "credits": []}
|
|
193
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
194
|
+
self.assertEqual(self.server.state["posts"], [])
|
|
195
|
+
|
|
155
196
|
def test_retries_a_lost_response_with_the_same_idempotency_key(self) -> None:
|
|
156
197
|
now = int(time.time())
|
|
157
198
|
self.server.state["usage"] = {"rate_limit": {"allowed": False,
|