claude-multiacc 2.0.20 → 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 +45 -18
- package/bin/claude +113 -59
- package/bin/claude-accounts +4 -1
- package/bin/codex +178 -43
- package/bin/codex-accounts +4 -1
- package/docs/ACCOUNT_OPERATIONS.md +7 -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 +331 -35
- package/tests/test_selector.py +104 -2
|
@@ -11,13 +11,15 @@ from datetime import datetime, timezone
|
|
|
11
11
|
from decimal import Decimal, InvalidOperation
|
|
12
12
|
|
|
13
13
|
SCHEMA = "claude-multiacc/pool-selection.v2"
|
|
14
|
-
# NOT bumped for the headroom band
|
|
15
|
-
# a
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
14
|
+
# NOT bumped for the headroom band, and NOT bumped for the session gate either. This
|
|
15
|
+
# string is a BUILD FENCE, not a changelog: a queued task records the version that
|
|
16
|
+
# reserved its account, and the claim function (agent-sdk
|
|
17
|
+
# 0005_delivery_claim_functions.sql) only lets a runner advertising the same string
|
|
18
|
+
# claim it — while the runner's value is a hardcoded constant shipped in its bundle.
|
|
19
|
+
# With Macs routinely several builds behind, bumping this stops every lagging Mac from
|
|
20
|
+
# claiming any new work until it updates. Both knobs are additive and optional: the
|
|
21
|
+
# request only grows optional keys, the response only grows fields, and every existing
|
|
22
|
+
# consumer keeps reading exactly what it read before.
|
|
21
23
|
SELECTOR_VERSION = "2.0.1"
|
|
22
24
|
TIME_RE = re.compile(
|
|
23
25
|
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-5][0-9]"
|
|
@@ -33,9 +35,13 @@ REQUEST_KEYS = {
|
|
|
33
35
|
"schema", "database_now", "policy", "required_engine", "producer_identity",
|
|
34
36
|
"excluded_identities", "candidates", "reservation_key"}
|
|
35
37
|
# Optional because a caller pinned to an older panel build still sends exactly
|
|
36
|
-
# REQUEST_KEYS; absent, the policy applies its own
|
|
37
|
-
|
|
38
|
+
# REQUEST_KEYS; absent, the policy applies its own defaults. app-robot's panel sends
|
|
39
|
+
# headroom_band from its own setting and never sends session_gate, so the gate default
|
|
40
|
+
# below is what every panel launch gets (operator's 2026-09-03 decision: session
|
|
41
|
+
# headroom gates FIRST, then the weekly band picks among the survivors).
|
|
42
|
+
OPTIONAL_REQUEST_KEYS = {"headroom_band", "session_gate"}
|
|
38
43
|
DEFAULT_HEADROOM_BAND = "30"
|
|
44
|
+
DEFAULT_SESSION_GATE = "50"
|
|
39
45
|
POLICIES = {
|
|
40
46
|
"default_claude", "default_codex", "default_both", "explicit", "producer_retry", "reviewer"}
|
|
41
47
|
|
|
@@ -180,13 +186,16 @@ def request_error(request: object) -> tuple[str, dict] | None:
|
|
|
180
186
|
keys = set(request)
|
|
181
187
|
if not REQUEST_KEYS <= keys or keys - REQUEST_KEYS - OPTIONAL_REQUEST_KEYS:
|
|
182
188
|
return "invalid_request", {"field": "$"}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
for knob in ("headroom_band", "session_gate"):
|
|
190
|
+
if knob not in request:
|
|
191
|
+
continue
|
|
192
|
+
value, _text = decimal_value(request[knob])
|
|
193
|
+
# decimal_value clamps to [0,100] and rejects non-numeric shapes; a knob the
|
|
194
|
+
# caller cannot express exactly must fail loudly rather than silently widen
|
|
195
|
+
# selection to the whole pool (band) or wave every session-heavy account
|
|
196
|
+
# through (gate). Both are validated identically so the panel can send either.
|
|
197
|
+
if value is None or isinstance(request[knob], bool):
|
|
198
|
+
return "invalid_request", {"field": knob}
|
|
190
199
|
try:
|
|
191
200
|
timestamp(request["database_now"])
|
|
192
201
|
except ValueError:
|
package/package.json
CHANGED
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
|
}
|
|
@@ -360,28 +360,131 @@ done
|
|
|
360
360
|
[ "$all1" = 1 ] && t_ok "Claude headroom band 0 restores strict ranking" \
|
|
361
361
|
|| t_fail "Claude zero headroom band" "the runner-up was selected"
|
|
362
362
|
|
|
363
|
-
# THE KEY CASE
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
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.
|
|
367
371
|
lj 10 85 85 > "$ACC/acct-01/limits.json"
|
|
368
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"
|
|
369
390
|
all1=1
|
|
370
391
|
for _ in $(seq 1 15); do
|
|
371
392
|
case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
372
393
|
done
|
|
373
|
-
[ "$all1" = "1" ] && t_ok "
|
|
374
|
-
|| t_fail "
|
|
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"
|
|
375
396
|
|
|
376
|
-
#
|
|
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"
|
|
402
|
+
all1=1
|
|
403
|
+
for _ in $(seq 1 15); do
|
|
404
|
+
case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
405
|
+
done
|
|
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"
|
|
428
|
+
|
|
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.
|
|
377
432
|
lj 40 20 40 > "$ACC/acct-01/limits.json"
|
|
378
433
|
lj 40 80 80 > "$ACC/acct-02/limits.json"
|
|
379
434
|
all1=1
|
|
380
435
|
for _ in $(seq 1 15); do
|
|
381
436
|
case "$(CLAUDE_MULTIACC_HEADROOM_BAND=0 claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
382
437
|
done
|
|
383
|
-
[ "$all1" = "1" ] && t_ok "strict mode:
|
|
384
|
-
|| t_fail "session
|
|
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
|
|
441
|
+
for _ in $(seq 1 15); do
|
|
442
|
+
case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
443
|
+
done
|
|
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"
|
|
385
488
|
|
|
386
489
|
# fully equal scores spread load across accounts
|
|
387
490
|
lj 10 10 10 > "$ACC/acct-01/limits.json"
|
|
@@ -409,6 +512,10 @@ done
|
|
|
409
512
|
{ [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
|
|
410
513
|
&& t_ok "CLAUDE_SHIM_SELECT=random restores uniform spread" \
|
|
411
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)"
|
|
412
519
|
|
|
413
520
|
# Unknown telemetry ranks behind every truthful reading, never as neutral or free.
|
|
414
521
|
printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$ACC/acct-01/limits.json"
|
|
@@ -478,17 +585,35 @@ printf '%s\nbucket=session percent=95 reason=limits\n' "$((now-10))" > "$ACC/acc
|
|
|
478
585
|
claude >/dev/null 2>&1
|
|
479
586
|
[ ! -f "$ACC/acct-01/.limited" ] && t_ok "expired marker auto-cleared" || t_fail "expired marker auto-cleared" "marker still present"
|
|
480
587
|
|
|
481
|
-
# ---- 9. all limited ->
|
|
588
|
+
# ---- 9. all limited -> the still-serving accounts go through the same two cuts, strict weekly ----
|
|
482
589
|
printf '%s\nx\n' "$((now+3600))" > "$ACC/acct-01/.limited"
|
|
483
590
|
printf '%s\nx\n' "$((now+3600))" > "$ACC/acct-02/.limited"
|
|
484
|
-
|
|
485
|
-
|
|
591
|
+
lj 97 10 97 > "$ACC/acct-01/limits.json"
|
|
592
|
+
lj 91 10 91 > "$ACC/acct-02/limits.json"
|
|
486
593
|
out="$(claude 2>&1)"
|
|
487
|
-
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"
|
|
488
595
|
grep -q "all-limited fallback=acct-02" "$ACC/selection.log" \
|
|
489
596
|
&& t_ok "fallback logged" || t_fail "fallback logged" "no all-limited line in selection.log"
|
|
490
597
|
rm -f "$ACC/acct-01/.limited" "$ACC/acct-02/.limited"
|
|
491
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
|
+
|
|
492
617
|
# ---- 9a2. the fallback tells "still serving" from "rejected right now" --------------
|
|
493
618
|
# acct-01: weekly at 99% — worse headroom, but still answering requests.
|
|
494
619
|
# acct-02: session at 100% — far better weekly (7%), but every request bounces until
|
|
@@ -512,8 +637,8 @@ check "all exhausted: the soonest reset is handed out" "CFG=acct-02" "$out"
|
|
|
512
637
|
grep -q "all-exhausted resets_in=" "$ACC/selection.log" \
|
|
513
638
|
&& t_ok "the all-exhausted pick is logged with its reset" || t_fail "all-exhausted log" "no line"
|
|
514
639
|
rm -f "$ACC/acct-01/.limited" "$ACC/acct-02/.limited"
|
|
515
|
-
|
|
516
|
-
|
|
640
|
+
lj 97 10 97 > "$ACC/acct-01/limits.json"
|
|
641
|
+
lj 91 10 91 > "$ACC/acct-02/limits.json"
|
|
517
642
|
|
|
518
643
|
# ---- 9a3. a token park is credential-scoped ----------------------------------------
|
|
519
644
|
# A dead portable token beside a LIVE login: this session runs the account on the
|
|
@@ -717,8 +842,8 @@ rm -f "$ACC/acct-01/server.token"
|
|
|
717
842
|
printf '%s' "$DEAD_CREDS" > "$ACC/acct-01/.credentials.json"
|
|
718
843
|
printf '%s' "$HEALTHY_CREDS" > "$ACC/acct-02/.credentials.json"
|
|
719
844
|
printf 'sk-ant-oat01-REVOKEDREVOKEDREVOKEDREVOKEDREVOKEDREVOKED' > "$ACC/acct-01/server.token"
|
|
720
|
-
|
|
721
|
-
|
|
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"
|
|
722
847
|
rm -f "$ACC/.last-pick" "$ACC/acct-01/.expired" "$ACC/acct-01/.server-token-verified"
|
|
723
848
|
out="$(claude --resume d6ccbac0-6643-4780-a99e-3afa1683478e 2>&1)"
|
|
724
849
|
check "revoked setup-token fails over before --resume" "CFG=acct-02" "$out"
|
|
@@ -1439,7 +1564,7 @@ expected="$(cat "$WORK/stdin13")"
|
|
|
1439
1564
|
|
|
1440
1565
|
# ---- 14. selection log written ------------------------------------------------
|
|
1441
1566
|
log_pattern='^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*acct-0[123] weekly=[0-9?]+% session=[0-9?]+%'
|
|
1442
|
-
log_pattern="$log_pattern band=30 band-count=[0-9]+ pwd="
|
|
1567
|
+
log_pattern="$log_pattern band=30 band-count=[0-9]+ session-gate=50 session-ok=[0-9]+ pwd="
|
|
1443
1568
|
grep -qE "$log_pattern" "$ACC/selection.log" \
|
|
1444
1569
|
&& t_ok "selection.log format" || t_fail "selection.log format" "no matching lines"
|
|
1445
1570
|
grep -qE 'sk-ant-oat|accessToken|refreshToken' "$ACC/selection.log" \
|
|
@@ -1931,10 +2056,13 @@ assert groups["weekly_scoped:Fable"] == "weekly", groups
|
|
|
1931
2056
|
EOF
|
|
1932
2057
|
[ $? -eq 0 ] && t_ok "limits records weekly_percent(55) + session_percent(88) with correct groups" \
|
|
1933
2058
|
|| t_fail "weekly/session classification" "see limits.json"
|
|
1934
|
-
#
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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"
|
|
1938
2066
|
rm -f "$ACC/acct-01/limits.json" "$ACC/acct-01/.limited"
|
|
1939
2067
|
|
|
1940
2068
|
# ---- 16-marker. the marker names ONE bucket and carries THAT bucket's reset ----------
|
|
@@ -2410,6 +2538,9 @@ EOF
|
|
|
2410
2538
|
grep -q "ranking=BLIND" "$SD/selection.log" \
|
|
2411
2539
|
&& t_ok "selection.log records that ranking ran blind" \
|
|
2412
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")"
|
|
2413
2544
|
grep -q "telemetry-age=9[0-9]\{5\}s" "$SD/selection.log" \
|
|
2414
2545
|
&& t_ok "the blind line carries the age of the outage" \
|
|
2415
2546
|
|| t_fail "blind ranking age" "$(tail -1 "$SD/selection.log")"
|
|
@@ -2431,17 +2562,20 @@ EOF
|
|
|
2431
2562
|
# ...and telemetry INSIDE the window must still rank. 900s used to be the window,
|
|
2432
2563
|
# which is below the ~3600s floor the endpoint itself enforces (Retry-After: 3600),
|
|
2433
2564
|
# so a healthy pool spent most of every hour ranking neutral for no reason.
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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"
|
|
2438
2572
|
: > "$SD/selection.log"
|
|
2439
2573
|
CLAUDE_ACCOUNTS_ROOT="$SD" claude >/dev/null 2>&1
|
|
2440
2574
|
! grep -q "ranking=BLIND" "$SD/selection.log" \
|
|
2441
2575
|
&& t_ok "20-minute-old telemetry still ranks (window matches the endpoint's own floor)" \
|
|
2442
2576
|
|| t_fail "stale window" "20-minute-old data was treated as blind"
|
|
2443
2577
|
grep -q "acct-01 weekly=4%" "$SD/selection.log" \
|
|
2444
|
-
&& 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%)" \
|
|
2445
2579
|
|| t_fail "headroom ranking" "$(tail -1 "$SD/selection.log")"
|
|
2446
2580
|
|
|
2447
2581
|
# ---- blind does not mean neutral --------------------------------------------
|
|
@@ -2467,6 +2601,10 @@ EOF
|
|
|
2467
2601
|
grep -q "ranking=DEGRADED" "$SD/selection.log" \
|
|
2468
2602
|
&& t_ok "a degraded pick is logged as degraded, not as blind" \
|
|
2469
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")"
|
|
2470
2608
|
grep -q "acct-02 weekly=4% .*ranking=DEGRADED" "$SD/selection.log" \
|
|
2471
2609
|
&& t_ok "the degraded line reports the stale reading it actually ranked on" \
|
|
2472
2610
|
|| t_fail "degraded weekly" "$(tail -1 "$SD/selection.log")"
|
|
@@ -2712,10 +2850,14 @@ RACER
|
|
|
2712
2850
|
# threshold on its session bucket (max 91%). So it is picked whenever it is eligible,
|
|
2713
2851
|
# and skipped only when the cutoff actually fires — which isolates the cutoff window
|
|
2714
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.
|
|
2715
2857
|
mk_cutoff_pool() { # $1 = age of both readings, in seconds
|
|
2716
2858
|
printf '{"fetched_at":%s,"weekly_percent":1,"session_percent":91,"max_percent":91,"weekly_resets_epoch":%s,"buckets":[]}' \
|
|
2717
2859
|
"$((now - $1))" "$((now + 200000))" > "$SD/acct-01/limits.json"
|
|
2718
|
-
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":[]}' \
|
|
2719
2861
|
"$((now - $1))" "$((now + 200000))" > "$SD/acct-02/limits.json"
|
|
2720
2862
|
: > "$SD/selection.log"
|
|
2721
2863
|
rm -f "$SD/.last-pick"
|
|
@@ -3346,7 +3488,7 @@ check "codex: passthrough with CODEX_HOME" "CFG=other" "$out"
|
|
|
3346
3488
|
out="$(CODEX_MULTIACC_DISABLE=1 codex 2>&1)"
|
|
3347
3489
|
check "codex: passthrough when disabled" "CFG=none" "$out"
|
|
3348
3490
|
|
|
3349
|
-
# ---- C3. headroom selection
|
|
3491
|
+
# ---- C3. headroom selection: session gate first, then the WEEKLY headroom band ------
|
|
3350
3492
|
cxlj() { printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":%s,"max_percent":%s,"buckets":[]}' "$now" "$1" "$2" "$3"; }
|
|
3351
3493
|
cxlj 80 10 80 > "$CX/acct-01/limits.json"
|
|
3352
3494
|
cxlj 20 10 20 > "$CX/acct-02/limits.json"
|
|
@@ -3377,15 +3519,118 @@ for _ in $(seq 1 10); do
|
|
|
3377
3519
|
done
|
|
3378
3520
|
[ "$all1" = 1 ] && t_ok "codex: headroom band 0 restores strict ranking" \
|
|
3379
3521
|
|| t_fail "codex zero headroom band" "the runner-up was selected"
|
|
3380
|
-
#
|
|
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.
|
|
3527
|
+
cxlj 10 85 85 > "$CX/acct-01/limits.json"
|
|
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"
|
|
3553
|
+
all1=1
|
|
3554
|
+
for _ in $(seq 1 15); do
|
|
3555
|
+
case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
3556
|
+
done
|
|
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.
|
|
3381
3563
|
cxlj 10 85 85 > "$CX/acct-01/limits.json"
|
|
3382
3564
|
cxlj 70 20 70 > "$CX/acct-02/limits.json"
|
|
3383
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
|
|
3384
3587
|
for _ in $(seq 1 15); do
|
|
3385
3588
|
case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
|
|
3386
3589
|
done
|
|
3387
|
-
[ "$all1" = "1" ] && t_ok "codex:
|
|
3388
|
-
|| t_fail "codex
|
|
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"
|
|
3389
3634
|
# equal scores spread load
|
|
3390
3635
|
cxlj 10 10 10 > "$CX/acct-01/limits.json"
|
|
3391
3636
|
cxlj 10 10 10 > "$CX/acct-02/limits.json"
|
|
@@ -3652,17 +3897,65 @@ c_n="$(printf '%s' "$c_distinct" | wc -w | tr -d ' ')"
|
|
|
3652
3897
|
rm -rf "$CX/acct-09"
|
|
3653
3898
|
rm -f "$CX/.last-pick"
|
|
3654
3899
|
|
|
3655
|
-
# ---- C6. all limited ->
|
|
3900
|
+
# ---- C6. all limited -> the still-serving accounts go through the same two cuts, strict weekly --
|
|
3656
3901
|
printf '%s\nbucket=7d percent=95 reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
|
|
3657
3902
|
printf '%s\nbucket=7d percent=99 reason=limits\n' "$((now+3600))" > "$CX/acct-02/.limited"
|
|
3658
3903
|
cxlj 95 10 95 > "$CX/acct-01/limits.json"
|
|
3659
3904
|
cxlj 99 10 99 > "$CX/acct-02/limits.json"
|
|
3660
3905
|
out="$(codex 2>&1)"
|
|
3661
|
-
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"
|
|
3662
3907
|
grep -q "all-limited fallback=acct-01" "$CX/selection.log" \
|
|
3663
3908
|
&& t_ok "codex: all-limited fallback logged" || t_fail "codex fallback log" "no all-limited line"
|
|
3664
3909
|
rm -f "$CX"/acct-*/.limited "$CX"/acct-*/limits.json
|
|
3665
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
|
+
|
|
3666
3959
|
# ---- C7. dead logins: .expired excludes, heals on newer credential ------------------
|
|
3667
3960
|
printf '%s\nreason=refresh-denied-http-400 marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
|
|
3668
3961
|
all2=1
|
|
@@ -3682,6 +3975,9 @@ mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
|
3682
3975
|
out="$(CODEX_SHIM_SELECT=random codex 2>&1)"
|
|
3683
3976
|
[ ! -f "$CX/acct-01/.expired" ] && t_ok "codex: newer auth.json clears a credential park" \
|
|
3684
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")"
|
|
3685
3981
|
# an org-blocked park survives a newer credential (policy, not credential)
|
|
3686
3982
|
printf '%s\nreason=org-blocked marked_at=t detail=x\n' "$now" > "$CX/acct-01/.expired"
|
|
3687
3983
|
sleep 1
|