claude-multiacc 2.0.20 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE_ACCS_TASK.md +10 -5
- package/README.md +45 -18
- package/bin/claude +113 -59
- package/bin/claude-accounts +4 -1
- package/bin/codex +178 -43
- package/bin/codex-accounts +4 -1
- package/docs/ACCOUNT_OPERATIONS.md +7 -4
- package/docs/TRACK_PROMPT.md +76 -0
- package/docs/UNIFIED_SELECTOR.md +40 -7
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/selector_policy.py +42 -7
- package/lib/selector_primitives.py +25 -16
- package/package.json +1 -1
- package/tests/run-tests.sh +331 -35
- package/tests/test_selector.py +104 -2
package/CLAUDE_ACCS_TASK.md
CHANGED
|
@@ -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
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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. `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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).
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 (
|
|
290
|
-
|
|
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
|
-
#
|
|
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,
|
|
414
|
-
#
|
|
415
|
-
#
|
|
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
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
#
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
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
|
|
449
|
-
# best-headroom selection burned one account to its limit while equally
|
|
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)"
|
|
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
|
|
1180
|
-
#
|
|
1181
|
-
#
|
|
1182
|
-
#
|
|
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=""
|
|
1189
|
-
local cand=()
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
|
|
1201
|
-
[ "$
|
|
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
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
|
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
|
|
1236
|
-
# spans the whole pool
|
|
1237
|
-
# "pick any account at all". That is how a fresh
|
|
1238
|
-
# already at 80% of its weekly limit while
|
|
1239
|
-
# reassuring 2% from eleven days ago. It cost
|
|
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 —
|
|
1260
|
-
#
|
|
1261
|
-
# degraded ranking actively wrong, so it is
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
package/bin/claude-accounts
CHANGED
|
@@ -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;
|
|
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']
|