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.
- package/CLAUDE_ACCS_TASK.md +10 -5
- package/README.md +47 -19
- package/bin/claude +225 -64
- package/bin/claude-accounts +34 -9
- package/bin/codex +202 -41
- package/bin/codex-accounts +4 -1
- package/docs/ACCOUNT_OPERATIONS.md +9 -4
- package/docs/TRACK_PROMPT.md +76 -0
- package/docs/UNIFIED_SELECTOR.md +40 -7
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/selector_policy.py +42 -7
- package/lib/selector_primitives.py +25 -16
- package/package.json +1 -1
- package/tests/run-tests.sh +412 -41
- package/tests/test_selector.py +104 -2
package/tests/run-tests.sh
CHANGED
|
@@ -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:
|
|
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
|
-
#
|
|
335
|
-
#
|
|
336
|
-
|
|
337
|
-
|
|
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 "
|
|
345
|
-
|| t_fail "
|
|
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
|
-
#
|
|
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 "
|
|
355
|
-
|| t_fail "session
|
|
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 ->
|
|
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
|
-
|
|
456
|
-
|
|
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
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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":
|
|
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
|
-
#
|
|
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:
|
|
3313
|
-
|| t_fail "codex
|
|
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 ->
|
|
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
|
|
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
|
|
3626
|
-
#
|
|
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)"
|