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/CLAUDE_ACCS_TASK.md +10 -5
- package/README.md +75 -20
- package/bin/claude +190 -63
- package/bin/claude-accounts +202 -34
- package/bin/codex +258 -46
- package/bin/codex-accounts +206 -22
- package/docs/ACCOUNT_OPERATIONS.md +31 -6
- 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/report.py +38 -2
- package/lib/selector_policy.py +42 -7
- package/lib/selector_primitives.py +25 -16
- package/package.json +1 -1
- package/tests/run-tests.sh +1517 -37
- package/tests/test_selector.py +104 -2
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
|
|
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
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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)"
|
|
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
|
-
|
|
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
|
-
#
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
#
|
|
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=""
|
|
480
|
-
local cand=()
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
492
|
-
[ "$
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|