claude-multiacc 2.0.36 → 2.0.38

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 CHANGED
@@ -177,17 +177,17 @@ buckets keep being tracked and the ≥90% rule keeps working. Unparseable entrie
177
177
  skipped, an all-legacy payload falls back to `five_hour`/`seven_day`, and a payload the
178
178
  code cannot read at all degrades that one account (fail open), never the run.
179
179
 
180
- **Limit resets are redeemed automatically, but only when one earns its keep.** Claude
180
+ **A parked account gets its limit reset automatically.** Claude
181
181
  Code 2.1.280 added `/limit-reset`: an account can hold a *grant* (the Opus 5.5 launch gave
182
182
  every Pro/Max account one reset, usable until 2026-10-22) that refills its five-hour and
183
183
  weekly limits at once while the weekly reset day stays put. The same usage call carries the
184
184
  grant's status (`?cedar_ember=1&skip_spend=1`, Claude Code's own read, so it costs nothing
185
185
  against the endpoint's hourly budget), and the limits pass claims it with
186
- `POST /api/organizations/<org>/reset_rate_limits` when a **weekly** limit the grant refills
187
- is ≥ 95% used or reported exhausted — the codex rule, narrowed because a Claude reset is one
188
- irreversible shot. A full five-hour window alone never spends it (it refills in hours and the
189
- pool picks another account), and it is held while the account's own week refills in under
190
- 24 h (`CLAUDE_MULTIACC_RESET_MIN_HORIZON`, seconds). The claim carries a request id derived
186
+ `POST /api/organizations/<org>/reset_rate_limits` whenever the pool has **parked** the account on a
187
+ window the grant refills — a bucket at/over the pool's own threshold (90%), a limit the server
188
+ reports exhausted, or an active park (a client 429, or the app-robot panel's own park) — and holds
189
+ it only when every such park lifts on its own within the hour (`CLAUDE_MULTIACC_RESET_MIN_HORIZON`,
190
+ seconds). The claim carries a request id derived
191
191
  from the account, grant, remaining count and week, persisted before the POST, so a lost
192
192
  response is retried with the same id and fleet peers converge on one claim; a claim whose
193
193
  answer never came back is settled by the grant's own remaining count dropping, and a claim
@@ -81,10 +81,10 @@ USAGE
81
81
  or runner daemon distributes.
82
82
  claude-accounts verify [--quick] auth matrix; full mode runs `-p "reply OK"` per account
83
83
  claude-accounts limits [--quiet] [--force] [--json]
84
- refresh usage buckets, apply >=90% markers. When a WEEKLY limit is at least
85
- 95% used or exhausted, automatically redeems the account's Claude Code limit
86
- reset (/limit-reset) with an idempotent request — never for a five-hour
87
- window alone, and not while the week refills on its own within 24h.
84
+ refresh usage buckets, apply >=90% markers. When the pool has parked an
85
+ account on a window its Claude Code limit reset (/limit-reset) refills,
86
+ redeems it with an idempotent request — unless every park lifts on its own
87
+ within the hour.
88
88
  Auto-refreshes long-expired
89
89
  OAuth access tokens via the refresh-token grant (rotated credential is
90
90
  persisted), so idle accounts keep fresh telemetry and stay selectable.
@@ -105,8 +105,8 @@ ENV
105
105
  CLAUDE_SHIM_RETRY=0 disable -p auto-retry
106
106
  CLAUDE_MULTIACC_DISABLE=1 bypass the shim entirely
107
107
  CLAUDE_MULTIACC_AUTO_RESET=0 disable automatic limit-reset redemption
108
- CLAUDE_MULTIACC_RESET_MIN_HORIZON seconds before the natural weekly reset
109
- inside which a reset is held (default 86400)
108
+ CLAUDE_MULTIACC_RESET_MIN_HORIZON a reset is held while every park lifts on
109
+ its own within this many seconds (default 3600)
110
110
  EOF
111
111
  }
112
112
 
@@ -2408,7 +2408,7 @@ for acct in manifest.get('accounts', []):
2408
2408
  if source == 'oauth':
2409
2409
  try:
2410
2410
  reset_result, reset_view = claude_reset.refresh_reset_credits(
2411
- d, aid, data, url, headers, say, int(now))
2411
+ d, aid, data, url, headers, say, int(now), threshold)
2412
2412
  except Exception as e:
2413
2413
  say(f'{aid}: limit reset automation failed unexpectedly ({type(e).__name__}); '
2414
2414
  f'failing open')
@@ -2481,6 +2481,16 @@ for acct in manifest.get('accounts', []):
2481
2481
  # human's /limit-reset) enters it here, from the grant's own count dropping; this
2482
2482
  # GET already came after it, so its numbers stand.
2483
2483
  observed = claude_reset.observed_reset(prev, reset_status, now)
2484
+ if observed:
2485
+ # Stamped when SEEN, which is later than the reset itself: every park
2486
+ # written in between counts as "before" it. A weekly window cannot
2487
+ # re-exhaust in that gap, but a 5h one can — so the 5h refill only counts
2488
+ # when this very (post-reset) reading MEASURED the session window; a silent
2489
+ # one proves nothing (the acct-14 shape). One second back, so a park this
2490
+ # pass writes can never tie with the stamp.
2491
+ kept = [t for t in observed[1]
2492
+ if session_measured or t not in claude_reset.SESSION_LIMITS]
2493
+ observed = (observed[0] - 1, kept) if kept else None
2484
2494
  reset_record = claude_reset.latest(claude_reset.record_from(prev, now), observed,
2485
2495
  claude_reset.local_record(d, now))
2486
2496
  if observed and reset_record is observed:
@@ -89,8 +89,10 @@ USAGE
89
89
  `codex exec` per account
90
90
  codex-accounts limits [--quiet] [--force] [--json]
91
91
  refresh usage windows from the ChatGPT usage endpoint, apply >=90% markers.
92
- When a fresh bucket is at least 95% used, automatically redeems an earned
93
- usage-limit reset if the account has one, using an idempotent request.
92
+ When the pool has parked the account (a window at/over the threshold, a
93
+ finished limit, or a client-reported limit that lifts more than an hour from
94
+ now), automatically redeems an earned usage-limit reset if the account has
95
+ one, using an idempotent request.
94
96
  Auto-refreshes long-expired access tokens via the OAuth refresh-token grant
95
97
  (rotated credential is persisted), so idle accounts keep fresh telemetry and
96
98
  stay selectable. Skips accounts fetched in the last 45s and honors
@@ -1620,7 +1622,8 @@ for acct in manifest.get('accounts', []):
1620
1622
  if sessionp is None and live:
1621
1623
  sessionp = 0
1622
1624
  try:
1623
- reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say, int(now))
1625
+ reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say,
1626
+ int(now), threshold)
1624
1627
  except Exception as e:
1625
1628
  say(f'{aid}: usage reset automation failed unexpectedly ({type(e).__name__}); failing open')
1626
1629
  reset_result = {'status': 'error'}
package/docs/CODEX.md CHANGED
@@ -44,8 +44,9 @@ Codex-specific notes:
44
44
  own public client id) and the rotated credential is persisted 0600. Overrides for
45
45
  tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
46
46
  `CODEX_MULTIACC_USAGE_URL`.
47
- - **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
48
- when the backend reports the limit finished. The limits refresher checks the reset
47
+ - **Earned usage resets are redeemed automatically for a parked account:** a bucket
48
+ at/over the pool's exclusion threshold (90%), a limit the backend reports finished, or
49
+ an active usage park (a client-reported 429) that lifts more than an hour from now. The limits refresher checks the reset
49
50
  credits belonging to that same account, uses the soonest-expiring available credit,
50
51
  and writes a per-account/window idempotency key before redemption so a lost response
51
52
  or simultaneous fleet poll cannot spend a second reset. Set
@@ -6,19 +6,15 @@ names (``clears``: five_hour, seven_day, seven_day_overage_included, ...) while
6
6
  weekly reset day stays where it was. The first grant arrived with the Opus 5.5
7
7
  launch on 2026-09-22 — one reset per Pro/Max account, usable until 2026-10-22.
8
8
 
9
- This is the Claude half of lib/codex_reset.py: the scheduled limits pass redeems a
10
- reset when a limit the grant refills is at least 95% used, or when the server reports
11
- that limit exhausted. Two rules narrow that for Claude, because a Claude reset is ONE
12
- irreversible shot that refills the WEEK (support.claude.com "What is a limit reset?",
13
- 2026-09-22: "Once you use it, you can't undo it"; "Your weekly limits still reset on
14
- their usual day and time"):
15
-
16
- * Only a WEEKLY limit (anything but ``five_hour``) triggers it. A full five-hour window
17
- refills on its own within hours and the pool simply picks another account; spending
18
- the week's refill on it throws nearly all of the reset away.
19
- * It is held while the account's own weekly reset is less than
20
- ``CLAUDE_MULTIACC_RESET_MIN_HORIZON`` seconds away (default 24h): the week is about
21
- to refill for free, and the grant is worth days of work later in its validity.
9
+ This is the Claude half of lib/codex_reset.py, and it follows the operator's rule:
10
+ an account the pool has PARKED on a window the grant refills gets its reset, so it goes
11
+ back to work instead of sitting out the wall. Parked means exactly what the shim means
12
+ by it — a bucket the grant clears at or above the pool's own exclusion threshold (90%
13
+ by default, the manifest's `threshold`), a limit the server reports exhausted, or an
14
+ active `.limited` park (a 429 the client received, or the app-robot panel's park) on
15
+ such a window. The one hold: when every such park lifts on its own within
16
+ ``CLAUDE_MULTIACC_RESET_MIN_HORIZON`` seconds (default 1h), the reset is kept — a
17
+ reset is one irreversible shot (support.claude.com "What is a limit reset?").
22
18
 
23
19
  What differs from codex beyond that is only the transport:
24
20
 
@@ -61,7 +57,7 @@ RESET_AT_USED_PERCENT = 95
61
57
  REDEEM_COOLDOWN_SECONDS = 900
62
58
  # Limit types that refill on their own within hours; never a reason to spend a reset.
63
59
  SESSION_LIMITS = frozenset({"five_hour"})
64
- DEFAULT_MIN_HORIZON_SECONDS = 86400
60
+ DEFAULT_MIN_HORIZON_SECONDS = 3600
65
61
  # A claim whose response was lost is retried with the same request id while it is
66
62
  # younger than this, and dropped afterwards unless the status block proved it landed.
67
63
  PENDING_TTL_SECONDS = 3600
@@ -296,13 +292,51 @@ def _percent(grant: dict, usage: dict, limit_type: str) -> int | None:
296
292
  return None
297
293
 
298
294
 
299
- def reset_trigger(status: dict, grant: dict, usage: dict) -> tuple[list[str], list[str]]:
300
- """(exhausted WEEKLY limits the grant refills, weekly limits it refills at >=95%)."""
301
- clears = set(grant["clears"]) - SESSION_LIMITS
302
- finished = sorted(clears & set(status["exhausted"]))
303
- near = sorted(limit for limit in clears
304
- if (_percent(grant, usage, limit) or 0) >= RESET_AT_USED_PERCENT)
305
- return finished, near
295
+ def _lift_epoch(limit_type: str, status: dict, usage: dict) -> float | None:
296
+ """When a parked window refills on its own: the session window's reset, or the week's."""
297
+ if limit_type in SESSION_LIMITS:
298
+ for limit in (usage.get("limits") or []) if isinstance(usage, dict) else []:
299
+ if isinstance(limit, dict) and str(limit.get("kind") or "").startswith("session"):
300
+ found = _epoch(limit.get("resets_at"))
301
+ if found is not None:
302
+ return found
303
+ bucket = usage.get("five_hour") if isinstance(usage, dict) else None
304
+ return _epoch(bucket.get("resets_at")) if isinstance(bucket, dict) else None
305
+ return _epoch(status.get("weekly_resets_at"))
306
+
307
+
308
+ def _active_park(account_dir: str, now: float) -> tuple[str | None, float] | None:
309
+ """(limit type, lift epoch) of the account's `.limited` park, when it is a USAGE park
310
+ still in force and not already lifted by a reset on record; else None."""
311
+ try:
312
+ with open(os.path.join(account_dir, ".limited"), errors="replace") as handle:
313
+ text = handle.read(4096)
314
+ until = int(text.splitlines()[0])
315
+ except (OSError, ValueError, IndexError):
316
+ return None
317
+ if until <= now or not ("reason=limits" in text or "reason=client-rate-limit" in text):
318
+ return None
319
+ record = latest(record_from(_load(os.path.join(account_dir, "limits.json")), now),
320
+ local_record(account_dir, now))
321
+ if marker_superseded(text, record):
322
+ return None
323
+ bucket = next((part[len("bucket="):] for part in text.split()
324
+ if part.startswith("bucket=")), "")
325
+ return bucket_limit_type(bucket), float(until)
326
+
327
+
328
+ def parked_on(status: dict, grant: dict, usage: dict, account_dir: str, threshold: int,
329
+ now: float) -> list[tuple[str, float | None]]:
330
+ """Every (limit type, when it lifts on its own) the pool parks this account on,
331
+ among the windows this grant refills."""
332
+ clears = set(grant["clears"])
333
+ parks = [(t, _lift_epoch(t, status, usage)) for t in sorted(clears & set(status["exhausted"]))]
334
+ parks += [(t, _lift_epoch(t, status, usage)) for t in sorted(clears)
335
+ if (_percent(grant, usage, t) or 0) >= threshold]
336
+ park = _active_park(account_dir, now)
337
+ if park and (park[0] == "*" or park[0] in clears):
338
+ parks.append(park)
339
+ return parks
306
340
 
307
341
 
308
342
  # ---- the reset record the whole fleet honours -------------------------------------
@@ -525,7 +559,8 @@ def _complete(state_path: str, pending: dict, outcome: str, now: int, cleared=No
525
559
 
526
560
 
527
561
  def try_auto_redeem(account_dir: str, account_id: str, usage: dict, status: dict | None,
528
- usage_url: str, headers: dict, say, now: int | None = None) -> dict:
562
+ usage_url: str, headers: dict, say, now: int | None = None,
563
+ threshold: int = RESET_AT_USED_PERCENT) -> dict:
529
564
  """Redeem one reset when policy says so and return a non-secret outcome document."""
530
565
  current = int(now if now is not None else time.time())
531
566
  if not auto_reset_enabled():
@@ -558,7 +593,7 @@ def try_auto_redeem(account_dir: str, account_id: str, usage: dict, status: dict
558
593
  return {"status": "cooldown", "outcome": state.get("outcome")}
559
594
  if not status["eligible"]:
560
595
  reason = status["ineligible_reason"] or "unknown"
561
- if reason in _REQUEST_SIDE_REASONS and _usage_peak(usage) >= RESET_AT_USED_PERCENT:
596
+ if reason in _REQUEST_SIDE_REASONS and _usage_peak(usage) >= threshold:
562
597
  # Not a fact about the account: the server would not answer THIS client. On the
563
598
  # one machine that can redeem, that silently turns redemption off fleet-wide.
564
599
  say(f"{account_id}: limit-reset status refused for this client ({reason}; "
@@ -571,20 +606,20 @@ def try_auto_redeem(account_dir: str, account_id: str, usage: dict, status: dict
571
606
  grant = _usable_grant(status, current)
572
607
  if grant is None:
573
608
  return {"status": "no_credit"}
574
- finished, near = reset_trigger(status, grant, usage)
575
- if not finished and not near:
609
+ parks = parked_on(status, grant, usage, account_dir, threshold, current)
610
+ if not parks:
576
611
  return {"status": "not_eligible"}
612
+ parked = sorted({t for t, _ in parks})
577
613
  if grant["blocking"]:
578
614
  # The client's own rule: a limit this grant does not refill is exhausted too,
579
615
  # so refilling the others would not let the account work.
580
616
  return {"status": "not_eligible", "reason": "blocked",
581
617
  "blocking": grant["blocking"]}
582
- if grant["use_requires_limit"] and not finished:
618
+ if grant["use_requires_limit"] and not set(grant["clears"]) & set(status["exhausted"]):
583
619
  return {"status": "not_eligible", "reason": "waiting_for_limit"}
584
- natural = _epoch(status["weekly_resets_at"])
585
- if natural is not None and natural - current < min_horizon():
586
- say(f"{account_id}: limit reset held — {', '.join(finished or near)} is at its "
587
- f"limit but the week refills on its own at {status['weekly_resets_at']}")
620
+ if all(lift is not None and lift - current < min_horizon() for _, lift in parks):
621
+ say(f"{account_id}: limit reset held — parked on {', '.join(parked)}, which "
622
+ f"lifts on its own within {min_horizon() // 60} minutes")
588
623
  return {"status": "not_eligible", "reason": "natural_reset_soon"}
589
624
  org = organization_uuid(account_dir)
590
625
  url = claim_url(usage_url, org) if org else None
@@ -600,7 +635,7 @@ def try_auto_redeem(account_dir: str, account_id: str, usage: dict, status: dict
600
635
  "request_id": request_id(org, account_uuid(account_dir) or account_id,
601
636
  grant, status),
602
637
  "resets_left_before": grant["resets_left"], "started_at": current,
603
- "clears": grant["clears"], "trigger": finished or near}
638
+ "clears": grant["clears"], "trigger": parked}
604
639
  _write(state_path, state)
605
640
  payload = {"program": PROGRAM, "grant_id": state["grant_id"],
606
641
  "request_id": state["request_id"]}
@@ -631,8 +666,7 @@ def try_auto_redeem(account_dir: str, account_id: str, usage: dict, status: dict
631
666
  left = _count(response.get("resets_left"))
632
667
  if outcome in {"reset", "already_used"}:
633
668
  said = "redeemed" if outcome == "reset" else "already redeemed"
634
- say(f"{account_id}: limit reset {said} automatically "
635
- f"({', '.join(finished or near)} at limit)")
669
+ say(f"{account_id}: limit reset {said} automatically (parked on {', '.join(parked)})")
636
670
  return _complete(state_path, state, outcome, current, cleared, left)
637
671
  if outcome == "unavailable":
638
672
  say(f"{account_id}: limit reset not confirmed (server unavailable); retry is idempotent")
@@ -648,7 +682,8 @@ def try_auto_redeem(account_dir: str, account_id: str, usage: dict, status: dict
648
682
  return {"status": outcome}
649
683
 
650
684
 
651
- def refresh_reset_credits(account_dir, account_id, usage, usage_url, headers, say, now):
685
+ def refresh_reset_credits(account_dir, account_id, usage, usage_url, headers, say, now,
686
+ threshold=RESET_AT_USED_PERCENT):
652
687
  """Read the allowance from this pass's usage response, redeem if due, report both.
653
688
 
654
689
  The view reported after a claim is the server's own post-claim count (the claim
@@ -658,7 +693,7 @@ def refresh_reset_credits(account_dir, account_id, usage, usage_url, headers, sa
658
693
  status = parse_status(usage)
659
694
  view = credits_view(status, now)
660
695
  result = try_auto_redeem(account_dir, account_id, usage, status, usage_url, headers,
661
- say, now)
696
+ say, now, threshold)
662
697
  if result.get("status") == "redeemed":
663
698
  left = result.get("resets_left")
664
699
  if view and type(left) is int:
@@ -22,6 +22,23 @@ from codex_reset_windows import reset_windows
22
22
  RESET_AT_USED_PERCENT = 95
23
23
  REDEEM_COOLDOWN_SECONDS = 900
24
24
  STATE_FILE = ".usage-reset.json"
25
+ # A park that lifts on its own within this long keeps the credit (operator rule,
26
+ # 2026-09-22: a PARKED account uses its reset; only a sub-hour wall is not worth one).
27
+ PARK_HOLD_SECONDS = 3600
28
+
29
+
30
+ def _active_park(account_dir: str, now: int) -> int | None:
31
+ """The epoch a USAGE park on this account lifts at (a >=threshold pass, or a 429 the
32
+ client received), or None. Error cool-downs and expired markers are not parks."""
33
+ try:
34
+ with open(os.path.join(account_dir, ".limited"), errors="replace") as handle:
35
+ text = handle.read(4096)
36
+ until = int(text.splitlines()[0])
37
+ except (OSError, ValueError, IndexError):
38
+ return None
39
+ if until <= now or not ("reason=limits" in text or "reason=client-rate-limit" in text):
40
+ return None
41
+ return until
25
42
 
26
43
 
27
44
  def auto_reset_enabled() -> bool:
@@ -111,11 +128,20 @@ def _redeem_request_id(account_id: str, usage: dict, headers: dict, now: int) ->
111
128
 
112
129
  def try_auto_redeem(account_dir: str, account_id: str, used_percent: int,
113
130
  usage: dict, usage_url: str, headers: dict, say,
114
- now: int | None = None, credit_details: dict | None = None) -> dict:
115
- """Redeem one available reset and return a non-secret outcome document."""
131
+ now: int | None = None, credit_details: dict | None = None,
132
+ threshold: int = RESET_AT_USED_PERCENT) -> dict:
133
+ """Redeem one available reset and return a non-secret outcome document.
134
+
135
+ Eligible whenever the pool has PARKED the account: a window at/over the pool's own
136
+ exclusion threshold (90 by default — the 95% line left accounts at 90-94% parked for
137
+ days with credits unused), a finished limit, or an active usage park that lifts more
138
+ than PARK_HOLD_SECONDS from now (a 429 the telemetry may not show)."""
116
139
  current = int(now if now is not None else time.time())
117
140
  finished, _ = reset_windows(usage, RESET_AT_USED_PERCENT)
118
- if not auto_reset_enabled() or (used_percent < RESET_AT_USED_PERCENT and not finished):
141
+ park = _active_park(account_dir, current)
142
+ parked = park is not None and park - current >= PARK_HOLD_SECONDS
143
+ if not auto_reset_enabled() or (used_percent < min(threshold, RESET_AT_USED_PERCENT)
144
+ and not finished and not parked):
119
145
  return {"status": "not_eligible"}
120
146
  state, disposition = _pending_request(account_dir, current)
121
147
  if disposition == "cooldown":
@@ -151,7 +177,8 @@ def try_auto_redeem(account_dir: str, account_id: str, used_percent: int,
151
177
  "redeemed_at": current, "suppress_until": current + REDEEM_COOLDOWN_SECONDS,
152
178
  "windows_reset": int(response.get("windows_reset") or 0)}
153
179
  _write(os.path.join(account_dir, STATE_FILE), complete)
154
- say(f"{account_id}: usage reset redeemed automatically at {used_percent}% used")
180
+ say(f"{account_id}: usage reset redeemed automatically at {used_percent}% used"
181
+ + ("" if used_percent >= threshold or finished else " (parked by a client-reported limit)"))
155
182
  return {"status": "redeemed", "outcome": outcome,
156
183
  "windows_reset": complete["windows_reset"], "redeemed_at": current}
157
184
  try:
@@ -18,7 +18,8 @@ def _read_credits(url, headers, account_id, say):
18
18
  return {}, {}
19
19
 
20
20
 
21
- def refresh_reset_credits(account_dir, account_id, used_percent, usage, usage_url, headers, say, now):
21
+ def refresh_reset_credits(account_dir, account_id, used_percent, usage, usage_url, headers, say, now,
22
+ threshold=None):
22
23
  """Read all accounts, share the GET with redemption, then read back after a mutation.
23
24
 
24
25
  Failed reads omit the count; neither an embedded summary nor subtraction proves
@@ -27,8 +28,9 @@ def refresh_reset_credits(account_dir, account_id, used_percent, usage, usage_ur
27
28
  """
28
29
  list_url, _ = _reset_urls(usage_url)
29
30
  details, view = _read_credits(list_url, headers, account_id, say)
31
+ extra = {} if threshold is None else {"threshold": threshold}
30
32
  result = try_auto_redeem(account_dir, account_id, used_percent, usage, usage_url,
31
- headers, say, now, credit_details=details)
33
+ headers, say, now, credit_details=details, **extra)
32
34
  if result.get("status") not in {"not_eligible", "cooldown", "no_credit", "error"}:
33
35
  _, view = _read_credits(list_url, headers, account_id, say)
34
36
  return result, view
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.36",
3
+ "version": "2.0.38",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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(3600) if session else None, "scope": None},
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 test_does_not_redeem_above_five_percent_remaining(self) -> None:
238
- self.server.state["usage"] = usage_doc(session=94, weekly=80, fable=94)
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 test_a_full_five_hour_window_alone_never_spends_the_week(self) -> None:
245
- # The grant refills the WEEK and exists once; a five-hour wall refills itself in
246
- # hours while the pool picks another account.
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
- self.assertEqual(self.limits_doc()["reset_credits_available"], 1)
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 test_a_week_that_refills_tomorrow_keeps_its_reset(self) -> None:
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=13 * 3600)
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()
@@ -463,6 +513,27 @@ class ClaudeResetReviewRegressionTest(ClaudePoolSandbox, unittest.TestCase):
463
513
  self.assertFalse(marker.exists())
464
514
  self.assertIn("limit reset observed", self.log())
465
515
 
516
+ def test_an_observed_reset_lifts_a_5h_park_only_on_a_measured_session(self) -> None:
517
+ # The reset is only SEEN at this pass, later than it happened: a five-hour park
518
+ # taken in between may be real post-reset exhaustion, and a silent session bucket
519
+ # cannot disprove it. A weekly park in the same gap cannot be (review, 2026-09-22).
520
+ self.server.state["usage"] = usage_doc(fable=96)
521
+ self.run_limits(CLAUDE_MULTIACC_AUTO_RESET="0")
522
+ marked = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 30))
523
+ park = self.account / ".limited"
524
+ park.write_text(f"{int(time.time()) + 3600}\nbucket=client:five_hour percent=100 "
525
+ f"marked_at={marked} reason=client-rate-limit\n", encoding="utf-8")
526
+ self.server.state["usage"] = usage_doc(fable=2, resets_left=0) # session silent
527
+ self.run_limits()
528
+ doc = self.limits_doc()
529
+ self.assertEqual(doc["reset_cleared"], "seven_day,seven_day_overage_included")
530
+ self.assertTrue(park.exists())
531
+ # ...while a weekly park from the same gap is lifted by the same record.
532
+ park.write_text(f"{int(time.time()) + 3 * 86400}\nbucket=client:seven_day percent=100 "
533
+ f"marked_at={marked} reason=client-rate-limit\n", encoding="utf-8")
534
+ self.run_limits()
535
+ self.assertFalse(park.exists())
536
+
466
537
  def test_an_expired_grant_is_never_read_as_a_reset(self) -> None:
467
538
  self.server.state["usage"] = usage_doc(fable=96)
468
539
  self.run_limits(CLAUDE_MULTIACC_AUTO_RESET="0")
@@ -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 test_does_not_redeem_above_five_percent_remaining(self) -> None:
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": 94, "limit_window_seconds": 18000,
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,