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/bin/claude-accounts
CHANGED
|
@@ -678,6 +678,8 @@ cmd_add() {
|
|
|
678
678
|
mutate_unlock
|
|
679
679
|
RESERVED_DIR="" # committed — the trap must not delete it now
|
|
680
680
|
trap - EXIT INT TERM
|
|
681
|
+
# Registered under the identity it read back: leave one Keychain item for the dir.
|
|
682
|
+
[ "$token" = "1" ] || keychain_prune "$d"
|
|
681
683
|
# Only the full-login path actually READ the identity back. Saying "verified" for a
|
|
682
684
|
# setup token would relaunder the very assumption this flow just warned about.
|
|
683
685
|
local verdict="sign-in verified"
|
|
@@ -784,9 +786,10 @@ def has_auth(aid):
|
|
|
784
786
|
if (os.path.isfile(c) and os.path.getsize(c) > 0) or (os.path.isfile(t) and os.path.getsize(t) > 0):
|
|
785
787
|
return True
|
|
786
788
|
# A login the client moved into the macOS Keychain still counts — deduping must
|
|
787
|
-
# not throw away the one duplicate that actually holds the grant
|
|
789
|
+
# not throw away the one duplicate that actually holds the grant, even one filed
|
|
790
|
+
# under a name its client no longer reads (the limits pass moves those back).
|
|
788
791
|
try:
|
|
789
|
-
return keychain.probe(d)['state'] in ('present', 'locked', 'corrupt')
|
|
792
|
+
return keychain.probe(d, legacy=True)['state'] in ('present', 'locked', 'corrupt')
|
|
790
793
|
except Exception:
|
|
791
794
|
return False
|
|
792
795
|
by_email = {}
|
|
@@ -1323,6 +1326,9 @@ cmd_login() {
|
|
|
1323
1326
|
if [ -z "$got" ] && [ "$force" != "1" ]; then
|
|
1324
1327
|
die "signed in, but the account identity could not be read back — refusing to call $id fixed (retry, or pass --force)"
|
|
1325
1328
|
fi
|
|
1329
|
+
# Only now, with the identity confirmed, may the stale Keychain items go: a sign-in
|
|
1330
|
+
# as the wrong account (died above) must leave every item as it was.
|
|
1331
|
+
keychain_prune "$d"
|
|
1326
1332
|
clear_auth_markers "$d" keep-token-park
|
|
1327
1333
|
if [ -s "$d/.credentials.json" ]; then
|
|
1328
1334
|
echo "$id login saved (.credentials.json, this machine, auto-refreshing)."
|
|
@@ -1859,36 +1865,49 @@ class FileStore:
|
|
|
1859
1865
|
|
|
1860
1866
|
class KeychainStore:
|
|
1861
1867
|
"""The same credential, held in the macOS Keychain by the client itself (see
|
|
1862
|
-
lib/keychain.py). Read
|
|
1863
|
-
|
|
1864
|
-
|
|
1868
|
+
lib/keychain.py). Read from the item the client reads (the canonical name; this
|
|
1869
|
+
pass alone also accepts the legacy first match, which oauth_store moves) and written
|
|
1870
|
+
back under the canonical name, which also drops the siblings holding the grant the
|
|
1871
|
+
refresh started from — NEVER copied out to a file: a file beside a Keychain item is
|
|
1872
|
+
a second copy of a ROTATING refresh grant, and the client reads the Keychain first,
|
|
1873
|
+
so the two would drift apart and strand one."""
|
|
1865
1874
|
kind = 'keychain'
|
|
1866
1875
|
|
|
1867
1876
|
def __init__(self, d, probe):
|
|
1868
1877
|
self.d = d
|
|
1869
|
-
self.
|
|
1878
|
+
self.grant = None
|
|
1879
|
+
self.moved = False
|
|
1870
1880
|
|
|
1871
1881
|
def read(self):
|
|
1872
|
-
p = keychain.probe(self.d)
|
|
1882
|
+
p = keychain.probe(self.d, legacy=True)
|
|
1873
1883
|
if p['state'] != 'present':
|
|
1874
1884
|
raise ValueError(f'keychain credential {p["state"]}')
|
|
1885
|
+
grant = p['doc']['claudeAiOauth'].get('refreshToken')
|
|
1886
|
+
self.grant = grant if isinstance(grant, str) else None
|
|
1875
1887
|
return p['doc']
|
|
1876
1888
|
|
|
1877
1889
|
def write(self, doc):
|
|
1878
|
-
if not keychain.write(self.d, doc,
|
|
1890
|
+
if not keychain.write(self.d, doc, supersedes=self.grant):
|
|
1879
1891
|
raise OSError('keychain write refused')
|
|
1880
1892
|
|
|
1881
1893
|
|
|
1882
1894
|
def oauth_store(d):
|
|
1883
1895
|
"""Where <d>'s OAuth login lives for THIS process: a FileStore, a KeychainStore,
|
|
1884
1896
|
the string 'locked' (Keychain item exists but this session cannot open it), or
|
|
1885
|
-
None. The file wins when both exist — same rule as lib/audit.oauth_login.
|
|
1897
|
+
None. The file wins when both exist — same rule as lib/audit.oauth_login.
|
|
1898
|
+
|
|
1899
|
+
A Keychain login found only under a name its client no longer reads (a USER-less
|
|
1900
|
+
sign-in's "unknown", lib/keychain.py) is moved under the canonical name here, once:
|
|
1901
|
+
until then the client, the shim and the audit all see no login."""
|
|
1886
1902
|
cpath = os.path.join(d, '.credentials.json')
|
|
1887
1903
|
if os.path.isfile(cpath):
|
|
1888
1904
|
return FileStore(cpath)
|
|
1889
|
-
p = keychain.probe(d)
|
|
1905
|
+
p = keychain.probe(d, legacy=True)
|
|
1890
1906
|
if p['state'] == 'present':
|
|
1891
|
-
|
|
1907
|
+
store = KeychainStore(d, p)
|
|
1908
|
+
if p['account'] != p['canonical']:
|
|
1909
|
+
store.moved = keychain.write(d, p['doc'])
|
|
1910
|
+
return store
|
|
1892
1911
|
if p['state'] == 'locked':
|
|
1893
1912
|
return 'locked'
|
|
1894
1913
|
return None
|
|
@@ -2096,6 +2115,9 @@ for acct in manifest.get('accounts', []):
|
|
|
2096
2115
|
except Exception as e:
|
|
2097
2116
|
say(f'{aid}: could not locate the oauth credential ({str(e)[:120]}); failing open')
|
|
2098
2117
|
store = None
|
|
2118
|
+
if getattr(store, 'moved', False):
|
|
2119
|
+
say(f'{aid}: Keychain login moved under {keychain.canonical_account()}, '
|
|
2120
|
+
f'the name its client reads')
|
|
2099
2121
|
locked = store == 'locked'
|
|
2100
2122
|
if locked:
|
|
2101
2123
|
store = None
|
|
@@ -2784,6 +2806,9 @@ for acct in manifest.get('accounts', []):
|
|
|
2784
2806
|
# The login may be in the macOS Keychain (lib/keychain.py). A Keychain this
|
|
2785
2807
|
# session cannot open is not a failure of the ACCOUNT: the real call below
|
|
2786
2808
|
# would fail for the session, not the grant, so it is a skip with a reason.
|
|
2809
|
+
# Only the canonical item counts — the one the client below reads; testing an
|
|
2810
|
+
# item under another name would run the client on a login it cannot see and
|
|
2811
|
+
# park an account its token serves.
|
|
2787
2812
|
kc = keychain.probe(d)
|
|
2788
2813
|
if kc['state'] == 'present':
|
|
2789
2814
|
has_creds, cred_doc = True, kc['doc']
|
package/bin/codex
CHANGED
|
@@ -178,6 +178,9 @@ mcp_nested_mirror() {
|
|
|
178
178
|
if [ -n "${CODEX_HOME:-}" ] \
|
|
179
179
|
|| [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CODEX_SHIM_ACTIVE:-}" ] \
|
|
180
180
|
|| [ ! -f "$MANIFEST" ]; then
|
|
181
|
+
# An auto-resume probe (see "auto-resume" below) asks which account selection would
|
|
182
|
+
# pick; with no selection to run, the answer is none — it must never start a session.
|
|
183
|
+
if [ "${CODEX_MULTIACC_AR_PROBE:-0}" = "1" ]; then printf 'pick= tier=none\n'; exit 3; fi
|
|
181
184
|
mcp_nested_mirror "$@" || true
|
|
182
185
|
exec "$REAL" "$@"
|
|
183
186
|
fi
|
|
@@ -729,10 +732,388 @@ expired_marked() { # $1 = acct dir
|
|
|
729
732
|
# refresher / verify / the retry path below — all of which write `.expired`.
|
|
730
733
|
auth_dead() { expired_marked "$1"; }
|
|
731
734
|
|
|
735
|
+
# ---- auto-resume -------------------------------------------------------------------
|
|
736
|
+
# An interactive codex that runs into a usage limit or a dead login does NOT exit: the
|
|
737
|
+
# TUI stays open on "You've hit your usage limit ... try again at <date>" and waits for a
|
|
738
|
+
# human. Auto-resume moves that session — same session id — onto another pooled account
|
|
739
|
+
# without a keystroke, and it does so without touching the exec below: the real codex
|
|
740
|
+
# still replaces this shim (same pid, same terminal), while a detached watcher
|
|
741
|
+
# (lib/autoresume.py) tails the session's rollout beside it. On a limit/auth verdict the
|
|
742
|
+
# watcher asks THIS shim which account a relaunch would get (probe mode), stops codex, and
|
|
743
|
+
# types ` CODEX_MULTIACC_AR=<token>:<sid> [CODEX_ACCOUNTS_ROOT=<root>] <this shim's path>`
|
|
744
|
+
# into the tmux pane's SHELL — never into the TUI (the pool root only when it is not the
|
|
745
|
+
# default one). The relaunch comes back through here: the token names a single-use relaunch
|
|
746
|
+
# file carrying the verdict and the argv (`resume <kept opts> <sid> <prompt>`); the old
|
|
747
|
+
# account is parked the way the exec retry path below parks one, and ordinary selection
|
|
748
|
+
# picks the next account. bin/claude carries the same hooks; docs/AUTORESUME.md is the
|
|
749
|
+
# reference. Fail open everywhere: nothing here may block before exec — no network, no
|
|
750
|
+
# waiting, no python in the foreground — and every path that cannot finish falls through
|
|
751
|
+
# to exactly today's exec. The one exception is a typed relaunch whose token is gone: it
|
|
752
|
+
# names the session to resume and exits 2, because its own (empty) argv would start a
|
|
753
|
+
# fresh session in the stopped one's place.
|
|
754
|
+
AR_DIR="$ACC_ROOT/tmp/autoresume"
|
|
755
|
+
AR_DEPTH=0 # how many relaunches this session's chain has already taken
|
|
756
|
+
AR_AVOID="" # acct-NN:until,... — accounts the chain must not land on again
|
|
757
|
+
AR_HIST="" # class:epoch,... — the chain's verdicts, for the watcher's budgets
|
|
758
|
+
AR_CHAIN="" # the chain's id, as the watcher named it
|
|
759
|
+
AR_ARGV=()
|
|
760
|
+
AR_ORIG_ARGV=()
|
|
761
|
+
AR_TIER="" # which cut produced the pick: eligible | soft | hard (probe answer)
|
|
762
|
+
AR_R_SID="" # the session a relaunch file named, for the dead-token hint
|
|
763
|
+
AR_PROBE=0
|
|
764
|
+
[ "${CODEX_MULTIACC_AR_PROBE:-0}" = "1" ] && AR_PROBE=1
|
|
765
|
+
# Spelled out, never ranges: under a UTF-8 locale bash matches `[a-z]` by collation, so
|
|
766
|
+
# `*[!a-z]*` lets "BAD" and "é" through. These validate what the relaunch file hands us.
|
|
767
|
+
AR_LOWER=abcdefghijklmnopqrstuvwxyz
|
|
768
|
+
AR_ALNUM="ABCDEFGHIJKLMNOPQRSTUVWXYZ${AR_LOWER}0123456789"
|
|
769
|
+
|
|
770
|
+
# Byte-identical with bin/claude's: a session id is hex and dashes, never an option.
|
|
771
|
+
sess_id_ok() { case "$1" in ''|-*|*[!0-9a-fA-F-]*) return 1 ;; *) return 0 ;; esac; }
|
|
772
|
+
|
|
773
|
+
ar_uuid_ok() { # $1 — a session id a relaunch may name: a bounded, dashed sess_id_ok
|
|
774
|
+
sess_id_ok "$1" || return 1
|
|
775
|
+
[ "${#1}" -le 64 ] || return 1
|
|
776
|
+
case "$1" in *-*) return 0 ;; esac
|
|
777
|
+
return 1
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
ar_word_ok() { # $1 value, $2 min length — [A-Za-z0-9]{min,64}: relaunch tokens, chain ids
|
|
781
|
+
case "$1" in ''|*[!$AR_ALNUM]*) return 1 ;; esac
|
|
782
|
+
[ "${#1}" -ge "$2" ] && [ "${#1}" -le 64 ]
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
ar_avoid_add() { # $1 comma list acct-NN:until -> appended to AR_AVOID, in-force entries only
|
|
786
|
+
# Every entry is re-validated here because the list arrives from a file or the probe's
|
|
787
|
+
# environment, and ar_avoided below does arithmetic on the until half.
|
|
788
|
+
local rest="$1" e id u n=0
|
|
789
|
+
while [ -n "$rest" ] && [ "$n" -lt 64 ]; do
|
|
790
|
+
e="${rest%%,*}"
|
|
791
|
+
case "$rest" in *,*) rest="${rest#*,}" ;; *) rest="" ;; esac
|
|
792
|
+
n=$((n + 1))
|
|
793
|
+
id="${e%%:*}"; u="${e#*:}"
|
|
794
|
+
case "$id" in acct-*[!0-9]*|acct-) continue ;; acct-*) ;; *) continue ;; esac
|
|
795
|
+
num_ok "$u" && [ "$u" -gt "$now" ] || continue
|
|
796
|
+
AR_AVOID="${AR_AVOID:+$AR_AVOID,}$id:$u"
|
|
797
|
+
done
|
|
798
|
+
return 0
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
ar_avoided() { # $1 acct dir — true while the relaunch chain asked to skip this account
|
|
802
|
+
# Fork-free: this runs once per candidate on every invocation, and AR_AVOID is empty
|
|
803
|
+
# for every launch that is not a relaunch or a probe.
|
|
804
|
+
local id="${1##*/}" rest="$AR_AVOID" e u
|
|
805
|
+
while [ -n "$rest" ]; do
|
|
806
|
+
e="${rest%%,*}"
|
|
807
|
+
case "$rest" in *,*) rest="${rest#*,}" ;; *) rest="" ;; esac
|
|
808
|
+
u="${e#*:}"
|
|
809
|
+
[ "${e%%:*}" = "$id" ] && num_ok "$u" && [ "$u" -gt "$now" ] && return 0
|
|
810
|
+
done
|
|
811
|
+
return 1
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
ar_set_hist() { # $1 comma list class:epoch -> AR_HIST: well-formed entries, newest 32
|
|
815
|
+
local rest="$1" e c t n=0 i keep=()
|
|
816
|
+
AR_HIST=""
|
|
817
|
+
while [ -n "$rest" ] && [ "$n" -lt 256 ]; do
|
|
818
|
+
e="${rest%%,*}"
|
|
819
|
+
case "$rest" in *,*) rest="${rest#*,}" ;; *) rest="" ;; esac
|
|
820
|
+
n=$((n + 1))
|
|
821
|
+
c="${e%%:*}"; t="${e#*:}"
|
|
822
|
+
case "$c" in ''|*[!$AR_LOWER]*) continue ;; esac
|
|
823
|
+
[ "${#c}" -le 16 ] && num_ok "$t" || continue
|
|
824
|
+
keep+=("$c:$t")
|
|
825
|
+
done
|
|
826
|
+
n=${#keep[@]}
|
|
827
|
+
i=$((n > 32 ? n - 32 : 0))
|
|
828
|
+
while [ "$i" -lt "$n" ]; do
|
|
829
|
+
AR_HIST="${AR_HIST:+$AR_HIST,}${keep[$i]}"
|
|
830
|
+
i=$((i + 1))
|
|
831
|
+
done
|
|
832
|
+
return 0
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
# The watcher's verdict, written in the exec retry path's own marker vocabulary (its park
|
|
836
|
+
# writers at the bottom of this file) and never weakening a park that already reaches
|
|
837
|
+
# further. A QUOTA verdict is only ever a
|
|
838
|
+
# 10-minute error-cooldown, never a client:7d marker: a weekly park is sticky until its
|
|
839
|
+
# reset (codex-accounts keeps it through every telemetry pass), and a watcher that read one
|
|
840
|
+
# rollout line must not pin an account out for days — a peer may have redeemed its reset
|
|
841
|
+
# in the meantime. The scheduled 5-minute limits pass marks the real window properly.
|
|
842
|
+
ar_mark_cooldown() { # $1 acct dir
|
|
843
|
+
local m="$1/.limited" cur="" until=$((now + 600))
|
|
844
|
+
if [ -f "$m" ]; then
|
|
845
|
+
IFS= read -r cur < "$m" 2>/dev/null || cur=""
|
|
846
|
+
num_ok "$cur" || cur=0
|
|
847
|
+
[ "$cur" -ge "$until" ] && return 0
|
|
848
|
+
fi
|
|
849
|
+
{
|
|
850
|
+
echo "$until"
|
|
851
|
+
echo "bucket=error-cooldown percent=? marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) reason=error-cooldown"
|
|
852
|
+
} 2>/dev/null > "$1/.limited.$$" \
|
|
853
|
+
&& mv -f "$1/.limited.$$" "$m" 2>/dev/null \
|
|
854
|
+
|| rm -f "$1/.limited.$$" 2>/dev/null || true
|
|
855
|
+
return 0
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
ar_mark_auth() { # $1 acct dir — the retry path's soft auth park (1h, self-healing)
|
|
859
|
+
local m="$1/.expired" soft=$((now + 3600)) cur
|
|
860
|
+
# A park still in force that is proven (no soft stamp: codex-accounts' verdict) or
|
|
861
|
+
# longer (org-blocked's 6h) says more than one failed session does: keep it.
|
|
862
|
+
if expired_marked "$1"; then
|
|
863
|
+
cur="$(LC_ALL=C sed -n 's/.*soft_until=\([0-9][0-9]*\).*/\1/p' "$m" 2>/dev/null | head -1)"
|
|
864
|
+
num_ok "$cur" || return 0
|
|
865
|
+
[ "$cur" -ge "$soft" ] && return 0
|
|
866
|
+
fi
|
|
867
|
+
{
|
|
868
|
+
echo "$now"
|
|
869
|
+
echo "reason=auth-error soft_until=$soft marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) detail=interactive session failed to authenticate"
|
|
870
|
+
} 2>/dev/null > "$1/.expired.$$" \
|
|
871
|
+
&& mv -f "$1/.expired.$$" "$m" 2>/dev/null \
|
|
872
|
+
|| rm -f "$1/.expired.$$" 2>/dev/null || true
|
|
873
|
+
sel_log "$(basename "$1") parked (auth-error until $soft) — see: codex-accounts expired"
|
|
874
|
+
return 0
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
# A typed relaunch whose token cannot be honoured (missing, expired, malformed). The
|
|
878
|
+
# watcher has already stopped the session and the typed line carries no argv of its own,
|
|
879
|
+
# so running on would open a FRESH session in its place: say how to get it back, and stop.
|
|
880
|
+
ar_token_dead() { # $1 session id (the token's, else the file's) or empty
|
|
881
|
+
local sid="$1"
|
|
882
|
+
ar_uuid_ok "$sid" || sid=""
|
|
883
|
+
sel_log "autoresume expired sid=${sid:--}"
|
|
884
|
+
if [ -n "$sid" ]; then
|
|
885
|
+
printf 'codex-multiacc: auto-resume could not continue automatically (the resume token expired). Resume with: codex resume %s\n' "$sid" >&2
|
|
886
|
+
else
|
|
887
|
+
printf 'codex-multiacc: auto-resume could not continue automatically (the resume token expired).\n' >&2
|
|
888
|
+
fi
|
|
889
|
+
exit 2
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
# Relaunch entry: load the single-use relaunch file a watcher left for this token. Sets
|
|
893
|
+
# AR_ARGV (the argv to run) and the AR_* chain state; returns 1 when there is nothing
|
|
894
|
+
# usable (AR_R_SID then holds the file's session id, if it named one).
|
|
895
|
+
ar_relaunch() { # $1 token
|
|
896
|
+
local tok="$1" base f line k v n=0 a age
|
|
897
|
+
local ver="" prov="" cls="" acct="" sid="" cwd="" depth="" avoid="" hist="" chain=""
|
|
898
|
+
ar_word_ok "$tok" 8 || return 1
|
|
899
|
+
base="$AR_DIR/r-$tok"
|
|
900
|
+
f="$base.relaunch"
|
|
901
|
+
if [ ! -f "$f" ]; then
|
|
902
|
+
rm -f "$base.argv" 2>/dev/null
|
|
903
|
+
return 1
|
|
904
|
+
fi
|
|
905
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
906
|
+
n=$((n + 1)); [ "$n" -le 64 ] || break
|
|
907
|
+
k="${line%%=*}"; v="${line#*=}"
|
|
908
|
+
[ "$k" != "$line" ] || continue
|
|
909
|
+
# Known keys only, each validated before anything can act on it.
|
|
910
|
+
case "$k" in
|
|
911
|
+
v) num_ok "$v" && ver="$v" ;;
|
|
912
|
+
provider) prov="$v" ;;
|
|
913
|
+
class) case "$v" in ''|*[!$AR_LOWER]*) ;; *) [ "${#v}" -le 16 ] && cls="$v" ;; esac ;;
|
|
914
|
+
acct) case "$v" in acct-*[!0-9]*|acct-) ;; acct-*) acct="$v" ;; esac ;;
|
|
915
|
+
sid) ar_uuid_ok "$v" && sid="$v" ;;
|
|
916
|
+
cwd) case "$v" in /*) cwd="$v" ;; esac ;;
|
|
917
|
+
depth) num_ok "$v" && depth="$v" ;;
|
|
918
|
+
avoid) avoid="$v" ;;
|
|
919
|
+
hist) hist="$v" ;;
|
|
920
|
+
chain) ar_word_ok "$v" 1 && chain="$v" ;;
|
|
921
|
+
esac
|
|
922
|
+
done < "$f"
|
|
923
|
+
age=$((now - $(file_mtime "$f")))
|
|
924
|
+
if [ "$ver" != "1" ] || [ "$prov" != "codex" ] || [ "$age" -gt 600 ] || [ ! -f "$base.argv" ]; then
|
|
925
|
+
rm -f "$f" "$base.argv" 2>/dev/null
|
|
926
|
+
AR_R_SID="$sid"
|
|
927
|
+
return 1
|
|
928
|
+
fi
|
|
929
|
+
n=0
|
|
930
|
+
while IFS= read -r -d '' a; do
|
|
931
|
+
n=$((n + 1)); [ "$n" -le 256 ] || break
|
|
932
|
+
AR_ARGV+=("$a")
|
|
933
|
+
done < "$base.argv"
|
|
934
|
+
rm -f "$f" "$base.argv" 2>/dev/null
|
|
935
|
+
if [ "${#AR_ARGV[@]}" -eq 0 ]; then
|
|
936
|
+
AR_R_SID="$sid"
|
|
937
|
+
return 1
|
|
938
|
+
fi
|
|
939
|
+
if [ -n "$acct" ] && [ -d "$ACC_ROOT/$acct" ]; then
|
|
940
|
+
case "$cls" in
|
|
941
|
+
quota) ar_mark_cooldown "$ACC_ROOT/$acct" ;;
|
|
942
|
+
auth) ar_mark_auth "$ACC_ROOT/$acct" ;;
|
|
943
|
+
esac
|
|
944
|
+
fi
|
|
945
|
+
AR_DEPTH="${depth:-0}"
|
|
946
|
+
ar_avoid_add "$avoid"
|
|
947
|
+
ar_set_hist "$hist"
|
|
948
|
+
AR_CHAIN="$chain"
|
|
949
|
+
if [ -n "$cwd" ] && [ -d "$cwd" ] && [ "$cwd" != "$PWD" ]; then
|
|
950
|
+
# Everything this shim resolved against the old directory has to survive the cd.
|
|
951
|
+
case "$ACC_ROOT" in /*) ;; *) ACC_ROOT="$PWD/$ACC_ROOT"; MANIFEST="$ACC_ROOT/accounts.json"; AR_DIR="$ACC_ROOT/tmp/autoresume" ;; esac
|
|
952
|
+
case "$REAL" in /*) ;; *) REAL="$PWD/$REAL" ;; esac
|
|
953
|
+
cd "$cwd" 2>/dev/null || true
|
|
954
|
+
fi
|
|
955
|
+
# Field 2 is the word `autoresume`, never an account id: lib/report.py and the
|
|
956
|
+
# *-accounts tools read an acct-NN in field 2 as a pick.
|
|
957
|
+
sel_log "autoresume relaunch chain=${AR_CHAIN:--} from=${acct:--} class=${cls:--} depth=$AR_DEPTH"
|
|
958
|
+
return 0
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
# The argv shapes a relaunch can rebuild as `resume <kept opts> <sid> <prompt>` — anything
|
|
962
|
+
# else (exec/e, -p profile, -c overrides, any subcommand, an unknown flag) gets no watcher.
|
|
963
|
+
# A positional must contain a space to count as a prompt: a single word may be a
|
|
964
|
+
# subcommand.
|
|
965
|
+
ar_argv_ok() { # the argv codex is about to get
|
|
966
|
+
local a resume=0 sid=0 prompt=0 want=0
|
|
967
|
+
for a in "$@"; do
|
|
968
|
+
if [ "$want" = 1 ]; then
|
|
969
|
+
case "$a" in ''|-*) return 1 ;; esac
|
|
970
|
+
want=0
|
|
971
|
+
continue
|
|
972
|
+
fi
|
|
973
|
+
case "$a" in
|
|
974
|
+
--dangerously-bypass-approvals-and-sandbox|--yolo) ;;
|
|
975
|
+
-m|--model) want=1 ;;
|
|
976
|
+
--model=?*) ;;
|
|
977
|
+
--last)
|
|
978
|
+
[ "$resume" = 1 ] && [ "$sid" = 0 ] || return 1
|
|
979
|
+
sid=1 ;;
|
|
980
|
+
resume)
|
|
981
|
+
[ "$resume" = 0 ] && [ "$prompt" = 0 ] || return 1
|
|
982
|
+
resume=1 ;;
|
|
983
|
+
-*) return 1 ;;
|
|
984
|
+
*)
|
|
985
|
+
if [ "$resume" = 1 ] && [ "$sid" = 0 ]; then
|
|
986
|
+
ar_uuid_ok "$a" || return 1 # codex reads it as SESSION_ID
|
|
987
|
+
sid=1
|
|
988
|
+
else
|
|
989
|
+
[ "$prompt" = 0 ] || return 1
|
|
990
|
+
case "$a" in *" "*) prompt=1 ;; *) return 1 ;; esac
|
|
991
|
+
fi ;;
|
|
992
|
+
esac
|
|
993
|
+
done
|
|
994
|
+
[ "$want" = 0 ] || return 1
|
|
995
|
+
# A bare `resume` opens the session picker: there is no session to follow yet.
|
|
996
|
+
[ "$resume" = 0 ] || [ "$sid" = 1 ]
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
# The relaunch line is typed into the pane's shell unquoted — this shim's own path, and the
|
|
1000
|
+
# pool root when it is not the default — so only paths that never need quoting qualify.
|
|
1001
|
+
ar_path_ok() { # $1 absolute path
|
|
1002
|
+
case "$1" in /*) ;; *) return 1 ;; esac
|
|
1003
|
+
case "$1" in *[!$AR_ALNUM/._+-]*) return 1 ;; esac
|
|
1004
|
+
return 0
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
ar_find_python() { # -> AR_PY, a python3 the watcher can run on. Never RUNS a candidate.
|
|
1008
|
+
AR_PY=""
|
|
1009
|
+
if [ -n "${CODEX_MULTIACC_PYTHON:-}" ] && [ -f "$CODEX_MULTIACC_PYTHON" ] \
|
|
1010
|
+
&& [ -x "$CODEX_MULTIACC_PYTHON" ]; then
|
|
1011
|
+
AR_PY="$CODEX_MULTIACC_PYTHON"
|
|
1012
|
+
return 0
|
|
1013
|
+
fi
|
|
1014
|
+
AR_PY="$(command -v python3 2>/dev/null)" || AR_PY=""
|
|
1015
|
+
[ -n "$AR_PY" ] || return 1
|
|
1016
|
+
# macOS ships /usr/bin/python3 as a stub that pops the Command Line Tools installer
|
|
1017
|
+
# when nothing backs it — a GUI dialog out of a background watcher. Only use it when
|
|
1018
|
+
# the CLT python or the developer dir `xcode-select -p` names is actually present, and
|
|
1019
|
+
# find that out with file tests (the xcode_select_link it reads, and Xcode's default
|
|
1020
|
+
# location it falls back to) rather than by running anything.
|
|
1021
|
+
case "${OSTYPE:-}" in darwin*)
|
|
1022
|
+
if [ "$AR_PY" = "/usr/bin/python3" ] \
|
|
1023
|
+
&& [ ! -x /Library/Developer/CommandLineTools/usr/bin/python3 ] \
|
|
1024
|
+
&& [ ! -d /var/db/xcode_select_link ] \
|
|
1025
|
+
&& [ ! -d /Applications/Xcode.app/Contents/Developer ]; then
|
|
1026
|
+
AR_PY=""
|
|
1027
|
+
return 1
|
|
1028
|
+
fi ;;
|
|
1029
|
+
esac
|
|
1030
|
+
return 0
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
# Gate + spawn, called ONLY from the plain interactive exec at the bottom, immediately
|
|
1034
|
+
# before it. Returns without doing anything unless every precondition holds, and never
|
|
1035
|
+
# waits on what it starts: the watcher is detached and the exec happens right after.
|
|
1036
|
+
ar_spawn_watcher() { # $1 provider, $2 picked acct dir, then the argv codex is about to get
|
|
1037
|
+
local prov="$1" pick="$2" lib st root nl='
|
|
1038
|
+
'
|
|
1039
|
+
shift 2
|
|
1040
|
+
case "${CODEX_MULTIACC_AUTORESUME:-1}" in 0|false|no|off) return 1 ;; esac
|
|
1041
|
+
[ -e "$ACC_ROOT/autoresume.off" ] && return 1
|
|
1042
|
+
# A pin is the caller overriding selection. A valid one execs long before this; an
|
|
1043
|
+
# invalid one fell through to a random pick, and moving that session is not ours to do.
|
|
1044
|
+
[ -z "${CODEX_ACCOUNT:-}" ] || return 1
|
|
1045
|
+
if [ "${CODEX_MULTIACC_AR_TEST_TTY:-0}" != "1" ]; then
|
|
1046
|
+
[ -t 0 ] && [ -t 1 ] || return 1
|
|
1047
|
+
fi
|
|
1048
|
+
[ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ] || return 1
|
|
1049
|
+
ar_argv_ok "$@" || return 1
|
|
1050
|
+
ar_find_python || return 1
|
|
1051
|
+
lib="$SELF_DIR/../lib/autoresume.py"
|
|
1052
|
+
[ -f "$lib" ] || return 1
|
|
1053
|
+
# The state file is line-oriented key=value: a value carrying a newline cannot be
|
|
1054
|
+
# written faithfully, so such a launch simply goes unsupervised.
|
|
1055
|
+
case "$PWD$TMUX$TMUX_PANE$SELF$ACC_ROOT$pick" in *"$nl"*) return 1 ;; esac
|
|
1056
|
+
root="$ACC_ROOT"
|
|
1057
|
+
case "$root" in /*) ;; *) root="$PWD/$root" ;; esac
|
|
1058
|
+
ar_path_ok "$SELF" && ar_path_ok "$root" || return 1
|
|
1059
|
+
[ -d "$AR_DIR" ] || mkdir -p "$AR_DIR" 2>/dev/null || return 1
|
|
1060
|
+
st="$AR_DIR/$$.state"
|
|
1061
|
+
# The ORIGINAL argv (A2), NUL-separated: the watcher rebuilds the relaunch from it.
|
|
1062
|
+
if [ "${#AR_ORIG_ARGV[@]}" -gt 0 ]; then
|
|
1063
|
+
printf '%s\0' "${AR_ORIG_ARGV[@]}" 2>/dev/null > "$AR_DIR/$$.argv.tmp"
|
|
1064
|
+
else
|
|
1065
|
+
: 2>/dev/null > "$AR_DIR/$$.argv.tmp"
|
|
1066
|
+
fi && mv -f "$AR_DIR/$$.argv.tmp" "$AR_DIR/$$.argv" 2>/dev/null || {
|
|
1067
|
+
rm -f "$AR_DIR/$$.argv.tmp" 2>/dev/null
|
|
1068
|
+
return 1
|
|
1069
|
+
}
|
|
1070
|
+
{
|
|
1071
|
+
printf 'v=1\nprovider=%s\npid=%s\nppid=%s\n' "$prov" "$$" "$PPID"
|
|
1072
|
+
printf 'acct=%s\nacct_dir=%s\ncwd=%s\nlaunched=%s\n' "${pick##*/}" "$pick" "$PWD" "$(date +%s)"
|
|
1073
|
+
printf 'tmux=%s\npane=%s\nself=%s\nacc_root=%s\n' "$TMUX" "$TMUX_PANE" "$SELF" "$root"
|
|
1074
|
+
printf 'depth=%s\navoid=%s\nhist=%s\nchain=%s\n' "$AR_DEPTH" "$AR_AVOID" "$AR_HIST" "$AR_CHAIN"
|
|
1075
|
+
} 2>/dev/null > "$st.tmp" && mv -f "$st.tmp" "$st" 2>/dev/null || {
|
|
1076
|
+
rm -f "$st.tmp" "$AR_DIR/$$.argv" 2>/dev/null
|
|
1077
|
+
return 1
|
|
1078
|
+
}
|
|
1079
|
+
# Detached and never waited on: the subshell returns as soon as the watcher is forked,
|
|
1080
|
+
# and the watcher ignores the terminal's signals until its own setsid().
|
|
1081
|
+
( trap '' INT QUIT HUP TSTP
|
|
1082
|
+
exec "$AR_PY" -I "$lib" watch --state "$st" </dev/null >/dev/null 2>&1 & ) >/dev/null 2>&1
|
|
1083
|
+
return 0
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
ar_scrub_env() { # the TUI (and everything it spawns) never sees an auto-resume variable
|
|
1087
|
+
local v
|
|
1088
|
+
for v in ${!CODEX_MULTIACC_AR@}; do
|
|
1089
|
+
case "$v" in CODEX_MULTIACC_AR|CODEX_MULTIACC_AR_*) unset "$v" ;; esac
|
|
1090
|
+
done
|
|
1091
|
+
return 0
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
# A1 — the relaunch entry, before any selection (the pin included): a token is consumed
|
|
1095
|
+
# exactly once, and a probe never consumes one. A2 — the argv selection starts from is
|
|
1096
|
+
# what a later relaunch rebuilds; codex never rewrites "$@" after this point.
|
|
1097
|
+
# The typed variable is <token>[:<session id>]: the id rides along so a token that cannot
|
|
1098
|
+
# be honoured still names the session to resume.
|
|
1099
|
+
if [ -n "${CODEX_MULTIACC_AR+x}" ]; then
|
|
1100
|
+
ar_tok="$CODEX_MULTIACC_AR"
|
|
1101
|
+
unset CODEX_MULTIACC_AR
|
|
1102
|
+
if [ "$AR_PROBE" != "1" ] && [ -n "$ar_tok" ]; then
|
|
1103
|
+
ar_sid=""
|
|
1104
|
+
case "$ar_tok" in *:*) ar_sid="${ar_tok#*:}"; ar_tok="${ar_tok%%:*}" ;; esac
|
|
1105
|
+
ar_relaunch "$ar_tok" || ar_token_dead "${ar_sid:-$AR_R_SID}"
|
|
1106
|
+
set -- ${AR_ARGV[@]+"${AR_ARGV[@]}"}
|
|
1107
|
+
fi
|
|
1108
|
+
fi
|
|
1109
|
+
[ -n "${CODEX_MULTIACC_AR_AVOID:-}" ] && ar_avoid_add "$CODEX_MULTIACC_AR_AVOID"
|
|
1110
|
+
AR_ORIG_ARGV=("$@")
|
|
1111
|
+
|
|
732
1112
|
# Explicit pin wins over everything — markers, and even missing auth: the
|
|
733
1113
|
# add/login ceremony pins to a dir that has no credentials yet, and the login
|
|
734
1114
|
# must land exactly there, never in a randomly selected account's dir.
|
|
735
|
-
|
|
1115
|
+
# A probe asks what SELECTION would pick, so it never takes (and never execs) a pin.
|
|
1116
|
+
if [ -n "${CODEX_ACCOUNT:-}" ] && [ "$AR_PROBE" != "1" ]; then
|
|
736
1117
|
d="$ACC_ROOT/$CODEX_ACCOUNT"
|
|
737
1118
|
if [ -d "$d" ]; then
|
|
738
1119
|
sel_log "$CODEX_ACCOUNT pinned pwd=$PWD"
|
|
@@ -774,6 +1155,9 @@ for d in "$ACC_ROOT"/acct-*; do
|
|
|
774
1155
|
continue
|
|
775
1156
|
fi
|
|
776
1157
|
over_threshold "$d" && continue
|
|
1158
|
+
# An auto-resume relaunch (or its probe) skips the account the session just left until
|
|
1159
|
+
# the reset it was handed. `eligible` only: the all-limited fallback still sees it.
|
|
1160
|
+
ar_avoided "$d" && continue
|
|
777
1161
|
eligible+=("$d")
|
|
778
1162
|
done
|
|
779
1163
|
|
|
@@ -798,6 +1182,8 @@ fi
|
|
|
798
1182
|
# No usable accounts => stock behavior (fail open, never block work), but say WHY when
|
|
799
1183
|
# the pool is merely un-authenticated: this is the one case the user can actually fix.
|
|
800
1184
|
if [ "${#valid[@]}" -eq 0 ]; then
|
|
1185
|
+
# A probe has nothing to offer and must never exec (nor log a fallback it never took).
|
|
1186
|
+
if [ "$AR_PROBE" = "1" ]; then printf 'pick= tier=none\n'; exit 3; fi
|
|
801
1187
|
if [ "${#expired[@]}" -gt 0 ]; then
|
|
802
1188
|
sel_log "all-expired: falling back to the default login (see: codex-accounts expired)"
|
|
803
1189
|
# Terminal only: a service-spawned `codex exec` must keep its stderr byte-clean,
|
|
@@ -911,6 +1297,7 @@ pick_best() { # args: candidate dirs
|
|
|
911
1297
|
}
|
|
912
1298
|
|
|
913
1299
|
if [ "${#eligible[@]}" -gt 0 ]; then
|
|
1300
|
+
AR_TIER=eligible
|
|
914
1301
|
if [ "${CODEX_SHIM_SELECT:-headroom}" = "random" ]; then
|
|
915
1302
|
PICK_DIR="${eligible[$((RANDOM % ${#eligible[@]}))]}"
|
|
916
1303
|
PICK_BAND_COUNT=${#eligible[@]}
|
|
@@ -932,12 +1319,16 @@ else
|
|
|
932
1319
|
for d in "${valid[@]}"; do
|
|
933
1320
|
if limited_hard_blocked "$d"; then hard+=("$d"); else soft+=("$d"); fi
|
|
934
1321
|
done
|
|
1322
|
+
# A probe only ASKS: the fallback lines below would claim a pick nobody launched.
|
|
935
1323
|
if [ "${#soft[@]}" -gt 0 ]; then
|
|
1324
|
+
AR_TIER=soft
|
|
936
1325
|
PICK_STRICT=1
|
|
937
1326
|
pick_best "${soft[@]}"
|
|
938
1327
|
PICK_STRICT=0
|
|
939
|
-
|
|
1328
|
+
[ "$AR_PROBE" = "1" ] \
|
|
1329
|
+
|| sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
|
|
940
1330
|
else
|
|
1331
|
+
AR_TIER=hard
|
|
941
1332
|
# Every account is exhausted RIGHT NOW: nothing serves, so hand out the one that
|
|
942
1333
|
# unblocks first — its rejection window is the shortest.
|
|
943
1334
|
PICK_DIR=""
|
|
@@ -951,11 +1342,12 @@ else
|
|
|
951
1342
|
PICK_DIR="$d"; best_reset="$r"
|
|
952
1343
|
fi
|
|
953
1344
|
done
|
|
954
|
-
|
|
1345
|
+
[ "$AR_PROBE" = "1" ] \
|
|
1346
|
+
|| sel_log "all-limited fallback=$(basename "$PICK_DIR") all-exhausted resets_in=$((best_reset > now ? best_reset - now : 0))s"
|
|
955
1347
|
# Every candidate rejects right now, so this pick WILL fail: say so on a terminal
|
|
956
1348
|
# instead of letting the operator read the client's bare limit error as a bad
|
|
957
1349
|
# choice by the pool. Throttled, and never on a service's stderr.
|
|
958
|
-
if [ -t 2 ]; then
|
|
1350
|
+
if [ -t 2 ] && [ "$AR_PROBE" != "1" ]; then
|
|
959
1351
|
exn="$ACC_ROOT/.exhausted-notice"
|
|
960
1352
|
exlast=0
|
|
961
1353
|
[ -f "$exn" ] && exlast="$(file_mtime "$exn")"
|
|
@@ -968,6 +1360,12 @@ else
|
|
|
968
1360
|
fi
|
|
969
1361
|
fi
|
|
970
1362
|
pick="$PICK_DIR"
|
|
1363
|
+
# A probe answers here, before anything that would count as a launch: no rotation
|
|
1364
|
+
# memory, no limits kick, no pick line in selection.log — and never an exec.
|
|
1365
|
+
if [ "$AR_PROBE" = "1" ]; then
|
|
1366
|
+
printf 'pick=%s tier=%s\n' "${pick##*/}" "$AR_TIER"
|
|
1367
|
+
exit 0
|
|
1368
|
+
fi
|
|
971
1369
|
# Remember the pick so the NEXT run does not hand back the same account. An explicit
|
|
972
1370
|
# CODEX_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
|
|
973
1371
|
# not a turn in the rotation.
|
|
@@ -1038,6 +1436,10 @@ fi
|
|
|
1038
1436
|
share_state_index "$pick"
|
|
1039
1437
|
if [ "$wants_retry" = "0" ]; then
|
|
1040
1438
|
export CODEX_HOME="$pick"
|
|
1439
|
+
# Auto-resume: a gated, detached watcher beside this exec — the exec itself (same pid,
|
|
1440
|
+
# same argv, same environment minus the auto-resume variables) is unchanged.
|
|
1441
|
+
ar_spawn_watcher codex "$pick" "$@" || true
|
|
1442
|
+
ar_scrub_env
|
|
1041
1443
|
exec "$REAL" "$@"
|
|
1042
1444
|
fi
|
|
1043
1445
|
|
|
@@ -73,6 +73,11 @@ auto-refreshing login — `.credentials.json`, or on macOS the login Keychain wh
|
|
|
73
73
|
the session can open it (Claude Code migrates the file into the Keychain on the first
|
|
74
74
|
refresh from a keychain-capable session; ssh sessions then see the account as
|
|
75
75
|
`KEYCHAIN LOCKED` and cannot run it, while the Mac's own session uses it normally).
|
|
76
|
+
The Keychain item is named after `$USER`; the CLI fills it from `id -un` when the caller's
|
|
77
|
+
env has none (a USER-less client files its login under `unknown`, where the next client
|
|
78
|
+
never reads it). The pool judges only the item under that name, as the client does; a
|
|
79
|
+
login found only under another name is moved there by the next limits pass, and a
|
|
80
|
+
sign-in whose identity matched leaves exactly one item, under that name.
|
|
76
81
|
That credential is **machine-local** (never synced),
|
|
77
82
|
which is what keeps two machines from invalidating each other's refresh token — so an
|
|
78
83
|
account you `add` on the Mac runs on the Mac, and you `add` it on the server (over SSH) if
|
|
@@ -402,13 +407,14 @@ derived from the pool root. Uninstalling an instance removes only that instance'
|
|
|
402
407
|
| `CLAUDE_MULTIACC_CLIENT_SCAN_TTL=<s>` | clean client-limit scan cache; default 20s |
|
|
403
408
|
| `CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY=<s>` | five-hour recovery grace; default 300s (weekly never clears early) |
|
|
404
409
|
| `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
|
|
410
|
+
| `CLAUDE_MULTIACC_AUTORESUME=0` | no [auto-resume](AUTORESUME.md) watcher for launches from this environment. `touch <pool>/autoresume.off` also stops running watchers |
|
|
405
411
|
| `CLAUDE_ACCOUNTS_ROOT=...` | relocate the pool; legacy spelling is `CLAUDE_ACCOUNTS_DIR` |
|
|
406
412
|
| `CLAUDE_MULTIACC_SYNC_TARGET=...` | sync target, overriding the manifest; `none` = local-only |
|
|
407
413
|
| `CLAUDE_MULTIACC_SYNC_ROOT=... / _SYNC_REPO=...` | remote pool root / addon repo that goes with it |
|
|
408
414
|
|
|
409
415
|
The codex shim honors the same switches spelled `CODEX_*`: `CODEX_ACCOUNT`,
|
|
410
416
|
`CODEX_HOME` (passthrough), `CODEX_MULTIACC_DISABLE`, `CODEX_SHIM_RETRY`,
|
|
411
|
-
`CODEX_SHIM_SELECT`, `CODEX_MULTIACC_HEADROOM_BAND`, `CODEX_MULTIACC_SESSION_GATE`,
|
|
417
|
+
`CODEX_MULTIACC_AUTORESUME`, `CODEX_SHIM_SELECT`, `CODEX_MULTIACC_HEADROOM_BAND`, `CODEX_MULTIACC_SESSION_GATE`,
|
|
412
418
|
`CODEX_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY` (5h client-marker recovery grace, default
|
|
413
419
|
300s — weekly markers never clear early), `CODEX_ACCOUNTS_ROOT` (legacy
|
|
414
420
|
`CODEX_ACCOUNTS_DIR`), `CODEX_MULTIACC_SYNC_TARGET`, `CODEX_MULTIACC_THRESHOLD`.
|