claude-multiacc 2.0.34 → 2.0.36

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,6 +177,37 @@ 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
181
+ Code 2.1.280 added `/limit-reset`: an account can hold a *grant* (the Opus 5.5 launch gave
182
+ every Pro/Max account one reset, usable until 2026-10-22) that refills its five-hour and
183
+ weekly limits at once while the weekly reset day stays put. The same usage call carries the
184
+ grant's status (`?cedar_ember=1&skip_spend=1`, Claude Code's own read, so it costs nothing
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
191
+ from the account, grant, remaining count and week, persisted before the POST, so a lost
192
+ response is retried with the same id and fleet peers converge on one claim; a claim whose
193
+ answer never came back is settled by the grant's own remaining count dropping, and a claim
194
+ the server refuses outright backs off (honouring `Retry-After`) instead of being re-sent every
195
+ pass. A confirmed reset zeroes the pre-reset reading's `fetched_at`, withdraws its numbers,
196
+ and drops the account's `.limited` marker — including a client-reported weekly one, which no
197
+ telemetry can otherwise lift — with the same `.client-limit-cleared` watermark a telemetry
198
+ clear writes. Because peers are pushed markers but never their deletion, the reset also rides
199
+ in `limits.json` for 8 days (`reset_redeemed_at`, `reset_cleared`): every machine's shim,
200
+ `list`/`status` and the app-robot panel treat any park written before it, for a window it
201
+ refilled, as lifted. A reset claimed elsewhere (a peer, or `/limit-reset` by hand) enters the
202
+ same record when one grant's own count drops. Only an OAuth login can
203
+ claim (the same credential the usage read proved). `reset_credits_available` /
204
+ `reset_credits_fetched_at` report the measured allowance exactly as the codex pool does. The
205
+ server answers eligibility by client (`ineligible_reason: surface` to anything that is not
206
+ the CLI), so the writer sends Claude Code's own User-Agent with the installed version and
207
+ `client-app/claude-multiacc`; with no readable version it names itself plainly and claims
208
+ nothing. `CLAUDE_MULTIACC_AUTO_RESET=0` is the kill switch. The separate `juniper_tide`
209
+ session reset (it spends weekly budget) is never touched.
210
+
180
211
  **A 0% bucket with no reset window is no data, not an empty account.** Real usage always
181
212
  carries the window it resets in, so `percent: 0` with a null `resets_at` is the endpoint
182
213
  declining to answer — and taking it at face value put two weekly-exhausted accounts at the
package/bin/claude CHANGED
@@ -301,6 +301,24 @@ client_marker_recovered() { # $1 account dir, $2 marker
301
301
  *) bucket="" ;;
302
302
  esac
303
303
  case "$(sel_lc "$bucket")" in *seven_day*|*7d*|*weekly*) return 1 ;; esac
304
+ # ...and a SESSION rejection is only disproved by a reading that actually measured
305
+ # the session window. `peak` below is max_percent, which is the WEEKLY buckets' peak
306
+ # whenever the 5h bucket says nothing — clearing a five_hour marker on it borrows
307
+ # across signals exactly as the writer refuses to. 2026-09-22, acct-14: telemetry
308
+ # fetched WHILE the client was being rejected still read the session bucket
309
+ # `percent: 0, resets_at: null`, so the writer flags that 0 as inferred; the marker
310
+ # was cleared on it, `--resume` landed on the account, and the run died in a minute.
311
+ # A silent session bucket proves nothing about a 429 the client actually received.
312
+ case "$(sel_lc "$bucket")" in
313
+ *five_hour*|*5h*|*session*)
314
+ # A literal test, not limits_field: that parser is integer-only and its
315
+ # `[^0-9]*` would happily skip past `true` and return a digit from the NEXT
316
+ # field, which would read as "inferred" on every document.
317
+ LC_ALL=C grep -q '"session_inferred"[[:space:]]*:[[:space:]]*true' \
318
+ "$1/limits.json" 2>/dev/null && return 1
319
+ cutoff_field "$1" session_percent >/dev/null 2>&1 || return 1
320
+ ;;
321
+ esac
304
322
  fetched="$(telem_fetched_at "$1" 2>/dev/null || echo 0)"
305
323
  # Defaulting peak to 100 when the reading is missing/stale is the second half of the
306
324
  # same rule: an UNINFORMATIVE reading must never clear a marker. It is what keeps a
@@ -335,6 +353,59 @@ client_marker_recovered() { # $1 account dir, $2 marker
335
353
  return 1
336
354
  }
337
355
 
356
+ # A confirmed limit reset (lib/claude_reset.py) lifts every park written BEFORE it for a
357
+ # window it refilled. The writer records it in limits.json — reset_redeemed_at (epoch) and
358
+ # reset_cleared (comma-separated limit types) — because that document is what reaches
359
+ # every machine: peers are pushed `.limited` files but never their deletion, so a marker
360
+ # the source cleared lives on elsewhere until its own reset, days away for a weekly one.
361
+ # Same rule as claude_reset.marker_superseded; a marker without marked_at never matches.
362
+ # Note the panel's `weekly_all` can name the Fable window; the launch grant refills both.
363
+ reset_supersedes_marker() { # $1 acct dir, $2 marker file
364
+ # Hot path (every account with a marker, every invocation): ONE sed for the record, the
365
+ # writer's precomputed UTC key instead of a date(1) fork, builtins for the rest.
366
+ local rec at="" key="" cleared="" line="" marked mk bucket t
367
+ [ -f "$1/limits.json" ] || return 1
368
+ # h/g restore the line between expressions, so a one-line document (every field on
369
+ # one line) yields all three just as the writer's indented one does.
370
+ rec="$(LC_ALL=C sed -n -e h \
371
+ -e 's/.*"reset_redeemed_at"[^0-9]*\([0-9][0-9]*\).*/A \1/p' -e g \
372
+ -e 's/.*"reset_redeemed_key"[^0-9]*\([0-9]\{14\}\).*/K \1/p' -e g \
373
+ -e 's/.*"reset_cleared"[[:space:]]*:[[:space:]]*"\([a-z0-9_,]*\)".*/C \1/p' \
374
+ "$1/limits.json" 2>/dev/null)"
375
+ [ -n "$rec" ] || return 1
376
+ # Split on the tags, not on newlines: sed keeps a final line's missing newline, so a
377
+ # one-line document runs the tagged values together.
378
+ case "$rec" in *"A "*) at="${rec#*A }"; at="${at%%[!0-9]*}" ;; esac
379
+ case "$rec" in *"K "*) key="${rec#*K }"; key="${key%%[!0-9]*}" ;; esac
380
+ case "$rec" in *"C "*) cleared="${rec#*C }"; cleared="${cleared%%[!a-z0-9_,]*}" ;; esac
381
+ num_ok "$at" && [ -n "$cleared" ] || return 1
382
+ [ $((now - at)) -le 691200 ] || return 1
383
+ if [ "${#key}" -ne 14 ]; then
384
+ key="$(iso_of_epoch "$at")"; key="${key//[!0-9]/}"; key="${key:0:14}"
385
+ fi
386
+ { IFS= read -r line; IFS= read -r line; } < "$2" 2>/dev/null
387
+ case "$line" in *marked_at=*) marked="${line#*marked_at=}"; marked="${marked%% *}" ;; *) return 1 ;; esac
388
+ mk="${marked%%.*}"; mk="${mk//[!0-9]/}"; mk="${mk:0:14}"
389
+ num_ok "$mk" && num_ok "$key" && [ "${#mk}" -eq 14 ] && [ "${#key}" -eq 14 ] || return 1
390
+ [ "$mk" -le "$key" ] || return 1
391
+ bucket=""
392
+ case "$line" in *bucket=*) bucket="${line#*bucket=}"; bucket="${bucket%% *}" ;; esac
393
+ bucket="$(sel_lc "$bucket")"
394
+ case "$bucket" in
395
+ client:*) t="${bucket#client:}" ;;
396
+ weekly_scoped:*fable*) t=seven_day_overage_included ;;
397
+ weekly_scoped:*opus*) t=seven_day_opus ;;
398
+ weekly_scoped:*sonnet*) t=seven_day_sonnet ;;
399
+ weekly_scoped:*) return 1 ;;
400
+ session*|five_hour|5h) t=five_hour ;;
401
+ weekly_all|seven_day|weekly|7d) t=seven_day ;;
402
+ ''|panel:observed|limit_reached|unknown) return 0 ;;
403
+ *) t="$bucket" ;;
404
+ esac
405
+ case ",$cleared," in *",$t,"*) return 0 ;; esac
406
+ return 1
407
+ }
408
+
338
409
  marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
339
410
  local m="$1/.limited" reset="" tok
340
411
  [ -f "$m" ] || return 1
@@ -349,6 +420,11 @@ marker_active() { # true if $1/.limited is still in force; clears cleanly-expire
349
420
  rm -f "$m" 2>/dev/null
350
421
  return 1
351
422
  fi
423
+ if reset_supersedes_marker "$1" "$m"; then
424
+ rm -f "$m" 2>/dev/null
425
+ sel_log "$(basename "$1") marker lifted — written before a confirmed limit reset"
426
+ return 1
427
+ fi
352
428
  if client_marker_recovered "$1" "$m"; then
353
429
  printf '%s\n' "$CLIENT_RECOVERY_FETCHED" > "$1/.client-limit-cleared.$$" 2>/dev/null \
354
430
  && mv -f "$1/.client-limit-cleared.$$" "$1/.client-limit-cleared" 2>/dev/null \
@@ -767,7 +843,7 @@ sess_transcript() { # $1 acct dir, $2 session id
767
843
  # rejects the very next request too, and that lands in a newer transcript.
768
844
  client_limit_scan() { # $1 acct dir
769
845
  local idx="$1/.sessions-index" memo="$1/.client-scan" id p line r t read_n=0 i last=""
770
- local ln claim ts ck ak ttl cleared sk
846
+ local ln claim ts ck ak ttl cleared sk rr="" rr_seen=""
771
847
  local ids=() claims=()
772
848
  [ "${CLAUDE_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 1
773
849
  [ -f "$idx" ] || return 1
@@ -820,6 +896,15 @@ client_limit_scan() { # $1 acct dir
820
896
  cleared=""
821
897
  [ -f "$1/.client-limit-cleared" ] \
822
898
  && { IFS= read -r cleared < "$1/.client-limit-cleared" 2>/dev/null || cleared=""; }
899
+ # A confirmed limit reset answers every rejection before it, on every machine the
900
+ # writer's limits.json reaches (the local watermark file never leaves the source).
901
+ if [ -z "$rr_seen" ]; then
902
+ rr_seen=1
903
+ rr="$(limits_field "$1" reset_redeemed_at 2>/dev/null)" || rr=""
904
+ fi
905
+ if num_ok "$rr" && { ! num_ok "$cleared" || [ "$rr" -gt "$cleared" ]; }; then
906
+ cleared="$rr"
907
+ fi
823
908
  if num_ok "$cleared"; then
824
909
  sk="$(iso_key "$(iso_of_epoch "$cleared")")"
825
910
  if num_ok "$ck" && num_ok "$sk"; then
@@ -81,7 +81,11 @@ 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. Auto-refreshes long-expired
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.
88
+ Auto-refreshes long-expired
85
89
  OAuth access tokens via the refresh-token grant (rotated credential is
86
90
  persisted), so idle accounts keep fresh telemetry and stay selectable.
87
91
  Skips accounts fetched in the last 45s and honors 429/refresh backoff;
@@ -100,6 +104,9 @@ ENV
100
104
  CLAUDE_ACCOUNT pin the shim to one account
101
105
  CLAUDE_SHIM_RETRY=0 disable -p auto-retry
102
106
  CLAUDE_MULTIACC_DISABLE=1 bypass the shim entirely
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)
103
110
  EOF
104
111
  }
105
112
 
@@ -207,6 +214,22 @@ doc = json.load(open(sys.argv[1]))
207
214
  root = sys.argv[2]
208
215
  sys.path = [sys.argv[3]] + [p for p in sys.path if p not in ('', '.')]
209
216
  from audit import audit_account # noqa: E402 (shared with the shim's rule)
217
+
218
+
219
+ def superseded_by_reset(d):
220
+ """lib/report.py's rule (and the shim's reset_supersedes_marker): a park written
221
+ before a confirmed limit reset, for a window it refilled, no longer binds."""
222
+ try:
223
+ import claude_reset, time as _time
224
+ with open(os.path.join(d, '.limited'), errors='replace') as handle:
225
+ text = handle.read(4096)
226
+ try:
227
+ ldoc = json.load(open(os.path.join(d, 'limits.json')))
228
+ except Exception:
229
+ ldoc = {}
230
+ return claude_reset.marker_superseded(text, claude_reset.record_from(ldoc, _time.time()))
231
+ except Exception:
232
+ return False
210
233
  machine = sys.argv[4]
211
234
  # Only render well-formed ids — a hand-edited manifest must not surface a traversal id.
212
235
  accounts = [a for a in doc.get('accounts', [])
@@ -224,6 +247,8 @@ for a in accounts:
224
247
  if os.path.getsize(os.path.join(d, 'server.token')) > 0 if os.path.isfile(os.path.join(d, 'server.token')) else False:
225
248
  auth.append('token')
226
249
  limited = os.path.isfile(os.path.join(d, '.limited'))
250
+ if limited and superseded_by_reset(d):
251
+ limited = False # written before a confirmed limit reset; the shim lifts it
227
252
  flags = []
228
253
  if st['state'] == 'expired':
229
254
  flags.append('EXPIRED-LOGIN')
@@ -284,6 +309,22 @@ doc = json.load(open(sys.argv[1]))
284
309
  root = sys.argv[2]
285
310
  sys.path = [sys.argv[3]] + [p for p in sys.path if p not in ('', '.')]
286
311
  from audit import audit_account, oauth_login # noqa: E402 (shared with the shim's rule)
312
+
313
+
314
+ def superseded_by_reset(d):
315
+ """lib/report.py's rule (and the shim's reset_supersedes_marker): a park written
316
+ before a confirmed limit reset, for a window it refilled, no longer binds."""
317
+ try:
318
+ import claude_reset, time as _time
319
+ with open(os.path.join(d, '.limited'), errors='replace') as handle:
320
+ text = handle.read(4096)
321
+ try:
322
+ ldoc = json.load(open(os.path.join(d, 'limits.json')))
323
+ except Exception:
324
+ ldoc = {}
325
+ return claude_reset.marker_superseded(text, claude_reset.record_from(ldoc, _time.time()))
326
+ except Exception:
327
+ return False
287
328
  machine = sys.argv[4]
288
329
  now = time.time()
289
330
  # Must match the shim's window (bin/claude), or status would call data "fresh" that
@@ -412,7 +453,9 @@ for a in doc.get('accounts', []):
412
453
  else:
413
454
  print(" limits : never fetched")
414
455
  mpath = os.path.join(d, '.limited')
415
- if os.path.isfile(mpath):
456
+ if os.path.isfile(mpath) and superseded_by_reset(d):
457
+ print(" marker : lifted — written before a confirmed limit reset (eligible)")
458
+ elif os.path.isfile(mpath):
416
459
  try:
417
460
  lines = open(mpath).read().splitlines()
418
461
  reset = int(lines[0]) if lines and lines[0].isdigit() else 0
@@ -1574,13 +1617,20 @@ cmd_limits() {
1574
1617
  case "$threshold" in ''|*[!0-9]*) threshold=90 ;; esac
1575
1618
  [ "$threshold" -gt 90 ] && threshold=90
1576
1619
  [ "$threshold" -lt 1 ] && threshold=90
1577
- "$PYBIN" - "$ACC_ROOT" "$threshold" "$quiet" "$USAGE_URL" "$force" "$LIB_DIR" <<'PYEOF' 2>>"$ACC_ROOT/limits.log"
1620
+ # Claude Code's own User-Agent (with the REAL installed version) is what makes the
1621
+ # usage endpoint answer the limit-reset status for this account; the writer learns the
1622
+ # version from the real binary (lib/claude_reset.py cli_version, bounded by a timeout).
1623
+ local real_claude
1624
+ real_claude="$(find_real_claude "$_self" 2>/dev/null)" || real_claude=""
1625
+ "$PYBIN" - "$ACC_ROOT" "$threshold" "$quiet" "$USAGE_URL" "$force" "$LIB_DIR" "$real_claude" <<'PYEOF' 2>>"$ACC_ROOT/limits.log"
1578
1626
  import hashlib, json, os, sys, time, urllib.request
1579
1627
  sys.path = [sys.argv[6]] + [p for p in sys.path if p not in ('', '.')]
1580
1628
  import keychain # noqa: E402 (macOS Keychain-held logins; a no-op elsewhere)
1581
1629
  from audit import creds_doc_state # noqa: E402 (one rule for "can this credential work")
1630
+ import claude_reset # noqa: E402 (automatic limit-reset redemption, the codex_reset twin)
1582
1631
 
1583
1632
  root, threshold, quiet, url, force = sys.argv[1], int(sys.argv[2]), sys.argv[3] == '1', sys.argv[4], sys.argv[5] == '1'
1633
+ USER_AGENT = claude_reset.user_agent(claude_reset.cli_version(sys.argv[7] if len(sys.argv) > 7 else ''))
1584
1634
  now = time.time()
1585
1635
  # Don't re-fetch an account whose data is younger than this (endpoint rate-limits).
1586
1636
  MIN_FETCH_INTERVAL = int(os.environ.get('CLAUDE_MULTIACC_MIN_FETCH', '240'))
@@ -1651,6 +1701,15 @@ def marker_bucket(txt):
1651
1701
  return part[7:]
1652
1702
  return ''
1653
1703
 
1704
+ def session_marker(txt):
1705
+ # The self-healing 5h/session window, named the way each client writes it:
1706
+ # claude's own type is `five_hour`, codex labels a short window `client:5h`, and a
1707
+ # bare `session` covers the writer's own marker. Anything else (a weekly name, an
1708
+ # unparseable line) is NOT a session marker and keeps its existing handling.
1709
+ b = marker_bucket(txt).lower()
1710
+ return 'five_hour' in b or '5h' in b or 'session' in b
1711
+
1712
+
1654
1713
  def weekly_marker(txt):
1655
1714
  # Matched the way both shims match it (bin/claude ~303, same token list): the
1656
1715
  # claude client writes client:seven_day / client:seven_day_opus, the codex client
@@ -2075,12 +2134,13 @@ for acct in manifest.get('accounts', []):
2075
2134
  say(f'{aid}: no fresh bearer (expired oauth and/or no token); limits left as-is')
2076
2135
  continue
2077
2136
 
2078
- req = urllib.request.Request(url, headers={
2137
+ headers = {
2079
2138
  'Authorization': 'Bearer ' + bearer,
2080
2139
  'anthropic-beta': 'oauth-2025-04-20',
2081
2140
  'Content-Type': 'application/json',
2082
- 'User-Agent': 'claude-multiacc/1.0',
2083
- })
2141
+ 'User-Agent': USER_AGENT,
2142
+ }
2143
+ req = urllib.request.Request(url, headers=headers)
2084
2144
  try:
2085
2145
  resp = urllib.request.urlopen(req, timeout=15)
2086
2146
  data = json.loads(resp.read().decode())
@@ -2325,6 +2385,7 @@ for acct in manifest.get('accounts', []):
2325
2385
  # So: a session bucket that said nothing inside a document that DID answer reads 0.
2326
2386
  # A document where NOTHING answered still writes no signals at all (see below) —
2327
2387
  # the 2026-09-04 all-zero shape stays unknown, which is the whole point.
2388
+ session_measured = bool(session)
2328
2389
  if sessionp is None and live:
2329
2390
  sessionp = 0
2330
2391
  # How long weekly_percent keeps meaning something. A weekly bucket only ever RISES
@@ -2338,13 +2399,39 @@ for acct in manifest.get('accounts', []):
2338
2399
  wresets = [int(b['resets_epoch']) for b in live
2339
2400
  if b['group'] != 'session' and b['percent'] == weeklyp
2340
2401
  and isinstance(b.get('resets_epoch'), int)]
2402
+ # Limit resets ride on this very response (the usage URL asks for the cedar_ember
2403
+ # status block), so reading the allowance costs no call against the endpoint's
2404
+ # hourly budget. Same policy as codex: a limit the grant refills at >=95%, or one
2405
+ # the server reports exhausted, spends the grant. Only an OAuth login can claim —
2406
+ # the same credential this fetch just proved works. Fail OPEN like everything else.
2407
+ reset_result, reset_view = {'status': 'not_eligible'}, {}
2408
+ if source == 'oauth':
2409
+ try:
2410
+ reset_result, reset_view = claude_reset.refresh_reset_credits(
2411
+ d, aid, data, url, headers, say, int(now))
2412
+ except Exception as e:
2413
+ say(f'{aid}: limit reset automation failed unexpectedly ({type(e).__name__}); '
2414
+ f'failing open')
2415
+ reset_result, reset_view = {'status': 'error'}, {}
2416
+ redeemed = reset_result.get('status') == 'redeemed'
2341
2417
  out = {'fetched_at': int(now), 'source': source}
2418
+ out.update(reset_view)
2342
2419
  if live:
2343
2420
  out['max_percent'] = maxp
2344
2421
  if weeklyp is not None:
2345
2422
  out['weekly_percent'] = weeklyp
2346
2423
  if sessionp is not None:
2347
2424
  out['session_percent'] = sessionp
2425
+ if sessionp is not None and not session_measured:
2426
+ # Ranking may use this 0 (that is the whole point — an idle account has to get
2427
+ # back into the band), but NOTHING may treat it as proof of recovery. 2026-09-22,
2428
+ # acct-14: a client:five_hour marker written from a real 429 at 13:06:31Z, and
2429
+ # telemetry fetched at 13:04:16Z — WHILE the account was being rejected — still
2430
+ # reported the session bucket `percent: 0, resets_at: null`. So a silent session
2431
+ # bucket does NOT prove an idle account; the endpoint simply may not report the
2432
+ # 5h window at all. The marker was cleared anyway, `--resume` landed straight on
2433
+ # it, and the run was rejected inside a minute. Recovery has to be MEASURED.
2434
+ out['session_inferred'] = True
2348
2435
  if weeklyp is not None:
2349
2436
  # Written with weekly_percent or not at all: the horizon describes THAT
2350
2437
  # reading, and the shim's degraded path needs both or neither.
@@ -2361,6 +2448,52 @@ for acct in manifest.get('accounts', []):
2361
2448
  say(f'{aid}: usage endpoint returned all-zero buckets with no reset windows — '
2362
2449
  f'no usable telemetry (account ranks as unknown, not as empty)')
2363
2450
  out['buckets'] = buckets
2451
+ reset_record = None
2452
+ reset_status = claude_reset.parse_status(data) if source == 'oauth' else None
2453
+ if reset_status is not None:
2454
+ out['reset_grants_seen'] = claude_reset.grants_seen(
2455
+ reset_status, now, reset_result if redeemed else None)
2456
+ elif isinstance(prev.get('reset_grants_seen'), dict):
2457
+ out['reset_grants_seen'] = prev['reset_grants_seen']
2458
+ observed = None
2459
+ if redeemed:
2460
+ # The claim response proves the limits refilled, but this usage GET happened
2461
+ # BEFORE it. Make the snapshot stale at once so it cannot re-exclude the account
2462
+ # (the codex writer's rule), and take the pre-reset numbers OUT of it: several
2463
+ # readers ignore freshness — the shim's forced-fallback check reads buckets
2464
+ # directly, the degraded path trusts a stale weekly until its horizon, and the
2465
+ # app-robot panel reads session/weekly percentages with no age test at all — and
2466
+ # every one of them would keep parking an account that was just refilled. The
2467
+ # buckets the reset did not touch stay; the next pass reads server truth.
2468
+ cleared = set(reset_result.get('cleared') or [])
2469
+ out['fetched_at'] = 0
2470
+ for key in ('weekly_resets_epoch', 'max_percent', 'weekly_percent',
2471
+ 'session_percent', 'session_inferred', 'no_data'):
2472
+ out.pop(key, None)
2473
+ out['buckets'] = [b for b in buckets
2474
+ if claude_reset.bucket_limit_type(b['name']) not in cleared]
2475
+ out['auto_reset'] = reset_result
2476
+ reset_record = (int(reset_result['redeemed_at']), sorted(cleared))
2477
+ else:
2478
+ # A reset is honoured fleet-wide through THIS document (peers never see a marker
2479
+ # deleted), so the record rides along on every later pass until nothing it could
2480
+ # supersede can still exist. A reset this machine did not claim (a peer's, or a
2481
+ # human's /limit-reset) enters it here, from the grant's own count dropping; this
2482
+ # GET already came after it, so its numbers stand.
2483
+ observed = claude_reset.observed_reset(prev, reset_status, now)
2484
+ reset_record = claude_reset.latest(claude_reset.record_from(prev, now), observed,
2485
+ claude_reset.local_record(d, now))
2486
+ if observed and reset_record is observed:
2487
+ say(f"{aid}: limit reset observed (a grant's count dropped; "
2488
+ f"refilled {', '.join(observed[1])}) — lifting older parks")
2489
+ else:
2490
+ observed = None
2491
+ if reset_record:
2492
+ out['reset_redeemed_at'] = reset_record[0]
2493
+ # The same moment as a UTC YYYYmmddHHMMSS key: the shim compares marked_at
2494
+ # against it with builtins, no date(1) fork on the per-invocation path.
2495
+ out['reset_redeemed_key'] = time.strftime('%Y%m%d%H%M%S', time.gmtime(reset_record[0]))
2496
+ out['reset_cleared'] = ','.join(reset_record[1])
2364
2497
  tmp = lpath + '.tmp'
2365
2498
  with open(tmp, 'w') as f:
2366
2499
  json.dump(out, f, indent=1)
@@ -2368,7 +2501,19 @@ for acct in manifest.get('accounts', []):
2368
2501
  # The fetch went through with this account's own bearer => its auth is alive.
2369
2502
  if clear_expired(d):
2370
2503
  say(f'{aid}: dead-auth marker cleared (authenticated successfully)')
2371
- offenders = [b for b in buckets if b['percent'] >= threshold]
2504
+ if redeemed or observed:
2505
+ # The shim's transcript scan would otherwise re-create a park from the very
2506
+ # rejection the reset just answered — whether or not a marker exists right now
2507
+ # and whatever wrote it. Never move the watermark backwards.
2508
+ cleared_path = os.path.join(d, '.client-limit-cleared')
2509
+ try:
2510
+ prior = int(open(cleared_path).read().split()[0])
2511
+ except Exception:
2512
+ prior = 0
2513
+ with open(cleared_path + '.tmp', 'w') as f:
2514
+ f.write(f'{max(prior, reset_record[0])}\n')
2515
+ os.replace(cleared_path + '.tmp', cleared_path)
2516
+ offenders = [] if redeemed else [b for b in buckets if b['percent'] >= threshold]
2372
2517
  mpath = os.path.join(d, '.limited')
2373
2518
  if offenders:
2374
2519
  # ONE bucket, described consistently: the marker's epoch is the reset of
@@ -2402,7 +2547,8 @@ for acct in manifest.get('accounts', []):
2402
2547
  if first.isdigit() and len(first) <= 18:
2403
2548
  cur_reset = int(first)
2404
2549
  keep_client = ('reason=client-rate-limit' in cur
2405
- and cur_reset > now and cur_reset >= reset_epoch)
2550
+ and cur_reset > now and cur_reset >= reset_epoch
2551
+ and not claude_reset.marker_superseded(cur, reset_record))
2406
2552
  except Exception:
2407
2553
  pass
2408
2554
  if keep_client:
@@ -2429,6 +2575,7 @@ for acct in manifest.get('accounts', []):
2429
2575
  # markers strand recovered accounts for days.
2430
2576
  keep = False
2431
2577
  client_recovered = False
2578
+ txt = ''
2432
2579
  try:
2433
2580
  txt = open(mpath).read()
2434
2581
  first = txt.splitlines()[0] if txt else ''
@@ -2442,10 +2589,23 @@ for acct in manifest.get('accounts', []):
2442
2589
  is_client = 'reason=client-rate-limit' in txt
2443
2590
  # See weekly_marker() above for why these three come first and in
2444
2591
  # this order; the shim applies the identical test per invocation.
2445
- if active and not live:
2592
+ if redeemed or claude_reset.marker_superseded(txt, reset_record):
2593
+ # A confirmed reset is first-hand proof the limits refilled — the
2594
+ # one thing that outranks even a client-reported weekly rejection
2595
+ # (the codex writer's identical rule). A marker written before it,
2596
+ # for a window it refilled, describes a state that no longer exists.
2597
+ keep = False
2598
+ elif active and not live:
2446
2599
  keep = True
2447
2600
  elif active and is_client and weekly_marker(txt):
2448
2601
  keep = True
2602
+ elif active and is_client and session_marker(txt) and not session_measured:
2603
+ # Same rule as the weekly one, one window down: a rejection the CLIENT
2604
+ # reported is only disproved by telemetry that actually looked at the
2605
+ # window it names. `max_percent` here comes from the WEEKLY buckets
2606
+ # when the session bucket is silent, and clearing a 5h marker on it is
2607
+ # the cross-signal borrow the aggregation above refuses to make.
2608
+ keep = True
2449
2609
  elif active and ('reason=error-cooldown' in txt
2450
2610
  or (is_client and recent_client)):
2451
2611
  keep = True
@@ -2456,14 +2616,24 @@ for acct in manifest.get('accounts', []):
2456
2616
  os.remove(mpath)
2457
2617
  if client_recovered:
2458
2618
  cleared = os.path.join(d, '.client-limit-cleared')
2619
+ try:
2620
+ prior = int(open(cleared).read().split()[0])
2621
+ except Exception:
2622
+ prior = 0
2623
+ # Never backwards: a reset confirmed earlier in this same pass has
2624
+ # already stamped a later moment than the pass's start.
2459
2625
  with open(cleared + '.tmp', 'w') as f:
2460
- f.write(f'{int(now)}\n')
2626
+ f.write(f'{max(prior, int(now))}\n')
2461
2627
  os.replace(cleared + '.tmp', cleared)
2462
2628
  # On a no-data pass `live` is empty and maxp is 0 only because
2463
2629
  # nothing was reported (see informative() above). Log that, instead
2464
2630
  # of a "0%" that reads like a proven-empty account — the exact
2465
2631
  # misreading behind the 2026-09-04 incident.
2466
2632
  seen = f'max {maxp}%' if live else 'no usable telemetry'
2633
+ if redeemed:
2634
+ seen = 'limit reset confirmed'
2635
+ elif claude_reset.marker_superseded(txt, reset_record):
2636
+ seen = 'written before a confirmed limit reset'
2467
2637
  say(f'{aid}: marker cleared ({seen})')
2468
2638
  elif not live:
2469
2639
  # One line per account, so a no-data pass is legible in limits.log:
@@ -1154,6 +1154,23 @@ def marker_bucket(txt):
1154
1154
  return part[7:]
1155
1155
  return ''
1156
1156
 
1157
+ def session_marker(txt):
1158
+ # The self-healing ~5h window as each client names it: codex labels a short window
1159
+ # `client:5h`, claude's own type is `five_hour`, and a bare `session` covers the
1160
+ # writer's own marker. Kept identical to bin/claude-accounts' copy.
1161
+ b = marker_bucket(txt).lower()
1162
+ return 'five_hour' in b or '5h' in b or 'session' in b
1163
+
1164
+
1165
+ def _prior_mark(path):
1166
+ """The existing .client-limit-cleared epoch (0 when absent/garbled): the watermark
1167
+ only ever moves forward, so a reset stamped earlier in a pass is never undone."""
1168
+ try:
1169
+ return int(open(path).read().split()[0])
1170
+ except Exception:
1171
+ return 0
1172
+
1173
+
1157
1174
  def weekly_marker(txt):
1158
1175
  # Matched the way both shims match it (bin/claude ~303, same token list): the
1159
1176
  # claude client writes client:seven_day / client:seven_day_opus, and the codex
@@ -1599,6 +1616,7 @@ for acct in manifest.get('accounts', []):
1599
1616
  # one is never silent — it reports at/near 100% with the reset it is waiting on.
1600
1617
  # So a missing or silent session reads 0 whenever the payload answered at all; a
1601
1618
  # payload where NOTHING answered still writes no signals and stays unknown below.
1619
+ session_measured = bool(session)
1602
1620
  if sessionp is None and live:
1603
1621
  sessionp = 0
1604
1622
  try:
@@ -1619,6 +1637,13 @@ for acct in manifest.get('accounts', []):
1619
1637
  out['weekly_percent'] = weeklyp
1620
1638
  if sessionp is not None:
1621
1639
  out['session_percent'] = sessionp
1640
+ if sessionp is not None and not session_measured:
1641
+ # Ranking may use this 0 — an idle account has to get back into the band — but
1642
+ # nothing may read it as proof of recovery. codex reports no window at all for a
1643
+ # closed one, so silence here is the NORMAL shape and says nothing whatsoever
1644
+ # about a 429 the client actually received. See the claude writer for the live
1645
+ # case that proved it (acct-14, 2026-09-22).
1646
+ out['session_inferred'] = True
1622
1647
  if not live:
1623
1648
  # Nothing usable in the entire payload. Keep the diagnostics (fetched_at,
1624
1649
  # source, plan, the raw buckets) and write NONE of the percent signals: a
@@ -1645,6 +1670,19 @@ for acct in manifest.get('accounts', []):
1645
1670
  # The fetch went through with this account's own bearer => its auth is alive.
1646
1671
  if clear_expired(d):
1647
1672
  say(f'{aid}: dead-auth marker cleared (authenticated successfully)')
1673
+ if reset_result.get('status') == 'redeemed':
1674
+ # bin/codex's rollout scan would otherwise re-create a park from the very
1675
+ # rejection this reset answered — whether or not a marker exists right now. The
1676
+ # clean pass below only stamps this for an active client marker; a redemption
1677
+ # needs it unconditionally (same rule as the claude writer). Never backwards.
1678
+ cleared_path = os.path.join(d, '.client-limit-cleared')
1679
+ try:
1680
+ prior = int(open(cleared_path).read().split()[0])
1681
+ except Exception:
1682
+ prior = 0
1683
+ with open(cleared_path + '.tmp', 'w') as f:
1684
+ f.write(f'{max(prior, int(time.time()))}\n')
1685
+ os.replace(cleared_path + '.tmp', cleared_path)
1648
1686
  offenders = [] if reset_result.get('status') == 'redeemed' else [
1649
1687
  b for b in buckets if b['percent'] >= threshold]
1650
1688
  mpath = os.path.join(d, '.limited')
@@ -1727,6 +1765,13 @@ for acct in manifest.get('accounts', []):
1727
1765
  keep = True
1728
1766
  elif active and is_client and weekly_marker(txt):
1729
1767
  keep = True
1768
+ elif active and is_client and session_marker(txt) and not session_measured:
1769
+ # Same rule one window down: a rejection the CLIENT reported is only
1770
+ # disproved by telemetry that looked at the window it names. With the
1771
+ # session window silent, max_percent is the WEEKLY peak, and clearing
1772
+ # a 5h marker on it is the cross-signal borrow the aggregation above
1773
+ # refuses to make.
1774
+ keep = True
1730
1775
  elif active and ('reason=error-cooldown' in txt
1731
1776
  or (is_client and recent_client)):
1732
1777
  keep = True
@@ -1746,7 +1791,7 @@ for acct in manifest.get('accounts', []):
1746
1791
  # 2026-09-04 and needed the identical brake with it.
1747
1792
  cleared = os.path.join(d, '.client-limit-cleared')
1748
1793
  with open(cleared + '.tmp', 'w') as f:
1749
- f.write(f'{int(now)}\n')
1794
+ f.write(f'{max(_prior_mark(cleared), int(now))}\n')
1750
1795
  os.replace(cleared + '.tmp', cleared)
1751
1796
  # On a no-data pass `live` is empty and maxp is 0 only because
1752
1797
  # nothing was reported (see informative() above). Log that, instead