claude-multiacc 2.0.19 → 2.0.21

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.
@@ -317,7 +317,7 @@ check "passthrough with CLAUDE_CODE_OAUTH_TOKEN" "CFG=none" "$out"
317
317
  out="$(CLAUDE_MULTIACC_DISABLE=1 claude 2>&1)"
318
318
  check "passthrough when disabled" "CFG=none" "$out"
319
319
 
320
- # ---- 5. headroom selection: most WEEKLY headroom wins (session is only a tiebreaker) --
320
+ # ---- 5. headroom selection: the session gate first, then the WEEKLY headroom band ----
321
321
  lj() { # lj <weekly> <session> <max> -> a fresh limits.json body
322
322
  printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":%s,"max_percent":%s,"buckets":[]}' "$now" "$1" "$2" "$3"
323
323
  }
@@ -331,28 +331,160 @@ done
331
331
  [ "$all2" = "1" ] && t_ok "picks the highest weekly-headroom account (weekly 20 over 80)" \
332
332
  || t_fail "headroom selection" "picked the more-utilized account"
333
333
 
334
- # THE KEY CASE from the research: a high (but sub-threshold) SESSION bucket must NOT
335
- # deprioritize an account whose weekly headroom is better. acct-01: session 85, weekly 10;
336
- # acct-02: session 20, weekly 70. Both eligible (max<90). acct-01 is the better pick —
337
- # its near-full bucket is the 5h session (self-heals), weekly is nearly untouched.
334
+ # The default 30-point band prevents strict headroom ranking from burning one account
335
+ # to zero while its near-peers idle. Both boundaries participate; 31 points does not.
336
+ lj 0 10 10 > "$ACC/acct-01/limits.json"
337
+ lj 30 10 30 > "$ACC/acct-02/limits.json"
338
+ hits1=0; hits2=0
339
+ for _ in $(seq 1 20); do
340
+ case "$(claude 2>&1)" in
341
+ *CFG=acct-01*) hits1=$((hits1+1)) ;;
342
+ *CFG=acct-02*) hits2=$((hits2+1)) ;;
343
+ esac
344
+ done
345
+ { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
346
+ && t_ok "30-point headroom band spreads direct Claude launches (acct-01=$hits1 acct-02=$hits2)" \
347
+ || t_fail "Claude headroom band spread" "acct-01=$hits1 acct-02=$hits2"
348
+ lj 31 10 31 > "$ACC/acct-02/limits.json"
349
+ all1=1
350
+ for _ in $(seq 1 10); do
351
+ case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
352
+ done
353
+ [ "$all1" = 1 ] && t_ok "an account 31 points behind stays outside the Claude band" \
354
+ || t_fail "Claude headroom band boundary" "the 31-point account was selected"
355
+ lj 20 10 20 > "$ACC/acct-02/limits.json"
356
+ all1=1
357
+ for _ in $(seq 1 10); do
358
+ case "$(CLAUDE_MULTIACC_HEADROOM_BAND=0 claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
359
+ done
360
+ [ "$all1" = 1 ] && t_ok "Claude headroom band 0 restores strict ranking" \
361
+ || t_fail "Claude zero headroom band" "the runner-up was selected"
362
+
363
+ # THE KEY CASE, REVERSED on 2026-09-03. It used to assert that a high (but
364
+ # sub-threshold) SESSION bucket must not deprioritize an account with better weekly
365
+ # headroom. The operator asked for the opposite: "among accounts where high session
366
+ # limits it must choose randomly from ones where highest weekly limits" — an account
367
+ # whose 5h bucket is nearly spent is about to be rejected whatever its weekly headroom.
368
+ # So the SESSION GATE (default 50) is the FIRST cut and the 30-point weekly band ranks
369
+ # only what clears it. acct-01: session 85 (past the gate), weekly 10; acct-02: session
370
+ # 20, weekly 70. Both eligible (max<90) — acct-02 wins every time now.
338
371
  lj 10 85 85 > "$ACC/acct-01/limits.json"
339
372
  lj 70 20 70 > "$ACC/acct-02/limits.json"
373
+ all2=1
374
+ for _ in $(seq 1 15); do
375
+ case "$(claude 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
376
+ done
377
+ [ "$all2" = "1" ] && t_ok "a session bucket past the gate is skipped while a fresher one exists (70w/20s over 10w/85s)" \
378
+ || t_fail "session gate" "the account with its 5h bucket at 85% was still selected"
379
+ # The pick has to be auditable: the log names the gate and how many cleared it.
380
+ : > "$ACC/selection.log"
381
+ claude >/dev/null 2>&1
382
+ grep -qE 'acct-02 weekly=70% session=20% band=30 band-count=1 session-gate=50 session-ok=1 pwd=' "$ACC/selection.log" \
383
+ && t_ok "the selection log carries session-gate=50 session-ok=1 when one of two clears" \
384
+ || t_fail "session gate log" "$(tail -1 "$ACC/selection.log")"
385
+
386
+ # Both session buckets inside the gate: the gate has nothing to say and weekly decides,
387
+ # exactly as before the change.
388
+ lj 10 45 45 > "$ACC/acct-01/limits.json"
389
+ lj 70 20 70 > "$ACC/acct-02/limits.json"
390
+ all1=1
391
+ for _ in $(seq 1 15); do
392
+ case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
393
+ done
394
+ [ "$all1" = "1" ] && t_ok "with both sessions inside the gate weekly headroom decides (10w/45s over 70w/20s)" \
395
+ || t_fail "session gate no-op" "the gate changed a ranking where every candidate cleared it"
396
+
397
+ # Nobody clears the gate: it COMPARES candidates, it never empties the pool — it steps
398
+ # aside and weekly ranks the whole set (10w wins), and the log says session-ok=0.
399
+ lj 10 85 85 > "$ACC/acct-01/limits.json"
400
+ lj 70 60 70 > "$ACC/acct-02/limits.json"
401
+ : > "$ACC/selection.log"
340
402
  all1=1
341
403
  for _ in $(seq 1 15); do
342
404
  case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
343
405
  done
344
- [ "$all1" = "1" ] && t_ok "high session does NOT beat better weekly headroom (10w/85s over 70w/20s)" \
345
- || t_fail "weekly-over-session" "ranked the account with less weekly headroom higher"
406
+ [ "$all1" = "1" ] && t_ok "with nobody inside the gate it steps aside and weekly ranks (10w/85s over 70w/60s)" \
407
+ || t_fail "session gate step-aside" "an empty gate emptied the pool instead of stepping aside"
408
+ grep -qE 'acct-01 weekly=10% session=85% band=30 band-count=1 session-gate=50 session-ok=0 pwd=' "$ACC/selection.log" \
409
+ && t_ok "the selection log carries session-ok=0 when the gate steps aside" \
410
+ || t_fail "session gate log" "$(tail -1 "$ACC/selection.log")"
411
+
412
+ # The gate is a knob, like the band: 100 turns it off and pure weekly ranking returns.
413
+ lj 10 85 85 > "$ACC/acct-01/limits.json"
414
+ lj 70 20 70 > "$ACC/acct-02/limits.json"
415
+ all1=1
416
+ for _ in $(seq 1 15); do
417
+ case "$(CLAUDE_MULTIACC_SESSION_GATE=100 claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
418
+ done
419
+ [ "$all1" = "1" ] && t_ok "CLAUDE_MULTIACC_SESSION_GATE=100 disables the gate" \
420
+ || t_fail "session gate off" "the gate still fired at 100"
421
+ # ...and garbage in that env var falls back to the 50-point default, never to "off".
422
+ all2=1
423
+ for _ in $(seq 1 15); do
424
+ case "$(CLAUDE_MULTIACC_SESSION_GATE=abc claude 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
425
+ done
426
+ [ "$all2" = "1" ] && t_ok "a non-numeric CLAUDE_MULTIACC_SESSION_GATE falls back to 50" \
427
+ || t_fail "session gate validation" "a garbage gate value changed the outcome"
346
428
 
347
- # equal weekly => session breaks the tie toward the account with more session headroom
429
+ # Equal weekly usage: the GATE decides, not a tiebreak. Session stopped being a ranking
430
+ # input inside the band on 2026-09-03 — acct-02's 80-point session bucket simply never
431
+ # reaches the weekly comparison, whether the band is strict or the default 30.
348
432
  lj 40 20 40 > "$ACC/acct-01/limits.json"
349
433
  lj 40 80 80 > "$ACC/acct-02/limits.json"
350
434
  all1=1
435
+ for _ in $(seq 1 15); do
436
+ case "$(CLAUDE_MULTIACC_HEADROOM_BAND=0 claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
437
+ done
438
+ [ "$all1" = "1" ] && t_ok "strict mode: the session gate decides an exact weekly tie" \
439
+ || t_fail "session gate tie" "a weekly tie was not resolved by the session gate"
440
+ all1=1
351
441
  for _ in $(seq 1 15); do
352
442
  case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
353
443
  done
354
- [ "$all1" = "1" ] && t_ok "equal weekly: lower session wins the tiebreak" \
355
- || t_fail "session tiebreak" "did not use session to break a weekly tie"
444
+ [ "$all1" = "1" ] && t_ok "the gate (not the band) removes the session-heavy half of a weekly tie" \
445
+ || t_fail "session gate tie" "the default band let the 80-point session account back in"
446
+
447
+ # Inside the gate, session is NOT a tiebreaker any more: an exact weekly tie between two
448
+ # gate-clearing accounts is a coin flip even in strict mode (it used to go to the lower
449
+ # session, through the old weekly*1000+session score).
450
+ lj 40 20 40 > "$ACC/acct-01/limits.json"
451
+ lj 40 45 45 > "$ACC/acct-02/limits.json"
452
+ hits1=0; hits2=0
453
+ for _ in $(seq 1 20); do
454
+ case "$(CLAUDE_MULTIACC_HEADROOM_BAND=0 claude 2>&1)" in
455
+ *CFG=acct-01*) hits1=$((hits1+1)) ;;
456
+ *CFG=acct-02*) hits2=$((hits2+1)) ;;
457
+ esac
458
+ done
459
+ { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
460
+ && t_ok "strict mode: session does not break an exact weekly tie inside the gate (acct-01=$hits1 acct-02=$hits2)" \
461
+ || t_fail "session tiebreak removed" "acct-01=$hits1 acct-02=$hits2 (want both >0)"
462
+
463
+ # Clearing the gate takes BOTH readings, as in pool-selection.v2: a fresh file with a
464
+ # session reading but no weekly one must not become the sole gate-clearer and win the
465
+ # all-gated tie over an account with a truthful weekly reading — not even with a
466
+ # max_percent to fall back on (the shims used to rank on that; the policy never could).
467
+ # (No writer produces such a file; this pins parity with lib/selector_policy.py.)
468
+ printf '{"fetched_at":%s,"max_percent":10,"session_percent":10,"buckets":[]}' "$now" > "$ACC/acct-01/limits.json"
469
+ lj 20 80 80 > "$ACC/acct-02/limits.json"
470
+ all2=1
471
+ for _ in $(seq 1 15); do
472
+ case "$(claude 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
473
+ done
474
+ [ "$all2" = "1" ] && t_ok "a session reading without a weekly one never clears the gate" \
475
+ || t_fail "gate needs both readings" "an unknown-weekly account beat a truthful weekly reading"
476
+
477
+ # ...and the converse: a weekly reading without a session one is not "known" either — it
478
+ # neither clears the gate nor ranks once the gate steps aside (10w/?s vs 70w/80s -> the
479
+ # 70w account, as in pool-selection.v2, where quota_known needs both readings).
480
+ printf '{"fetched_at":%s,"weekly_percent":10,"max_percent":10,"buckets":[]}' "$now" > "$ACC/acct-01/limits.json"
481
+ lj 70 80 80 > "$ACC/acct-02/limits.json"
482
+ all2=1
483
+ for _ in $(seq 1 15); do
484
+ case "$(claude 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
485
+ done
486
+ [ "$all2" = "1" ] && t_ok "a weekly reading without a session one is unknown to both cuts" \
487
+ || t_fail "known needs both readings" "a session-less weekly reading ranked as known"
356
488
 
357
489
  # fully equal scores spread load across accounts
358
490
  lj 10 10 10 > "$ACC/acct-01/limits.json"
@@ -380,6 +512,10 @@ done
380
512
  { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
381
513
  && t_ok "CLAUDE_SHIM_SELECT=random restores uniform spread" \
382
514
  || t_fail "random opt-out" "acct-01=$hits1 acct-02=$hits2"
515
+ # ...and its log line must not claim a gate that never ran (codex review, 2026-09-04).
516
+ grep -qE 'band=30 band-count=2 session-gate=off session-ok=2 pwd=' "$ACC/selection.log" \
517
+ && t_ok "random mode logs session-gate=off instead of a cut it never made" \
518
+ || t_fail "random mode gate log" "$(grep 'SHIM_SELECT\|session-gate' "$ACC/selection.log" | tail -1)"
383
519
 
384
520
  # Unknown telemetry ranks behind every truthful reading, never as neutral or free.
385
521
  printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$ACC/acct-01/limits.json"
@@ -449,17 +585,35 @@ printf '%s\nbucket=session percent=95 reason=limits\n' "$((now-10))" > "$ACC/acc
449
585
  claude >/dev/null 2>&1
450
586
  [ ! -f "$ACC/acct-01/.limited" ] && t_ok "expired marker auto-cleared" || t_fail "expired marker auto-cleared" "marker still present"
451
587
 
452
- # ---- 9. all limited -> least utilized fallback ------------------------------
588
+ # ---- 9. all limited -> the still-serving accounts go through the same two cuts, strict weekly ----
453
589
  printf '%s\nx\n' "$((now+3600))" > "$ACC/acct-01/.limited"
454
590
  printf '%s\nx\n' "$((now+3600))" > "$ACC/acct-02/.limited"
455
- printf '{"fetched_at":%s,"max_percent":97,"buckets":[]}' "$now" > "$ACC/acct-01/limits.json"
456
- printf '{"fetched_at":%s,"max_percent":91,"buckets":[]}' "$now" > "$ACC/acct-02/limits.json"
591
+ lj 97 10 97 > "$ACC/acct-01/limits.json"
592
+ lj 91 10 91 > "$ACC/acct-02/limits.json"
457
593
  out="$(claude 2>&1)"
458
- check "all-limited falls back to least utilized" "CFG=acct-02" "$out"
594
+ check "all-limited falls back to the still-serving account with the most weekly headroom" "CFG=acct-02" "$out"
459
595
  grep -q "all-limited fallback=acct-02" "$ACC/selection.log" \
460
596
  && t_ok "fallback logged" || t_fail "fallback logged" "no all-limited line in selection.log"
461
597
  rm -f "$ACC/acct-01/.limited" "$ACC/acct-02/.limited"
462
598
 
599
+ # The fallback applies the SAME two cuts: with every account limit-marked but still
600
+ # serving, an account past the session gate (10w/85s) yields to one inside it (70w/20s)
601
+ # even though its weekly headroom is far better — exactly as in ordinary selection.
602
+ # (On the pre-gate rule this picked 10w/85s, the best strict weekly score.)
603
+ printf '%s\nbucket=weekly_all percent=95 marked_at=x reason=limits\n' "$((now+3600))" > "$ACC/acct-01/.limited"
604
+ printf '%s\nbucket=weekly_all percent=95 marked_at=x reason=limits\n' "$((now+3600))" > "$ACC/acct-02/.limited"
605
+ lj 10 85 85 > "$ACC/acct-01/limits.json"
606
+ lj 70 20 70 > "$ACC/acct-02/limits.json"
607
+ all2=1
608
+ for _ in $(seq 1 12); do
609
+ case "$(claude 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
610
+ done
611
+ [ "$all2" = "1" ] && t_ok "the all-limited fallback applies the session gate before strict weekly (70w/20s over 10w/85s)" \
612
+ || t_fail "fallback session gate" "the fallback handed out the account past the session gate"
613
+ grep -q "all-limited fallback=acct-02 weekly=70%" "$ACC/selection.log" \
614
+ && t_ok "the fallback line names the gated pick" || t_fail "fallback gate log" "$(tail -1 "$ACC/selection.log")"
615
+ rm -f "$ACC/acct-01/.limited" "$ACC/acct-02/.limited"
616
+
463
617
  # ---- 9a2. the fallback tells "still serving" from "rejected right now" --------------
464
618
  # acct-01: weekly at 99% — worse headroom, but still answering requests.
465
619
  # acct-02: session at 100% — far better weekly (7%), but every request bounces until
@@ -483,8 +637,8 @@ check "all exhausted: the soonest reset is handed out" "CFG=acct-02" "$out"
483
637
  grep -q "all-exhausted resets_in=" "$ACC/selection.log" \
484
638
  && t_ok "the all-exhausted pick is logged with its reset" || t_fail "all-exhausted log" "no line"
485
639
  rm -f "$ACC/acct-01/.limited" "$ACC/acct-02/.limited"
486
- printf '{"fetched_at":%s,"max_percent":97,"buckets":[]}' "$now" > "$ACC/acct-01/limits.json"
487
- printf '{"fetched_at":%s,"max_percent":91,"buckets":[]}' "$now" > "$ACC/acct-02/limits.json"
640
+ lj 97 10 97 > "$ACC/acct-01/limits.json"
641
+ lj 91 10 91 > "$ACC/acct-02/limits.json"
488
642
 
489
643
  # ---- 9a3. a token park is credential-scoped ----------------------------------------
490
644
  # A dead portable token beside a LIVE login: this session runs the account on the
@@ -688,8 +842,8 @@ rm -f "$ACC/acct-01/server.token"
688
842
  printf '%s' "$DEAD_CREDS" > "$ACC/acct-01/.credentials.json"
689
843
  printf '%s' "$HEALTHY_CREDS" > "$ACC/acct-02/.credentials.json"
690
844
  printf 'sk-ant-oat01-REVOKEDREVOKEDREVOKEDREVOKEDREVOKEDREVOKED' > "$ACC/acct-01/server.token"
691
- printf '{"fetched_at":%s,"max_percent":1,"buckets":[]}' "$now" > "$ACC/acct-01/limits.json"
692
- printf '{"fetched_at":%s,"max_percent":50,"buckets":[]}' "$now" > "$ACC/acct-02/limits.json"
845
+ lj 1 1 1 > "$ACC/acct-01/limits.json" # ranks first, so its preflight is what runs
846
+ lj 50 1 50 > "$ACC/acct-02/limits.json"
693
847
  rm -f "$ACC/.last-pick" "$ACC/acct-01/.expired" "$ACC/acct-01/.server-token-verified"
694
848
  out="$(claude --resume d6ccbac0-6643-4780-a99e-3afa1683478e 2>&1)"
695
849
  check "revoked setup-token fails over before --resume" "CFG=acct-02" "$out"
@@ -1036,7 +1190,8 @@ mkclientlimit() { # mkclientlimit <acct dir> <session id> <resetsAt> [rejection
1036
1190
  }
1037
1191
  SID1="81bf8b20-013f-4414-8878-e0289bec9ad0"
1038
1192
  RESET1=$(( $(date +%s) + 1800 ))
1039
- mkclientlimit "$ACC/acct-01" "$SID1" "$RESET1"
1193
+ REJECTED1="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
1194
+ mkclientlimit "$ACC/acct-01" "$SID1" "$RESET1" "$REJECTED1"
1040
1195
  all2=1
1041
1196
  for _ in $(seq 1 12); do
1042
1197
  case "$(claude 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
@@ -1048,6 +1203,9 @@ first="$(head -1 "$ACC/acct-01/.limited" 2>/dev/null)"
1048
1203
  || t_fail "client marker reset" "want $RESET1, got '${first:-<none>}'"
1049
1204
  grep -q 'reason=client-rate-limit' "$ACC/acct-01/.limited" 2>/dev/null \
1050
1205
  && t_ok "marker is tagged client-rate-limit" || t_fail "client marker reason" "$(cat "$ACC/acct-01/.limited" 2>/dev/null)"
1206
+ grep -q "marked_at=$REJECTED1" "$ACC/acct-01/.limited" 2>/dev/null \
1207
+ && t_ok "client marker preserves the rejection timestamp" \
1208
+ || t_fail "client marker timestamp" "$(cat "$ACC/acct-01/.limited" 2>/dev/null)"
1051
1209
  out="$(CLAUDE_ACCOUNT=acct-01 claude 2>&1)"
1052
1210
  check "explicit pin still wins over a client-reported limit" "CFG=acct-01" "$out"
1053
1211
 
@@ -1055,6 +1213,25 @@ check "explicit pin still wins over a client-reported limit" "CFG=acct-01" "$out
1055
1213
  CLAUDE_MULTIACC_USAGE_URL="file://$WORK/usage-mid.json" claude-accounts limits --quiet
1056
1214
  [ -f "$ACC/acct-01/.limited" ] && t_ok "client-rate-limit marker survives a clean limits refresh" \
1057
1215
  || t_fail "client marker vs limits" "marker was cleared while the window was still open"
1216
+ # A later successful usage read under the threshold supersedes the client marker. The
1217
+ # live pool had five accounts at 0-3% hidden behind days-old client markers, leaving one
1218
+ # nominally-empty (but actually rejected) account to receive every direct launch.
1219
+ # A fleet sync refreshes the file's mtime, so recovery must use its semantic timestamp.
1220
+ { head -1 "$ACC/acct-01/.limited"; \
1221
+ sed 's/marked_at=[^ ]*/marked_at=2020-01-01T00:00:00Z/' "$ACC/acct-01/.limited" | tail -1; \
1222
+ } > "$ACC/acct-01/.limited.tmp"
1223
+ mv "$ACC/acct-01/.limited.tmp" "$ACC/acct-01/.limited"
1224
+ CLAUDE_MULTIACC_USAGE_URL="file://$WORK/usage-mid.json" claude-accounts limits --force --quiet
1225
+ [ ! -f "$ACC/acct-01/.limited" ] \
1226
+ && t_ok "newer low telemetry clears an old client-rate-limit marker" \
1227
+ || t_fail "old client marker vs limits" "marker survived confirmed recovery"
1228
+ [ -f "$ACC/acct-01/.client-limit-cleared" ] \
1229
+ && t_ok "confirmed client-limit recovery records a transcript watermark" \
1230
+ || t_fail "client recovery watermark" "watermark missing"
1231
+ claude >/dev/null 2>&1
1232
+ [ ! -f "$ACC/acct-01/.limited" ] \
1233
+ && t_ok "an old transcript cannot recreate a telemetry-disproved marker" \
1234
+ || t_fail "client recovery watermark" "old rejection recreated the marker"
1058
1235
  rm -f "$ACC/acct-01/.limited"
1059
1236
 
1060
1237
  # an ALREADY-ELAPSED rejection is history, not an exclusion
@@ -1386,7 +1563,9 @@ expected="$(cat "$WORK/stdin13")"
1386
1563
  [ "$out" = "$expected" ] && t_ok "stdin/stdout byte fidelity (-p pipe)" || t_fail "stdin fidelity" "got: $out"
1387
1564
 
1388
1565
  # ---- 14. selection log written ------------------------------------------------
1389
- grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*acct-0[123] weekly=[0-9?]+% session=[0-9?]+% pwd=' "$ACC/selection.log" \
1566
+ log_pattern='^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*acct-0[123] weekly=[0-9?]+% session=[0-9?]+%'
1567
+ log_pattern="$log_pattern band=30 band-count=[0-9]+ session-gate=50 session-ok=[0-9]+ pwd="
1568
+ grep -qE "$log_pattern" "$ACC/selection.log" \
1390
1569
  && t_ok "selection.log format" || t_fail "selection.log format" "no matching lines"
1391
1570
  grep -qE 'sk-ant-oat|accessToken|refreshToken' "$ACC/selection.log" \
1392
1571
  && t_fail "selection.log has no secrets" "a token leaked into the log" \
@@ -1877,10 +2056,13 @@ assert groups["weekly_scoped:Fable"] == "weekly", groups
1877
2056
  EOF
1878
2057
  [ $? -eq 0 ] && t_ok "limits records weekly_percent(55) + session_percent(88) with correct groups" \
1879
2058
  || t_fail "weekly/session classification" "see limits.json"
1880
- # a session bucket at 88 must NOT poison weekly ranking: score uses weekly(55), not 88
1881
- sc="$(python3 -c "import json;d=json.load(open('$ACC/acct-01/limits.json'));print(d['weekly_percent']*1000+d['session_percent'])")"
1882
- [ "$sc" = "55088" ] && t_ok "ranking score weighs weekly over session (55088)" \
1883
- || t_fail "ranking score" "got $sc"
2059
+ # The two numbers must stay SEPARATE on disk. There is no combined score any more
2060
+ # (2026-09-03): the shim GATES on session_percent and BANDS on weekly_percent, so a
2061
+ # session bucket at 88 must never be folded into or allowed to poison — the weekly
2062
+ # reading the band ranks on.
2063
+ sc="$(python3 -c "import json;d=json.load(open('$ACC/acct-01/limits.json'));print('%s/%s' % (d['weekly_percent'], d['session_percent']))")"
2064
+ [ "$sc" = "55/88" ] && t_ok "weekly and session are recorded as separate ranking inputs (55 weekly / 88 session)" \
2065
+ || t_fail "weekly/session ranking inputs" "got $sc"
1884
2066
  rm -f "$ACC/acct-01/limits.json" "$ACC/acct-01/.limited"
1885
2067
 
1886
2068
  # ---- 16-marker. the marker names ONE bucket and carries THAT bucket's reset ----------
@@ -2356,6 +2538,9 @@ EOF
2356
2538
  grep -q "ranking=BLIND" "$SD/selection.log" \
2357
2539
  && t_ok "selection.log records that ranking ran blind" \
2358
2540
  || t_fail "blind ranking log" "no ranking=BLIND line: $(tail -1 "$SD/selection.log")"
2541
+ grep -qE "ranking=BLIND telemetry-age=[0-9]+s band=30 band-count=[0-9]+ session-gate=50 session-ok=0 pwd=" "$SD/selection.log" \
2542
+ && t_ok "the blind line carries both cuts in the standard field order" \
2543
+ || t_fail "blind line fields" "$(tail -1 "$SD/selection.log")"
2359
2544
  grep -q "telemetry-age=9[0-9]\{5\}s" "$SD/selection.log" \
2360
2545
  && t_ok "the blind line carries the age of the outage" \
2361
2546
  || t_fail "blind ranking age" "$(tail -1 "$SD/selection.log")"
@@ -2377,17 +2562,20 @@ EOF
2377
2562
  # ...and telemetry INSIDE the window must still rank. 900s used to be the window,
2378
2563
  # which is below the ~3600s floor the endpoint itself enforces (Retry-After: 3600),
2379
2564
  # so a healthy pool spent most of every hour ranking neutral for no reason.
2380
- for i in 01 02; do
2381
- printf '{"fetched_at":%s,"source":"oauth","max_percent":%s,"weekly_percent":%s,"session_percent":1,"buckets":[]}' \
2382
- "$((now - 1200))" "$((i + 3))" "$((i + 3))" > "$SD/acct-$i/limits.json"
2383
- done
2565
+ # acct-02 is put OUTSIDE the 30-point band (weekly 40 against acct-01's 4) on purpose:
2566
+ # inside the band the two are peers and the pick is a coin flip, which would make this
2567
+ # assertion about the freshness window flaky for reasons that have nothing to do with it.
2568
+ printf '{"fetched_at":%s,"source":"oauth","max_percent":4,"weekly_percent":4,"session_percent":1,"buckets":[]}' \
2569
+ "$((now - 1200))" > "$SD/acct-01/limits.json"
2570
+ printf '{"fetched_at":%s,"source":"oauth","max_percent":40,"weekly_percent":40,"session_percent":1,"buckets":[]}' \
2571
+ "$((now - 1200))" > "$SD/acct-02/limits.json"
2384
2572
  : > "$SD/selection.log"
2385
2573
  CLAUDE_ACCOUNTS_ROOT="$SD" claude >/dev/null 2>&1
2386
2574
  ! grep -q "ranking=BLIND" "$SD/selection.log" \
2387
2575
  && t_ok "20-minute-old telemetry still ranks (window matches the endpoint's own floor)" \
2388
2576
  || t_fail "stale window" "20-minute-old data was treated as blind"
2389
2577
  grep -q "acct-01 weekly=4%" "$SD/selection.log" \
2390
- && t_ok "the pool ranks on real numbers and picks the account with more headroom" \
2578
+ && t_ok "the pool ranks on real numbers and picks the account with more headroom (4% over 40%)" \
2391
2579
  || t_fail "headroom ranking" "$(tail -1 "$SD/selection.log")"
2392
2580
 
2393
2581
  # ---- blind does not mean neutral --------------------------------------------
@@ -2413,6 +2601,10 @@ EOF
2413
2601
  grep -q "ranking=DEGRADED" "$SD/selection.log" \
2414
2602
  && t_ok "a degraded pick is logged as degraded, not as blind" \
2415
2603
  || t_fail "degraded log" "$(tail -1 "$SD/selection.log")"
2604
+ # Session is unknown in a degraded pool, so nobody clears the gate: session-ok=0.
2605
+ grep -qE "ranking=DEGRADED telemetry-age=[0-9]+s band=30 band-count=[0-9]+ session-gate=50 session-ok=0 pwd=" "$SD/selection.log" \
2606
+ && t_ok "the degraded line carries both cuts in the standard field order" \
2607
+ || t_fail "degraded line fields" "$(tail -1 "$SD/selection.log")"
2416
2608
  grep -q "acct-02 weekly=4% .*ranking=DEGRADED" "$SD/selection.log" \
2417
2609
  && t_ok "the degraded line reports the stale reading it actually ranked on" \
2418
2610
  || t_fail "degraded weekly" "$(tail -1 "$SD/selection.log")"
@@ -2658,10 +2850,14 @@ RACER
2658
2850
  # threshold on its session bucket (max 91%). So it is picked whenever it is eligible,
2659
2851
  # and skipped only when the cutoff actually fires — which isolates the cutoff window
2660
2852
  # from the ranking window instead of conflating "excluded" with "outranked".
2853
+ # BOTH sessions sit above the 50-point session gate (91 and 55) deliberately: with
2854
+ # acct-02 inside the gate it would be the only gated candidate and win on the gate
2855
+ # alone, and these assertions would then be measuring the gate instead of the cutoff
2856
+ # window. Nobody clearing the gate makes it step aside, so weekly alone ranks here.
2661
2857
  mk_cutoff_pool() { # $1 = age of both readings, in seconds
2662
2858
  printf '{"fetched_at":%s,"weekly_percent":1,"session_percent":91,"max_percent":91,"weekly_resets_epoch":%s,"buckets":[]}' \
2663
2859
  "$((now - $1))" "$((now + 200000))" > "$SD/acct-01/limits.json"
2664
- printf '{"fetched_at":%s,"weekly_percent":50,"session_percent":5,"max_percent":50,"weekly_resets_epoch":%s,"buckets":[]}' \
2860
+ printf '{"fetched_at":%s,"weekly_percent":50,"session_percent":55,"max_percent":55,"weekly_resets_epoch":%s,"buckets":[]}' \
2665
2861
  "$((now - $1))" "$((now + 200000))" > "$SD/acct-02/limits.json"
2666
2862
  : > "$SD/selection.log"
2667
2863
  rm -f "$SD/.last-pick"
@@ -3292,7 +3488,7 @@ check "codex: passthrough with CODEX_HOME" "CFG=other" "$out"
3292
3488
  out="$(CODEX_MULTIACC_DISABLE=1 codex 2>&1)"
3293
3489
  check "codex: passthrough when disabled" "CFG=none" "$out"
3294
3490
 
3295
- # ---- C3. headroom selection --------------------------------------------------------
3491
+ # ---- C3. headroom selection: session gate first, then the WEEKLY headroom band ------
3296
3492
  cxlj() { printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":%s,"max_percent":%s,"buckets":[]}' "$now" "$1" "$2" "$3"; }
3297
3493
  cxlj 80 10 80 > "$CX/acct-01/limits.json"
3298
3494
  cxlj 20 10 20 > "$CX/acct-02/limits.json"
@@ -3302,15 +3498,139 @@ for _ in $(seq 1 15); do
3302
3498
  done
3303
3499
  [ "$all2" = "1" ] && t_ok "codex: picks the highest weekly-headroom account" \
3304
3500
  || t_fail "codex headroom selection" "picked the more-utilized account"
3305
- # high session must NOT beat better weekly headroom
3501
+ # Keep direct-provider behavior aligned with pool-selection.v2: both accounts inside
3502
+ # the default 30-point band receive launches; a caller can set 0 for strict ranking.
3503
+ cxlj 0 10 10 > "$CX/acct-01/limits.json"
3504
+ cxlj 30 10 30 > "$CX/acct-02/limits.json"
3505
+ hits1=0; hits2=0
3506
+ for _ in $(seq 1 20); do
3507
+ case "$(codex 2>&1)" in
3508
+ *CFG=acct-01*) hits1=$((hits1+1)) ;;
3509
+ *CFG=acct-02*) hits2=$((hits2+1)) ;;
3510
+ esac
3511
+ done
3512
+ { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
3513
+ && t_ok "codex: 30-point headroom band spreads launches (acct-01=$hits1 acct-02=$hits2)" \
3514
+ || t_fail "codex headroom band spread" "acct-01=$hits1 acct-02=$hits2"
3515
+ cxlj 20 10 20 > "$CX/acct-02/limits.json"
3516
+ all1=1
3517
+ for _ in $(seq 1 10); do
3518
+ case "$(CODEX_MULTIACC_HEADROOM_BAND=0 codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
3519
+ done
3520
+ [ "$all1" = 1 ] && t_ok "codex: headroom band 0 restores strict ranking" \
3521
+ || t_fail "codex zero headroom band" "the runner-up was selected"
3522
+ # THE KEY CASE, REVERSED on 2026-09-03, in parity with the claude shim (section 5):
3523
+ # the operator asked for "among accounts where high session limits it must choose
3524
+ # randomly from ones where highest weekly limits", so a nearly-spent 5h bucket is the
3525
+ # FIRST cut and the 30-point weekly band ranks only what clears the gate. acct-01:
3526
+ # session 85 (past the gate), weekly 10; acct-02: session 20, weekly 70 -> acct-02.
3306
3527
  cxlj 10 85 85 > "$CX/acct-01/limits.json"
3307
3528
  cxlj 70 20 70 > "$CX/acct-02/limits.json"
3529
+ all2=1
3530
+ for _ in $(seq 1 15); do
3531
+ case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
3532
+ done
3533
+ [ "$all2" = "1" ] && t_ok "codex: a session bucket past the gate is skipped while a fresher one exists (70w/20s over 10w/85s)" \
3534
+ || t_fail "codex session gate" "the account with its 5h bucket at 85% was still selected"
3535
+ : > "$CX/selection.log"
3536
+ codex >/dev/null 2>&1
3537
+ grep -qE 'acct-02 weekly=70% session=20% band=30 band-count=1 session-gate=50 session-ok=1 pwd=' "$CX/selection.log" \
3538
+ && t_ok "codex: the selection log carries session-gate=50 session-ok=1 when one of two clears" \
3539
+ || t_fail "codex session gate log" "$(tail -1 "$CX/selection.log")"
3540
+ # Both sessions inside the gate: it has nothing to say and weekly decides as before.
3541
+ cxlj 10 45 45 > "$CX/acct-01/limits.json"
3542
+ cxlj 70 20 70 > "$CX/acct-02/limits.json"
3543
+ all1=1
3544
+ for _ in $(seq 1 15); do
3545
+ case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
3546
+ done
3547
+ [ "$all1" = "1" ] && t_ok "codex: with both sessions inside the gate weekly headroom decides (10w/45s over 70w/20s)" \
3548
+ || t_fail "codex session gate no-op" "the gate changed a ranking where every candidate cleared it"
3549
+ # Nobody clears it: the gate compares, it never empties the pool — it steps aside.
3550
+ cxlj 10 85 85 > "$CX/acct-01/limits.json"
3551
+ cxlj 70 60 70 > "$CX/acct-02/limits.json"
3552
+ : > "$CX/selection.log"
3308
3553
  all1=1
3309
3554
  for _ in $(seq 1 15); do
3310
3555
  case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
3311
3556
  done
3312
- [ "$all1" = "1" ] && t_ok "codex: high session does NOT beat better weekly headroom" \
3313
- || t_fail "codex weekly-over-session" "ranked the account with less weekly headroom higher"
3557
+ [ "$all1" = "1" ] && t_ok "codex: with nobody inside the gate it steps aside and weekly ranks (10w/85s over 70w/60s)" \
3558
+ || t_fail "codex session gate step-aside" "an empty gate emptied the pool instead of stepping aside"
3559
+ grep -qE 'acct-01 weekly=10% session=85% band=30 band-count=1 session-gate=50 session-ok=0 pwd=' "$CX/selection.log" \
3560
+ && t_ok "codex: the selection log carries session-ok=0 when the gate steps aside" \
3561
+ || t_fail "codex session gate log" "$(tail -1 "$CX/selection.log")"
3562
+ # The gate is a knob, like the band: 100 turns it off; garbage falls back to 50.
3563
+ cxlj 10 85 85 > "$CX/acct-01/limits.json"
3564
+ cxlj 70 20 70 > "$CX/acct-02/limits.json"
3565
+ all1=1
3566
+ for _ in $(seq 1 15); do
3567
+ case "$(CODEX_MULTIACC_SESSION_GATE=100 codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
3568
+ done
3569
+ [ "$all1" = "1" ] && t_ok "codex: CODEX_MULTIACC_SESSION_GATE=100 disables the gate" \
3570
+ || t_fail "codex session gate off" "the gate still fired at 100"
3571
+ all2=1
3572
+ for _ in $(seq 1 15); do
3573
+ case "$(CODEX_MULTIACC_SESSION_GATE=abc codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
3574
+ done
3575
+ [ "$all2" = "1" ] && t_ok "codex: a non-numeric CODEX_MULTIACC_SESSION_GATE falls back to 50" \
3576
+ || t_fail "codex session gate validation" "a garbage gate value changed the outcome"
3577
+ # Equal weekly usage: the GATE decides, never a tiebreak — strict band or default 30.
3578
+ cxlj 40 20 40 > "$CX/acct-01/limits.json"
3579
+ cxlj 40 80 80 > "$CX/acct-02/limits.json"
3580
+ all1=1
3581
+ for _ in $(seq 1 15); do
3582
+ case "$(CODEX_MULTIACC_HEADROOM_BAND=0 codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
3583
+ done
3584
+ [ "$all1" = "1" ] && t_ok "codex: strict mode: the session gate decides an exact weekly tie" \
3585
+ || t_fail "codex session gate tie" "a weekly tie was not resolved by the session gate"
3586
+ all1=1
3587
+ for _ in $(seq 1 15); do
3588
+ case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
3589
+ done
3590
+ [ "$all1" = "1" ] && t_ok "codex: the gate (not the band) removes the session-heavy half of a weekly tie" \
3591
+ || t_fail "codex session gate tie" "the default band let the 80-point session account back in"
3592
+
3593
+ # Inside the gate, session is NOT a tiebreaker any more: an exact weekly tie between two
3594
+ # gate-clearing accounts is a coin flip even in strict mode (it used to go to the lower
3595
+ # session, through the old weekly*1000+session score).
3596
+ cxlj 40 20 40 > "$CX/acct-01/limits.json"
3597
+ cxlj 40 45 45 > "$CX/acct-02/limits.json"
3598
+ hits1=0; hits2=0
3599
+ for _ in $(seq 1 20); do
3600
+ case "$(CODEX_MULTIACC_HEADROOM_BAND=0 codex 2>&1)" in
3601
+ *CFG=acct-01*) hits1=$((hits1+1)) ;;
3602
+ *CFG=acct-02*) hits2=$((hits2+1)) ;;
3603
+ esac
3604
+ done
3605
+ { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
3606
+ && t_ok "codex: strict mode: session does not break an exact weekly tie inside the gate (acct-01=$hits1 acct-02=$hits2)" \
3607
+ || t_fail "codex: session tiebreak removed" "acct-01=$hits1 acct-02=$hits2 (want both >0)"
3608
+
3609
+ # Clearing the gate takes BOTH readings, as in pool-selection.v2: a fresh file with a
3610
+ # session reading but no weekly one must not become the sole gate-clearer and win the
3611
+ # all-gated tie over an account with a truthful weekly reading — not even with a
3612
+ # max_percent to fall back on (the shims used to rank on that; the policy never could).
3613
+ # (No writer produces such a file; this pins parity with lib/selector_policy.py.)
3614
+ printf '{"fetched_at":%s,"max_percent":10,"session_percent":10,"buckets":[]}' "$now" > "$CX/acct-01/limits.json"
3615
+ cxlj 20 80 80 > "$CX/acct-02/limits.json"
3616
+ all2=1
3617
+ for _ in $(seq 1 15); do
3618
+ case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
3619
+ done
3620
+ [ "$all2" = "1" ] && t_ok "codex: a session reading without a weekly one never clears the gate" \
3621
+ || t_fail "codex: gate needs both readings" "an unknown-weekly account beat a truthful weekly reading"
3622
+
3623
+ # ...and the converse: a weekly reading without a session one is not "known" either — it
3624
+ # neither clears the gate nor ranks once the gate steps aside (10w/?s vs 70w/80s -> the
3625
+ # 70w account, as in pool-selection.v2, where quota_known needs both readings).
3626
+ printf '{"fetched_at":%s,"weekly_percent":10,"max_percent":10,"buckets":[]}' "$now" > "$CX/acct-01/limits.json"
3627
+ cxlj 70 80 80 > "$CX/acct-02/limits.json"
3628
+ all2=1
3629
+ for _ in $(seq 1 15); do
3630
+ case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
3631
+ done
3632
+ [ "$all2" = "1" ] && t_ok "codex: a weekly reading without a session one is unknown to both cuts" \
3633
+ || t_fail "codex: known needs both readings" "a session-less weekly reading ranked as known"
3314
3634
  # equal scores spread load
3315
3635
  cxlj 10 10 10 > "$CX/acct-01/limits.json"
3316
3636
  cxlj 10 10 10 > "$CX/acct-02/limits.json"
@@ -3577,17 +3897,65 @@ c_n="$(printf '%s' "$c_distinct" | wc -w | tr -d ' ')"
3577
3897
  rm -rf "$CX/acct-09"
3578
3898
  rm -f "$CX/.last-pick"
3579
3899
 
3580
- # ---- C6. all limited -> least-utilized fallback -------------------------------------
3900
+ # ---- C6. all limited -> the still-serving accounts go through the same two cuts, strict weekly --
3581
3901
  printf '%s\nbucket=7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
3582
3902
  printf '%s\nbucket=7d percent=99 reason=limits\n' "$((now+3600))" > "$CX/acct-02/.limited"
3583
3903
  cxlj 95 10 95 > "$CX/acct-01/limits.json"
3584
3904
  cxlj 99 10 99 > "$CX/acct-02/limits.json"
3585
3905
  out="$(codex 2>&1)"
3586
- check "codex: all-limited falls back to least utilized" "CFG=acct-01" "$out"
3906
+ check "codex: all-limited falls back to the still-serving account with the most weekly headroom" "CFG=acct-01" "$out"
3587
3907
  grep -q "all-limited fallback=acct-01" "$CX/selection.log" \
3588
3908
  && t_ok "codex: all-limited fallback logged" || t_fail "codex fallback log" "no all-limited line"
3589
3909
  rm -f "$CX"/acct-*/.limited "$CX"/acct-*/limits.json
3590
3910
 
3911
+ # The fallback applies the SAME two cuts (parity with the claude shim, section 9): an
3912
+ # account past the session gate yields to one inside it even with far better weekly.
3913
+ printf '%s\nbucket=7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
3914
+ printf '%s\nbucket=7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-02/.limited"
3915
+ cxlj 10 85 85 > "$CX/acct-01/limits.json"
3916
+ cxlj 70 20 70 > "$CX/acct-02/limits.json"
3917
+ all2=1
3918
+ for _ in $(seq 1 12); do
3919
+ case "$(codex 2>&1)" in *CFG=acct-02*) ;; *) all2=0 ;; esac
3920
+ done
3921
+ [ "$all2" = "1" ] && t_ok "codex: the all-limited fallback applies the session gate before strict weekly (70w/20s over 10w/85s)" \
3922
+ || t_fail "codex fallback session gate" "the fallback handed out the account past the session gate"
3923
+ grep -q "all-limited fallback=acct-02 weekly=70%" "$CX/selection.log" \
3924
+ && t_ok "codex: the fallback line names the gated pick" || t_fail "codex fallback gate log" "$(tail -1 "$CX/selection.log")"
3925
+ rm -f "$CX"/acct-*/.limited "$CX"/acct-*/limits.json
3926
+
3927
+ # ---- C6b. the fallback tells "still serving" from "rejected right now" ----------------
3928
+ # Ported from the claude shim (2026-08-29) when the session gate reached the fallback.
3929
+ # acct-01: 7d window at 99% — worse headroom, but still answering. acct-02: 5h window at
3930
+ # 100% — far better weekly (7%), but every request bounces until the reset.
3931
+ printf '%s\nbucket=7d percent=99 marked_at=x reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
3932
+ printf '%s\nbucket=5h percent=100 marked_at=x reason=limits\n' "$((now+600))" > "$CX/acct-02/.limited"
3933
+ cxlj 99 10 99 > "$CX/acct-01/limits.json"
3934
+ cxlj 7 100 100 > "$CX/acct-02/limits.json"
3935
+ out="$(codex 2>&1)"
3936
+ check "codex: a still-serving limited account beats an exhausted one with more headroom" "CFG=acct-01" "$out"
3937
+ # The codex-review case (2026-09-04): the exhausted account is the only one INSIDE the
3938
+ # session gate; the gate must not resurrect it over a still-serving account outside it.
3939
+ printf '%s\nbucket=7d percent=100 marked_at=x reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
3940
+ printf '%s\nbucket=7d percent=95 marked_at=x reason=limits\n' "$((now+3600))" > "$CX/acct-02/.limited"
3941
+ cxlj 100 20 100 > "$CX/acct-01/limits.json"
3942
+ cxlj 95 85 95 > "$CX/acct-02/limits.json"
3943
+ out="$(codex 2>&1)"
3944
+ check "codex: an exhausted account inside the gate never beats a still-serving one outside it" "CFG=acct-02" "$out"
3945
+ # A real client rejection (a 429 the server sent) is exhausted whatever percent says.
3946
+ printf '%s\nbucket=client:5h percent=95 marked_at=x reason=client-rate-limit\n' "$((now+600))" > "$CX/acct-01/.limited"
3947
+ cxlj 95 10 95 > "$CX/acct-01/limits.json"
3948
+ out="$(codex 2>&1)"
3949
+ check "codex: a client-rejected account is not the fallback while another still serves" "CFG=acct-02" "$out"
3950
+ # Every account exhausted RIGHT NOW: hand out the one that unblocks first.
3951
+ printf '%s\nbucket=5h percent=100 marked_at=x reason=limits\n' "$((now+7200))" > "$CX/acct-01/.limited"
3952
+ printf '%s\nbucket=5h percent=100 marked_at=x reason=limits\n' "$((now+600))" > "$CX/acct-02/.limited"
3953
+ out="$(codex 2>&1)"
3954
+ check "codex: all exhausted: the soonest reset is handed out" "CFG=acct-02" "$out"
3955
+ grep -q "all-exhausted resets_in=" "$CX/selection.log" \
3956
+ && t_ok "codex: the all-exhausted pick is logged with its reset" || t_fail "codex all-exhausted log" "no line"
3957
+ rm -f "$CX"/acct-*/.limited "$CX"/acct-*/limits.json
3958
+
3591
3959
  # ---- C7. dead logins: .expired excludes, heals on newer credential ------------------
3592
3960
  printf '%s\nreason=refresh-denied-http-400 marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
3593
3961
  all2=1
@@ -3607,6 +3975,9 @@ mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
3607
3975
  out="$(CODEX_SHIM_SELECT=random codex 2>&1)"
3608
3976
  [ ! -f "$CX/acct-01/.expired" ] && t_ok "codex: newer auth.json clears a credential park" \
3609
3977
  || t_fail "codex park heal" ".expired survived a newer credential"
3978
+ grep -qE 'band=30 band-count=[0-9]+ session-gate=off session-ok=[0-9]+ pwd=' "$CX/selection.log" \
3979
+ && t_ok "codex: random mode logs session-gate=off instead of a cut it never made" \
3980
+ || t_fail "codex random mode gate log" "$(tail -1 "$CX/selection.log")"
3610
3981
  # an org-blocked park survives a newer credential (policy, not credential)
3611
3982
  printf '%s\nreason=org-blocked marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
3612
3983
  sleep 1
@@ -3622,13 +3993,13 @@ codex >/dev/null 2>&1
3622
3993
  || t_fail "codex soft park" "an elapsed soft park still excluded the account"
3623
3994
 
3624
3995
  # ---- C8. exec auto-retry ------------------------------------------------------------
3625
- # acct-01 gets the strictly better score so headroom selection deterministically
3626
- # picks it FIRST; the scripted failure then forces the retry onto acct-02.
3996
+ # Strict mode makes acct-01's better score deterministic for these retry-path tests;
3997
+ # the scripted failure then forces the retry onto acct-02.
3627
3998
  cx_first_01() { cxlj 5 5 5 > "$CX/acct-01/limits.json"; cxlj 20 20 20 > "$CX/acct-02/limits.json"; }
3628
3999
  cx_first_01
3629
4000
  # rate limit: retry on the other account + self-expiring cooldown for the failed one
3630
4001
  echo "fail:acct-01" > "$FAKE_CTL2"
3631
- out="$(codex exec "hello" < /dev/null 2>&1)"
4002
+ out="$(CODEX_MULTIACC_HEADROOM_BAND=0 codex exec "hello" < /dev/null 2>&1)"
3632
4003
  case "$out" in *CFG=acct-02*) t_ok "codex: exec retries a rate-limited account on another" ;;
3633
4004
  *) t_fail "codex retry" "did not land on acct-02: $out" ;; esac
3634
4005
  if [ -f "$CX/acct-01/.limited" ]; then
@@ -3642,7 +4013,7 @@ rm -f "$CX/acct-01/.limited" "$FAKE_CTL2"
3642
4013
  # auth failure parks (soft) instead of a cooldown
3643
4014
  cx_first_01
3644
4015
  echo "authfail:acct-01" > "$FAKE_CTL2"
3645
- codex exec "hello" < /dev/null >/dev/null 2>&1
4016
+ CODEX_MULTIACC_HEADROOM_BAND=0 codex exec "hello" < /dev/null >/dev/null 2>&1
3646
4017
  if [ -f "$CX/acct-01/.expired" ]; then
3647
4018
  grep -q "reason=auth-error" "$CX/acct-01/.expired" && grep -q "soft_until=" "$CX/acct-01/.expired" \
3648
4019
  && t_ok "codex: auth failure parks the account with a soft stamp" \
@@ -3654,7 +4025,7 @@ rm -f "$CX/acct-01/.expired" "$FAKE_CTL2"
3654
4025
  # org-disabled failure parks as org-blocked
3655
4026
  cx_first_01
3656
4027
  echo "orgfail:acct-01" > "$FAKE_CTL2"
3657
- codex exec "hello" < /dev/null >/dev/null 2>&1
4028
+ CODEX_MULTIACC_HEADROOM_BAND=0 codex exec "hello" < /dev/null >/dev/null 2>&1
3658
4029
  grep -q "reason=org-blocked" "$CX/acct-01/.expired" 2>/dev/null \
3659
4030
  && t_ok "codex: workspace-disabled failure parks as org-blocked" \
3660
4031
  || t_fail "codex org park from run" "marker: $(cat "$CX/acct-01/.expired" 2>/dev/null || echo none)"