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.
@@ -1520,6 +1520,14 @@ REFRESH_DENIED_BACKOFF = 21600 # 4xx = grant likely revoked: 6h; re-login neede
1520
1520
  # exactly which ceremony fixes it.
1521
1521
  USAGE_DENIED_BACKOFF = 21600
1522
1522
  USAGE_FAIL_BACKOFF = 900 # anything else non-2xx: 15 min, doubling to 30
1523
+ # A real client 429 beats an immediately-following usage response, which may be cached.
1524
+ # A later successful response under the threshold is newer first-hand evidence and must
1525
+ # release the account instead of preserving a false marker until a days-away reset.
1526
+ try:
1527
+ CLIENT_LIMIT_CONFIRM_DELAY = max(
1528
+ 0, min(3600, int(os.environ.get('CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY', '300'))))
1529
+ except ValueError:
1530
+ CLIENT_LIMIT_CONFIRM_DELAY = 300
1523
1531
 
1524
1532
  def say(msg):
1525
1533
  if not quiet:
@@ -2148,7 +2156,10 @@ for acct in manifest.get('accounts', []):
2148
2156
  # weekly_percent — peak of the durable (weekly/monthly) buckets; the PRIMARY
2149
2157
  # ranking signal, because weekly headroom only returns on the
2150
2158
  # account's fixed weekly reset (days away).
2151
- # session_percent— peak of the self-healing 5h bucket; a soft tiebreaker only.
2159
+ # session_percent— peak of the self-healing 5h bucket; the session GATE's input:
2160
+ # the shim ranks only accounts at/under CLAUDE_MULTIACC_SESSION_GATE
2161
+ # (default 50) while any clear it. (A soft tiebreaker until
2162
+ # 2026-09-03 — the operator asked for session FIRST, then weekly.)
2152
2163
  maxp = max([b['percent'] for b in buckets] or [0])
2153
2164
  weekly = [b['percent'] for b in buckets if b['group'] != 'session']
2154
2165
  session = [b['percent'] for b in buckets if b['group'] == 'session']
@@ -2207,23 +2218,37 @@ for acct in manifest.get('accounts', []):
2207
2218
  # A shim-written marker outlives a clean limits pass while its own window
2208
2219
  # is still open:
2209
2220
  # error-cooldown — the account failed a real call moments ago.
2210
- # client-rate-limit — Claude Code itself was REJECTED on this account and
2211
- # recorded the reset the API handed it. That is
2212
- # first-hand evidence; a usage payload that disagrees
2213
- # (different bucket set, cached edge) must not unpark
2214
- # the account early and send work straight back into
2215
- # the 429.
2221
+ # client-rate-limit — Claude Code itself was REJECTED on this account.
2222
+ # Preserve that over a possibly-cached response during
2223
+ # a short grace period. A later successful low-usage
2224
+ # response disproves it; otherwise false/shared-session
2225
+ # markers strand recovered accounts for days.
2216
2226
  keep = False
2227
+ client_recovered = False
2217
2228
  try:
2218
2229
  txt = open(mpath).read()
2219
2230
  first = txt.splitlines()[0] if txt else ''
2220
- if ('reason=error-cooldown' in txt or 'reason=client-rate-limit' in txt) \
2221
- and first.isdigit() and int(first) > now:
2231
+ active = first.isdigit() and int(first) > now
2232
+ marked_at = next((part[10:] for part in txt.split()
2233
+ if part.startswith('marked_at=')), '')
2234
+ marked_epoch = parse_iso(marked_at)
2235
+ if marked_epoch is None:
2236
+ marked_epoch = os.path.getmtime(mpath)
2237
+ recent_client = now - marked_epoch < CLIENT_LIMIT_CONFIRM_DELAY
2238
+ client_recovered = active and 'reason=client-rate-limit' in txt \
2239
+ and not recent_client
2240
+ if active and ('reason=error-cooldown' in txt
2241
+ or ('reason=client-rate-limit' in txt and recent_client)):
2222
2242
  keep = True
2223
2243
  except Exception:
2224
2244
  pass
2225
2245
  if not keep:
2226
2246
  os.remove(mpath)
2247
+ if client_recovered:
2248
+ cleared = os.path.join(d, '.client-limit-cleared')
2249
+ with open(cleared + '.tmp', 'w') as f:
2250
+ f.write(f'{int(now)}\n')
2251
+ os.replace(cleared + '.tmp', cleared)
2227
2252
  say(f'{aid}: marker cleared (max {maxp}%)')
2228
2253
  if not quiet:
2229
2254
  detail = ' '.join(f"{b['name']}={b['percent']}%" for b in buckets)
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 > most-headroom among
7
- # limit-eligible accounts > 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.
@@ -140,18 +142,99 @@ fresh_field() { # fresh_field <acct dir> <json key> -> integer if telemetry fres
140
142
  printf '%s\n' "$v"
141
143
  }
142
144
 
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))
145
+ # RANKING — TWO CUTS, in this order, then chance. The operator's ask (2026-09-03):
146
+ # "among accounts where high session limits it must choose randomly from ones where
147
+ # highest weekly limits."
148
+ # 1. SESSION GATE (SESSION_GATE, default 50): a candidate clears the gate when its ~5h
149
+ # session usage is KNOWN and at or under the gate. When at least one candidate clears
150
+ # it, only those are ranked further — an account whose session bucket is nearly spent
151
+ # is about to be rejected whatever its weekly headroom is. When NOBODY clears it the
152
+ # gate steps aside and every candidate is ranked: the gate compares candidates, it
153
+ # never empties the pool.
154
+ # 2. WEEKLY BAND (HEADROOM_BAND, default 30, kept at the operator's request): among the
155
+ # gated candidates the lowest KNOWN weekly usage leads, and every gated candidate
156
+ # within the band of it is a peer. Weekly dominates because a weekly window only
157
+ # refills on its multi-day reset while the ~5h window self-heals (same asymmetry as
158
+ # the claude pool) — which is exactly why session acts as the gate and is NOT a
159
+ # tiebreaker inside the band any more: the operator asked for a random choice among
160
+ # the best-weekly accounts.
161
+ # Stale/unreadable telemetry never clears the gate and never enters the band — an unknown
162
+ # account can never beat a candidate with truthful usage telemetry — but when NOTHING is
163
+ # known the candidates all tie, which keeps an entirely blind pool selectable.
164
+ # Band 0 (or the all-limited PICK_STRICT fallback) means exact weekly ties only; gate 100
165
+ # turns the gate off.
166
+
167
+ HEADROOM_BAND="${CODEX_MULTIACC_HEADROOM_BAND:-30}"
168
+ case "$HEADROOM_BAND" in ''|*[!0-9]*|??????*) HEADROOM_BAND=30 ;; esac
169
+ [ "$HEADROOM_BAND" -gt 100 ] && HEADROOM_BAND=100
170
+ SESSION_GATE="${CODEX_MULTIACC_SESSION_GATE:-50}"
171
+ case "$SESSION_GATE" in ''|*[!0-9]*|??????*) SESSION_GATE=50 ;; esac
172
+ [ "$SESSION_GATE" -gt 100 ] && SESSION_GATE=100
173
+ # What the selection log prints for the gate: "off" in random mode, where no gate ran —
174
+ # the log must never claim a cut that was not made.
175
+ SESSION_GATE_LOG="$SESSION_GATE"
176
+ # The codex shim has no DEGRADED ranking mode (the claude shim ranks on still-valid stale
177
+ # weekly readings when nothing in its pool is fresh). The constant exists so pick_best
178
+ # can stay byte-identical with bin/claude.
179
+ SEL_DEGRADED=0
180
+
181
+ # weekly_percent ONLY. A reading without it is unknown here, exactly as it is unknown to
182
+ # pool-selection.v2 (which never sees max_percent). The old max_percent fallback let a
183
+ # weekly-less file rank — and, once the session gate existed, CLEAR the gate — on a number
184
+ # that may well be the session bucket's own peak (codex review, 2026-09-04).
185
+ rank_weekly_of() { # $1 = acct dir -> comparable weekly use, or fail when unknown
186
+ local w
187
+ if w="$(fresh_field "$1" weekly_percent)"; then
188
+ printf '%s\n' "$w"
189
+ return 0
190
+ fi
191
+ return 1
192
+ }
193
+
194
+ rank_session_of() { # $1 = acct dir -> fresh session use, or fail when unknown
195
+ fresh_field "$1" session_percent
196
+ }
197
+
198
+ # The .limited marker's own fields (line 1: reset epoch; line 2: "bucket=…
199
+ # percent=… … reason=…"). All tolerant: an unreadable or bare marker answers
200
+ # "unknown", never an error — these feed the all-limited fallback only.
201
+ limited_reset_of() { # $1 acct dir -> the marker's reset epoch, or 0 (unknown)
202
+ local m="$1/.limited" r=""
203
+ [ -f "$m" ] && { IFS= read -r r < "$m" 2>/dev/null || r=""; }
204
+ if num_ok "$r"; then printf '%s\n' "$r"; else printf '0\n'; fi
205
+ }
206
+
207
+ limited_percent_of() { # $1 acct dir -> the marked window's percent, or -1 (unknown)
208
+ local m="$1/.limited" line=""
209
+ [ -f "$m" ] && line="$(sed -n 2p "$m" 2>/dev/null)"
210
+ case "$line" in
211
+ *percent=*) line="${line#*percent=}"; line="${line%% *}" ;;
212
+ *) line="" ;;
213
+ esac
214
+ if num_ok "$line"; then printf '%s\n' "$line"; else printf '%s\n' "-1"; fi
215
+ }
216
+
217
+ # Rejected RIGHT NOW, not merely near the threshold: the marked window is exhausted
218
+ # (100%), or the marker records a real client rejection (a 429 the server sent) or an
219
+ # error cooldown. A window at 90-99% still answers requests — the difference the
220
+ # all-limited fallback lives on, because "degraded service beats a hard failure" only
221
+ # holds for an account that can actually serve. Same rule as the claude shim.
222
+ limited_hard_blocked() { # $1 acct dir
223
+ local m="$1/.limited" line="" p
224
+ if [ -f "$m" ]; then
225
+ line="$(sed -n 2p "$m" 2>/dev/null)"
226
+ case "$line" in *reason=client-rate-limit*|*reason=error-cooldown*) return 0 ;; esac
227
+ p="$(limited_percent_of "$1")"
228
+ if [ "$p" -ge 0 ] 2>/dev/null; then
229
+ [ "$p" -ge 100 ]
230
+ return
231
+ fi
232
+ return 1
233
+ fi
234
+ # No marker (the over-threshold backstop put it in valid-but-not-eligible):
235
+ # fresh telemetry's peak decides; stale/unknown reads as still serving.
236
+ p="$(fresh_field "$1" max_percent)" || return 1
237
+ [ "$p" -ge 100 ]
155
238
  }
156
239
 
157
240
  # Backstop for a lost/failed marker write: fresh telemetry with ANY bucket at/over the
@@ -454,62 +537,138 @@ remember_pick() { # $1 acct dir — best effort. stderr is silenced BEFORE the r
454
537
  return 0
455
538
  }
456
539
 
457
- # Pick the account with the MOST headroom (lowest ranking score = most weekly headroom,
458
- # session as tiebreaker). Ties break randomly so equally-idle accounts still spread load.
459
- # Sets PICK_DIR/PICK_SCORE as globals — it must never touch "$@", which holds the
460
- # user's codex arguments.
540
+ # Two cuts, then chance: the session gate first, the weekly headroom band second, and a
541
+ # random peer with the account just handed out avoided. Sets PICK_DIR / PICK_BAND_COUNT /
542
+ # PICK_GATE_COUNT as globals — it must never touch "$@", which holds the user's codex
543
+ # arguments.
461
544
  PICK_DIR=""
462
- PICK_SCORE=""
545
+ PICK_BAND_COUNT=0
546
+ PICK_GATE_COUNT=0
547
+ PICK_STRICT=0
463
548
  pick_best() { # args: candidate dirs
464
- local d avoid best="" bestv=1000000 ties=0 i n
465
- local cand=() score=()
549
+ local d w s k sk avoid best="" bestw=101 band ceiling ties=0 i n
550
+ local cand=() weekly=() known=() gated=() pool=()
466
551
  avoid="$(last_pick_id)"
552
+ PICK_GATE_COUNT=0
467
553
  for d in "$@"; do
468
554
  cand+=("$d")
469
- score+=("$(sel_score_of "$d")")
555
+ w="$(rank_weekly_of "$d")" && k=1 || k=0
556
+ s="$(rank_session_of "$d")" && sk=1 || sk=0
557
+ # "Known" takes BOTH readings — pool-selection.v2's quota_known rule. A weekly figure
558
+ # on its own neither ranks nor clears the gate, and a session figure on its own could
559
+ # otherwise be the sole gate-clearer and win the all-gated tie over an account whose
560
+ # truthful weekly reading merely failed the gate. The one exception is a DEGRADED pool
561
+ # (SEL_DEGRADED=1): nothing is fresh anywhere, the gate has necessarily stepped aside,
562
+ # and a still-valid stale weekly reading is the only truth there is. (Neither writer
563
+ # emits one field without the other; this is parity with lib/selector_policy.py.)
564
+ if [ "$k" = 1 ] && { [ "$sk" = 1 ] || [ "$SEL_DEGRADED" = 1 ]; }; then
565
+ weekly+=("$w"); known+=(1)
566
+ else
567
+ weekly+=(100); known+=(0)
568
+ fi
569
+ if [ "$k" = 1 ] && [ "$sk" = 1 ] && [ "$s" -le "$SESSION_GATE" ]; then
570
+ gated+=(1); PICK_GATE_COUNT=$((PICK_GATE_COUNT + 1))
571
+ else
572
+ gated+=(0)
573
+ fi
470
574
  done
471
575
  n=${#cand[@]}
472
576
  i=0
473
577
  while [ "$i" -lt "$n" ]; do
474
- [ "${score[$i]}" -lt "$bestv" ] && bestv="${score[$i]}"
578
+ # Nobody clears the gate => everybody does: the gate compares, it never empties the pool.
579
+ [ "$PICK_GATE_COUNT" -eq 0 ] && gated[$i]=1
580
+ if [ "${gated[$i]}" = 1 ] && [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -lt "$bestw" ]; then
581
+ bestw="${weekly[$i]}"
582
+ fi
475
583
  i=$((i + 1))
476
584
  done
477
- # Reservoir-sample among the equally-best, skipping the account just handed out.
585
+ band="$HEADROOM_BAND"
586
+ [ "$PICK_STRICT" = 1 ] && band=0
587
+ ceiling=$((bestw + band)); [ "$ceiling" -gt 100 ] && ceiling=100
478
588
  i=0
479
589
  while [ "$i" -lt "$n" ]; do
480
- if [ "${score[$i]}" -eq "$bestv" ] && [ "${cand[$i]##*/}" != "$avoid" ]; then
590
+ if [ "${gated[$i]}" = 1 ]; then
591
+ if [ "$bestw" -gt 100 ]; then
592
+ pool+=("$i") # no weekly reading anywhere: all gated tie
593
+ elif [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -le "$ceiling" ]; then
594
+ pool+=("$i")
595
+ fi
596
+ fi
597
+ i=$((i + 1))
598
+ done
599
+ PICK_BAND_COUNT=${#pool[@]}
600
+ # Reservoir-sample the peers, skipping the account just handed out.
601
+ for i in "${pool[@]}"; do
602
+ if [ "${cand[$i]##*/}" != "$avoid" ]; then
481
603
  ties=$((ties + 1))
482
604
  [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
483
605
  fi
484
- i=$((i + 1))
485
606
  done
486
607
  if [ -z "$best" ]; then
487
- # The only account at the best score IS the one just used — degraded rotation beats
488
- # refusing to pick (and in a two-account pool this is the other half of the
489
- # alternation).
490
- i=0
491
- while [ "$i" -lt "$n" ]; do
492
- if [ "${score[$i]}" -eq "$bestv" ]; then
493
- ties=$((ties + 1))
494
- [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
495
- fi
496
- i=$((i + 1))
608
+ for i in "${pool[@]}"; do
609
+ ties=$((ties + 1))
610
+ [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
497
611
  done
498
612
  fi
499
613
  PICK_DIR="$best"
500
- PICK_SCORE="$bestv"
501
614
  }
502
615
 
503
616
  if [ "${#eligible[@]}" -gt 0 ]; then
504
617
  if [ "${CODEX_SHIM_SELECT:-headroom}" = "random" ]; then
505
618
  PICK_DIR="${eligible[$((RANDOM % ${#eligible[@]}))]}"
619
+ PICK_BAND_COUNT=${#eligible[@]}
620
+ PICK_GATE_COUNT=${#eligible[@]}
621
+ SESSION_GATE_LOG=off
506
622
  else
507
623
  pick_best "${eligible[@]}"
508
624
  fi
509
625
  else
510
- # Every account is limit-marked: degraded service beats a hard failure (100% rule).
511
- pick_best "${valid[@]}"
512
- sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
626
+ # Every account is limit-marked: degraded service beats a hard failure (100% rule)
627
+ # but not every limited account is equally dead. A window at 90-99% still answers;
628
+ # one at 100% (or a real client 429) rejects every request until its reset. The claude
629
+ # shim learned this on 2026-08-29; the codex shim ranked the whole valid set on weekly
630
+ # headroom until the session gate reached the fallback, where an exhausted account
631
+ # that happened to clear the gate would beat a still-serving one that did not
632
+ # (codex review, 2026-09-04).
633
+ soft=()
634
+ hard=()
635
+ for d in "${valid[@]}"; do
636
+ if limited_hard_blocked "$d"; then hard+=("$d"); else soft+=("$d"); fi
637
+ done
638
+ if [ "${#soft[@]}" -gt 0 ]; then
639
+ PICK_STRICT=1
640
+ pick_best "${soft[@]}"
641
+ PICK_STRICT=0
642
+ sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
643
+ else
644
+ # Every account is exhausted RIGHT NOW: nothing serves, so hand out the one that
645
+ # unblocks first — its rejection window is the shortest.
646
+ PICK_DIR=""
647
+ best_reset=0
648
+ for d in "${hard[@]}"; do
649
+ r="$(limited_reset_of "$d")"
650
+ if [ -z "$PICK_DIR" ]; then
651
+ PICK_DIR="$d"; best_reset="$r"; continue
652
+ fi
653
+ if [ "$r" -gt 0 ] && { [ "$best_reset" -eq 0 ] || [ "$r" -lt "$best_reset" ]; }; then
654
+ PICK_DIR="$d"; best_reset="$r"
655
+ fi
656
+ done
657
+ sel_log "all-limited fallback=$(basename "$PICK_DIR") all-exhausted resets_in=$((best_reset > now ? best_reset - now : 0))s"
658
+ # Every candidate rejects right now, so this pick WILL fail: say so on a terminal
659
+ # instead of letting the operator read the client's bare limit error as a bad
660
+ # choice by the pool. Throttled, and never on a service's stderr.
661
+ if [ -t 2 ]; then
662
+ exn="$ACC_ROOT/.exhausted-notice"
663
+ exlast=0
664
+ [ -f "$exn" ] && exlast="$(file_mtime "$exn")"
665
+ if [ $((now - exlast)) -gt 600 ]; then
666
+ : 2>/dev/null > "$exn" || true
667
+ printf 'codex-multiacc: every usable account is at a limit right now; %s frees up in %ds and was chosen for that.\n' \
668
+ "$(basename "$PICK_DIR")" "$((best_reset > now ? best_reset - now : 0))" >&2
669
+ fi
670
+ fi
671
+ fi
513
672
  fi
514
673
  pick="$PICK_DIR"
515
674
  # Remember the pick so the NEXT run does not hand back the same account. An explicit
@@ -538,7 +697,9 @@ if [ "$stale" = 1 ] && [ -x "$SELF_DIR/codex-accounts" ]; then
538
697
  fi
539
698
 
540
699
  acct="$(basename "$pick")"
541
- sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')% session=$(fresh_field "$pick" session_percent || echo '?')% pwd=$PWD"
700
+ sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
701
+ "session=$(fresh_field "$pick" session_percent || echo '?')%" \
702
+ "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT} session-gate=${SESSION_GATE_LOG} session-ok=${PICK_GATE_COUNT} pwd=$PWD"
542
703
 
543
704
  export CODEX_SHIM_ACTIVE=1
544
705
 
@@ -1461,7 +1461,10 @@ for acct in manifest.get('accounts', []):
1461
1461
  # THREE selection signals, same reset asymmetry as the claude pool:
1462
1462
  # max_percent — peak of ALL buckets; drives >=90% EXCLUSION.
1463
1463
  # weekly_percent — peak of the durable buckets; the PRIMARY ranking signal.
1464
- # session_percent— peak of the self-healing ~5h buckets; a soft tiebreaker.
1464
+ # session_percent— peak of the self-healing ~5h buckets; the session GATE's input:
1465
+ # the shim ranks only accounts at/under CODEX_MULTIACC_SESSION_GATE
1466
+ # (default 50) while any clear it. (A soft tiebreaker until
1467
+ # 2026-09-03 — the operator asked for session FIRST, then weekly.)
1465
1468
  maxp = max([b['percent'] for b in buckets] or [0])
1466
1469
  weekly = [b['percent'] for b in buckets if b['group'] != 'session']
1467
1470
  session = [b['percent'] for b in buckets if b['group'] == 'session']
@@ -332,8 +332,11 @@ derived from the pool root. Uninstalling an instance removes only that instance'
332
332
  | `CLAUDE_ACCOUNT=acct-03` | pin this invocation to one account (wins over markers) |
333
333
  | `CLAUDE_CONFIG_DIR=...` | shim passes straight through (scripts can pin the old way) |
334
334
  | `CLAUDE_MULTIACC_DISABLE=1` | bypass selection entirely |
335
+ | `CLAUDE_MULTIACC_HEADROOM_BAND=0..100` | weekly points treated as peers; default `30`, `0` is strict |
336
+ | `CLAUDE_MULTIACC_SESSION_GATE=0..100` | max 5h-session usage that still gets ranked; default `50`, `100` disables the gate |
335
337
  | `CLAUDE_MULTIACC_CLIENT_LIMITS=0` | ignore the client's own rate-limit records |
336
338
  | `CLAUDE_MULTIACC_CLIENT_SCAN_TTL=<s>` | clean client-limit scan cache; default 20s |
339
+ | `CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY=<s>` | low-telemetry recovery grace; default 300s |
337
340
  | `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
338
341
  | `CLAUDE_ACCOUNTS_ROOT=...` | relocate the pool; legacy spelling is `CLAUDE_ACCOUNTS_DIR` |
339
342
  | `CLAUDE_MULTIACC_SYNC_TARGET=...` | sync target, overriding the manifest; `none` = local-only |
@@ -341,8 +344,9 @@ derived from the pool root. Uninstalling an instance removes only that instance'
341
344
 
342
345
  The codex shim honors the same switches spelled `CODEX_*`: `CODEX_ACCOUNT`,
343
346
  `CODEX_HOME` (passthrough), `CODEX_MULTIACC_DISABLE`, `CODEX_SHIM_RETRY`,
344
- `CODEX_SHIM_SELECT`, `CODEX_ACCOUNTS_ROOT` (legacy `CODEX_ACCOUNTS_DIR`),
345
- `CODEX_MULTIACC_SYNC_TARGET`, `CODEX_MULTIACC_THRESHOLD`.
347
+ `CODEX_SHIM_SELECT`, `CODEX_MULTIACC_HEADROOM_BAND`, `CODEX_MULTIACC_SESSION_GATE`,
348
+ `CODEX_ACCOUNTS_ROOT` (legacy `CODEX_ACCOUNTS_DIR`), `CODEX_MULTIACC_SYNC_TARGET`,
349
+ `CODEX_MULTIACC_THRESHOLD`.
346
350
 
347
351
  ## Verification
348
352
 
@@ -380,8 +384,9 @@ repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
380
384
  before direct, TUI, or `--resume` work sees the 401. Run `claude-accounts verify` to
381
385
  check every token immediately; `claude-accounts expired` reports token-only accounts
382
386
  as `UNVERIFIED` until that proof exists.
383
- - **Everything marked limited** — the shim still runs (least-utilized fallback);
384
- check `selection.log` for `all-limited` lines.
387
+ - **Everything marked limited** — the shim still runs: the still-serving limited accounts
388
+ go through the same two cuts (session gate, then strict best-weekly) and one is handed
389
+ out anyway; check `selection.log` for `all-limited fallback=` lines.
385
390
  - **Sync fails** — `tail ~/.claude-accounts/sync.log`; it's ssh/rsync to the manifest's
386
391
  `server` (BatchMode — needs key auth).
387
392
  - **A service bypasses the shim** — it spawns an absolute path. Point its env
@@ -0,0 +1,76 @@
1
+ # App-Robot Delivery Tracking Prompt
2
+
3
+ Replace `<TASK_URL>` with the delivery URL, then paste the full prompt into Claude Code.
4
+
5
+ ```text
6
+ Track app-robot delivery task <TASK_URL> to completion and actively repair anything
7
+ that prevents forward progress.
8
+
9
+ Operating rules:
10
+
11
+ 1. Start with a read-only timeline audit: current task/step/run, step age, last changed
12
+ evidence, worker/execution ownership, retry count, external operations, blockers,
13
+ and production build SHA.
14
+
15
+ 2. Treat 15 minutes without changed durable evidence as a stall. Do not accept log
16
+ activity, repeated probes, heartbeats, or newly minted attempts as progress.
17
+
18
+ 3. Never repeat the same failed action more than once. On the second identical failure,
19
+ stop retrying, identify the shared root cause, add a focused regression test, open a
20
+ fix PR, pass exact-head CI, merge/deploy it, verify production, then resume the task.
21
+
22
+ 4. Use app-robot's supported operations only. Preserve at-most-once semantics for store
23
+ submissions and other external mutations. Never bypass an operator-disabled store lane.
24
+
25
+ 5. Check that every supervising turn has a claimable execution, bounded retry and launch
26
+ budgets, expiring reservations, and a periodic reconciler. Repair the platform if not.
27
+
28
+ 6. Parallelize independent diagnosis and fixes, but serialize overlapping PRs. Batch
29
+ related fixes where safe. Merge overlapping PRs once in dependency order instead of
30
+ rebasing each after every squash.
31
+
32
+ 7. Run focused impacted tests during development. If local test admission waits over two
33
+ minutes, diagnose the admission and database lifecycle and use CI as the merge gate.
34
+ Do not wait indefinitely or start duplicate suites.
35
+
36
+ 8. Perform one bounded adversarial review of each original root-cause area, using at most
37
+ three reviewers. Aggregate confirmed findings into at most one follow-up PR per
38
+ subsystem. Do not review review-generated follow-ups unless they introduce a genuinely
39
+ new subsystem. Launch no recursive review fan-out.
40
+
41
+ 9. Cap each review workflow at 20 minutes. At the deadline, aggregate completed evidence
42
+ and stop incomplete or redundant workers. Cap each implementation agent at 20 minutes
43
+ to produce a pushed commit or test result; otherwise stop it and narrow or reassign the
44
+ task. A stalled agent is not progress.
45
+
46
+ 10. Do not wait for manual cleanup that is not on the delivery critical path. Convert the
47
+ cause into a bounded code fix and regression test, then let exact-head CI verify it.
48
+
49
+ 11. Every 10 minutes report: current step, age, last real progress, active blocker, exact
50
+ remediation, queue/CI/deploy status, remaining critical path, and any wall-clock budget
51
+ deviation.
52
+
53
+ 12. Persist a restart-safe checkpoint before context or account exhaustion: task/run IDs,
54
+ operation IDs, PR queue, production build, monitors, and exact next commands.
55
+
56
+ 13. Target four hours for a new delivery task and two hours for a repair-only critical
57
+ path, excluding unavoidable external store review. Use these sub-budgets:
58
+ - initial audit: 10 minutes
59
+ - root-cause diagnosis: 20 minutes per independent failure
60
+ - implementation agent: 20 minutes to a pushed artifact
61
+ - review workflow: 20 minutes, maximum three reviewers
62
+ - unchanged CI or deploy: investigate after 15 minutes
63
+ Report a budget miss immediately; do not silently extend it.
64
+
65
+ 14. Completion means: all enabled lanes have accepted outcomes; policy-disabled lanes are
66
+ recorded as notified person waits; all confirmed root-cause fixes are merged, deployed,
67
+ and production-verified; the repair queue is empty; bounded reviews are finished.
68
+ Then mark /goal complete.
69
+
70
+ First response: give me the evidence-based critical path and the maximum wall-clock budget
71
+ for each remaining step. Then begin; do not merely propose a plan.
72
+ ```
73
+
74
+ The limits prevent an open-ended review-of-review loop. External store review can outlast
75
+ the target, but the task must reach a durable submitted or policy-blocked state before the
76
+ tracking goal is considered complete.
@@ -20,19 +20,52 @@ The request schema is `claude-multiacc/pool-selection.v2` and contains:
20
20
  - the policy (`default_claude`, `default_codex`, `default_both`, `explicit`,
21
21
  `producer_retry`, or `reviewer`);
22
22
  - any required provider, producer identity and explicit exclusions;
23
- - the runner generation's candidate telemetry plus exact reservation history.
23
+ - the runner generation's candidate telemetry plus exact reservation history;
24
+ - optionally `headroom_band` and `session_gate`, the two ranking knobs below.
25
+
26
+ Both knobs are optional so a caller pinned to an older panel build keeps working: each
27
+ is a decimal clamped to 0–100, and a value that is not a plain decimal (a bool, a list,
28
+ `null`, `1e5`) is refused as `invalid_request` naming that field rather than silently
29
+ widening selection. `headroom_band` defaults to `30`, `session_gate` to `50`.
24
30
 
25
31
  Candidates must be active, fresh, provider-capable, not currently limited, and not
26
32
  covered by a live reservation. Percentages are finite decimals clamped to 0–100.
27
33
  Canonical account IDs are NFC-normalized and trimmed with case preserved; an
28
34
  NFC/case-folded key rejects case-only duplicate identities before ranking.
29
- Known quota ranks ahead of unknown quota; higher minimum weekly/session headroom
30
- wins; equal capacity goes to the least-recently-selected account, then a stable
31
- Claude-before-Codex/account-id tie order.
32
35
 
33
- Success returns the concrete engine/account/runner generation, normalized score basis,
34
- an RFC 8785/SHA-256 candidate-snapshot digest and a selection digest. Stable failures
35
- are `invalid_request`, `unsupported_schema`, `duplicate_candidate_identity`, and
36
+ ## Ranking: the session gate, then the weekly band
37
+
38
+ The eligible set is ranked in two cuts (operator's decision, 2026-09-03: "among
39
+ accounts where high session limits it must choose randomly from ones where highest
40
+ weekly limits").
41
+
42
+ 1. **Session gate.** An account clears the gate when its quota is known and its session
43
+ (5h) usage is at most `session_gate` points. If any account clears it, only those are
44
+ ranked further — a nearly spent 5h bucket is about to reject the launch whatever the
45
+ weekly headroom says. If nobody clears it the gate steps aside and every known
46
+ account is ranked: the gate compares, it never empties the pool. Unknown quota never
47
+ clears it. `session_gate: 100` disables the gate.
48
+ 2. **Weekly band.** Among the gated accounts, the largest weekly remaining is the
49
+ leader and `band_floor` is that minus `headroom_band`; every gated account at or
50
+ above the floor is a peer. The band is measured on WEEKLY remaining, not on
51
+ `min(weekly, session)` — session headroom already had its say in the gate.
52
+ `headroom_band: 0` restores strict best-weekly ranking.
53
+ 3. **Inside the band.** The least-recently-selected peer wins, then a spread digest over
54
+ (reservation key, identity), then a stable Claude-before-Codex/account-id order. This
55
+ is what makes a burst of parallel launches fan out instead of stacking on one
56
+ account, and it is deterministic: the same request always yields the same winner.
57
+
58
+ With no usable quota anywhere the band cannot apply (`band_floor` is `null`) and the
59
+ plain ordering stands: known quota ahead of unknown, then highest effective headroom,
60
+ then least-recently-selected.
61
+
62
+ Success returns the concrete engine/account/runner generation, normalized score basis
63
+ (which still reports `effective_headroom` = min(weekly, session) for observability),
64
+ `headroom_band` / `band_floor` / `band_count`, `session_gate` / `session_ok_count` (how
65
+ many accounts cleared the gate; `0` means nobody did and the gate stepped aside), an
66
+ RFC 8785/SHA-256 candidate-snapshot digest and a selection digest. Both knobs are inputs
67
+ to the selection digest, because both can change which account wins. Stable failures are
68
+ `invalid_request`, `unsupported_schema`, `duplicate_candidate_identity`, and
36
69
  `no_candidate`. A reviewer stays on the producer's provider and avoids its account when
37
70
  another eligible account exists.
38
71