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/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
- if [ -n "${CODEX_ACCOUNT:-}" ]; then
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
- sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
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
- sel_log "all-limited fallback=$(basename "$PICK_DIR") all-exhausted resets_in=$((best_reset > now ? best_reset - now : 0))s"
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`.