claude-multiacc 2.0.41 → 2.0.42
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 +49 -2
- package/bin/claude +394 -2
- package/bin/codex +406 -4
- package/docs/ACCOUNT_OPERATIONS.md +2 -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/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/run-tests.sh +935 -3
- package/tests/test_autoresume.py +2565 -0
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
|
|
|
@@ -402,13 +402,14 @@ derived from the pool root. Uninstalling an instance removes only that instance'
|
|
|
402
402
|
| `CLAUDE_MULTIACC_CLIENT_SCAN_TTL=<s>` | clean client-limit scan cache; default 20s |
|
|
403
403
|
| `CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY=<s>` | five-hour recovery grace; default 300s (weekly never clears early) |
|
|
404
404
|
| `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
|
|
405
|
+
| `CLAUDE_MULTIACC_AUTORESUME=0` | no [auto-resume](AUTORESUME.md) watcher for launches from this environment. `touch <pool>/autoresume.off` also stops running watchers |
|
|
405
406
|
| `CLAUDE_ACCOUNTS_ROOT=...` | relocate the pool; legacy spelling is `CLAUDE_ACCOUNTS_DIR` |
|
|
406
407
|
| `CLAUDE_MULTIACC_SYNC_TARGET=...` | sync target, overriding the manifest; `none` = local-only |
|
|
407
408
|
| `CLAUDE_MULTIACC_SYNC_ROOT=... / _SYNC_REPO=...` | remote pool root / addon repo that goes with it |
|
|
408
409
|
|
|
409
410
|
The codex shim honors the same switches spelled `CODEX_*`: `CODEX_ACCOUNT`,
|
|
410
411
|
`CODEX_HOME` (passthrough), `CODEX_MULTIACC_DISABLE`, `CODEX_SHIM_RETRY`,
|
|
411
|
-
`CODEX_SHIM_SELECT`, `CODEX_MULTIACC_HEADROOM_BAND`, `CODEX_MULTIACC_SESSION_GATE`,
|
|
412
|
+
`CODEX_MULTIACC_AUTORESUME`, `CODEX_SHIM_SELECT`, `CODEX_MULTIACC_HEADROOM_BAND`, `CODEX_MULTIACC_SESSION_GATE`,
|
|
412
413
|
`CODEX_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY` (5h client-marker recovery grace, default
|
|
413
414
|
300s — weekly markers never clear early), `CODEX_ACCOUNTS_ROOT` (legacy
|
|
414
415
|
`CODEX_ACCOUNTS_DIR`), `CODEX_MULTIACC_SYNC_TARGET`, `CODEX_MULTIACC_THRESHOLD`.
|