claude-multiacc 2.0.33 → 2.0.34

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.
@@ -2305,6 +2305,28 @@ for acct in manifest.get('accounts', []):
2305
2305
  maxp = max([b['percent'] for b in live] or [0])
2306
2306
  weeklyp = max(weekly) if weekly else None
2307
2307
  sessionp = max(session) if session else None
2308
+ # ...but only the WEEKLY half of that rule is about a missing answer. The two kinds
2309
+ # of bucket go silent for opposite reasons, and 2026-09-22 cost a day of picks to
2310
+ # the difference. A weekly window always exists — it is a fixed calendar week,
2311
+ # running whether or not the account is — so a weekly bucket with nothing to say is
2312
+ # an endpoint that DECLINED, and inventing a number for it is the 2026-09-04 bug.
2313
+ # The 5h SESSION window only exists while it is OPEN: leave an account alone for
2314
+ # five hours and there is no window left to describe, so the endpoint answers
2315
+ # `percent: 0, resets_at: null` — not silence but "nothing has been used". An
2316
+ # exhausted session is never silent: a spent 5h bucket ALWAYS carries the window it
2317
+ # resets in (the live pool on 2026-09-22 — every session bucket above 0% had a real
2318
+ # resets_at, every silent one belonged to an idle account), so a silent session
2319
+ # cannot be hiding a full one. Recording it as UNKNOWN is what broke selection:
2320
+ # pick_best's quota_known rule needs BOTH readings, so an IDLE account — precisely
2321
+ # the one with the most headroom — was scored weekly=100 and dropped out of the
2322
+ # band, leaving only the busy accounts that still held an open 5h window. From
2323
+ # selection.log at 2026-09-22T12:37:36Z: band-count=1, and that one band member was
2324
+ # acct-17 at 95% weekly, while acct-13 and acct-14 sat at 0% and unrankable.
2325
+ # So: a session bucket that said nothing inside a document that DID answer reads 0.
2326
+ # A document where NOTHING answered still writes no signals at all (see below) —
2327
+ # the 2026-09-04 all-zero shape stays unknown, which is the whole point.
2328
+ if sessionp is None and live:
2329
+ sessionp = 0
2308
2330
  # How long weekly_percent keeps meaning something. A weekly bucket only ever RISES
2309
2331
  # until its reset, so before that moment a stale percent is still a valid lower
2310
2332
  # bound and the shim can rank on it when nothing fresher exists; after it, the
@@ -1585,6 +1585,22 @@ for acct in manifest.get('accounts', []):
1585
1585
  maxp = max([b['percent'] for b in live] or [0])
1586
1586
  weeklyp = max(weekly) if weekly else None
1587
1587
  sessionp = max(session) if session else None
1588
+ # ...but only the WEEKLY half of that rule is about a missing answer — same
1589
+ # asymmetry the claude writer documents, and codex feels it harder. A durable
1590
+ # window always exists, so a silent one is an endpoint that DECLINED. A ~5h session
1591
+ # window only exists while it is OPEN, and codex does not even keep a placeholder
1592
+ # for a closed one: an idle account's payload carries the 7d window and NO session
1593
+ # window whatsoever, so `session` here is empty for every quiet account. That made
1594
+ # session_percent permanently absent across this pool, and pick_best's quota_known
1595
+ # rule needs BOTH readings — so EVERY codex account scored unknown, bestw never left
1596
+ # 101, and the band degenerated to the all-gated tie: codex has been picking
1597
+ # uniformly at RANDOM rather than by headroom (selection.log: `session=?%` on every
1598
+ # ranked line). A session window that is closed has nothing in it, and an exhausted
1599
+ # one is never silent — it reports at/near 100% with the reset it is waiting on.
1600
+ # So a missing or silent session reads 0 whenever the payload answered at all; a
1601
+ # payload where NOTHING answered still writes no signals and stays unknown below.
1602
+ if sessionp is None and live:
1603
+ sessionp = 0
1588
1604
  try:
1589
1605
  reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say, int(now))
1590
1606
  except Exception as e:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.33",
3
+ "version": "2.0.34",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2458,10 +2458,18 @@ done
2458
2458
  && t_ok "an account with no weekly reading never enters the band (0 of 10 picks)" \
2459
2459
  || t_fail "session-only ranking" "the weekly-less account took $nd_sess_hits of 10 picks"
2460
2460
 
2461
- # The mirror image: a weekly reading with nothing to say about the session bucket. Here
2462
- # the rival is WORSE on weekly (80 against 37) and still wins every pick, because it is
2463
- # the only candidate that can clear the session gate — which is exactly what a fabricated
2464
- # `session_percent: 0` would have handed the silent account for free.
2461
+ # The mirror image — a weekly reading whose 5h bucket has nothing to say — is NOT the
2462
+ # same defect, and treating it as one broke selection on 2026-09-22. A weekly window
2463
+ # always exists (a fixed calendar week, running whether or not the account is), so a
2464
+ # silent weekly bucket is an endpoint that DECLINED. The 5h window only exists while it
2465
+ # is OPEN, so a silent session bucket is an account that has simply been IDLE — and an
2466
+ # exhausted session is never silent: a spent 5h bucket always reports ~100% with the
2467
+ # reset it is waiting on. Recording idle as UNKNOWN cost the pool its entire band,
2468
+ # because quota_known needs BOTH readings: the accounts with the MOST headroom
2469
+ # (acct-13/acct-14, 0% weekly) were scored 100 and dropped out, leaving acct-17 at 95%
2470
+ # weekly as the only band member — selection.log 2026-09-22T12:37:36Z, band-count=1,
2471
+ # four picks in a row onto an account already out of weekly headroom. A closed window
2472
+ # holds no usage: it reads 0.
2465
2473
  cat > "$WORK/usage-weekly-only.json" <<'EOF'
2466
2474
  {"limits":[
2467
2475
  {"kind":"session","group":"session","percent":0,"resets_at":null,"scope":null},
@@ -2476,10 +2484,33 @@ assert 'no_data' not in d, d
2476
2484
  assert (d['weekly_percent'], d['max_percent']) == (37, 37), d
2477
2485
  assert d['weekly_resets_epoch'] == calendar.timegm(
2478
2486
  time.strptime("2099-01-05T00:00:00", "%Y-%m-%dT%H:%M:%S")), d
2479
- assert 'session_percent' not in d, d # the 5h bucket reported nothing at all
2487
+ # The 5h window is CLOSED, not undocumented, so it holds no usage. max_percent is still
2488
+ # taken over the INFORMATIVE buckets only, so this idle 0 never becomes a bucket reading.
2489
+ assert d['session_percent'] == 0, d
2480
2490
  EOF
2481
- [ $? -eq 0 ] && t_ok "a weekly-only reading records weekly+max+horizon and NO session_percent" \
2482
- || t_fail "weekly-only signals" "see $ND/acct-01/limits.json"
2491
+ [ $? -eq 0 ] && t_ok "an idle 5h bucket beside a real weekly one records session_percent 0" \
2492
+ || t_fail "idle-session signals" "see $ND/acct-01/limits.json"
2493
+
2494
+ # A payload with NO session bucket at ALL is the same story told by omission — the shape
2495
+ # every codex account produces while it is quiet. It must rank, not vanish.
2496
+ cat > "$WORK/usage-no-session-bucket.json" <<'EOF'
2497
+ {"limits":[
2498
+ {"kind":"weekly_all","group":"weekly","percent":37,"resets_at":"2099-01-05T00:00:00+00:00","scope":null}
2499
+ ]}
2500
+ EOF
2501
+ ndlimits "$WORK/usage-no-session-bucket.json" --quiet >/dev/null
2502
+ python3 - "$ND/acct-01/limits.json" <<'EOF'
2503
+ import json, sys
2504
+ d = json.load(open(sys.argv[1]))
2505
+ assert 'no_data' not in d, d
2506
+ assert (d['weekly_percent'], d['session_percent'], d['max_percent']) == (37, 0, 37), d
2507
+ EOF
2508
+ [ $? -eq 0 ] && t_ok "a payload with no session bucket at all still records session_percent 0" \
2509
+ || t_fail "absent-session signals" "see $ND/acct-01/limits.json"
2510
+
2511
+ # ...and that account RANKS: idle plus the better weekly must beat a busier rival — the
2512
+ # pick the pool was losing. acct-02 is worse on weekly (80 vs 37) and clears the gate
2513
+ # too, so the band alone separates them.
2483
2514
  ndl 80 10 80 > "$ND/acct-02/limits.json"
2484
2515
  rm -f "$ND/.pick-seq" "$ND"/acct-0*/.last-pick "$ND"/acct-0*/.limited
2485
2516
  : > "$ND/selection.log"
@@ -2487,12 +2518,26 @@ nd_wk_hits=0
2487
2518
  for _ in $(seq 1 6); do
2488
2519
  case "$(CLAUDE_ACCOUNTS_ROOT="$ND" claude 2>&1)" in *CFG=acct-01*) nd_wk_hits=$((nd_wk_hits+1)) ;; esac
2489
2520
  done
2490
- [ "$nd_wk_hits" = "0" ] \
2491
- && t_ok "an unknown session reading never clears the gate, even with the better weekly (0 of 6)" \
2492
- || t_fail "weekly-only gate" "the session-less account cleared the gate $nd_wk_hits of 6 times"
2493
- grep -q "acct-02 weekly=80% session=10% band=30 band-count=1 session-gate=50 session-ok=1" "$ND/selection.log" \
2494
- && t_ok "the log shows one account clearing the gate and one band member" \
2495
- || t_fail "weekly-only gate log" "$(tail -1 "$ND/selection.log" 2>/dev/null)"
2521
+ [ "$nd_wk_hits" = "6" ] \
2522
+ && t_ok "an idle account with the better weekly takes every pick (6 of 6)" \
2523
+ || t_fail "idle-session ranking" "the idle account took $nd_wk_hits of 6"
2524
+ grep -q "acct-01 weekly=37% session=0% band=30 band-count=1 session-gate=50 session-ok=2" "$ND/selection.log" \
2525
+ && t_ok "the log shows both clearing the gate and the idle account alone in the band" \
2526
+ || t_fail "idle-session log" "$(tail -1 "$ND/selection.log" 2>/dev/null)"
2527
+
2528
+ # The guard that must NOT move: a document where NOTHING answered is still unknown. The
2529
+ # all-zero 2026-09-04 shape has no informative bucket anywhere, so it writes no signals
2530
+ # at all and never picks up a fabricated session 0 on the way out.
2531
+ ndlimits "$WORK/usage-allzero.json" --quiet >/dev/null
2532
+ python3 - "$ND/acct-01/limits.json" <<'EOF'
2533
+ import json, sys
2534
+ d = json.load(open(sys.argv[1]))
2535
+ assert d.get('no_data') is True, d
2536
+ for k in ('max_percent', 'weekly_percent', 'session_percent', 'weekly_resets_epoch'):
2537
+ assert k not in d, (k, d)
2538
+ EOF
2539
+ [ $? -eq 0 ] && t_ok "an all-zero payload still writes no session_percent (2026-09-04 holds)" \
2540
+ || t_fail "allzero still unknown" "see $ND/acct-01/limits.json"
2496
2541
 
2497
2542
  # ---- 16-nodata-blind. a fresh timestamp is not a usable reading ---------------------
2498
2543
  # The third face of the same defect: blindness was judged on fetched_at alone, so a pool
@@ -5872,6 +5917,13 @@ done
5872
5917
  && t_ok "codex: an account with no weekly reading never enters the band (0 of 10 picks)" \
5873
5918
  || t_fail "codex session-only ranking" "the weekly-less account took $cs_hits of 10 picks"
5874
5919
 
5920
+ # The mirror image — a weekly reading whose ~5h window has nothing to say — is NOT the
5921
+ # same defect, and codex feels it hardest: an idle account's payload carries the 7d
5922
+ # window and no usable session window at all, so session_percent went permanently
5923
+ # absent across the whole pool. quota_known needs BOTH readings, so EVERY codex account
5924
+ # scored unknown, bestw never left 101, and the band collapsed into the all-gated tie —
5925
+ # codex picked uniformly at RANDOM instead of by headroom (selection.log 2026-09-22:
5926
+ # `session=?%` on every ranked line). A closed window holds no usage: it reads 0.
5875
5927
  cat > "$WORK/cx-usage-weekly-only.json" <<EOF
5876
5928
  {"email":"nd@cx","plan_type":"pro",
5877
5929
  "rate_limit":{"allowed":true,"limit_reached":false,
@@ -5885,13 +5937,32 @@ import json, sys
5885
5937
  d = json.load(open(sys.argv[1]))
5886
5938
  assert 'no_data' not in d, d
5887
5939
  assert (d['weekly_percent'], d['max_percent']) == (37, 37), d
5888
- assert 'session_percent' not in d, d # the 5h window reported nothing at all
5940
+ # The ~5h window is CLOSED, not undocumented, so it holds no usage. max_percent is still
5941
+ # taken over the INFORMATIVE windows only, so this idle 0 never becomes a reading.
5942
+ assert d['session_percent'] == 0, d
5889
5943
  EOF
5890
- [ $? -eq 0 ] && t_ok "codex: a weekly-only reading records weekly+max and NO session_percent" \
5891
- || t_fail "codex weekly-only signals" "see $CD/acct-01/limits.json"
5892
- # ...and the rivals are WORSE on weekly (80 against 37) and still take every pick,
5893
- # because they are the only ones that can clear the session gate — which is exactly what
5894
- # a fabricated `session_percent: 0` would have handed the silent account for free.
5944
+ [ $? -eq 0 ] && t_ok "codex: an idle 5h window beside a real weekly one records session_percent 0" \
5945
+ || t_fail "codex idle-session signals" "see $CD/acct-01/limits.json"
5946
+
5947
+ # The same story told by omission: no session window in the payload at all.
5948
+ cat > "$WORK/cx-usage-no-session.json" <<EOF
5949
+ {"email":"nd@cx","plan_type":"pro",
5950
+ "rate_limit":{"allowed":true,"limit_reached":false,
5951
+ "secondary_window":{"used_percent":37,"limit_window_seconds":604800,"reset_at":$((now+90000))}},
5952
+ "additional_rate_limits":[]}
5953
+ EOF
5954
+ cdlimits "$WORK/cx-usage-no-session.json" --quiet >/dev/null
5955
+ python3 - "$CD/acct-01/limits.json" <<'EOF'
5956
+ import json, sys
5957
+ d = json.load(open(sys.argv[1]))
5958
+ assert 'no_data' not in d, d
5959
+ assert (d['weekly_percent'], d['session_percent'], d['max_percent']) == (37, 0, 37), d
5960
+ EOF
5961
+ [ $? -eq 0 ] && t_ok "codex: a payload with no session window at all records session_percent 0" \
5962
+ || t_fail "codex absent-session signals" "see $CD/acct-01/limits.json"
5963
+
5964
+ # ...and it RANKS: idle plus the better weekly (37) must beat busier rivals (80), which
5965
+ # is the pick this pool was losing on every single invocation.
5895
5966
  cdl 80 10 80 > "$CD/acct-02/limits.json"
5896
5967
  cdl 80 10 80 > "$CD/acct-03/limits.json"
5897
5968
  rm -f "$CD/.pick-seq" "$CD"/acct-0*/.last-pick
@@ -5900,12 +5971,25 @@ cwk_hits=0
5900
5971
  for _ in $(seq 1 6); do
5901
5972
  case "$(CODEX_ACCOUNTS_ROOT="$CD" codex 2>&1)" in *CFG=acct-01*) cwk_hits=$((cwk_hits+1)) ;; esac
5902
5973
  done
5903
- [ "$cwk_hits" = "0" ] \
5904
- && t_ok "codex: an unknown session reading never clears the gate, even with the better weekly (0 of 6)" \
5905
- || t_fail "codex weekly-only gate" "the session-less account cleared the gate $cwk_hits of 6 times"
5906
- grep -q "band=30 band-count=2 session-gate=50 session-ok=2" "$CD/selection.log" \
5907
- && t_ok "codex: the log shows exactly the two accounts that reported a session bucket" \
5908
- || t_fail "codex weekly-only gate log" "$(tail -1 "$CD/selection.log" 2>/dev/null)"
5974
+ [ "$cwk_hits" = "6" ] \
5975
+ && t_ok "codex: an idle account with the better weekly takes every pick (6 of 6)" \
5976
+ || t_fail "codex idle-session ranking" "the idle account took $cwk_hits of 6"
5977
+ grep -q "acct-01 weekly=37% session=0% band=30 band-count=1 session-gate=50 session-ok=3" "$CD/selection.log" \
5978
+ && t_ok "codex: all three clear the gate and the idle account is alone in the band" \
5979
+ || t_fail "codex idle-session log" "$(tail -1 "$CD/selection.log" 2>/dev/null)"
5980
+
5981
+ # The guard that must NOT move: a payload where NOTHING answered stays unknown, and
5982
+ # never picks up a fabricated session 0 on the way out.
5983
+ cdlimits "$WORK/cx-usage-allzero.json" --quiet >/dev/null
5984
+ python3 - "$CD/acct-01/limits.json" <<'EOF'
5985
+ import json, sys
5986
+ d = json.load(open(sys.argv[1]))
5987
+ assert d.get('no_data') is True, d
5988
+ for k in ('max_percent', 'weekly_percent', 'session_percent'):
5989
+ assert k not in d, (k, d)
5990
+ EOF
5991
+ [ $? -eq 0 ] && t_ok "codex: an all-zero payload still writes no session_percent (2026-09-04 holds)" \
5992
+ || t_fail "codex allzero still unknown" "see $CD/acct-01/limits.json"
5909
5993
  rm -f "$CD"/acct-0*/.limited
5910
5994
 
5911
5995
  # ---- C14. oauth refresh via the token endpoint --------------------------------------