claude-multiacc 2.0.34 → 2.0.35

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/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
@@ -1651,6 +1651,15 @@ def marker_bucket(txt):
1651
1651
  return part[7:]
1652
1652
  return ''
1653
1653
 
1654
+ def session_marker(txt):
1655
+ # The self-healing 5h/session window, named the way each client writes it:
1656
+ # claude's own type is `five_hour`, codex labels a short window `client:5h`, and a
1657
+ # bare `session` covers the writer's own marker. Anything else (a weekly name, an
1658
+ # unparseable line) is NOT a session marker and keeps its existing handling.
1659
+ b = marker_bucket(txt).lower()
1660
+ return 'five_hour' in b or '5h' in b or 'session' in b
1661
+
1662
+
1654
1663
  def weekly_marker(txt):
1655
1664
  # Matched the way both shims match it (bin/claude ~303, same token list): the
1656
1665
  # claude client writes client:seven_day / client:seven_day_opus, the codex client
@@ -2325,6 +2334,7 @@ for acct in manifest.get('accounts', []):
2325
2334
  # So: a session bucket that said nothing inside a document that DID answer reads 0.
2326
2335
  # A document where NOTHING answered still writes no signals at all (see below) —
2327
2336
  # the 2026-09-04 all-zero shape stays unknown, which is the whole point.
2337
+ session_measured = bool(session)
2328
2338
  if sessionp is None and live:
2329
2339
  sessionp = 0
2330
2340
  # How long weekly_percent keeps meaning something. A weekly bucket only ever RISES
@@ -2345,6 +2355,16 @@ for acct in manifest.get('accounts', []):
2345
2355
  out['weekly_percent'] = weeklyp
2346
2356
  if sessionp is not None:
2347
2357
  out['session_percent'] = sessionp
2358
+ if sessionp is not None and not session_measured:
2359
+ # Ranking may use this 0 (that is the whole point — an idle account has to get
2360
+ # back into the band), but NOTHING may treat it as proof of recovery. 2026-09-22,
2361
+ # acct-14: a client:five_hour marker written from a real 429 at 13:06:31Z, and
2362
+ # telemetry fetched at 13:04:16Z — WHILE the account was being rejected — still
2363
+ # reported the session bucket `percent: 0, resets_at: null`. So a silent session
2364
+ # bucket does NOT prove an idle account; the endpoint simply may not report the
2365
+ # 5h window at all. The marker was cleared anyway, `--resume` landed straight on
2366
+ # it, and the run was rejected inside a minute. Recovery has to be MEASURED.
2367
+ out['session_inferred'] = True
2348
2368
  if weeklyp is not None:
2349
2369
  # Written with weekly_percent or not at all: the horizon describes THAT
2350
2370
  # reading, and the shim's degraded path needs both or neither.
@@ -2446,6 +2466,13 @@ for acct in manifest.get('accounts', []):
2446
2466
  keep = True
2447
2467
  elif active and is_client and weekly_marker(txt):
2448
2468
  keep = True
2469
+ elif active and is_client and session_marker(txt) and not session_measured:
2470
+ # Same rule as the weekly one, one window down: a rejection the CLIENT
2471
+ # reported is only disproved by telemetry that actually looked at the
2472
+ # window it names. `max_percent` here comes from the WEEKLY buckets
2473
+ # when the session bucket is silent, and clearing a 5h marker on it is
2474
+ # the cross-signal borrow the aggregation above refuses to make.
2475
+ keep = True
2449
2476
  elif active and ('reason=error-cooldown' in txt
2450
2477
  or (is_client and recent_client)):
2451
2478
  keep = True
@@ -1154,6 +1154,14 @@ 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
+
1157
1165
  def weekly_marker(txt):
1158
1166
  # Matched the way both shims match it (bin/claude ~303, same token list): the
1159
1167
  # claude client writes client:seven_day / client:seven_day_opus, and the codex
@@ -1599,6 +1607,7 @@ for acct in manifest.get('accounts', []):
1599
1607
  # one is never silent — it reports at/near 100% with the reset it is waiting on.
1600
1608
  # So a missing or silent session reads 0 whenever the payload answered at all; a
1601
1609
  # payload where NOTHING answered still writes no signals and stays unknown below.
1610
+ session_measured = bool(session)
1602
1611
  if sessionp is None and live:
1603
1612
  sessionp = 0
1604
1613
  try:
@@ -1619,6 +1628,13 @@ for acct in manifest.get('accounts', []):
1619
1628
  out['weekly_percent'] = weeklyp
1620
1629
  if sessionp is not None:
1621
1630
  out['session_percent'] = sessionp
1631
+ if sessionp is not None and not session_measured:
1632
+ # Ranking may use this 0 — an idle account has to get back into the band — but
1633
+ # nothing may read it as proof of recovery. codex reports no window at all for a
1634
+ # closed one, so silence here is the NORMAL shape and says nothing whatsoever
1635
+ # about a 429 the client actually received. See the claude writer for the live
1636
+ # case that proved it (acct-14, 2026-09-22).
1637
+ out['session_inferred'] = True
1622
1638
  if not live:
1623
1639
  # Nothing usable in the entire payload. Keep the diagnostics (fetched_at,
1624
1640
  # source, plan, the raw buckets) and write NONE of the percent signals: a
@@ -1727,6 +1743,13 @@ for acct in manifest.get('accounts', []):
1727
1743
  keep = True
1728
1744
  elif active and is_client and weekly_marker(txt):
1729
1745
  keep = True
1746
+ elif active and is_client and session_marker(txt) and not session_measured:
1747
+ # Same rule one window down: a rejection the CLIENT reported is only
1748
+ # disproved by telemetry that looked at the window it names. With the
1749
+ # session window silent, max_percent is the WEEKLY peak, and clearing
1750
+ # a 5h marker on it is the cross-signal borrow the aggregation above
1751
+ # refuses to make.
1752
+ keep = True
1730
1753
  elif active and ('reason=error-cooldown' in txt
1731
1754
  or (is_client and recent_client)):
1732
1755
  keep = True
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.34",
3
+ "version": "2.0.35",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2539,6 +2539,76 @@ EOF
2539
2539
  [ $? -eq 0 ] && t_ok "an all-zero payload still writes no session_percent (2026-09-04 holds)" \
2540
2540
  || t_fail "allzero still unknown" "see $ND/acct-01/limits.json"
2541
2541
 
2542
+ # ---- 16-idle-marker. an INFERRED session 0 may rank, but it may never unpark ---------
2543
+ # 2026-09-22, the second half of the same day: with idle accounts ranking again, the
2544
+ # operator's `claude --resume` landed on acct-14 and was rejected inside a minute.
2545
+ # selection.log: "acct-14 client limit cleared by newer telemetry (0%)" one second before
2546
+ # the pick, and "LIMITED by its own session (five_hour)" 54s after it. acct-14 held a
2547
+ # client:five_hour marker from a REAL 429, and its telemetry — fetched at 13:04:16Z,
2548
+ # WHILE the client was being rejected — still reported the session bucket `percent: 0,
2549
+ # resets_at: null`. So a silent session bucket does NOT prove an idle account: the
2550
+ # endpoint may simply never report the 5h window. Recovery has to be MEASURED. The
2551
+ # clearing rule read max_percent, which is the WEEKLY peak when the session bucket is
2552
+ # silent — the cross-signal borrow the aggregation itself refuses to make.
2553
+ mk_client_marker() { # mk_client_marker <acct dir> <bucket> <age seconds> <reset in>
2554
+ python3 - "$1" "$2" "$3" "$4" <<'PJ'
2555
+ import sys, time, os
2556
+ d, bucket, age, ahead = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
2557
+ now = int(time.time())
2558
+ marked = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(now - age))
2559
+ with open(os.path.join(d, '.limited'), 'w') as f:
2560
+ f.write(f'{now + ahead}\n'
2561
+ f'bucket=client:{bucket} percent=100 marked_at={marked} '
2562
+ f'reason=client-rate-limit\n')
2563
+ PJ
2564
+ }
2565
+ # the acct-14 shape: a live 5h rejection, and telemetry whose session bucket is silent
2566
+ mk_client_marker "$ND/acct-01" five_hour 1200 7200
2567
+ ndlimits "$WORK/usage-weekly-only.json" --quiet >/dev/null
2568
+ { [ -f "$ND/acct-01/.limited" ] \
2569
+ && python3 -c "import json,sys; d=json.load(open('$ND/acct-01/limits.json')); sys.exit(0 if d.get('session_inferred') is True and d['session_percent'] == 0 else 1)"; } \
2570
+ && t_ok "an inferred session 0 cannot clear a client:five_hour marker (writer)" \
2571
+ || t_fail "inferred unpark (writer)" "marker gone, or session_inferred not recorded"
2572
+
2573
+ # ...and the SHIM must agree, because it is the shim that logged the clear before the
2574
+ # bad pick. The rival is WORSE on weekly and must still take every launch.
2575
+ ndl 60 10 60 > "$ND/acct-02/limits.json"
2576
+ rm -f "$ND/.pick-seq" "$ND"/acct-0*/.last-pick "$ND/acct-02/.limited"
2577
+ : > "$ND/selection.log"
2578
+ nd_park_hits=0
2579
+ for _ in $(seq 1 6); do
2580
+ case "$(CLAUDE_ACCOUNTS_ROOT="$ND" claude 2>&1)" in *CFG=acct-01*) nd_park_hits=$((nd_park_hits+1)) ;; esac
2581
+ done
2582
+ { [ "$nd_park_hits" = "0" ] && [ -f "$ND/acct-01/.limited" ] \
2583
+ && ! grep -q "cleared by newer telemetry" "$ND/selection.log"; } \
2584
+ && t_ok "the shim never unparks a 5h marker on an inferred reading (0 of 6 picks)" \
2585
+ || t_fail "inferred unpark (shim)" \
2586
+ "the parked account took $nd_park_hits of 6; $(tail -1 "$ND/selection.log" 2>/dev/null)"
2587
+
2588
+ # The #22 behaviour that must SURVIVE: a MEASURED low session reading still clears a 5h
2589
+ # marker. That window self-heals in hours, which is why #22 (2026-09-03) had to clear it
2590
+ # — this fix narrows the evidence, it does not park an account until its reset.
2591
+ mk_client_marker "$ND/acct-01" five_hour 1200 7200
2592
+ cat > "$WORK/usage-measured-session.json" <<'EOF'
2593
+ {"limits":[
2594
+ {"kind":"session","group":"session","percent":5,"resets_at":"2099-01-01T00:00:00+00:00","scope":null},
2595
+ {"kind":"weekly_all","group":"weekly","percent":37,"resets_at":"2099-01-05T00:00:00+00:00","scope":null}
2596
+ ]}
2597
+ EOF
2598
+ ndlimits "$WORK/usage-measured-session.json" --quiet >/dev/null
2599
+ { [ ! -f "$ND/acct-01/.limited" ] \
2600
+ && python3 -c "import json,sys; d=json.load(open('$ND/acct-01/limits.json')); sys.exit(0 if 'session_inferred' not in d and d['session_percent'] == 5 else 1)"; } \
2601
+ && t_ok "a MEASURED low session reading still clears a five_hour marker (#22 holds)" \
2602
+ || t_fail "measured recovery" "see $ND/acct-01/.limited"
2603
+
2604
+ # A WEEKLY marker is untouched by any of this — it was already un-clearable.
2605
+ mk_client_marker "$ND/acct-01" seven_day 1200 7200
2606
+ ndlimits "$WORK/usage-measured-session.json" --quiet >/dev/null
2607
+ [ -f "$ND/acct-01/.limited" ] \
2608
+ && t_ok "a weekly marker still outlives even a measured reading (2026-09-04 holds)" \
2609
+ || t_fail "weekly marker cleared" "see $ND/acct-01"
2610
+ rm -f "$ND"/acct-0*/.limited "$ND"/acct-0*/.client-limit-cleared
2611
+
2542
2612
  # ---- 16-nodata-blind. a fresh timestamp is not a usable reading ---------------------
2543
2613
  # The third face of the same defect: blindness was judged on fetched_at alone, so a pool
2544
2614
  # of freshly-written no_data documents looked FRESH — selection.log carried no
@@ -5990,6 +6060,34 @@ for k in ('max_percent', 'weekly_percent', 'session_percent'):
5990
6060
  EOF
5991
6061
  [ $? -eq 0 ] && t_ok "codex: an all-zero payload still writes no session_percent (2026-09-04 holds)" \
5992
6062
  || t_fail "codex allzero still unknown" "see $CD/acct-01/limits.json"
6063
+
6064
+ # ...and the same guard one window down: an INFERRED session 0 may rank, never unpark.
6065
+ # codex reports no window at all for a closed one, so silence is its NORMAL shape and
6066
+ # says nothing about a 429 the client actually received. codex's only telemetry-driven
6067
+ # clear lives here in `limits` (the shim has none — marker_active waits for the reset),
6068
+ # so this is the single place the rule has to hold.
6069
+ mk_client_marker "$CD/acct-01" 5h 1200 7200
6070
+ cdlimits "$WORK/cx-usage-weekly-only.json" --quiet >/dev/null
6071
+ { [ -f "$CD/acct-01/.limited" ] \
6072
+ && python3 -c "import json,sys; d=json.load(open('$CD/acct-01/limits.json')); sys.exit(0 if d.get('session_inferred') is True and d['session_percent'] == 0 else 1)"; } \
6073
+ && t_ok "codex: an inferred session 0 cannot clear a client:5h marker" \
6074
+ || t_fail "codex inferred unpark" "marker gone, or session_inferred not recorded"
6075
+
6076
+ # ...while a MEASURED short-window reading still clears it (#22 holds on codex too).
6077
+ mk_client_marker "$CD/acct-01" 5h 1200 7200
6078
+ cat > "$WORK/cx-usage-measured-session.json" <<EOF
6079
+ {"email":"nd@cx","plan_type":"pro",
6080
+ "rate_limit":{"allowed":true,"limit_reached":false,
6081
+ "primary_window":{"used_percent":5,"limit_window_seconds":18000,"reset_at":$((now+9000))},
6082
+ "secondary_window":{"used_percent":37,"limit_window_seconds":604800,"reset_at":$((now+90000))}},
6083
+ "additional_rate_limits":[]}
6084
+ EOF
6085
+ cdlimits "$WORK/cx-usage-measured-session.json" --quiet >/dev/null
6086
+ { [ ! -f "$CD/acct-01/.limited" ] \
6087
+ && python3 -c "import json,sys; d=json.load(open('$CD/acct-01/limits.json')); sys.exit(0 if 'session_inferred' not in d and d['session_percent'] == 5 else 1)"; } \
6088
+ && t_ok "codex: a MEASURED low session reading still clears a 5h marker (#22 holds)" \
6089
+ || t_fail "codex measured recovery" "see $CD/acct-01/.limited"
6090
+ rm -f "$CD"/acct-0*/.limited "$CD"/acct-0*/.client-limit-cleared
5993
6091
  rm -f "$CD"/acct-0*/.limited
5994
6092
 
5995
6093
  # ---- C14. oauth refresh via the token endpoint --------------------------------------