claude-multiacc 2.0.41 → 2.0.43
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/README.md +60 -2
- package/bin/claude +414 -5
- package/bin/claude-accounts +36 -11
- package/bin/codex +406 -4
- package/docs/ACCOUNT_OPERATIONS.md +7 -1
- package/docs/AUTORESUME.md +321 -0
- package/docs/CODEX.md +12 -1
- package/docs/VERIFICATION.md +9 -1
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.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/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/autoresume.py +2271 -0
- package/lib/common.sh +24 -1
- package/lib/keychain.py +274 -59
- package/package.json +1 -1
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/fake_security.sh +90 -0
- package/tests/run-tests.sh +1178 -67
- package/tests/test_autoresume.py +2565 -0
- package/tests/test_keychain.py +323 -50
package/README.md
CHANGED
|
@@ -160,7 +160,9 @@ fails open into stock `claude`, and logs why to `selection.log`.
|
|
|
160
160
|
The shim prints nothing, logs `timestamp account cwd` (never prompt text) to
|
|
161
161
|
`selection.log`, and `exec`s the real binary — stdin/stdout/exit codes pass through
|
|
162
162
|
byte-identically. If anything is missing (no manifest, no accounts, unreadable state,
|
|
163
|
-
even an unset `HOME`) it fails **open** into plain passthrough.
|
|
163
|
+
even an unset `HOME`) it fails **open** into plain passthrough. In tmux, an interactive
|
|
164
|
+
launch also starts a detached [auto-resume](#auto-resume) watcher just before that `exec`;
|
|
165
|
+
the `exec` itself does not change.
|
|
164
166
|
|
|
165
167
|
**Limit-aware marking.** `claude-accounts limits` (every 15 min via launchd on the Mac,
|
|
166
168
|
cron on the server, plus an opportunistic non-blocking kick from the shim when data is
|
|
@@ -272,6 +274,17 @@ account's `.credentials.json` (0600), or its macOS Keychain item.
|
|
|
272
274
|
> login made over ssh migrates into the Keychain the first time a GUI-session process
|
|
273
275
|
> refreshes its token — the pool reads both places (`lib/keychain.py`), and an account
|
|
274
276
|
> that must work from everywhere should carry a portable token (`claude-accounts mint`).
|
|
277
|
+
> The item's **account name** is the client's `$USER` — and a client started without USER
|
|
278
|
+
> files it under `unknown`, beside the older item, where the next client never looks. So
|
|
279
|
+
> `claude-accounts`/`codex-accounts` and the shim fill `USER`/`LOGNAME` from `id -un` when
|
|
280
|
+
> the caller's env has none, and the pool judges only the item under that canonical name —
|
|
281
|
+
> the one the client reads (one `security` call per lookup). A login found only under
|
|
282
|
+
> another name counts nowhere until the scheduled limits pass moves it under the canonical
|
|
283
|
+
> name. A token refresh writes back under the canonical name and drops the siblings it
|
|
284
|
+
> made stale (the same grant, a dead one) but keeps another live grant; a sign-in whose
|
|
285
|
+
> identity checked out prunes every sibling; `remove` deletes every item.
|
|
286
|
+
> `python3 lib/keychain.py prune <acct dir>` prunes by hand (only when the canonical item
|
|
287
|
+
> is a live login; it never writes).
|
|
275
288
|
> Override: `CLAUDE_MULTIACC_KEYCHAIN=0` disables the lookup. This is what keeps idle accounts' telemetry fresh so they win
|
|
276
289
|
selection over busy accounts; without it, stale telemetry ranks neutral and a truly-idle
|
|
277
290
|
account would lose to a busy-but-fresh one. Refresh failures fail open and back off via
|
|
@@ -394,6 +407,45 @@ also sees the model's own answer: a `-p` run that merely *mentions* a 403 must n
|
|
|
394
407
|
an account, and if one slips through it returns to the pool by itself. Output is buffered so a retried call
|
|
395
408
|
never double-emits. Only engages when stdin is finite (tty / regular file / `/dev/null`)
|
|
396
409
|
and ≥2 accounts are eligible; service-spawned pipes take the plain exec path untouched.
|
|
410
|
+
Interactive sessions are not retried this way. They get auto-resume, described next.
|
|
411
|
+
|
|
412
|
+
## Auto-resume
|
|
413
|
+
|
|
414
|
+
An interactive `claude` or `codex` session in **tmux** can stop on a usage limit, a failed
|
|
415
|
+
login, an API error or (claude) a crash. It is then **resumed automatically, same session
|
|
416
|
+
id**, on another pooled account with headroom. There is no `/exit`, no `--resume` and no
|
|
417
|
+
"continue" to type. It covers the everyday launches:
|
|
418
|
+
|
|
419
|
+
- `claude --dangerously-skip-permissions`, `--resume <id>` and `--model …`;
|
|
420
|
+
- `codex --dangerously-bypass-approvals-and-sandbox` and `codex resume …`.
|
|
421
|
+
|
|
422
|
+
Every other launch, including `-p` and `codex exec`, runs exactly as before.
|
|
423
|
+
|
|
424
|
+
**How it works.**
|
|
425
|
+
|
|
426
|
+
- **Launch.** The shim's `exec` is unchanged. Just before it, a detached watcher starts
|
|
427
|
+
(`lib/autoresume.py`, which needs `python3`). It reads that run's transcript (codex: its
|
|
428
|
+
rollout).
|
|
429
|
+
- **Probe.** Once an error settles, the watcher asks the shim, in probe mode, whether
|
|
430
|
+
another account has headroom. If none does, it **holds** and touches nothing, and
|
|
431
|
+
Claude's own "continuing automatically at …" carries on.
|
|
432
|
+
- **Relaunch.** Otherwise it stops the TUI with SIGTERM and waits for the pane's shell
|
|
433
|
+
prompt. It then types a one-line relaunch of the shim into that **shell**. It never
|
|
434
|
+
sends a keystroke to the TUI, whose limit menu can add funds or spend the one-shot
|
|
435
|
+
`/limit-reset`.
|
|
436
|
+
- **Resume.** The relaunched shim parks the old account with the usual markers, then runs
|
|
437
|
+
normal selection minus the account it just left. The session resumes with a short
|
|
438
|
+
prompt telling the model it was restarted and should continue.
|
|
439
|
+
|
|
440
|
+
Every step appends an `autoresume <event>` line to `selection.log`.
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
touch ~/.claude-accounts/autoresume.off # claude pool off, already-running sessions included
|
|
444
|
+
touch ~/.codex-accounts/autoresume.off # codex pool off
|
|
445
|
+
CLAUDE_MULTIACC_AUTORESUME=0 claude # one launch (CODEX_MULTIACC_AUTORESUME=0 codex)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
See [auto-resume: error classes, gates, budgets, logs and limitations](docs/AUTORESUME.md).
|
|
397
449
|
|
|
398
450
|
## MCP servers for every account
|
|
399
451
|
|
|
@@ -404,7 +456,7 @@ account, and a stock `claude mcp add` lands in **one random account**. That is h
|
|
|
404
456
|
now owns a **registry**, `<pool>/mcp-servers.json`, that is reconciled into every account:
|
|
405
457
|
|
|
406
458
|
```bash
|
|
407
|
-
claude-accounts mcp add
|
|
459
|
+
claude-accounts mcp add docs-search -- node /path/docs-search.mjs serve # both pools
|
|
408
460
|
claude-accounts mcp add --provider claude local-dev -e KEY=v -- node /path/server.mjs serve
|
|
409
461
|
claude-accounts mcp add --scope project --project "$HOME/design-lab/run1" appinspire -e APPINSPIRE_LIBRARY_DIR="$HOME/.appinspire-mcp/library" -- node "$HOME/appinspire-mcp/bin/appinspire-mcp.mjs" serve
|
|
410
462
|
claude-accounts mcp remove adspower-local-api # retire EVERYWHERE: every account, every project entry
|
|
@@ -441,6 +493,12 @@ claude-accounts mcp apply # re-apply now (repair drift)
|
|
|
441
493
|
from **inside** a pooled session (an agent's Bash tool, `npx appinspire-mcp install` run by
|
|
442
494
|
an agent): the session's config dir is a pool account, so the same mirror applies. A
|
|
443
495
|
config dir outside the pool is plain passthrough.
|
|
496
|
+
- **Runner-managed servers**: on app-robot fleet Macs the runner publishes `appinspire-mcp` into
|
|
497
|
+
each Mac's machine-local overlay with that Mac's own paths. Never `mcp add` it to the synced
|
|
498
|
+
registry (directly, through a mirrored `claude|codex mcp add`, or via `appinspire-mcp install`):
|
|
499
|
+
the synced entry wins and every Mac launches one machine's Node and package paths. Never
|
|
500
|
+
`mcp remove` it either: the tombstone deletes the managed entry on every Mac. Drop a stray
|
|
501
|
+
registry entry without a tombstone, then `mcp apply` and `sync`.
|
|
444
502
|
- **Replicas**: on a pool whose `sync-role` file says `replica`, `add`/`remove` and the
|
|
445
503
|
shim's mirror land in the machine-local overlay (owner `local`) instead of the synced
|
|
446
504
|
registry, because the source's next push would overwrite them. Make registry changes on
|
package/bin/claude
CHANGED
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
set -u
|
|
14
14
|
|
|
15
|
+
# Claude Code names its macOS Keychain item after $USER, and under Bun a client with no
|
|
16
|
+
# USER calls the user "unknown" — so a caller whose env lacks it (app-robot's panel
|
|
17
|
+
# ceremony, 2026-09-24) gets a login filed under a name the next client never reads
|
|
18
|
+
# (lib/keychain.py). Every client this shim starts, passthrough included, inherits a
|
|
19
|
+
# real name; lib/common.sh does the same for claude-accounts.
|
|
20
|
+
if [ -z "${USER:-}" ]; then
|
|
21
|
+
USER="$(id -un 2>/dev/null || true)"
|
|
22
|
+
if [ -n "$USER" ]; then export USER; else unset USER; fi
|
|
23
|
+
fi
|
|
24
|
+
if [ -z "${LOGNAME:-}" ] && [ -n "${USER:-}" ]; then export LOGNAME="$USER"; fi
|
|
25
|
+
|
|
15
26
|
# ${HOME:-} guards: with HOME stripped (env -i, some cron/systemd units) the shim
|
|
16
27
|
# must still fail OPEN into plain passthrough, never abort on an unbound variable.
|
|
17
28
|
# CLAUDE_ACCOUNTS_ROOT scopes the pool to one app-robot instance; CLAUDE_ACCOUNTS_DIR
|
|
@@ -177,10 +188,14 @@ mcp_nested_mirror() {
|
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
# Fast passthrough: caller pinned a config dir or token, addon disabled, recursion
|
|
180
|
-
# guard, or no account data yet. Byte-identical behavior to stock claude
|
|
191
|
+
# guard, or no account data yet. Byte-identical behavior to stock claude (a USER the
|
|
192
|
+
# caller left out is filled at the top — see there).
|
|
181
193
|
if [ -n "${CLAUDE_CONFIG_DIR:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] \
|
|
182
194
|
|| [ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CLAUDE_SHIM_ACTIVE:-}" ] \
|
|
183
195
|
|| [ ! -f "$MANIFEST" ]; then
|
|
196
|
+
# An auto-resume probe asks who WOULD be picked and must never start a client; a
|
|
197
|
+
# passthrough has no pool pick to report (see ---- auto-resume ----).
|
|
198
|
+
[ "${CLAUDE_MULTIACC_AR_PROBE:-}" = "1" ] && { printf 'pick= tier=none\n'; exit 3; }
|
|
184
199
|
mcp_nested_mirror "$@" || true
|
|
185
200
|
exec "$REAL" "$@"
|
|
186
201
|
fi
|
|
@@ -213,6 +228,11 @@ case "${CLAUDE_MULTIACC_KEYCHAIN:-}" in
|
|
|
213
228
|
*) if [ "$(uname -s)" = "Darwin" ]; then KC_ON=1; else KC_ON=0; fi ;;
|
|
214
229
|
esac
|
|
215
230
|
if [ "$KC_ON" = "1" ] && ! command -v security >/dev/null 2>&1; then KC_ON=0; fi
|
|
231
|
+
# The client reads ONLY the item under its own account name — $USER (filled above),
|
|
232
|
+
# anything outside [A-Za-z0-9._-] becoming "claude-code-user" — so that one item is what
|
|
233
|
+
# this shim judges: a sibling under another name is invisible to the client it launches.
|
|
234
|
+
KC_ACCOUNT="${USER:-}"
|
|
235
|
+
case "$KC_ACCOUNT" in ''|*[!A-Za-z0-9._-]*) KC_ACCOUNT=claude-code-user ;; esac
|
|
216
236
|
kc_service() { # $1 = acct dir -> the keychain service name the client uses for it
|
|
217
237
|
local h=""
|
|
218
238
|
if command -v shasum >/dev/null 2>&1; then h="$(printf '%s' "$1" | shasum -a 256 2>/dev/null)"
|
|
@@ -227,7 +247,7 @@ kc_lookup() { # $1 = acct dir; sets KC_JSON; rc 0 readable, 1 none, 2 exists but
|
|
|
227
247
|
[ "$KC_ON" = "1" ] || { KC_JSON=""; return 1; }
|
|
228
248
|
if [ "$KC_MEMO_DIR" = "$1" ]; then return "$KC_MEMO_RC"; fi
|
|
229
249
|
local out rc
|
|
230
|
-
out="$(security find-generic-password -s "$(kc_service "$1")" -w 2>/dev/null)"; rc=$?
|
|
250
|
+
out="$(security find-generic-password -a "$KC_ACCOUNT" -s "$(kc_service "$1")" -w 2>/dev/null)"; rc=$?
|
|
231
251
|
KC_MEMO_DIR="$1"; KC_JSON=""; KC_MEMO_RC=1
|
|
232
252
|
case "$rc" in
|
|
233
253
|
0) case "$out" in *claudeAiOauth*) KC_JSON="$out"; KC_MEMO_RC=0 ;; esac ;;
|
|
@@ -239,7 +259,7 @@ kc_mtime() { # $1 = acct dir -> epoch of the item's last write (0 when none); at
|
|
|
239
259
|
# are readable even when the secret is locked, so a marker still self-heals
|
|
240
260
|
[ "$KC_ON" = "1" ] || { echo 0; return 0; }
|
|
241
261
|
local stamp
|
|
242
|
-
stamp="$(security find-generic-password -s "$(kc_service "$1")" 2>/dev/null \
|
|
262
|
+
stamp="$(security find-generic-password -a "$KC_ACCOUNT" -s "$(kc_service "$1")" 2>/dev/null \
|
|
243
263
|
| LC_ALL=C sed -n 's/.*"mdat"<timedate>=0x[0-9A-Fa-f]* *"\([0-9]\{14\}\)Z.*/\1/p' | head -1)"
|
|
244
264
|
case "$stamp" in
|
|
245
265
|
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])
|
|
@@ -262,6 +282,379 @@ sel_log() {
|
|
|
262
282
|
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" 2>/dev/null >> "$ACC_ROOT/selection.log" || true
|
|
263
283
|
}
|
|
264
284
|
|
|
285
|
+
# ---- auto-resume ----------------------------------------------------------------
|
|
286
|
+
# An interactive session that stops on a usage limit, a failed login, an API error or a
|
|
287
|
+
# crash is resumed — same session id, no keystroke — on another pooled account. The TUI
|
|
288
|
+
# at a limit does not exit; it idles on "continuing automatically at <reset>", which can
|
|
289
|
+
# be days away. The shim's share of the job is small on purpose and never blocks:
|
|
290
|
+
# - the plain interactive exec stays byte-identical; just before it, a detached watcher
|
|
291
|
+
# (lib/autoresume.py) is spawned for the pid the client is about to become;
|
|
292
|
+
# - when the watcher moves a session it stops the client, writes a single-use relaunch
|
|
293
|
+
# file and types ` CLAUDE_MULTIACC_AR=<token>:<sid> [CLAUDE_ACCOUNTS_ROOT=<root>] <this
|
|
294
|
+
# shim's path>` into the pane's SHELL — never into the TUI, whose limit menu can "Add
|
|
295
|
+
# funds" or spend the one-shot /limit-reset — and that command lands HERE: the token
|
|
296
|
+
# names the file, which holds the verdict to mark on the old account BEFORE selection,
|
|
297
|
+
# the argv to resume with, and the chain state (depth/avoid/hist) the next watcher
|
|
298
|
+
# carries on; the pool root is typed only when it is not the default one;
|
|
299
|
+
# - a probe run (CLAUDE_MULTIACC_AR_PROBE=1) is how the watcher asks "who would be
|
|
300
|
+
# picked now?" — the real candidate loop, then `pick=<acct> tier=<t>` and exit, so the
|
|
301
|
+
# watcher only kills a session there is somewhere to move to.
|
|
302
|
+
# Everything fails OPEN: a missing python, a non-tmux terminal or an argv this code does
|
|
303
|
+
# not recognise all mean "exec exactly as before, unsupervised". The one exception is a
|
|
304
|
+
# typed relaunch whose token is gone: it names the session to resume and exits 2, because
|
|
305
|
+
# its own (empty) argv would start a fresh session in the stopped one's place.
|
|
306
|
+
# Tokens and relaunch/state files live in $ACC_ROOT/tmp/autoresume; the watcher prunes.
|
|
307
|
+
AR_DIR="$ACC_ROOT/tmp/autoresume"
|
|
308
|
+
AR_PROBE=0
|
|
309
|
+
[ "${CLAUDE_MULTIACC_AR_PROBE:-}" = "1" ] && AR_PROBE=1
|
|
310
|
+
AR_DEPTH=0 # relaunches so far in this chain (the watcher caps it)
|
|
311
|
+
AR_AVOID="" # acct-NN:<until epoch>,... — accounts this chain just left
|
|
312
|
+
AR_HIST="" # class:epoch,... — the watcher's hourly budgets
|
|
313
|
+
AR_CHAIN="" # opaque chain id, minted and read by the watcher
|
|
314
|
+
AR_LOADED=0
|
|
315
|
+
AR_ARGV=()
|
|
316
|
+
AR_ORIG_ARGV=()
|
|
317
|
+
AR_R_CLASS="" AR_R_ACCT="" AR_R_RESET="" AR_R_RTYPE="" AR_R_MARKED="" AR_R_SID=""
|
|
318
|
+
AR_TRUST=0 # this run continues a relaunch: its cwd is trusted in the picked account
|
|
319
|
+
AR_PY=""
|
|
320
|
+
|
|
321
|
+
# A chain field copied from a file (or the probe's env) into the next state file: one
|
|
322
|
+
# line, a narrow charset, bounded. Anything else is dropped — the chain restarts, and
|
|
323
|
+
# nothing but the watcher's budgets depends on it.
|
|
324
|
+
ar_field_ok() { # $1 value
|
|
325
|
+
[ "${#1}" -le 2048 ] || return 1
|
|
326
|
+
case "$1" in *[!A-Za-z0-9:,._-]*) return 1 ;; esac
|
|
327
|
+
return 0
|
|
328
|
+
}
|
|
329
|
+
# The probe gets the watcher's AVOID list from its env; a normal run only ever takes it
|
|
330
|
+
# from a relaunch file.
|
|
331
|
+
[ "$AR_PROBE" = 1 ] && ar_field_ok "${CLAUDE_MULTIACC_AR_AVOID:-}" \
|
|
332
|
+
&& AR_AVOID="${CLAUDE_MULTIACC_AR_AVOID:-}"
|
|
333
|
+
|
|
334
|
+
# Load a relaunch token. Runs BEFORE RUN_MODEL reads argv below (the relaunch argv may
|
|
335
|
+
# carry --model) and before anything else consults $PWD. sess_id_ok and the markers are
|
|
336
|
+
# defined further down, so the id class is spelled inline and the verdict is applied
|
|
337
|
+
# later, by ar_relaunch_apply, still ahead of any selection.
|
|
338
|
+
ar_relaunch_load() { # $1 token -> 0 with AR_ARGV and the AR_R_*/chain fields set
|
|
339
|
+
local tok="$1" rf af line k v n=0 a="" ok=0 ver="" prov="" sid="" cwd=""
|
|
340
|
+
local cls="" acct="" reset="" rtype="" marked="" depth=0 avoid="" hist="" chain=""
|
|
341
|
+
# Alphanumerics only: the token becomes part of a path, so no '/' and no '..'.
|
|
342
|
+
case "$tok" in *[!A-Za-z0-9]*) return 1 ;; esac
|
|
343
|
+
[ "${#tok}" -ge 8 ] && [ "${#tok}" -le 64 ] || return 1
|
|
344
|
+
rf="$AR_DIR/r-$tok.relaunch"
|
|
345
|
+
af="$AR_DIR/r-$tok.argv"
|
|
346
|
+
# -f before every read: a FIFO planted under that name would block the read forever,
|
|
347
|
+
# a hang before exec. Unknown keys are ignored; known ones are validated one by one.
|
|
348
|
+
if [ -f "$rf" ]; then
|
|
349
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
350
|
+
n=$((n + 1)); [ "$n" -le 64 ] || break
|
|
351
|
+
case "$line" in *=*) ;; *) line=""; continue ;; esac
|
|
352
|
+
k="${line%%=*}"; v="${line#*=}"; line=""
|
|
353
|
+
case "$k" in
|
|
354
|
+
v) ver="$v" ;;
|
|
355
|
+
provider) prov="$v" ;;
|
|
356
|
+
class) case "$v" in ''|*[!a-z_]*) ;; *) cls="$v" ;; esac ;;
|
|
357
|
+
acct) case "$v" in acct-*[!0-9]*|acct-) ;; acct-*) acct="$v" ;; esac ;;
|
|
358
|
+
reset) num_ok "$v" && reset="$v" ;;
|
|
359
|
+
rtype) case "$v" in five_hour|seven_day) rtype="$v" ;; esac ;;
|
|
360
|
+
marked) case "$v" in ''|*[!0-9TZ:.+-]*) ;; *) marked="$v" ;; esac ;;
|
|
361
|
+
sid) case "$v" in ''|-*|*[!0-9a-fA-F-]*) ;; *) sid="$v" ;; esac ;;
|
|
362
|
+
cwd) case "$v" in /*) cwd="$v" ;; esac ;;
|
|
363
|
+
depth) num_ok "$v" && depth="$v" ;;
|
|
364
|
+
avoid) ar_field_ok "$v" && avoid="$v" ;;
|
|
365
|
+
hist) ar_field_ok "$v" && hist="$v" ;;
|
|
366
|
+
chain) ar_field_ok "$v" && [ "${#v}" -le 64 ] && chain="$v" ;;
|
|
367
|
+
esac
|
|
368
|
+
done < "$rf"
|
|
369
|
+
if [ "$ver" = 1 ] && [ "$prov" = claude ] && [ -f "$af" ] \
|
|
370
|
+
&& [ $((now - $(file_mtime "$rf"))) -le 600 ]; then
|
|
371
|
+
AR_ARGV=()
|
|
372
|
+
while IFS= read -r -d '' a; do
|
|
373
|
+
AR_ARGV+=("$a"); a=""
|
|
374
|
+
[ "${#AR_ARGV[@]}" -lt 512 ] || break
|
|
375
|
+
done < "$af"
|
|
376
|
+
# A writer that omits the final NUL still means its last element.
|
|
377
|
+
[ -n "$a" ] && AR_ARGV+=("$a")
|
|
378
|
+
[ "${#AR_ARGV[@]}" -gt 0 ] && ok=1
|
|
379
|
+
fi
|
|
380
|
+
fi
|
|
381
|
+
# Single use, whatever the outcome: a token is never honoured twice.
|
|
382
|
+
rm -f "$rf" "$af" 2>/dev/null
|
|
383
|
+
if [ "$ok" != 1 ]; then
|
|
384
|
+
AR_ARGV=()
|
|
385
|
+
AR_R_SID="$sid"
|
|
386
|
+
return 1
|
|
387
|
+
fi
|
|
388
|
+
AR_LOADED=1
|
|
389
|
+
AR_R_CLASS="$cls"; AR_R_ACCT="$acct"; AR_R_RESET="$reset"; AR_R_RTYPE="$rtype"
|
|
390
|
+
AR_R_MARKED="$marked"
|
|
391
|
+
AR_DEPTH="$depth"; AR_HIST="$hist"; AR_CHAIN="$chain"; AR_AVOID="$avoid"
|
|
392
|
+
# The pane's shell sits wherever the session was launched, but the session's own cwd is
|
|
393
|
+
# what --resume looks the transcript up by. Everything already resolved against the old
|
|
394
|
+
# directory (a relative pool root or PATH entry) has to survive the cd.
|
|
395
|
+
if [ -n "$cwd" ] && [ -d "$cwd" ] && [ "$cwd" != "$PWD" ]; then
|
|
396
|
+
case "$ACC_ROOT" in /*) ;; *) ACC_ROOT="$PWD/$ACC_ROOT"; MANIFEST="$ACC_ROOT/accounts.json"; AR_DIR="$ACC_ROOT/tmp/autoresume" ;; esac
|
|
397
|
+
case "$REAL" in /*) ;; *) REAL="$PWD/$REAL" ;; esac
|
|
398
|
+
cd -- "$cwd" 2>/dev/null || true
|
|
399
|
+
fi
|
|
400
|
+
return 0
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# A typed relaunch whose token cannot be honoured (missing, expired, malformed). The
|
|
404
|
+
# watcher has already stopped the session and the typed line carries no argv of its own,
|
|
405
|
+
# so running on would open a FRESH session in its place: say how to get it back, and stop.
|
|
406
|
+
ar_token_dead() { # $1 session id (the token's, else the file's) or empty
|
|
407
|
+
local sid="$1"
|
|
408
|
+
case "$sid" in
|
|
409
|
+
''|-*|*[!0123456789abcdefABCDEF-]*) sid="" ;;
|
|
410
|
+
*-*) [ "${#sid}" -le 64 ] || sid="" ;;
|
|
411
|
+
*) sid="" ;;
|
|
412
|
+
esac
|
|
413
|
+
sel_log "autoresume expired sid=${sid:--}"
|
|
414
|
+
if [ -n "$sid" ]; then
|
|
415
|
+
printf 'claude-multiacc: auto-resume could not continue automatically (the resume token expired). Resume with: claude --resume %s\n' "$sid" >&2
|
|
416
|
+
else
|
|
417
|
+
printf 'claude-multiacc: auto-resume could not continue automatically (the resume token expired).\n' >&2
|
|
418
|
+
fi
|
|
419
|
+
exit 2
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# The verdict the watcher saw, written through the SAME writers the transcript scans use
|
|
423
|
+
# (never shortening a longer park), so the old account is out of the pool before this
|
|
424
|
+
# very selection runs — and for every other launch too. Other classes (model-scoped,
|
|
425
|
+
# transient, blocked, crash) leave no marker: the watcher's AVOID list covers them.
|
|
426
|
+
ar_relaunch_apply() {
|
|
427
|
+
local d=""
|
|
428
|
+
[ "$AR_LOADED" = 1 ] || return 0
|
|
429
|
+
[ -n "$AR_R_ACCT" ] && [ -d "$ACC_ROOT/$AR_R_ACCT" ] && d="$ACC_ROOT/$AR_R_ACCT"
|
|
430
|
+
if [ -n "$d" ]; then
|
|
431
|
+
case "$AR_R_CLASS" in
|
|
432
|
+
quota)
|
|
433
|
+
if num_ok "$AR_R_RESET" && [ "$AR_R_RESET" -gt "$now" ] && [ -n "$AR_R_RTYPE" ]; then
|
|
434
|
+
mark_client_limit "$d" "$AR_R_RESET" "$AR_R_RTYPE" "$AR_R_MARKED"
|
|
435
|
+
fi ;;
|
|
436
|
+
auth) mark_client_auth_dead "$d" ;;
|
|
437
|
+
esac
|
|
438
|
+
fi
|
|
439
|
+
# Field 2 is the word `autoresume`, never an account id: lib/report.py and the
|
|
440
|
+
# *-accounts tools read an acct-NN in field 2 as a pick.
|
|
441
|
+
sel_log "autoresume relaunch chain=${AR_CHAIN:--} from=${AR_R_ACCT:--}" \
|
|
442
|
+
"class=${AR_R_CLASS:--} depth=$AR_DEPTH"
|
|
443
|
+
return 0
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# AVOID: accounts this chain just left, each until its own epoch. Filters `eligible` only
|
|
447
|
+
# — never `valid` — so a pool with nowhere else to go still reaches the all-limited
|
|
448
|
+
# fallback exactly as before. Builtins only: this runs once per candidate.
|
|
449
|
+
ar_avoided() { # $1 acct dir
|
|
450
|
+
local rest e id until
|
|
451
|
+
[ -n "$AR_AVOID" ] || return 1
|
|
452
|
+
rest="$AR_AVOID,"
|
|
453
|
+
while [ -n "$rest" ]; do
|
|
454
|
+
e="${rest%%,*}"; rest="${rest#*,}"
|
|
455
|
+
id="${e%%:*}"; until="${e#*:}"
|
|
456
|
+
[ "$id" = "${1##*/}" ] || continue
|
|
457
|
+
num_ok "$until" && [ "$until" -gt "$now" ] && return 0
|
|
458
|
+
done
|
|
459
|
+
return 1
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
# Probe answer, and the end of a probe run. `none` (exit 3) means nothing would be picked
|
|
463
|
+
# at all; otherwise the tier says which cut the pick came from.
|
|
464
|
+
ar_probe_exit() { # $1 = none, or empty to derive the tier from the selection just made
|
|
465
|
+
local t="${1:-}"
|
|
466
|
+
if [ -z "$t" ]; then
|
|
467
|
+
if [ "${#eligible[@]}" -gt 0 ]; then t=eligible
|
|
468
|
+
elif [ "${#soft[@]}" -gt 0 ]; then t=soft
|
|
469
|
+
else t=hard; fi
|
|
470
|
+
fi
|
|
471
|
+
if [ "$t" = none ] || [ -z "${pick:-}" ]; then
|
|
472
|
+
printf 'pick= tier=none\n'
|
|
473
|
+
exit 3
|
|
474
|
+
fi
|
|
475
|
+
printf 'pick=%s tier=%s\n' "${pick##*/}" "$t"
|
|
476
|
+
exit 0
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# The watcher only understands sessions it can resume faithfully: an interactive TUI with
|
|
480
|
+
# these flags and at most one prompt. A single word may be a subcommand (`claude doctor`,
|
|
481
|
+
# `claude mcp …`), so a positional counts as a prompt only when it contains a space.
|
|
482
|
+
# Anything else — -p, --fork-session, a bare --resume picker — runs unsupervised.
|
|
483
|
+
ar_argv_ok() { # args: the session's argv
|
|
484
|
+
local a want="" pos=0
|
|
485
|
+
for a in "$@"; do
|
|
486
|
+
if [ -n "$want" ]; then
|
|
487
|
+
case "$want" in
|
|
488
|
+
uuid) sess_id_ok "$a" || return 1
|
|
489
|
+
case "$a" in *-*) ;; *) return 1 ;; esac ;;
|
|
490
|
+
*) case "$a" in ''|-*) return 1 ;; esac ;;
|
|
491
|
+
esac
|
|
492
|
+
want=""
|
|
493
|
+
continue
|
|
494
|
+
fi
|
|
495
|
+
case "$a" in
|
|
496
|
+
--dangerously-skip-permissions|--allow-dangerously-skip-permissions|-c|--continue) ;;
|
|
497
|
+
-r|--resume) want=uuid ;;
|
|
498
|
+
--resume=*) a="${a#--resume=}"
|
|
499
|
+
sess_id_ok "$a" || return 1
|
|
500
|
+
case "$a" in *-*) ;; *) return 1 ;; esac ;;
|
|
501
|
+
--model|--effort|--permission-mode) want=value ;;
|
|
502
|
+
--model=*) [ -n "${a#--model=}" ] || return 1 ;;
|
|
503
|
+
-*) return 1 ;;
|
|
504
|
+
*' '*) pos=$((pos + 1)); [ "$pos" -le 1 ] || return 1 ;;
|
|
505
|
+
*) return 1 ;;
|
|
506
|
+
esac
|
|
507
|
+
done
|
|
508
|
+
[ -z "$want" ]
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# The relaunch line is typed into the pane's shell unquoted — this shim's own path, and the
|
|
512
|
+
# pool root when it is not the default — so only paths that never need quoting qualify.
|
|
513
|
+
# Spelled out, never ranges: under a UTF-8 locale bash matches `[a-z]` by collation.
|
|
514
|
+
ar_path_ok() { # $1 absolute path
|
|
515
|
+
case "$1" in
|
|
516
|
+
/*) ;;
|
|
517
|
+
*) return 1 ;;
|
|
518
|
+
esac
|
|
519
|
+
case "$1" in
|
|
520
|
+
*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/._+-]*) return 1 ;;
|
|
521
|
+
esac
|
|
522
|
+
return 0
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# A python the watcher can run on. /usr/bin/python3 on a Mac without developer tools is a
|
|
526
|
+
# stub that pops an "install the command line tools" dialog and fails — only trusted when
|
|
527
|
+
# what it forwards to is there. File tests only: xcode-select itself would be a fork.
|
|
528
|
+
ar_python() {
|
|
529
|
+
local py="${CLAUDE_MULTIACC_PYTHON:-}"
|
|
530
|
+
if [ -n "$py" ] && [ -f "$py" ] && [ -x "$py" ]; then AR_PY="$py"; return 0; fi
|
|
531
|
+
py="$(command -v python3 2>/dev/null)" || return 1
|
|
532
|
+
case "$py" in /*) ;; *) return 1 ;; esac
|
|
533
|
+
if [ "$py" = /usr/bin/python3 ]; then
|
|
534
|
+
case "${OSTYPE:-}" in
|
|
535
|
+
darwin*)
|
|
536
|
+
[ -x /Library/Developer/CommandLineTools/usr/bin/python3 ] \
|
|
537
|
+
|| [ -x /var/db/xcode_select_link/usr/bin/python3 ] \
|
|
538
|
+
|| [ -x "${DEVELOPER_DIR:-/nonexistent}/usr/bin/python3" ] \
|
|
539
|
+
|| return 1 ;;
|
|
540
|
+
esac
|
|
541
|
+
fi
|
|
542
|
+
AR_PY="$py"
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
ar_start_watcher() { # $1 provider, $2 picked acct dir
|
|
546
|
+
local prov="$1" dir="$2" lib st tmp launched root nl=$'\n'
|
|
547
|
+
# Kill switches: the env for one shell, the file for the whole pool (the watcher
|
|
548
|
+
# re-reads the file every tick, so it also stops sessions already running).
|
|
549
|
+
case "${CLAUDE_MULTIACC_AUTORESUME:-1}" in 0|false|no|off) return 1 ;; esac
|
|
550
|
+
[ -e "$ACC_ROOT/autoresume.off" ] && return 1
|
|
551
|
+
# A pin is the caller overriding selection. A valid one execs long before this; an
|
|
552
|
+
# invalid one fell through to a random pick, and moving that session is not ours to do.
|
|
553
|
+
[ -z "${CLAUDE_ACCOUNT:-}" ] || return 1
|
|
554
|
+
if [ "${CLAUDE_MULTIACC_AR_TEST_TTY:-}" != 1 ]; then
|
|
555
|
+
[ -t 0 ] && [ -t 1 ] || return 1
|
|
556
|
+
fi
|
|
557
|
+
# The relaunch is typed into the pane's shell: no tmux, nothing to type into.
|
|
558
|
+
[ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ] || return 1
|
|
559
|
+
ar_argv_ok ${AR_ORIG_ARGV[@]+"${AR_ORIG_ARGV[@]}"} || return 1
|
|
560
|
+
ar_python || return 1
|
|
561
|
+
lib="$SELF_DIR/../lib/autoresume.py"
|
|
562
|
+
[ -f "$lib" ] || return 1
|
|
563
|
+
# The state file is key=value lines; a value with a newline cannot be carried.
|
|
564
|
+
case "$PWD$dir$ACC_ROOT$SELF$TMUX$TMUX_PANE" in *"$nl"*) return 1 ;; esac
|
|
565
|
+
# The watcher and the relaunch run elsewhere: hand them absolute pool paths.
|
|
566
|
+
root="$ACC_ROOT"
|
|
567
|
+
case "$root" in /*) ;; *) root="$PWD/$root"; dir="$PWD/$dir" ;; esac
|
|
568
|
+
ar_path_ok "$SELF" && ar_path_ok "$root" || return 1
|
|
569
|
+
[ -d "$AR_DIR" ] || mkdir -p "$AR_DIR" 2>/dev/null || return 1
|
|
570
|
+
# The ORIGINAL argv (before the fallback-model pin): the watcher rebuilds the relaunch
|
|
571
|
+
# from it, and the relaunch re-runs selection, which pins again for the NEW pick.
|
|
572
|
+
if [ "${#AR_ORIG_ARGV[@]}" -gt 0 ]; then
|
|
573
|
+
printf '%s\0' "${AR_ORIG_ARGV[@]}" 2>/dev/null > "$AR_DIR/$$.argv" || return 1
|
|
574
|
+
else
|
|
575
|
+
: 2>/dev/null > "$AR_DIR/$$.argv" || return 1
|
|
576
|
+
fi
|
|
577
|
+
launched="$(date +%s)"
|
|
578
|
+
num_ok "$launched" || launched="$now"
|
|
579
|
+
# $$ is the pid the client keeps across exec — the name of its sessions/<pid>.json.
|
|
580
|
+
# Written atomically: the watcher must never parse half a state file.
|
|
581
|
+
st="$AR_DIR/$$.state"
|
|
582
|
+
tmp="$AR_DIR/.$$.state.tmp"
|
|
583
|
+
printf '%s\n' "v=1" "provider=$prov" "pid=$$" "ppid=$PPID" "acct=${dir##*/}" \
|
|
584
|
+
"acct_dir=$dir" "cwd=$PWD" "launched=$launched" "tmux=$TMUX" "pane=$TMUX_PANE" \
|
|
585
|
+
"self=$SELF" "acc_root=$root" "depth=$AR_DEPTH" "avoid=$AR_AVOID" \
|
|
586
|
+
"hist=$AR_HIST" "chain=$AR_CHAIN" 2>/dev/null > "$tmp" \
|
|
587
|
+
&& mv -f "$tmp" "$st" 2>/dev/null \
|
|
588
|
+
|| { rm -f "$tmp" 2>/dev/null; return 1; }
|
|
589
|
+
# Detached and never waited for; the watcher setsid()s itself at once. It inherits
|
|
590
|
+
# this environment on purpose (the probe strips the account-specific part).
|
|
591
|
+
( trap '' INT QUIT HUP TSTP
|
|
592
|
+
exec "$AR_PY" -I "$lib" watch --state "$st" </dev/null >/dev/null 2>&1 &
|
|
593
|
+
) >/dev/null 2>&1
|
|
594
|
+
return 0
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# A relaunch may land on an account that never opened this directory, and Claude asks "Is
|
|
598
|
+
# this a project you trust?" there even under --dangerously-skip-permissions: a dialog
|
|
599
|
+
# nobody is watching would stall the resumed session. So the relaunch (and only it) marks
|
|
600
|
+
# its cwd trusted in the picked account's .claude.json. Bounded file work, every failure
|
|
601
|
+
# ignored; a probe never gets here.
|
|
602
|
+
ar_trust() { # $1 picked acct dir
|
|
603
|
+
[ "$AR_TRUST" = 1 ] || return 0
|
|
604
|
+
[ -f "$1/.claude.json" ] || return 0
|
|
605
|
+
ar_python || return 0
|
|
606
|
+
"$AR_PY" -I "$SELF_DIR/../lib/autoresume.py" trust --acct-dir "$1" --cwd "$PWD" \
|
|
607
|
+
</dev/null >/dev/null 2>&1 || true
|
|
608
|
+
return 0
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# Called right before the plain interactive exec. Whatever the gate decided, the TUI
|
|
612
|
+
# never sees an auto-resume variable (a nested `claude` must not inherit a probe flag).
|
|
613
|
+
ar_spawn_watcher() { # $1 provider, $2 picked acct dir
|
|
614
|
+
local rc=0 v
|
|
615
|
+
ar_start_watcher "$@" || rc=$?
|
|
616
|
+
for v in ${!CLAUDE_MULTIACC_AR_*}; do unset "$v"; done
|
|
617
|
+
unset CLAUDE_MULTIACC_AR
|
|
618
|
+
return "$rc"
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# The relaunch entry itself: CLAUDE_MULTIACC_AR=<token>[:<session id>]. The id rides along
|
|
622
|
+
# so a token that cannot be honoured can still name the session. A probe never consumes a
|
|
623
|
+
# token: that belongs to the relaunch.
|
|
624
|
+
if [ -n "${CLAUDE_MULTIACC_AR+x}" ]; then
|
|
625
|
+
ar_tok="$CLAUDE_MULTIACC_AR"
|
|
626
|
+
unset CLAUDE_MULTIACC_AR
|
|
627
|
+
if [ "$AR_PROBE" != 1 ] && [ -n "$ar_tok" ]; then
|
|
628
|
+
ar_sid=""
|
|
629
|
+
case "$ar_tok" in *:*) ar_sid="${ar_tok#*:}"; ar_tok="${ar_tok%%:*}" ;; esac
|
|
630
|
+
ar_relaunch_load "$ar_tok" || ar_token_dead "${ar_sid:-$AR_R_SID}"
|
|
631
|
+
set -- "${AR_ARGV[@]}"
|
|
632
|
+
AR_TRUST=1
|
|
633
|
+
unset ar_sid
|
|
634
|
+
fi
|
|
635
|
+
unset ar_tok
|
|
636
|
+
fi
|
|
637
|
+
# The token preflight below can prove the pick's token dead and re-enter this shim with
|
|
638
|
+
# `exec "$SELF"`. A relaunch has spent its token by then, so it hands its chain (depth,
|
|
639
|
+
# the accounts it left, its budgets) over in the environment — without it the re-entered
|
|
640
|
+
# selection could land right back on the account the session just left. Only such a
|
|
641
|
+
# re-entry (PREFLIGHT_DEPTH set) may use it, and never a probe or a fresh token.
|
|
642
|
+
if [ -n "${CLAUDE_MULTIACC_AR_CARRY+x}" ]; then
|
|
643
|
+
ar_c="$CLAUDE_MULTIACC_AR_CARRY"
|
|
644
|
+
unset CLAUDE_MULTIACC_AR_CARRY
|
|
645
|
+
if [ "$AR_PROBE" != 1 ] && [ "$AR_LOADED" != 1 ] && [ -n "${CLAUDE_MULTIACC_PREFLIGHT_DEPTH:-}" ]; then
|
|
646
|
+
ar_d="${ar_c%%|*}"; ar_c="${ar_c#*|}"
|
|
647
|
+
ar_a="${ar_c%%|*}"; ar_c="${ar_c#*|}"
|
|
648
|
+
ar_h="${ar_c%%|*}"; ar_c="${ar_c#*|}"
|
|
649
|
+
if num_ok "$ar_d" && ar_field_ok "$ar_a" && ar_field_ok "$ar_h" && ar_field_ok "$ar_c" \
|
|
650
|
+
&& [ "${#ar_c}" -le 64 ]; then
|
|
651
|
+
AR_DEPTH="$ar_d"; AR_AVOID="$ar_a"; AR_HIST="$ar_h"; AR_CHAIN="$ar_c"; AR_TRUST=1
|
|
652
|
+
fi
|
|
653
|
+
fi
|
|
654
|
+
unset ar_c ar_d ar_a ar_h
|
|
655
|
+
fi
|
|
656
|
+
AR_ORIG_ARGV=(${1+"$@"})
|
|
657
|
+
|
|
265
658
|
# ---- model-scoped limits ------------------------------------------------------
|
|
266
659
|
# A usage bucket can be scoped to ONE MODEL ("weekly_scoped:Fable"), and such a bucket
|
|
267
660
|
# says nothing about the account's ability to serve a DIFFERENT model. The limits
|
|
@@ -1310,12 +1703,16 @@ token_preflight() { # $1 acct dir, $2 setup-token; rc 1 only for proven invalid
|
|
|
1310
1703
|
return 0
|
|
1311
1704
|
}
|
|
1312
1705
|
|
|
1706
|
+
# A relaunch marks the account it left before anything reads the pool (auto-resume).
|
|
1707
|
+
ar_relaunch_apply
|
|
1708
|
+
|
|
1313
1709
|
# Explicit pin wins over everything — markers, and even missing auth: the
|
|
1314
1710
|
# add/login ceremony pins to a dir that has no credentials yet, and the login
|
|
1315
1711
|
# must land exactly there, never in a randomly selected account's dir.
|
|
1316
1712
|
if [ -n "${CLAUDE_ACCOUNT:-}" ]; then
|
|
1317
1713
|
d="$ACC_ROOT/$CLAUDE_ACCOUNT"
|
|
1318
1714
|
if [ -d "$d" ]; then
|
|
1715
|
+
[ "$AR_PROBE" = 1 ] && ar_probe_exit none
|
|
1319
1716
|
sel_log "$CLAUDE_ACCOUNT pinned pwd=$PWD"
|
|
1320
1717
|
export CLAUDE_CONFIG_DIR="$d"
|
|
1321
1718
|
export CLAUDE_SHIM_ACTIVE=1
|
|
@@ -1379,6 +1776,7 @@ for d in "$ACC_ROOT"/acct-*; do
|
|
|
1379
1776
|
continue
|
|
1380
1777
|
fi
|
|
1381
1778
|
over_threshold "$d" && continue
|
|
1779
|
+
ar_avoided "$d" && continue
|
|
1382
1780
|
eligible+=("$d")
|
|
1383
1781
|
done
|
|
1384
1782
|
|
|
@@ -1403,6 +1801,7 @@ fi
|
|
|
1403
1801
|
# No usable accounts => stock behavior (fail open, never block work), but say WHY when
|
|
1404
1802
|
# the pool is merely un-authenticated: this is the one case the user can actually fix.
|
|
1405
1803
|
if [ "${#valid[@]}" -eq 0 ]; then
|
|
1804
|
+
[ "$AR_PROBE" = 1 ] && ar_probe_exit none
|
|
1406
1805
|
if [ "${#expired[@]}" -gt 0 ]; then
|
|
1407
1806
|
sel_log "all-expired: falling back to the default login (see: claude-accounts expired)"
|
|
1408
1807
|
# Terminal only: a service-spawned `claude -p` must keep its stderr byte-clean, and
|
|
@@ -1611,7 +2010,8 @@ else
|
|
|
1611
2010
|
PICK_DIR="$d"; best_reset="$r"
|
|
1612
2011
|
fi
|
|
1613
2012
|
done
|
|
1614
|
-
|
|
2013
|
+
[ "$AR_PROBE" = 1 ] \
|
|
2014
|
+
|| sel_log "all-limited fallback=$(basename "$PICK_DIR") all-exhausted resets_in=$((best_reset > now ? best_reset - now : 0))s"
|
|
1615
2015
|
# Every candidate rejects right now, so this pick WILL fail: say so on a terminal
|
|
1616
2016
|
# instead of letting the operator read the client's bare limit error as a bad
|
|
1617
2017
|
# choice by the pool (2026-08-29). Throttled, and never on a service's stderr.
|
|
@@ -1632,7 +2032,8 @@ else
|
|
|
1632
2032
|
# Report the number this fallback ACTUALLY ranked on. Asking fresh_field here printed
|
|
1633
2033
|
# `weekly=?%` even when the pick was made on a perfectly good stale reading, so anyone
|
|
1634
2034
|
# reading only this event concluded the choice had no usage input at all.
|
|
1635
|
-
|
|
2035
|
+
# (A probe picks nothing: its fallback lines would read as picks that never ran.)
|
|
2036
|
+
if [ "${#soft[@]}" -gt 0 ] && [ "$AR_PROBE" != 1 ]; then
|
|
1636
2037
|
if [ "$degraded" = 1 ]; then
|
|
1637
2038
|
sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(stale_weekly "$PICK_DIR" || echo '?')% ranking=DEGRADED"
|
|
1638
2039
|
elif [ "$blind" = 1 ]; then
|
|
@@ -1643,6 +2044,9 @@ else
|
|
|
1643
2044
|
fi
|
|
1644
2045
|
fi
|
|
1645
2046
|
pick="$PICK_DIR"
|
|
2047
|
+
# An auto-resume probe ends here: before the token preflight (a real inference), the
|
|
2048
|
+
# model pin, remember_pick, the limits kick and the pick line — it chose nothing.
|
|
2049
|
+
[ "$AR_PROBE" = 1 ] && ar_probe_exit
|
|
1646
2050
|
|
|
1647
2051
|
# The preflight can prove the selected token dead without exposing its 401 to the
|
|
1648
2052
|
# caller. Re-enter selection so the new .expired marker is applied and another
|
|
@@ -1654,6 +2058,9 @@ if [ -n "$tok" ] && ! token_preflight "$pick" "$tok"; then
|
|
|
1654
2058
|
num_ok "$preflight_depth" || preflight_depth=0
|
|
1655
2059
|
if [ "$preflight_depth" -lt 64 ]; then
|
|
1656
2060
|
export CLAUDE_MULTIACC_PREFLIGHT_DEPTH=$((preflight_depth + 1))
|
|
2061
|
+
# An auto-resume chain survives the re-entry (see ---- auto-resume ----). '|' is
|
|
2062
|
+
# outside every chain field's charset, so it separates them unambiguously.
|
|
2063
|
+
[ -n "$AR_CHAIN" ] && export CLAUDE_MULTIACC_AR_CARRY="$AR_DEPTH|$AR_AVOID|$AR_HIST|$AR_CHAIN"
|
|
1657
2064
|
exec "$SELF" "$@"
|
|
1658
2065
|
fi
|
|
1659
2066
|
fi
|
|
@@ -1793,6 +2200,8 @@ if [ "$wants_retry" = "0" ]; then
|
|
|
1793
2200
|
tok="$(acct_token "$pick")"
|
|
1794
2201
|
[ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
|
|
1795
2202
|
sel_capture_session "$pick"
|
|
2203
|
+
ar_trust "$pick"
|
|
2204
|
+
ar_spawn_watcher claude "$pick" || true
|
|
1796
2205
|
exec "$REAL" "$@"
|
|
1797
2206
|
fi
|
|
1798
2207
|
|