claude-multiacc 2.0.35 → 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
@@ -353,6 +353,59 @@ client_marker_recovered() { # $1 account dir, $2 marker
353
353
  return 1
354
354
  }
355
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
+
356
409
  marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
357
410
  local m="$1/.limited" reset="" tok
358
411
  [ -f "$m" ] || return 1
@@ -367,6 +420,11 @@ marker_active() { # true if $1/.limited is still in force; clears cleanly-expire
367
420
  rm -f "$m" 2>/dev/null
368
421
  return 1
369
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
370
428
  if client_marker_recovered "$1" "$m"; then
371
429
  printf '%s\n' "$CLIENT_RECOVERY_FETCHED" > "$1/.client-limit-cleared.$$" 2>/dev/null \
372
430
  && mv -f "$1/.client-limit-cleared.$$" "$1/.client-limit-cleared" 2>/dev/null \
@@ -785,7 +843,7 @@ sess_transcript() { # $1 acct dir, $2 session id
785
843
  # rejects the very next request too, and that lands in a newer transcript.
786
844
  client_limit_scan() { # $1 acct dir
787
845
  local idx="$1/.sessions-index" memo="$1/.client-scan" id p line r t read_n=0 i last=""
788
- local ln claim ts ck ak ttl cleared sk
846
+ local ln claim ts ck ak ttl cleared sk rr="" rr_seen=""
789
847
  local ids=() claims=()
790
848
  [ "${CLAUDE_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 1
791
849
  [ -f "$idx" ] || return 1
@@ -838,6 +896,15 @@ client_limit_scan() { # $1 acct dir
838
896
  cleared=""
839
897
  [ -f "$1/.client-limit-cleared" ] \
840
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
841
908
  if num_ok "$cleared"; then
842
909
  sk="$(iso_key "$(iso_of_epoch "$cleared")")"
843
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'))
@@ -2084,12 +2134,13 @@ for acct in manifest.get('accounts', []):
2084
2134
  say(f'{aid}: no fresh bearer (expired oauth and/or no token); limits left as-is')
2085
2135
  continue
2086
2136
 
2087
- req = urllib.request.Request(url, headers={
2137
+ headers = {
2088
2138
  'Authorization': 'Bearer ' + bearer,
2089
2139
  'anthropic-beta': 'oauth-2025-04-20',
2090
2140
  'Content-Type': 'application/json',
2091
- 'User-Agent': 'claude-multiacc/1.0',
2092
- })
2141
+ 'User-Agent': USER_AGENT,
2142
+ }
2143
+ req = urllib.request.Request(url, headers=headers)
2093
2144
  try:
2094
2145
  resp = urllib.request.urlopen(req, timeout=15)
2095
2146
  data = json.loads(resp.read().decode())
@@ -2348,7 +2399,23 @@ for acct in manifest.get('accounts', []):
2348
2399
  wresets = [int(b['resets_epoch']) for b in live
2349
2400
  if b['group'] != 'session' and b['percent'] == weeklyp
2350
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'
2351
2417
  out = {'fetched_at': int(now), 'source': source}
2418
+ out.update(reset_view)
2352
2419
  if live:
2353
2420
  out['max_percent'] = maxp
2354
2421
  if weeklyp is not None:
@@ -2381,6 +2448,52 @@ for acct in manifest.get('accounts', []):
2381
2448
  say(f'{aid}: usage endpoint returned all-zero buckets with no reset windows — '
2382
2449
  f'no usable telemetry (account ranks as unknown, not as empty)')
2383
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])
2384
2497
  tmp = lpath + '.tmp'
2385
2498
  with open(tmp, 'w') as f:
2386
2499
  json.dump(out, f, indent=1)
@@ -2388,7 +2501,19 @@ for acct in manifest.get('accounts', []):
2388
2501
  # The fetch went through with this account's own bearer => its auth is alive.
2389
2502
  if clear_expired(d):
2390
2503
  say(f'{aid}: dead-auth marker cleared (authenticated successfully)')
2391
- 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]
2392
2517
  mpath = os.path.join(d, '.limited')
2393
2518
  if offenders:
2394
2519
  # ONE bucket, described consistently: the marker's epoch is the reset of
@@ -2422,7 +2547,8 @@ for acct in manifest.get('accounts', []):
2422
2547
  if first.isdigit() and len(first) <= 18:
2423
2548
  cur_reset = int(first)
2424
2549
  keep_client = ('reason=client-rate-limit' in cur
2425
- 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))
2426
2552
  except Exception:
2427
2553
  pass
2428
2554
  if keep_client:
@@ -2449,6 +2575,7 @@ for acct in manifest.get('accounts', []):
2449
2575
  # markers strand recovered accounts for days.
2450
2576
  keep = False
2451
2577
  client_recovered = False
2578
+ txt = ''
2452
2579
  try:
2453
2580
  txt = open(mpath).read()
2454
2581
  first = txt.splitlines()[0] if txt else ''
@@ -2462,7 +2589,13 @@ for acct in manifest.get('accounts', []):
2462
2589
  is_client = 'reason=client-rate-limit' in txt
2463
2590
  # See weekly_marker() above for why these three come first and in
2464
2591
  # this order; the shim applies the identical test per invocation.
2465
- 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:
2466
2599
  keep = True
2467
2600
  elif active and is_client and weekly_marker(txt):
2468
2601
  keep = True
@@ -2483,14 +2616,24 @@ for acct in manifest.get('accounts', []):
2483
2616
  os.remove(mpath)
2484
2617
  if client_recovered:
2485
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.
2486
2625
  with open(cleared + '.tmp', 'w') as f:
2487
- f.write(f'{int(now)}\n')
2626
+ f.write(f'{max(prior, int(now))}\n')
2488
2627
  os.replace(cleared + '.tmp', cleared)
2489
2628
  # On a no-data pass `live` is empty and maxp is 0 only because
2490
2629
  # nothing was reported (see informative() above). Log that, instead
2491
2630
  # of a "0%" that reads like a proven-empty account — the exact
2492
2631
  # misreading behind the 2026-09-04 incident.
2493
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'
2494
2637
  say(f'{aid}: marker cleared ({seen})')
2495
2638
  elif not live:
2496
2639
  # One line per account, so a no-data pass is legible in limits.log:
@@ -1162,6 +1162,15 @@ def session_marker(txt):
1162
1162
  return 'five_hour' in b or '5h' in b or 'session' in b
1163
1163
 
1164
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
+
1165
1174
  def weekly_marker(txt):
1166
1175
  # Matched the way both shims match it (bin/claude ~303, same token list): the
1167
1176
  # claude client writes client:seven_day / client:seven_day_opus, and the codex
@@ -1661,6 +1670,19 @@ for acct in manifest.get('accounts', []):
1661
1670
  # The fetch went through with this account's own bearer => its auth is alive.
1662
1671
  if clear_expired(d):
1663
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)
1664
1686
  offenders = [] if reset_result.get('status') == 'redeemed' else [
1665
1687
  b for b in buckets if b['percent'] >= threshold]
1666
1688
  mpath = os.path.join(d, '.limited')
@@ -1769,7 +1791,7 @@ for acct in manifest.get('accounts', []):
1769
1791
  # 2026-09-04 and needed the identical brake with it.
1770
1792
  cleared = os.path.join(d, '.client-limit-cleared')
1771
1793
  with open(cleared + '.tmp', 'w') as f:
1772
- f.write(f'{int(now)}\n')
1794
+ f.write(f'{max(_prior_mark(cleared), int(now))}\n')
1773
1795
  os.replace(cleared + '.tmp', cleared)
1774
1796
  # On a no-data pass `live` is empty and maxp is 0 only because
1775
1797
  # nothing was reported (see informative() above). Log that, instead