claude-multiacc 2.0.20 → 2.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/codex CHANGED
@@ -3,8 +3,10 @@
3
3
  # Every invocation runs under a randomly picked ChatGPT subscription account with limit
4
4
  # headroom. Self-contained on purpose: no sourcing, so a broken repo file can never
5
5
  # break `codex`.
6
- # Selection: CODEX_HOME passthrough > CODEX_ACCOUNT pin > random among limit-eligible
7
- # accounts in the 30-point headroom band > least-utilized fallback (degraded beats down).
6
+ # Selection: CODEX_HOME passthrough > CODEX_ACCOUNT pin > random among the limit-eligible
7
+ # accounts that clear the 50-point session gate and then sit in the 30-point weekly
8
+ # headroom band > all-limited fallback: the same two cuts over the still-serving limited
9
+ # accounts, strict weekly (degraded beats down).
8
10
  # Accounts whose login is DEAD (a `.expired` marker from a failed refresh/verify/run)
9
11
  # are never selected — not even as the all-limited fallback — because they fail every
10
12
  # call outright; `codex-accounts expired` / `relogin` fix them.
@@ -106,11 +108,38 @@ fi
106
108
  # scraped number goes through here.
107
109
  num_ok() { case "$1" in ''|*[!0-9]*) return 1 ;; esac; [ "${#1}" -le 18 ]; }
108
110
 
111
+ iso_of_epoch() { # $1 seconds -> UTC ISO8601 ('' when neither date(1) dialect works)
112
+ date -u -r "$1" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null
113
+ }
114
+
115
+ # Comparable digit string for an ISO timestamp: 2026-08-21T17:07:59.321Z -> 20260821170759.
116
+ # Locale-proof (plain integers), and short enough that num_ok always passes. Byte-parallel
117
+ # with bin/claude (~637), because client_limit_scan below compares a rollout record's
118
+ # timestamp against the .client-limit-cleared watermark exactly the way that shim does.
119
+ iso_key() { local t="${1%%.*}"; t="$(printf '%s' "$t" | LC_ALL=C tr -cd '0-9')"; printf '%s\n' "${t}"; }
120
+
109
121
  sel_log() {
110
122
  printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" 2>/dev/null >> "$ACC_ROOT/selection.log" || true
111
123
  }
112
124
 
113
125
  marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
126
+ # Parity with bin/claude's client_marker_recovered, by construction: this shim has NO
127
+ # telemetry-based clearing path. A marker leaves here only when its OWN reset epoch has
128
+ # passed, so no usage reading — informative or not — can unpark an account.
129
+ # The bucket names this pool actually carries are written by client_limit_scan below
130
+ # from the rollout's own `window_minutes`: `bucket=client:7d` (>= 1440 minutes) and
131
+ # `bucket=client:5h` (under it), plus a bare `bucket=client:primary|secondary` for a
132
+ # report that named no window at all. Those two raw key names are what the scan wrote
133
+ # before 2026-09-04 and they are NOT weekly tokens — live payloads report `primary` as
134
+ # the 10080-minute window — so the weekly guard could never match a codex marker.
135
+ # That guard is the rule 2026-09-04 forced on the claude side: acct-13/acct-14 were
136
+ # served fake-zero telemetry (every bucket percent 0, resets_at null), which read as 0%
137
+ # and deleted their truthful client:seven_day markers, and an interactive session was
138
+ # handed both weekly-exhausted accounts in a row. Codex's one telemetry-driven clear
139
+ # lives in `codex-accounts limits`: it keeps a client:7d marker until its own reset and
140
+ # clears an aged client:5h one on an informative pass (#22, 2026-09-03), stamping
141
+ # `.client-limit-cleared` as it does so this scan cannot re-mark from the same rollout.
142
+ # If a recovery path is ever added HERE, it must apply that identical rule.
114
143
  local m="$1/.limited" reset=""
115
144
  [ -f "$m" ] || return 1
116
145
  IFS= read -r reset < "$m" 2>/dev/null || reset=""
@@ -140,33 +169,101 @@ fresh_field() { # fresh_field <acct dir> <json key> -> integer if telemetry fres
140
169
  printf '%s\n' "$v"
141
170
  }
142
171
 
143
- # RANKING score lower is better (more headroom). Weekly headroom dominates: a weekly
144
- # window only refills on its multi-day reset, while the ~5h window self-heals, so
145
- # session is a mild tiebreaker only (same reset asymmetry as the claude pool).
146
- # score = weekly%*1000 + session% weekly,session in [0,100]
147
- # Stale/unreadable telemetry ranks LAST (weekly 100, session 100), never "free".
148
- # Equal worst-case scores keep an entirely unknown pool selectable, but an unknown
149
- # account can never beat a candidate with truthful usage telemetry.
150
- sel_score_of() { # $1 = acct dir
151
- local w s
152
- w="$(fresh_field "$1" weekly_percent)" || w="$(fresh_field "$1" max_percent)" || w=100
153
- s="$(fresh_field "$1" session_percent)" || s=100
154
- printf '%s\n' $((w * 1000 + s))
155
- }
172
+ # RANKING — TWO CUTS, in this order, then chance. The operator's ask (2026-09-03):
173
+ # "among accounts where high session limits it must choose randomly from ones where
174
+ # highest weekly limits."
175
+ # 1. SESSION GATE (SESSION_GATE, default 50): a candidate clears the gate when its ~5h
176
+ # session usage is KNOWN and at or under the gate. When at least one candidate clears
177
+ # it, only those are ranked further — an account whose session bucket is nearly spent
178
+ # is about to be rejected whatever its weekly headroom is. When NOBODY clears it the
179
+ # gate steps aside and every candidate is ranked: the gate compares candidates, it
180
+ # never empties the pool.
181
+ # 2. WEEKLY BAND (HEADROOM_BAND, default 30, kept at the operator's request): among the
182
+ # gated candidates the lowest KNOWN weekly usage leads, and every gated candidate
183
+ # within the band of it is a peer. Weekly dominates because a weekly window only
184
+ # refills on its multi-day reset while the ~5h window self-heals (same asymmetry as
185
+ # the claude pool) — which is exactly why session acts as the gate and is NOT a
186
+ # tiebreaker inside the band any more: the operator asked for a random choice among
187
+ # the best-weekly accounts.
188
+ # Stale/unreadable telemetry never clears the gate and never enters the band — an unknown
189
+ # account can never beat a candidate with truthful usage telemetry — but when NOTHING is
190
+ # known the candidates all tie, which keeps an entirely blind pool selectable.
191
+ # Band 0 (or the all-limited PICK_STRICT fallback) means exact weekly ties only; gate 100
192
+ # turns the gate off.
156
193
 
157
194
  HEADROOM_BAND="${CODEX_MULTIACC_HEADROOM_BAND:-30}"
158
195
  case "$HEADROOM_BAND" in ''|*[!0-9]*|??????*) HEADROOM_BAND=30 ;; esac
159
196
  [ "$HEADROOM_BAND" -gt 100 ] && HEADROOM_BAND=100
160
-
197
+ SESSION_GATE="${CODEX_MULTIACC_SESSION_GATE:-50}"
198
+ case "$SESSION_GATE" in ''|*[!0-9]*|??????*) SESSION_GATE=50 ;; esac
199
+ [ "$SESSION_GATE" -gt 100 ] && SESSION_GATE=100
200
+ # What the selection log prints for the gate: "off" in random mode, where no gate ran —
201
+ # the log must never claim a cut that was not made.
202
+ SESSION_GATE_LOG="$SESSION_GATE"
203
+ # The codex shim has no DEGRADED ranking mode (the claude shim ranks on still-valid stale
204
+ # weekly readings when nothing in its pool is fresh). The constant exists so pick_best
205
+ # can stay byte-identical with bin/claude.
206
+ SEL_DEGRADED=0
207
+
208
+ # weekly_percent ONLY. A reading without it is unknown here, exactly as it is unknown to
209
+ # pool-selection.v2 (which never sees max_percent). The old max_percent fallback let a
210
+ # weekly-less file rank — and, once the session gate existed, CLEAR the gate — on a number
211
+ # that may well be the session bucket's own peak (codex review, 2026-09-04).
161
212
  rank_weekly_of() { # $1 = acct dir -> comparable weekly use, or fail when unknown
162
213
  local w
163
- if w="$(fresh_field "$1" weekly_percent)" || w="$(fresh_field "$1" max_percent)"; then
214
+ if w="$(fresh_field "$1" weekly_percent)"; then
164
215
  printf '%s\n' "$w"
165
216
  return 0
166
217
  fi
167
218
  return 1
168
219
  }
169
220
 
221
+ rank_session_of() { # $1 = acct dir -> fresh session use, or fail when unknown
222
+ fresh_field "$1" session_percent
223
+ }
224
+
225
+ # The .limited marker's own fields (line 1: reset epoch; line 2: "bucket=…
226
+ # percent=… … reason=…"). All tolerant: an unreadable or bare marker answers
227
+ # "unknown", never an error — these feed the all-limited fallback only.
228
+ limited_reset_of() { # $1 acct dir -> the marker's reset epoch, or 0 (unknown)
229
+ local m="$1/.limited" r=""
230
+ [ -f "$m" ] && { IFS= read -r r < "$m" 2>/dev/null || r=""; }
231
+ if num_ok "$r"; then printf '%s\n' "$r"; else printf '0\n'; fi
232
+ }
233
+
234
+ limited_percent_of() { # $1 acct dir -> the marked window's percent, or -1 (unknown)
235
+ local m="$1/.limited" line=""
236
+ [ -f "$m" ] && line="$(sed -n 2p "$m" 2>/dev/null)"
237
+ case "$line" in
238
+ *percent=*) line="${line#*percent=}"; line="${line%% *}" ;;
239
+ *) line="" ;;
240
+ esac
241
+ if num_ok "$line"; then printf '%s\n' "$line"; else printf '%s\n' "-1"; fi
242
+ }
243
+
244
+ # Rejected RIGHT NOW, not merely near the threshold: the marked window is exhausted
245
+ # (100%), or the marker records a real client rejection (a 429 the server sent) or an
246
+ # error cooldown. A window at 90-99% still answers requests — the difference the
247
+ # all-limited fallback lives on, because "degraded service beats a hard failure" only
248
+ # holds for an account that can actually serve. Same rule as the claude shim.
249
+ limited_hard_blocked() { # $1 acct dir
250
+ local m="$1/.limited" line="" p
251
+ if [ -f "$m" ]; then
252
+ line="$(sed -n 2p "$m" 2>/dev/null)"
253
+ case "$line" in *reason=client-rate-limit*|*reason=error-cooldown*) return 0 ;; esac
254
+ p="$(limited_percent_of "$1")"
255
+ if [ "$p" -ge 0 ] 2>/dev/null; then
256
+ [ "$p" -ge 100 ]
257
+ return
258
+ fi
259
+ return 1
260
+ fi
261
+ # No marker (the over-threshold backstop put it in valid-but-not-eligible):
262
+ # fresh telemetry's peak decides; stale/unknown reads as still serving.
263
+ p="$(fresh_field "$1" max_percent)" || return 1
264
+ [ "$p" -ge 100 ]
265
+ }
266
+
170
267
  # Backstop for a lost/failed marker write: fresh telemetry with ANY bucket at/over the
171
268
  # threshold excludes the account even if .limited is missing. Stale/unreadable => not
172
269
  # over (fail open — telemetry must never invent exclusions).
@@ -219,7 +316,7 @@ rl_field() { # rl_field <json fragment> <key> -> leading integer of that key's v
219
316
  # Prints "<reset-epoch> <window>"; fails when there is none.
220
317
  client_limit_scan() { # $1 acct dir
221
318
  local day f line frag pct reset best=0 bestwin="" scanned=0 thr="${CODEX_MULTIACC_THRESHOLD:-90}" which
222
- local memo="$1/.client-scan" last="" ttl
319
+ local memo="$1/.client-scan" last="" ttl mins win ts ck sk cleared
223
320
  [ "${CODEX_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 1
224
321
  sessions_owned "$1" || return 1
225
322
  # A CLEAN result is remembered for a few seconds so a tight loop of `codex exec` runs
@@ -281,15 +378,57 @@ EOF
281
378
  | LC_ALL=C grep -a '^{".*"rate_limits"' \
282
379
  | tail -1)"
283
380
  [ -n "$line" ] || continue
381
+ # A clean `codex-accounts limits` pass that DELETED this account's client marker
382
+ # supersedes every report at or before its stamp. Without this watermark the clear
383
+ # achieved nothing: the rollout is still on disk, so the very next launch re-read
384
+ # the same tail and rewrote the same park — a delete/rewrite flap once per pass,
385
+ # which is exactly what codex gaining 5h clearing bought us on 2026-09-04.
386
+ # bin/claude's scan (~800) has consumed the same file, with the same name and the
387
+ # same comparison, since the #22 five-hour clearing landed.
388
+ ts="$(printf '%s' "$line" | LC_ALL=C sed -n \
389
+ 's/.*"timestamp"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
390
+ ck="$(iso_key "$ts")"
391
+ cleared=""
392
+ [ -f "$1/.client-limit-cleared" ] \
393
+ && { IFS= read -r cleared < "$1/.client-limit-cleared" 2>/dev/null || cleared=""; }
394
+ if num_ok "$cleared"; then
395
+ sk="$(iso_key "$(iso_of_epoch "$cleared")")"
396
+ if num_ok "$ck" && num_ok "$sk"; then
397
+ [ "$ck" -le "$sk" ] && continue
398
+ elif [ "$(file_mtime "$f")" -le "$cleared" ]; then
399
+ # An undatable report falls back to the rollout's mtime, exactly as the claude
400
+ # scan does: no timestamp, no way to prove it is newer than the clear.
401
+ continue
402
+ fi
403
+ fi
284
404
  for which in primary secondary; do
285
405
  frag="$(printf '%s' "$line" | LC_ALL=C sed -n "s/.*\"$which\"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p")"
286
406
  [ -n "$frag" ] || continue
287
407
  pct="$(rl_field "$frag" used_percent)"
288
408
  reset="$(rl_field "$frag" resets_at)"
409
+ mins="$(rl_field "$frag" window_minutes)"
289
410
  num_ok "$pct" || continue
290
411
  num_ok "$reset" || continue
291
412
  [ "$pct" -ge "$thr" ] || continue
292
- if [ "$reset" -gt "$best" ]; then best="$reset"; bestwin="$which:$pct"; fi
413
+ # The marker's bucket has to name the KIND of window that was spent, because that
414
+ # token is the only thing the weekly guard (codex-accounts weekly_marker) can read.
415
+ # `primary`/`secondary` are just the rollout's key names and map to nothing fixed —
416
+ # live payloads report primary as the 10080-minute window — so a marker named after
417
+ # them was never protected by that guard (2026-09-04). window_minutes rides in the
418
+ # same fragment, so the label is derived HERE, at write time: a day or more is the
419
+ # weekly window (7d), which only refills on its multi-day reset and therefore
420
+ # outlives every later usage payload; anything shorter is the self-healing 5h
421
+ # bucket that #22 (2026-09-03) had to keep clearable. A report carrying no
422
+ # window_minutes keeps the raw key name and so stays clearable too — the same
423
+ # fail-open direction, still bounded by the marker's own reset epoch.
424
+ if num_ok "$mins" && [ "$mins" -ge 1440 ]; then
425
+ win="7d"
426
+ elif num_ok "$mins"; then
427
+ win="5h"
428
+ else
429
+ win="$which"
430
+ fi
431
+ if [ "$reset" -gt "$best" ]; then best="$reset"; bestwin="$win:$pct"; fi
293
432
  done
294
433
  [ "$best" -gt "$now" ] && break 2
295
434
  done
@@ -302,7 +441,13 @@ EOF
302
441
  printf '%s %s\n' "$best" "$bestwin"
303
442
  }
304
443
 
305
- mark_client_limit() { # $1 acct dir, $2 reset epoch, $3 window:pct
444
+ mark_client_limit() { # $1 acct dir, $2 reset epoch, $3 window:pct (window: 7d|5h|primary|secondary)
445
+ # $3's window half is the SEMANTIC label client_limit_scan derived from window_minutes,
446
+ # so line 2 reads `bucket=client:7d` / `bucket=client:5h` and the writer's weekly guard
447
+ # matches it. A `client:primary` / `client:secondary` marker still on disk was written
448
+ # before 2026-09-04: it names no window, so it keeps the pre-guard, clearable behavior
449
+ # and simply expires at its own reset epoch. The sel_log line below keeps showing the
450
+ # window:pct detail either way.
306
451
  local m="$1/.limited" cur=""
307
452
  # Never shorten a marker that already reaches further out, and never rewrite the
308
453
  # same one on every invocation.
@@ -467,44 +612,69 @@ remember_pick() { # $1 acct dir — best effort. stderr is silenced BEFORE the r
467
612
  return 0
468
613
  }
469
614
 
470
- # Pick the account with the MOST headroom (lowest ranking score = most weekly headroom,
471
- # session as tiebreaker). Ties break randomly so equally-idle accounts still spread load.
472
- # Sets PICK_DIR/PICK_SCORE as globals — it must never touch "$@", which holds the
473
- # user's codex arguments.
615
+ # Two cuts, then chance: the session gate first, the weekly headroom band second, and a
616
+ # random peer with the account just handed out avoided. Sets PICK_DIR / PICK_BAND_COUNT /
617
+ # PICK_GATE_COUNT as globals — it must never touch "$@", which holds the user's codex
618
+ # arguments.
474
619
  PICK_DIR=""
475
- PICK_SCORE=""
476
620
  PICK_BAND_COUNT=0
621
+ PICK_GATE_COUNT=0
477
622
  PICK_STRICT=0
478
623
  pick_best() { # args: candidate dirs
479
- local d w avoid best="" bestv=1000000 bestw=101 ceiling=100 ties=0 i n match
480
- local cand=() score=() weekly=() known=() pool=()
624
+ local d w s k sk avoid best="" bestw=101 band ceiling ties=0 i n
625
+ local cand=() weekly=() known=() gated=() pool=()
481
626
  avoid="$(last_pick_id)"
627
+ PICK_GATE_COUNT=0
482
628
  for d in "$@"; do
483
629
  cand+=("$d")
484
- score+=("$(sel_score_of "$d")")
485
- if w="$(rank_weekly_of "$d")"; then weekly+=("$w"); known+=(1)
486
- else weekly+=(100); known+=(0); fi
630
+ w="$(rank_weekly_of "$d")" && k=1 || k=0
631
+ s="$(rank_session_of "$d")" && sk=1 || sk=0
632
+ # "Known" takes BOTH readings — pool-selection.v2's quota_known rule. A weekly figure
633
+ # on its own neither ranks nor clears the gate, and a session figure on its own could
634
+ # otherwise be the sole gate-clearer and win the all-gated tie over an account whose
635
+ # truthful weekly reading merely failed the gate. The one exception is a DEGRADED pool
636
+ # (SEL_DEGRADED=1): nothing is fresh anywhere, the gate has necessarily stepped aside,
637
+ # and a still-valid stale weekly reading is the only truth there is. (The writers DO
638
+ # emit one-signal documents — per-signal informative aggregation, 2026-09-04 — and
639
+ # such a document is exactly as unknown here as an empty one; parity with
640
+ # lib/selector_policy.py's quota_known.)
641
+ if [ "$k" = 1 ] && { [ "$sk" = 1 ] || [ "$SEL_DEGRADED" = 1 ]; }; then
642
+ weekly+=("$w"); known+=(1)
643
+ else
644
+ weekly+=(100); known+=(0)
645
+ fi
646
+ if [ "$k" = 1 ] && [ "$sk" = 1 ] && [ "$s" -le "$SESSION_GATE" ]; then
647
+ gated+=(1); PICK_GATE_COUNT=$((PICK_GATE_COUNT + 1))
648
+ else
649
+ gated+=(0)
650
+ fi
487
651
  done
488
652
  n=${#cand[@]}
489
653
  i=0
490
654
  while [ "$i" -lt "$n" ]; do
491
- [ "${score[$i]}" -lt "$bestv" ] && bestv="${score[$i]}"
492
- [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -lt "$bestw" ] && bestw="${weekly[$i]}"
655
+ # Nobody clears the gate => everybody does: the gate compares, it never empties the pool.
656
+ [ "$PICK_GATE_COUNT" -eq 0 ] && gated[$i]=1
657
+ if [ "${gated[$i]}" = 1 ] && [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -lt "$bestw" ]; then
658
+ bestw="${weekly[$i]}"
659
+ fi
493
660
  i=$((i + 1))
494
661
  done
495
- ceiling=$((bestw + HEADROOM_BAND)); [ "$ceiling" -gt 100 ] && ceiling=100
662
+ band="$HEADROOM_BAND"
663
+ [ "$PICK_STRICT" = 1 ] && band=0
664
+ ceiling=$((bestw + band)); [ "$ceiling" -gt 100 ] && ceiling=100
496
665
  i=0
497
666
  while [ "$i" -lt "$n" ]; do
498
- match=0
499
- if [ "$PICK_STRICT" = 0 ] && [ "$HEADROOM_BAND" -gt 0 ] && [ "$bestw" -le 100 ]; then
500
- [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -le "$ceiling" ] && match=1
501
- else
502
- [ "${score[$i]}" -eq "$bestv" ] && match=1
667
+ if [ "${gated[$i]}" = 1 ]; then
668
+ if [ "$bestw" -gt 100 ]; then
669
+ pool+=("$i") # no weekly reading anywhere: all gated tie
670
+ elif [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -le "$ceiling" ]; then
671
+ pool+=("$i")
672
+ fi
503
673
  fi
504
- [ "$match" = 1 ] && pool+=("$i")
505
674
  i=$((i + 1))
506
675
  done
507
676
  PICK_BAND_COUNT=${#pool[@]}
677
+ # Reservoir-sample the peers, skipping the account just handed out.
508
678
  for i in "${pool[@]}"; do
509
679
  if [ "${cand[$i]##*/}" != "$avoid" ]; then
510
680
  ties=$((ties + 1))
@@ -518,22 +688,64 @@ pick_best() { # args: candidate dirs
518
688
  done
519
689
  fi
520
690
  PICK_DIR="$best"
521
- PICK_SCORE="$(sel_score_of "$best")"
522
691
  }
523
692
 
524
693
  if [ "${#eligible[@]}" -gt 0 ]; then
525
694
  if [ "${CODEX_SHIM_SELECT:-headroom}" = "random" ]; then
526
695
  PICK_DIR="${eligible[$((RANDOM % ${#eligible[@]}))]}"
527
696
  PICK_BAND_COUNT=${#eligible[@]}
697
+ PICK_GATE_COUNT=${#eligible[@]}
698
+ SESSION_GATE_LOG=off
528
699
  else
529
700
  pick_best "${eligible[@]}"
530
701
  fi
531
702
  else
532
- # Every account is limit-marked: degraded service beats a hard failure (100% rule).
533
- PICK_STRICT=1
534
- pick_best "${valid[@]}"
535
- PICK_STRICT=0
536
- sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
703
+ # Every account is limit-marked: degraded service beats a hard failure (100% rule)
704
+ # but not every limited account is equally dead. A window at 90-99% still answers;
705
+ # one at 100% (or a real client 429) rejects every request until its reset. The claude
706
+ # shim learned this on 2026-08-29; the codex shim ranked the whole valid set on weekly
707
+ # headroom until the session gate reached the fallback, where an exhausted account
708
+ # that happened to clear the gate would beat a still-serving one that did not
709
+ # (codex review, 2026-09-04).
710
+ soft=()
711
+ hard=()
712
+ for d in "${valid[@]}"; do
713
+ if limited_hard_blocked "$d"; then hard+=("$d"); else soft+=("$d"); fi
714
+ done
715
+ if [ "${#soft[@]}" -gt 0 ]; then
716
+ PICK_STRICT=1
717
+ pick_best "${soft[@]}"
718
+ PICK_STRICT=0
719
+ sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
720
+ else
721
+ # Every account is exhausted RIGHT NOW: nothing serves, so hand out the one that
722
+ # unblocks first — its rejection window is the shortest.
723
+ PICK_DIR=""
724
+ best_reset=0
725
+ for d in "${hard[@]}"; do
726
+ r="$(limited_reset_of "$d")"
727
+ if [ -z "$PICK_DIR" ]; then
728
+ PICK_DIR="$d"; best_reset="$r"; continue
729
+ fi
730
+ if [ "$r" -gt 0 ] && { [ "$best_reset" -eq 0 ] || [ "$r" -lt "$best_reset" ]; }; then
731
+ PICK_DIR="$d"; best_reset="$r"
732
+ fi
733
+ done
734
+ sel_log "all-limited fallback=$(basename "$PICK_DIR") all-exhausted resets_in=$((best_reset > now ? best_reset - now : 0))s"
735
+ # Every candidate rejects right now, so this pick WILL fail: say so on a terminal
736
+ # instead of letting the operator read the client's bare limit error as a bad
737
+ # choice by the pool. Throttled, and never on a service's stderr.
738
+ if [ -t 2 ]; then
739
+ exn="$ACC_ROOT/.exhausted-notice"
740
+ exlast=0
741
+ [ -f "$exn" ] && exlast="$(file_mtime "$exn")"
742
+ if [ $((now - exlast)) -gt 600 ]; then
743
+ : 2>/dev/null > "$exn" || true
744
+ printf 'codex-multiacc: every usable account is at a limit right now; %s frees up in %ds and was chosen for that.\n' \
745
+ "$(basename "$PICK_DIR")" "$((best_reset > now ? best_reset - now : 0))" >&2
746
+ fi
747
+ fi
748
+ fi
537
749
  fi
538
750
  pick="$PICK_DIR"
539
751
  # Remember the pick so the NEXT run does not hand back the same account. An explicit
@@ -564,7 +776,7 @@ fi
564
776
  acct="$(basename "$pick")"
565
777
  sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
566
778
  "session=$(fresh_field "$pick" session_percent || echo '?')%" \
567
- "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT} pwd=$PWD"
779
+ "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT} session-gate=${SESSION_GATE_LOG} session-ok=${PICK_GATE_COUNT} pwd=$PWD"
568
780
 
569
781
  export CODEX_SHIM_ACTIVE=1
570
782