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.
@@ -128,11 +128,16 @@ MAC (source of truth) SERVER 138.197.36.107 (mirror)
128
128
 
129
129
  1. `CLAUDE_CONFIG_DIR` already set → pass straight through (scripts can pin).
130
130
  2. `CLAUDE_ACCOUNT=acct-03` set → use that account (even if limit-marked — explicit wins).
131
- 3. Else pick uniformly at random among accounts with valid auth
132
- (`.credentials.json` on Mac; `server.token` on server also exports
133
- `CLAUDE_CODE_OAUTH_TOKEN`) **that are not limit-marked** (no `.limited` marker).
134
- If EVERY account is marked, fall back to the least-utilized account and log a warning —
135
- degraded service beats a hard failure (the 100% rule).
131
+ 3. Else, among accounts with valid auth (`.credentials.json` on Mac; `server.token` on
132
+ server also exports `CLAUDE_CODE_OAUTH_TOKEN`) **that are not limit-marked** (no
133
+ `.limited` marker), select in two cuts (operator decision, 2026-09-03): first the
134
+ **session gate** only accounts whose fresh 5h usage is at most
135
+ `CLAUDE_MULTIACC_SESSION_GATE` (default 50) stay in, unless nobody clears it; then the
136
+ **30-point weekly headroom band** — accounts within `CLAUDE_MULTIACC_HEADROOM_BAND`
137
+ points of the lowest weekly usage are peers, sampled at random with the previous pick
138
+ avoided. If EVERY account is marked, the still-serving ones go through the same two cuts
139
+ with a strict (exact-tie) band and one is handed out with a warning — degraded service
140
+ beats a hard failure (the 100% rule). README.md is the authoritative description.
136
141
  4. Append `timestamp account cwd` to `selection.log` (never prompt text); print NOTHING;
137
142
  `exec` the dynamically-resolved real binary — stdin/stdout/exit codes pass through
138
143
  byte-identically → `-p` pipe-safe.
package/README.md CHANGED
@@ -16,7 +16,7 @@ completely independent — separate manifests, credentials, telemetry, and logs
16
16
  either provider can be used, re-authenticated, or emptied without touching the other.
17
17
 
18
18
  Tested on: macOS (bash 3.2, zsh, Claude Code 2.1.207+, Codex CLI 0.147) and Ubuntu 24.04
19
- (bash 5.2). The compatibility suite currently covers 645 sandboxed cases with no
19
+ (bash 5.2). The compatibility suite currently covers 834 sandboxed cases with no
20
20
  network/quota use; the unified selector has its own adversarial contract suite.
21
21
 
22
22
  ## Unified selector for app-robot
@@ -32,8 +32,9 @@ claude-multiacc select --request-json - --response-json -
32
32
 
33
33
  The command reads one strict UTF-8 JSON request and emits one JSON response using
34
34
  the versioned `claude-multiacc/pool-selection.v2` protocol. It ranks the caller's
35
- locked candidate snapshot by usable weekly/session headroom and returns one concrete
36
- provider/account plus canonical snapshot and selection digests. `both` is a policy,
35
+ locked candidate snapshot with the same two cuts as the shims — a session-headroom gate,
36
+ then a band on weekly headroom — and returns one concrete provider/account plus canonical
37
+ snapshot and selection digests. `both` is a policy,
37
38
  never an execution engine. The caller remains responsible for durable reservations,
38
39
  database locks, retries, and launching the selected CLI.
39
40
  Successful responses also carry `eligible_count` and `eligible_alternative_count`,
@@ -72,14 +73,26 @@ repo bin/ first on PATH (rc-file block) /root/claude-multiacc/ (addon repo)
72
73
  4. Among the accounts that remain valid **on this machine** (an OAuth login —
73
74
  `.credentials.json`, or a macOS Keychain item this session can open — or a
74
75
  `server.token` that passes its first inference preflight) and not limit-excluded,
75
- form a **30-percentage-point weekly-headroom band** around the account with the most
76
- remaining headroom (see below). The pool **rotates away from the account it just handed
76
+ rank in two cuts (see below). **Session gate first:** keep only the accounts whose
77
+ 5-hour bucket is known-fresh and at most 50% used an account about to be rejected on
78
+ its session does not deserve a launch whatever its weekly headroom. If *nobody* clears
79
+ the gate it steps aside and every candidate stays in: the gate compares, it never
80
+ empties the pool. **Then the weekly band:** among the accounts that cleared the gate,
81
+ form a **30-percentage-point weekly-headroom band** around the one with the most
82
+ remaining weekly headroom. The pool **rotates away from the account it just handed
77
83
  out** and samples the other accounts in that band at random. This prevents one nominally
78
84
  emptiest account from taking every launch while near-peers idle, while accounts more than
79
- 30 points behind remain protected. `CLAUDE_MULTIACC_HEADROOM_BAND=0` restores strict
80
- best-headroom selection; `CLAUDE_SHIM_SELECT=random` uses the whole eligible pool.
81
- 5. If every account is limit-excluded the highest-headroom *authenticable* account
82
- anyway + a warning in `selection.log` (degraded beats down: the 100% rule).
85
+ 30 points behind remain protected. `CLAUDE_MULTIACC_SESSION_GATE=100` turns the gate off
86
+ (default `50`); `CLAUDE_MULTIACC_HEADROOM_BAND=0` restores strict best-weekly selection
87
+ (default `30`); `CLAUDE_SHIM_SELECT=random` uses the whole eligible pool. Every pick
88
+ records both cuts in `selection.log`: `band=`/`band-count=` (the band width and how many
89
+ peers it held) and `session-gate=`/`session-ok=` (the gate and how many candidates
90
+ cleared it — `session-ok=0` means nobody did and the gate stepped aside; in
91
+ `CLAUDE_SHIM_SELECT=random` mode the line reads `session-gate=off`, because no cut was made).
92
+ 5. If every account is limit-excluded → the still-serving limited accounts (a bucket at
93
+ 90–99%, not an exhausted one) go through the same two cuts — session gate, then strict
94
+ best-weekly — and one is handed out anyway, with an `all-limited fallback=` line in
95
+ `selection.log` (degraded beats down: the 100% rule).
83
96
  6. If nothing is usable at all → stock passthrough, with the reason in `selection.log`
84
97
  (and, on a terminal, one line naming the accounts to fix).
85
98
 
@@ -87,13 +100,26 @@ repo bin/ first on PATH (rc-file block) /root/claude-multiacc/ (addon repo)
87
100
  Fable request consumes at once: the 5-hour **session** bucket, the **weekly all-models**
88
101
  bucket, and the **weekly Fable** bucket. Anthropic's docs confirm these reset on very
89
102
  different horizons — the session bucket refills every ~5 hours, but weekly buckets only
90
- refill on the account's fixed weekly reset (days away). So the picker ranks primarily on
91
- **weekly headroom** (the peak of the durable buckets) to form the 30-point band. Accounts
92
- inside that band are deliberately treated as peers and spread randomly; with a zero-width
93
- band the self-healing session bucket is the tiebreaker. An account at 10% weekly remains
94
- protected from one at 70% weekly because the latter is outside the band. (`score = weekly%
95
- × 1000 + session%` in strict mode, lowest wins.) **Exclusion** still fires on *any* bucket
96
- 90% a full session bucket
103
+ refill on the account's fixed weekly reset (days away). The two buckets therefore answer
104
+ two different questions, and the picker asks them in that order (operator's decision,
105
+ 2026-09-03: *"among accounts where high session limits it must choose randomly from ones
106
+ where highest weekly limits"*).
107
+
108
+ - **Session is a gate, not a tiebreaker.** A candidate clears the gate when its 5-hour
109
+ usage is *known* (fresh telemetry) and at most `CLAUDE_MULTIACC_SESSION_GATE` points
110
+ spent — default `50`, `100` disables the gate. Unknown session usage never clears it.
111
+ An account that is 85% through its session is minutes from a rejection, so it waits
112
+ while anyone fresher is available; if nobody clears the gate, everybody is ranked.
113
+ - **Weekly headroom picks the winner.** Among the accounts that cleared the gate, the
114
+ picker ranks on **weekly headroom** (the peak of the durable buckets) and treats
115
+ everything within `CLAUDE_MULTIACC_HEADROOM_BAND` points of the leader — default `30`,
116
+ `0` is strict best-weekly-only — as a peer, spreading launches over that band at
117
+ random. Session usage is **not** a tiebreaker inside the band: it acts only through
118
+ the gate. So an account at 10% weekly stays protected from one at 70% weekly, but
119
+ 10% weekly with an 85%-spent session loses to 70% weekly with a 20% one, because the
120
+ first never reaches the band.
121
+
122
+ **Exclusion** still fires on *any* bucket ≥ 90% — a full session bucket
97
123
  really does block right now — but that marker expires when the session resets, not days
98
124
  later.
99
125
 
@@ -286,8 +312,9 @@ above about the claude pool holds for the codex pool with these translations:
286
312
  | Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
287
313
  | `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |
288
314
 
289
- Same selection rule (weekly headroom primary, session tiebreak, ≥90% any-bucket
290
- exclusion, equal scores rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
315
+ Same selection rule (session gate first `CODEX_MULTIACC_SESSION_GATE`, default 50 —
316
+ then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
317
+ exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
291
318
  credential/policy scoping and soft expiry), same fail-open guarantees, same sync
292
319
  safety guards. The `codex` shim engages the buffered auto-retry only for
293
320
  `codex exec` runs with finite stdin, exactly like `-p` on the claude side.
package/bin/claude CHANGED
@@ -3,7 +3,9 @@
3
3
  # Every invocation runs under a randomly picked subscription account with limit headroom.
4
4
  # Self-contained on purpose: no sourcing, so a broken repo file can never break `claude`.
5
5
  # Selection: CLAUDE_CONFIG_DIR/CLAUDE_CODE_OAUTH_TOKEN passthrough > CLAUDE_ACCOUNT pin >
6
- # random within the 30-point headroom band > least-utilized fallback (degraded beats down).
6
+ # two cuts the 50-point session gate, then random within the 30-point weekly headroom
7
+ # band (previous pick avoided) > all-limited fallback: the same two cuts over the
8
+ # still-serving limited accounts, strict weekly (degraded beats down).
7
9
  # Accounts whose login is DEAD (expired refresh token, or a `.expired` marker from a
8
10
  # failed auth) are never selected — not even as the all-limited fallback — because they
9
11
  # fail every call outright; `claude-accounts expired` / `relogin` fix them.
@@ -410,9 +412,9 @@ stale_weekly() { # $1 = acct dir
410
412
  }
411
413
 
412
414
  # TRUE when this account contributes nothing to ranking: no in-window telemetry at all.
413
- # When every candidate is blind, every score is the same neutral constant, pick_best
414
- # sees one enormous tie, and selection quietly becomes uniform random — the failure
415
- # this whole file exists to prevent.
415
+ # When every candidate is blind, no candidate has a known reading, pick_best sees one
416
+ # enormous tie, and selection quietly becomes uniform random — the failure this whole
417
+ # file exists to prevent.
416
418
  telem_blind() { # $1 = acct dir
417
419
  ! within_window "$1" "$STALE_AFTER"
418
420
  }
@@ -425,36 +427,52 @@ age_human() { # $1 = seconds
425
427
  else printf '%dm\n' $((s / 60)); fi
426
428
  }
427
429
 
428
- # RANKING score lower is better (more headroom). Weekly headroom dominates: a weekly
429
- # bucket only refills on the account's fixed weekly reset (days away), while the 5h
430
- # session bucket self-heals, so session is a mild tiebreaker only. (Anthropic's docs
431
- # confirm this reset asymmetry an account whose only near-full bucket is the cheap
432
- # session one must NOT rank behind one burning durable weekly headroom.)
433
- # score = weekly%*1000 + session% weekly,session in [0,100]
434
- # Stale/unreadable telemetry ranks LAST (weekly 100, session 100), never "free"
435
- # EXCEPT in a blind pool (SEL_DEGRADED=1), where a still-valid stale weekly reading is
436
- # used instead. Unknown data must never beat a truthful usage reading; when every
437
- # candidate is unknown, the equal worst-case scores still preserve fail-open selection.
430
+ # RANKING — two cuts, no score, no tiebreaker (operator's ask, 2026-09-03: "among
431
+ # accounts where high session limits it must choose randomly from ones where highest
432
+ # weekly limits"). Session used to be a mild tiebreaker inside the score, which meant an
433
+ # account whose 5h bucket was nearly spent about to be rejected whatever its weekly
434
+ # headroom ranked exactly like a fresh one. It is now the FIRST cut instead:
435
+ # 1. SESSION GATE — candidates with a KNOWN session reading <= SESSION_GATE are the
436
+ # only ones ranked further. Unknown session never clears it. If NOBODY clears it the
437
+ # gate steps aside and everyone is ranked: the gate compares, it never empties the
438
+ # pool.
439
+ # 2. WEEKLY BAND — among those, the lowest KNOWN weekly usage leads and everything
440
+ # within HEADROOM_BAND points of it is a peer; peers are sampled at random with the
441
+ # previous pick skipped. Weekly still dominates because a weekly bucket only refills
442
+ # on the account's fixed weekly reset (days away) while the 5h session bucket
443
+ # self-heals — the gate is about "will this run be rejected now", not about headroom.
444
+ # Stale/unreadable telemetry ranks LAST (unknown weekly, and unknown never clears the
445
+ # gate), never "free" — EXCEPT in a blind pool (SEL_DEGRADED=1), where a still-valid stale
446
+ # weekly reading is used instead. Unknown data must never beat a truthful usage reading;
447
+ # when every candidate is unknown they tie, which preserves fail-open selection.
438
448
  SEL_DEGRADED=0
439
- sel_score_of() { # $1 = acct dir
440
- local w s
441
- if ! w="$(fresh_field "$1" weekly_percent)" && ! w="$(fresh_field "$1" max_percent)"; then
442
- if [ "$SEL_DEGRADED" = 1 ]; then w="$(stale_weekly "$1")" || w=100; else w=100; fi
443
- fi
444
- s="$(fresh_field "$1" session_percent)" || s=100
445
- printf '%s\n' $((w * 1000 + s))
446
- }
447
449
 
448
- # The direct shims use the same 30-point headroom band as pool-selection.v2. Strict
449
- # best-headroom selection burned one account to its limit while equally healthy
450
- # neighbours idled. A caller can set 0 to restore strict ranking.
450
+ # The direct shims use the same 30-point headroom band as pool-selection.v2, on WEEKLY
451
+ # usage. Strict best-headroom selection burned one account to its limit while equally
452
+ # healthy neighbours idled. A caller can set 0 to restore strict ranking (exact weekly
453
+ # ties only). The operator confirmed on 2026-09-03 that the 30-point band stays.
451
454
  HEADROOM_BAND="${CLAUDE_MULTIACC_HEADROOM_BAND:-30}"
452
455
  case "$HEADROOM_BAND" in ''|*[!0-9]*|??????*) HEADROOM_BAND=30 ;; esac
453
456
  [ "$HEADROOM_BAND" -gt 100 ] && HEADROOM_BAND=100
454
457
 
458
+ # The session gate: a candidate is "healthy enough to run now" when its KNOWN 5h usage is
459
+ # at or under this many points. It runs BEFORE the weekly band, so a nearly-spent session
460
+ # bucket takes an account out of contention however good its weekly headroom is. 100
461
+ # disables the gate (every known-session account clears it).
462
+ SESSION_GATE="${CLAUDE_MULTIACC_SESSION_GATE:-50}"
463
+ case "$SESSION_GATE" in ''|*[!0-9]*|??????*) SESSION_GATE=50 ;; esac
464
+ [ "$SESSION_GATE" -gt 100 ] && SESSION_GATE=100
465
+ # What the selection log prints for the gate: "off" in random mode, where no gate ran —
466
+ # the log must never claim a cut that was not made.
467
+ SESSION_GATE_LOG="$SESSION_GATE"
468
+
469
+ # weekly_percent ONLY. A reading without it is unknown here, exactly as it is unknown to
470
+ # pool-selection.v2 (which never sees max_percent). The old max_percent fallback let a
471
+ # weekly-less file rank — and, once the session gate existed, CLEAR the gate — on a number
472
+ # that may well be the session bucket's own peak (codex review, 2026-09-04).
455
473
  rank_weekly_of() { # $1 = acct dir -> comparable weekly use, or fail when unknown
456
474
  local w
457
- if w="$(fresh_field "$1" weekly_percent)" || w="$(fresh_field "$1" max_percent)"; then
475
+ if w="$(fresh_field "$1" weekly_percent)"; then
458
476
  printf '%s\n' "$w"
459
477
  return 0
460
478
  fi
@@ -462,6 +480,13 @@ rank_weekly_of() { # $1 = acct dir -> comparable weekly use, or fail when unknow
462
480
  stale_weekly "$1"
463
481
  }
464
482
 
483
+ rank_session_of() { # $1 = acct dir -> fresh session use, or fail when unknown
484
+ # Deliberately fresh-only, with no stale/degraded fallback: an old 5h reading says
485
+ # nothing about a bucket that self-heals every five hours, and an unknown session must
486
+ # not clear the gate.
487
+ fresh_field "$1" session_percent
488
+ }
489
+
465
490
  # The .limited marker's own fields (line 1: reset epoch; line 2: "bucket=…
466
491
  # percent=… … reason=…"). All tolerant: an unreadable or bare marker answers
467
492
  # "unknown", never an error — these feed the all-limited fallback only.
@@ -1176,45 +1201,69 @@ remember_pick() { # $1 acct dir — best effort. stderr is silenced BEFORE the r
1176
1201
  return 0
1177
1202
  }
1178
1203
 
1179
- # Pick the account with the MOST headroom (lowest ranking score = most weekly headroom,
1180
- # session as tiebreaker). Ties break randomly so equally-idle accounts still spread load.
1181
- # Sets PICK_DIR/PICK_SCORE as globalsit must never touch "$@", which holds the
1182
- # user's claude arguments.
1204
+ # Pick an account by the two-cut rule above: the session gate first, then random among
1205
+ # the peers inside the 30-point WEEKLY band, with the account handed out last time
1206
+ # skipped. No score and no tiebreaker the operator asked for a random choice among the
1207
+ # best-weekly accounts (2026-09-03), so session only ever acts through the gate.
1208
+ # Sets PICK_DIR/PICK_BAND_COUNT/PICK_GATE_COUNT as globals — it must never touch "$@",
1209
+ # which holds the user's claude arguments.
1183
1210
  PICK_DIR=""
1184
- PICK_SCORE=""
1185
1211
  PICK_BAND_COUNT=0
1212
+ PICK_GATE_COUNT=0
1186
1213
  PICK_STRICT=0
1187
1214
  pick_best() { # args: candidate dirs
1188
- local d w avoid best="" bestv=1000000 bestw=101 ceiling=100 ties=0 i n match
1189
- local cand=() score=() weekly=() known=() pool=()
1215
+ local d w s k sk avoid best="" bestw=101 band ceiling ties=0 i n
1216
+ local cand=() weekly=() known=() gated=() pool=()
1190
1217
  avoid="$(last_pick_id)"
1218
+ PICK_GATE_COUNT=0
1191
1219
  for d in "$@"; do
1192
1220
  cand+=("$d")
1193
- score+=("$(sel_score_of "$d")")
1194
- if w="$(rank_weekly_of "$d")"; then weekly+=("$w"); known+=(1)
1195
- else weekly+=(100); known+=(0); fi
1221
+ w="$(rank_weekly_of "$d")" && k=1 || k=0
1222
+ s="$(rank_session_of "$d")" && sk=1 || sk=0
1223
+ # "Known" takes BOTH readings — pool-selection.v2's quota_known rule. A weekly figure
1224
+ # on its own neither ranks nor clears the gate, and a session figure on its own could
1225
+ # otherwise be the sole gate-clearer and win the all-gated tie over an account whose
1226
+ # truthful weekly reading merely failed the gate. The one exception is a DEGRADED pool
1227
+ # (SEL_DEGRADED=1): nothing is fresh anywhere, the gate has necessarily stepped aside,
1228
+ # and a still-valid stale weekly reading is the only truth there is. (Neither writer
1229
+ # emits one field without the other; this is parity with lib/selector_policy.py.)
1230
+ if [ "$k" = 1 ] && { [ "$sk" = 1 ] || [ "$SEL_DEGRADED" = 1 ]; }; then
1231
+ weekly+=("$w"); known+=(1)
1232
+ else
1233
+ weekly+=(100); known+=(0)
1234
+ fi
1235
+ if [ "$k" = 1 ] && [ "$sk" = 1 ] && [ "$s" -le "$SESSION_GATE" ]; then
1236
+ gated+=(1); PICK_GATE_COUNT=$((PICK_GATE_COUNT + 1))
1237
+ else
1238
+ gated+=(0)
1239
+ fi
1196
1240
  done
1197
1241
  n=${#cand[@]}
1198
1242
  i=0
1199
1243
  while [ "$i" -lt "$n" ]; do
1200
- [ "${score[$i]}" -lt "$bestv" ] && bestv="${score[$i]}"
1201
- [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -lt "$bestw" ] && bestw="${weekly[$i]}"
1244
+ # Nobody clears the gate => everybody does: the gate compares, it never empties the pool.
1245
+ [ "$PICK_GATE_COUNT" -eq 0 ] && gated[$i]=1
1246
+ if [ "${gated[$i]}" = 1 ] && [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -lt "$bestw" ]; then
1247
+ bestw="${weekly[$i]}"
1248
+ fi
1202
1249
  i=$((i + 1))
1203
1250
  done
1204
- ceiling=$((bestw + HEADROOM_BAND)); [ "$ceiling" -gt 100 ] && ceiling=100
1251
+ band="$HEADROOM_BAND"
1252
+ [ "$PICK_STRICT" = 1 ] && band=0
1253
+ ceiling=$((bestw + band)); [ "$ceiling" -gt 100 ] && ceiling=100
1205
1254
  i=0
1206
1255
  while [ "$i" -lt "$n" ]; do
1207
- match=0
1208
- if [ "$PICK_STRICT" = 0 ] && [ "$HEADROOM_BAND" -gt 0 ] && [ "$bestw" -le 100 ]; then
1209
- [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -le "$ceiling" ] && match=1
1210
- else
1211
- [ "${score[$i]}" -eq "$bestv" ] && match=1
1256
+ if [ "${gated[$i]}" = 1 ]; then
1257
+ if [ "$bestw" -gt 100 ]; then
1258
+ pool+=("$i") # no weekly reading anywhere: all gated tie
1259
+ elif [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -le "$ceiling" ]; then
1260
+ pool+=("$i")
1261
+ fi
1212
1262
  fi
1213
- [ "$match" = 1 ] && pool+=("$i")
1214
1263
  i=$((i + 1))
1215
1264
  done
1216
1265
  PICK_BAND_COUNT=${#pool[@]}
1217
- # Reservoir-sample inside the band, skipping the account just handed out.
1266
+ # Reservoir-sample the peers, skipping the account just handed out.
1218
1267
  for i in "${pool[@]}"; do
1219
1268
  if [ "${cand[$i]##*/}" != "$avoid" ]; then
1220
1269
  ties=$((ties + 1))
@@ -1228,15 +1277,15 @@ pick_best() { # args: candidate dirs
1228
1277
  done
1229
1278
  fi
1230
1279
  PICK_DIR="$best"
1231
- PICK_SCORE="$(sel_score_of "$best")"
1232
1280
  }
1233
1281
 
1234
1282
  # Telemetry going stale is not a per-run detail, it is a pool-wide outage: with no
1235
- # in-window data ANYWHERE every account scores the identical NEUTRAL value, the tie
1236
- # spans the whole pool, and "pick the account with the most headroom" silently becomes
1237
- # "pick any account at all". That is how a fresh session lands on the one account
1238
- # already at 80% of its weekly limit while `claude-accounts status` still shows a
1239
- # reassuring 2% from eleven days ago. It cost eleven days of blind picks once.
1283
+ # in-window data ANYWHERE every account reads as UNKNOWN no weekly, and nothing that
1284
+ # can clear the session gate — so the tie spans the whole pool and "pick the account with
1285
+ # the most headroom" silently becomes "pick any account at all". That is how a fresh
1286
+ # session lands on the one account already at 80% of its weekly limit while
1287
+ # `claude-accounts status` still shows a reassuring 2% from eleven days ago. It cost
1288
+ # eleven days of blind picks once.
1240
1289
  # Two answers, and the order matters: rank on whatever old readings are still true
1241
1290
  # BEFORE picking, and say out loud which of the two happened.
1242
1291
  blind=1 # 1 = no candidate has in-window telemetry
@@ -1256,10 +1305,10 @@ assess_telemetry() { # args: the dirs actually being chosen between
1256
1305
  stale_weekly "$d" >/dev/null && stale_ok=$((stale_ok + 1))
1257
1306
  done
1258
1307
  # All or nothing. A candidate whose reading has no horizon — a limits.json written
1259
- # before this field existed, or one whose week has already turned — scores neutral
1260
- # 50, and 50 would beat a NEIGHBOUR's true-but-worse 70. Mixing the two makes the
1261
- # degraded ranking actively wrong, so it is only used when every candidate can be
1262
- # compared on the same footing.
1308
+ # before this field existed, or one whose week has already turned — stays UNKNOWN even
1309
+ # in degraded mode, so it would drop out of the band entirely while its neighbours rank
1310
+ # on stale numbers. Mixing the two makes the degraded ranking actively wrong, so it is
1311
+ # only used when every candidate can be compared on the same footing.
1263
1312
  [ "$n" -gt 0 ] && [ "$stale_ok" -eq "$n" ] && degraded=1
1264
1313
  return 0
1265
1314
  }
@@ -1273,6 +1322,8 @@ if [ "${#eligible[@]}" -gt 0 ]; then
1273
1322
  if [ "${CLAUDE_SHIM_SELECT:-headroom}" = "random" ]; then
1274
1323
  PICK_DIR="${eligible[$((RANDOM % ${#eligible[@]}))]}"
1275
1324
  PICK_BAND_COUNT=${#eligible[@]}
1325
+ PICK_GATE_COUNT=${#eligible[@]}
1326
+ SESSION_GATE_LOG=off
1276
1327
  else
1277
1328
  pick_best "${eligible[@]}"
1278
1329
  fi
@@ -1404,10 +1455,12 @@ if [ "$blind" = 1 ]; then
1404
1455
  if [ "$degraded" = 1 ]; then
1405
1456
  sel_log "$acct weekly=$(stale_weekly "$pick" || echo '?')% session=?%" \
1406
1457
  "ranking=DEGRADED telemetry-age=${blind_age}s band=${HEADROOM_BAND}" \
1407
- "band-count=${PICK_BAND_COUNT} pwd=$PWD"
1458
+ "band-count=${PICK_BAND_COUNT} session-gate=${SESSION_GATE_LOG}" \
1459
+ "session-ok=${PICK_GATE_COUNT} pwd=$PWD"
1408
1460
  else
1409
1461
  sel_log "$acct weekly=?% session=?% ranking=BLIND telemetry-age=${blind_age}s" \
1410
- "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT} pwd=$PWD"
1462
+ "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT}" \
1463
+ "session-gate=${SESSION_GATE_LOG} session-ok=${PICK_GATE_COUNT} pwd=$PWD"
1411
1464
  fi
1412
1465
  # Terminal only, at most hourly — a service-spawned `claude -p` must keep its stderr
1413
1466
  # byte-clean, and this is advice, never a failure.
@@ -1431,7 +1484,8 @@ if [ "$blind" = 1 ]; then
1431
1484
  else
1432
1485
  sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
1433
1486
  "session=$(fresh_field "$pick" session_percent || echo '?')%" \
1434
- "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT} pwd=$PWD"
1487
+ "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT}" \
1488
+ "session-gate=${SESSION_GATE_LOG} session-ok=${PICK_GATE_COUNT} pwd=$PWD"
1435
1489
  fi
1436
1490
 
1437
1491
  export CLAUDE_SHIM_ACTIVE=1
@@ -2156,7 +2156,10 @@ for acct in manifest.get('accounts', []):
2156
2156
  # weekly_percent — peak of the durable (weekly/monthly) buckets; the PRIMARY
2157
2157
  # ranking signal, because weekly headroom only returns on the
2158
2158
  # account's fixed weekly reset (days away).
2159
- # 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.)
2160
2163
  maxp = max([b['percent'] for b in buckets] or [0])
2161
2164
  weekly = [b['percent'] for b in buckets if b['group'] != 'session']
2162
2165
  session = [b['percent'] for b in buckets if b['group'] == 'session']