claude-multiacc 2.0.40 → 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/lib/report.py +16 -1
- 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/tests/test_claude_reset.py +30 -0
package/tests/run-tests.sh
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
set -u
|
|
6
6
|
|
|
7
7
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
8
|
-
WORK="$(mktemp -d "${TMPDIR:-/tmp}/multiacc-test.XXXXXX")"
|
|
8
|
+
WORK="$(mktemp -d "${TMPDIR:-/tmp}/multiacc-test.XXXXXX")" || exit 1
|
|
9
|
+
[ -n "$WORK" ] && [ -d "$WORK" ] || exit 1
|
|
9
10
|
# Loopback stub servers register their pid here. Deleting $WORK does not kill a running
|
|
10
11
|
# python process, so an interrupted run (CI cancel, Ctrl-C) would otherwise leave one
|
|
11
12
|
# listening until the machine goes away.
|
|
@@ -16,7 +17,13 @@ cleanup() {
|
|
|
16
17
|
kill "$p" 2>/dev/null
|
|
17
18
|
wait "$p" 2>/dev/null || true
|
|
18
19
|
done
|
|
19
|
-
|
|
20
|
+
# The auto-resume sections reap their own processes; this is the backstop for an
|
|
21
|
+
# interrupted run. Every fake client and watcher they start carries $WORK in its argv.
|
|
22
|
+
# Never with an empty WORK: `pkill -f /` would match nearly every process the user owns.
|
|
23
|
+
if [ -n "${WORK:-}" ] && [ -d "$WORK" ]; then
|
|
24
|
+
pkill -KILL -f "$WORK/" 2>/dev/null || true
|
|
25
|
+
rm -rf "$WORK"
|
|
26
|
+
fi
|
|
20
27
|
}
|
|
21
28
|
trap cleanup EXIT
|
|
22
29
|
|
|
@@ -169,6 +176,56 @@ if [ "${1:-}" = "setup-token" ]; then
|
|
|
169
176
|
echo "Your token: sk-ant-oat01-FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE"
|
|
170
177
|
exit 0
|
|
171
178
|
fi
|
|
179
|
+
if [ -n "${FAKE_TUI:-}" ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
180
|
+
# The auto-resume sections' INTERACTIVE client: it behaves the way the real TUI does at
|
|
181
|
+
# an API error — registers sessions/<pid>.json in the account dir, writes the error
|
|
182
|
+
# record into the (shared) transcript and then IDLES; SIGTERM deletes the registry and
|
|
183
|
+
# exits 143, and a crash (SIGKILL) leaves the registry behind. The scripted error only
|
|
184
|
+
# fires on a fresh launch; a `--resume <sid>` launch appends what the real client does
|
|
185
|
+
# after the auto-resume prompt — a normal reply — unless FAKE_TUI_QUIET is set.
|
|
186
|
+
acct="$(basename "$CLAUDE_CONFIG_DIR")"
|
|
187
|
+
sid=""; prev=""
|
|
188
|
+
for a in "$@"; do
|
|
189
|
+
case "$prev" in -r|--resume) sid="$a" ;; esac
|
|
190
|
+
case "$a" in --resume=*) sid="${a#--resume=}" ;; esac
|
|
191
|
+
prev="$a"
|
|
192
|
+
done
|
|
193
|
+
resumed=0; [ -n "$sid" ] && resumed=1
|
|
194
|
+
[ -n "$sid" ] || sid="${FAKE_TUI_SID:-5f0c1d2e-3a4b-4c5d-8e9f-a0b1c2d3e4f5}"
|
|
195
|
+
[ -n "${FAKE_TUI_PIDS:-}" ] && echo "$$" >> "$FAKE_TUI_PIDS"
|
|
196
|
+
echo "CFG=$acct ARGS=$*" >> "${FAKE_TUI_LOG:-/dev/null}"
|
|
197
|
+
proj="$CLAUDE_CONFIG_DIR/projects/-fake-tui"
|
|
198
|
+
mkdir -p "$CLAUDE_CONFIG_DIR/sessions" "$proj"
|
|
199
|
+
tr="$proj/$sid.jsonl"
|
|
200
|
+
ts() { date -u +%Y-%m-%dT%H:%M:%S.000Z; }
|
|
201
|
+
apierr() { # $1 error code, $2 extra JSON members (with a leading comma) or empty
|
|
202
|
+
printf '{"type":"assistant","isApiErrorMessage":true,"error":"%s"%s,"timestamp":"%s","isSidechain":false,"sessionId":"%s","message":{"content":[{"type":"text","text":"API Error"}]}}\n' \
|
|
203
|
+
"$1" "${2:-}" "$(ts)" "$sid" >> "$tr"
|
|
204
|
+
}
|
|
205
|
+
if [ "$resumed" = 1 ] && [ -z "${FAKE_TUI_QUIET:-}" ]; then
|
|
206
|
+
printf '{"type":"assistant","timestamp":"%s","isSidechain":false,"sessionId":"%s","message":{"content":[{"type":"text","text":"Resumed."}]}}\n' \
|
|
207
|
+
"$(ts)" "$sid" >> "$tr"
|
|
208
|
+
fi
|
|
209
|
+
reg="$CLAUDE_CONFIG_DIR/sessions/$$.json"
|
|
210
|
+
printf '{"pid":%s,"sessionId":"%s","cwd":"%s","kind":"interactive","status":"busy","startedAt":%s000}\n' \
|
|
211
|
+
"$$" "$sid" "$PWD" "$(date +%s)" > "$reg"
|
|
212
|
+
trap 'rm -f "$reg"; exit 143' TERM
|
|
213
|
+
trap 'rm -f "$reg"; exit 129' HUP
|
|
214
|
+
if [ "$resumed" = 0 ]; then
|
|
215
|
+
sleep "${FAKE_TUI_DELAY:-0.3}"
|
|
216
|
+
case "$FAKE_TUI" in
|
|
217
|
+
quota) apierr rate_limit ",\"quotaLimits\":{\"status\":\"rejected\",\"resetsAt\":$(( $(date +%s) + 7200 )),\"rateLimitType\":\"five_hour\"}" ;;
|
|
218
|
+
auth) apierr authentication_failed ;;
|
|
219
|
+
invalid) apierr invalid_request ;;
|
|
220
|
+
crash) sleep 0.3; kill -9 $$ ;;
|
|
221
|
+
esac
|
|
222
|
+
fi
|
|
223
|
+
# Bounded, in short sleeps so a TERM is handled within a tenth of a second.
|
|
224
|
+
i=0
|
|
225
|
+
while [ "$i" -lt "${FAKE_TUI_LIFE:-400}" ]; do sleep 0.1; i=$((i + 1)); done
|
|
226
|
+
rm -f "$reg"
|
|
227
|
+
exit 0
|
|
228
|
+
fi
|
|
172
229
|
if [ $# -eq 0 ] && [ -n "${FAKE_DO_LOGIN:-}" ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
173
230
|
# simulate an interactive session in which the user completed /login
|
|
174
231
|
printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-new","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999}}' > "$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
@@ -260,6 +317,8 @@ if [ -n "${FAKE_SESSION_ID:-}" ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
|
260
317
|
sleep "${FAKE_SESSION_HOLD:-4}"
|
|
261
318
|
rm -f "$CLAUDE_CONFIG_DIR/sessions/$$.json"
|
|
262
319
|
fi
|
|
320
|
+
# what an interactive client would inherit from the shim (auto-resume must leak nothing)
|
|
321
|
+
[ -n "${FAKE_PRINT_ENV:-}" ] && echo "AR_ENV=$(env | grep -c '^CLAUDE_MULTIACC_AR')"
|
|
263
322
|
echo "CFG=$acct TOK=${CLAUDE_CODE_OAUTH_TOKEN:-none} ARGS=$*"
|
|
264
323
|
EOF
|
|
265
324
|
chmod +x "$FAKEBIN/claude"
|
|
@@ -352,6 +411,12 @@ export CLAUDE_MULTIACC_KEYCHAIN=0
|
|
|
352
411
|
# runs under the operator's real HOME, whose rc files are not under test — section 18
|
|
353
412
|
# turns the probe on against its own fake HOME.
|
|
354
413
|
export CLAUDE_MULTIACC_PATH_PROBE=0
|
|
414
|
+
# Auto-resume spawns a detached watcher for an interactive launch inside tmux. The suite
|
|
415
|
+
# may itself run inside tmux, and no legacy section is about auto-resume, so it is off
|
|
416
|
+
# (and the pane invisible) everywhere except the sections that turn it on against a fake
|
|
417
|
+
# tmux — a watcher here would otherwise type into the developer's REAL pane.
|
|
418
|
+
export CLAUDE_MULTIACC_AUTORESUME=0 CODEX_MULTIACC_AUTORESUME=0
|
|
419
|
+
unset TMUX TMUX_PANE 2>/dev/null || true
|
|
355
420
|
|
|
356
421
|
now="$(date +%s)"
|
|
357
422
|
|
|
@@ -4852,6 +4917,61 @@ with open(path + ".tmp", "w") as f:
|
|
|
4852
4917
|
os.replace(path + ".tmp", path)
|
|
4853
4918
|
EOF
|
|
4854
4919
|
|
|
4920
|
+
# The auto-resume sections' INTERACTIVE codex (sourced by the fake below when FAKE_TUI is
|
|
4921
|
+
# set): it writes a rollout the way the real TUI does — line 1 session_meta (id ==
|
|
4922
|
+
# session_id, cwd, originator codex-tui), held open like the native binary holds it — then
|
|
4923
|
+
# the scripted token_count + task_complete error, and IDLES; SIGTERM exits 0 (what the node
|
|
4924
|
+
# wrapper reports). A `resume <sid>` launch appends task_started (the resumed turn) to the
|
|
4925
|
+
# SAME rollout instead of an error.
|
|
4926
|
+
cat > "$WORK/fake-tui-codex.sh" <<'EOF'
|
|
4927
|
+
acct="$(basename "$CODEX_HOME")"
|
|
4928
|
+
sid=""; resumed=0
|
|
4929
|
+
if [ "${1:-}" = resume ]; then
|
|
4930
|
+
resumed=1
|
|
4931
|
+
for a in "$@"; do
|
|
4932
|
+
case "$a" in
|
|
4933
|
+
*' '*) ;;
|
|
4934
|
+
????????-????-????-????-????????????) [ -n "$sid" ] || sid="$a" ;;
|
|
4935
|
+
esac
|
|
4936
|
+
done
|
|
4937
|
+
fi
|
|
4938
|
+
[ -n "${FAKE_TUI_PIDS:-}" ] && echo "$$" >> "$FAKE_TUI_PIDS"
|
|
4939
|
+
echo "CFG=$acct ARGS=$*" >> "${FAKE_TUI_LOG:-/dev/null}"
|
|
4940
|
+
ts() { date -u +%Y-%m-%dT%H:%M:%S.000Z; }
|
|
4941
|
+
ro=""
|
|
4942
|
+
if [ -n "$sid" ]; then
|
|
4943
|
+
for f in "$CODEX_HOME"/sessions/*/*/*/rollout-*-"$sid".jsonl; do [ -f "$f" ] && ro="$f"; done
|
|
4944
|
+
fi
|
|
4945
|
+
if [ -z "$ro" ]; then
|
|
4946
|
+
[ -n "$sid" ] || sid="${FAKE_TUI_SID:-6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d}"
|
|
4947
|
+
day="$CODEX_HOME/sessions/$(date +%Y/%m/%d)"
|
|
4948
|
+
mkdir -p "$day"
|
|
4949
|
+
ro="$day/rollout-$(date +%Y-%m-%dT%H-%M-%S)-$sid.jsonl"
|
|
4950
|
+
printf '{"timestamp":"%s","type":"session_meta","payload":{"id":"%s","session_id":"%s","timestamp":"%s","cwd":"%s","originator":"codex-tui","cli_version":"0.0.0","source":"cli"}}\n' \
|
|
4951
|
+
"$(ts)" "$sid" "$sid" "$(ts)" "$PWD" > "$ro"
|
|
4952
|
+
fi
|
|
4953
|
+
exec 9>>"$ro"
|
|
4954
|
+
trap 'exit 0' TERM
|
|
4955
|
+
trap 'exit 0' HUP
|
|
4956
|
+
if [ "$resumed" = 1 ]; then
|
|
4957
|
+
[ -n "${FAKE_TUI_QUIET:-}" ] \
|
|
4958
|
+
|| printf '{"timestamp":"%s","type":"event_msg","payload":{"type":"task_started","turn_id":"2"}}\n' "$(ts)" >&9
|
|
4959
|
+
else
|
|
4960
|
+
sleep "${FAKE_TUI_DELAY:-0.3}"
|
|
4961
|
+
case "$FAKE_TUI" in
|
|
4962
|
+
quota)
|
|
4963
|
+
printf '{"timestamp":"%s","type":"event_msg","payload":{"type":"token_count","info":null,"rate_limits":{"limit_id":"premium","primary":null,"secondary":null}}}\n' "$(ts)" >&9
|
|
4964
|
+
printf '{"timestamp":"%s","type":"event_msg","payload":{"type":"token_count","info":null,"rate_limits":{"limit_id":"codex","primary":{"used_percent":100.0,"window_minutes":300,"resets_at":%s},"secondary":{"used_percent":40.0,"window_minutes":10080,"resets_at":%s}}}}\n' \
|
|
4965
|
+
"$(ts)" "$(( $(date +%s) + 5400 ))" "$(( $(date +%s) + 500000 ))" >&9
|
|
4966
|
+
printf '{"timestamp":"%s","type":"event_msg","payload":{"type":"task_complete","turn_id":"1","last_agent_message":null,"error":{"message":"You'"'"'ve hit your usage limit. Upgrade to Pro, or try again at Sep 26th, 2026 11:22 AM.","codex_error_info":"usage_limit_exceeded"},"completed_at":%s}}\n' \
|
|
4967
|
+
"$(ts)" "$(date +%s)" >&9 ;;
|
|
4968
|
+
esac
|
|
4969
|
+
fi
|
|
4970
|
+
i=0
|
|
4971
|
+
while [ "$i" -lt "${FAKE_TUI_LIFE:-400}" ]; do sleep 0.1; i=$((i + 1)); done
|
|
4972
|
+
exit 0
|
|
4973
|
+
EOF
|
|
4974
|
+
|
|
4855
4975
|
# Fake "real" codex: prints which CODEX_HOME it ran under; scriptable login +
|
|
4856
4976
|
# failures via a control file. (Note: must not contain the hyphenated shim marker.)
|
|
4857
4977
|
cat > "$FAKEBIN/codex" <<EOF
|
|
@@ -4864,6 +4984,7 @@ if [ "\${1:-}" = "mcp" ] && [ -n "\${CODEX_HOME:-}" ]; then
|
|
|
4864
4984
|
[ "\$rc" = 0 ] && [ -n "\${FAKE_MCP_WRITE_THEN_FAIL:-}" ] && exit 1
|
|
4865
4985
|
exit \$rc
|
|
4866
4986
|
fi
|
|
4987
|
+
if [ -n "\${FAKE_TUI:-}" ] && [ -n "\${CODEX_HOME:-}" ]; then . "$WORK/fake-tui-codex.sh"; fi
|
|
4867
4988
|
if [ "\${1:-}" = "login" ]; then
|
|
4868
4989
|
shift
|
|
4869
4990
|
# record the flags the CLI chose (device-auth default vs --browser opt-out)
|
|
@@ -4899,6 +5020,8 @@ for a in "\$@"; do
|
|
|
4899
5020
|
prev="\$a"
|
|
4900
5021
|
done
|
|
4901
5022
|
[ -n "\$outfile" ] && printf 'OK' > "\$outfile"
|
|
5023
|
+
# what an interactive client would inherit from the shim (auto-resume must leak nothing)
|
|
5024
|
+
[ -n "\${FAKE_PRINT_ENV:-}" ] && echo "AR_ENV=\$(env | grep -c '^CODEX_MULTIACC_AR')"
|
|
4902
5025
|
echo "CFG=\$acct"
|
|
4903
5026
|
EOF
|
|
4904
5027
|
chmod +x "$FAKEBIN/codex"
|
|
@@ -6117,7 +6240,7 @@ printf '%s' "$out" | grep -q "acct-01" \
|
|
|
6117
6240
|
&& t_fail "codex fetch throttle" "fresh account was re-fetched: $out" \
|
|
6118
6241
|
|| t_ok "codex: fresh account skipped by the fetch throttle"
|
|
6119
6242
|
# 429 backoff honored
|
|
6120
|
-
python3 - "$CX/acct-02/limits.json" "$
|
|
6243
|
+
python3 - "$CX/acct-02/limits.json" "$(date +%s)" <<'EOF'
|
|
6121
6244
|
import json, sys
|
|
6122
6245
|
json.dump({'fetched_at': 0, 'retry_after': int(sys.argv[2]) + 300, 'backoff': 120},
|
|
6123
6246
|
open(sys.argv[1], 'w'))
|
|
@@ -7885,6 +8008,808 @@ fi
|
|
|
7885
8008
|
out="$(HOME="$RCH" claude-accounts status 2>&1)"
|
|
7886
8009
|
check "status honours CLAUDE_MULTIACC_PATH_PROBE=0" "probe disabled" "$out"
|
|
7887
8010
|
|
|
8011
|
+
# ============================ AUTO-RESUME (end to end) ============================
|
|
8012
|
+
# An interactive session that stops on a usage limit, a dead login or a crash is resumed
|
|
8013
|
+
# on another account: the shim spawns lib/autoresume.py beside its UNCHANGED exec, the
|
|
8014
|
+
# watcher tails the session's transcript (codex: rollout), stops the TUI and types the
|
|
8015
|
+
# relaunch into the pane's SHELL, and the relaunched shim marks the old account before it
|
|
8016
|
+
# selects. Here the TUI is the fakes' FAKE_TUI mode and tmux is a fake that never talks to
|
|
8017
|
+
# a server: display-message answers from the environment, and Enter runs the typed line
|
|
8018
|
+
# with `bash -c` in the background — the pane's shell, with the pane's environment (none
|
|
8019
|
+
# of the client's CLAUDE_CONFIG_DIR/CODEX_HOME exports). Every case gets its own pool, cut-
|
|
8020
|
+
# down timing knobs and hard timeouts, and ends in ar_reap, which kills what it started.
|
|
8021
|
+
ARB="$WORK/ar"
|
|
8022
|
+
ARH="$WORK/ar-home" # a sandbox HOME: nothing here may touch the operator's
|
|
8023
|
+
ARW="$WORK/ar-work dir" # the session's directory (with a space, on purpose)
|
|
8024
|
+
ARPY="$(command -v python3)"
|
|
8025
|
+
mkdir -p "$ARB" "$ARH" "$ARW"
|
|
8026
|
+
cat > "$FAKEBIN/tmux" <<'EOF'
|
|
8027
|
+
#!/usr/bin/env bash
|
|
8028
|
+
# fake tmux for the auto-resume sections (never a server; shadows any real tmux on PATH)
|
|
8029
|
+
st="${FAKE_TMUX_DIR:-}"
|
|
8030
|
+
[ -n "$st" ] && [ -d "$st" ] || exit 1
|
|
8031
|
+
printf '%s\n' "$*" >> "$st/calls"
|
|
8032
|
+
while [ $# -gt 0 ]; do
|
|
8033
|
+
case "$1" in -S|-L) shift; [ $# -gt 0 ] && shift ;; *) break ;; esac
|
|
8034
|
+
done
|
|
8035
|
+
verb="${1:-}"; [ $# -gt 0 ] && shift
|
|
8036
|
+
case "$verb" in
|
|
8037
|
+
display-message)
|
|
8038
|
+
# -p answers the format the watcher asked for; without -p it is a status-line notice.
|
|
8039
|
+
p=0; fmt=""
|
|
8040
|
+
while [ $# -gt 0 ]; do
|
|
8041
|
+
case "$1" in
|
|
8042
|
+
-p) p=1; shift ;;
|
|
8043
|
+
-t) shift; [ $# -gt 0 ] && shift ;;
|
|
8044
|
+
*) fmt="$1"; shift ;;
|
|
8045
|
+
esac
|
|
8046
|
+
done
|
|
8047
|
+
[ "$p" = 1 ] || exit 0
|
|
8048
|
+
printf '%s\n' "$fmt" | sed -e "s/#{pane_pid}/${FAKE_TMUX_PANE_PID:-1}/g" \
|
|
8049
|
+
-e "s/#{pane_current_command}/${FAKE_TMUX_PANE_CMD:-zsh}/g" \
|
|
8050
|
+
-e "s/#{pane_in_mode}/${FAKE_TMUX_PANE_MODE:-0}/g" \
|
|
8051
|
+
-e "s/#{synchronize-panes}/${FAKE_TMUX_PANE_SYNC:-0}/g" ;;
|
|
8052
|
+
send-keys)
|
|
8053
|
+
lit=0
|
|
8054
|
+
while [ $# -gt 0 ]; do
|
|
8055
|
+
case "$1" in
|
|
8056
|
+
-t) shift; [ $# -gt 0 ] && shift; continue ;;
|
|
8057
|
+
-R) shift; continue ;;
|
|
8058
|
+
-l) lit=1; shift; continue ;;
|
|
8059
|
+
esac
|
|
8060
|
+
if [ "$lit" = 1 ]; then
|
|
8061
|
+
printf '%s' "$1" >> "$st/line"
|
|
8062
|
+
else
|
|
8063
|
+
case "$1" in
|
|
8064
|
+
C-u) : > "$st/line" ;;
|
|
8065
|
+
Enter)
|
|
8066
|
+
line="$(cat "$st/line" 2>/dev/null)"; : > "$st/line"
|
|
8067
|
+
printf '%s\n' "$line" >> "$st/typed"
|
|
8068
|
+
# The trailing `:` keeps bash from exec'ing the command in its own place: like
|
|
8069
|
+
# the pane's real shell, it stays the relaunched shim's parent (its PPID).
|
|
8070
|
+
( unset CLAUDE_CONFIG_DIR CLAUDE_CODE_OAUTH_TOKEN CLAUDE_SHIM_ACTIVE CODEX_HOME CODEX_SHIM_ACTIVE
|
|
8071
|
+
exec bash -c "$line; :" ) </dev/null >> "$st/shell.out" 2>&1 & ;;
|
|
8072
|
+
esac
|
|
8073
|
+
fi
|
|
8074
|
+
shift
|
|
8075
|
+
done ;;
|
|
8076
|
+
esac
|
|
8077
|
+
exit 0
|
|
8078
|
+
EOF
|
|
8079
|
+
chmod +x "$FAKEBIN/tmux"
|
|
8080
|
+
# Records a spawn instead of watching: the gate cases need a watcher that leaves its state
|
|
8081
|
+
# file in place, so "no state file" can only mean "no spawn".
|
|
8082
|
+
cat > "$ARB/stub-python" <<'EOF'
|
|
8083
|
+
#!/bin/sh
|
|
8084
|
+
echo "$*" >> "${FAKE_STUB_PY_LOG:-/dev/null}"
|
|
8085
|
+
EOF
|
|
8086
|
+
chmod +x "$ARB/stub-python"
|
|
8087
|
+
|
|
8088
|
+
ar_wait() { # ar_wait <seconds> <cmd...>: poll every 0.1 s until cmd succeeds; 1 on timeout
|
|
8089
|
+
local n=0 lim=$(($1 * 10))
|
|
8090
|
+
shift
|
|
8091
|
+
while ! "$@"; do
|
|
8092
|
+
n=$((n + 1)); [ "$n" -lt "$lim" ] || return 1
|
|
8093
|
+
sleep 0.1
|
|
8094
|
+
done
|
|
8095
|
+
return 0
|
|
8096
|
+
}
|
|
8097
|
+
ar_lines() { [ -f "$2" ] && [ "$(($(wc -l < "$2")))" -ge "$1" ]; } # <n> <file>
|
|
8098
|
+
ar_logged() { grep -Eq "$2" "$1/selection.log" 2>/dev/null; } # <pool> <ERE>
|
|
8099
|
+
ar_no_watcher() { ! pgrep -f "$1/tmp/autoresume/" >/dev/null 2>&1; } # <pool>
|
|
8100
|
+
ar_acct_of() { printf '%s' "$1" | sed -n 's/^CFG=\(acct-[0-9]*\) .*/\1/p'; }
|
|
8101
|
+
# A stopped client may linger as a zombie until its parent reaps it: that is gone, not alive.
|
|
8102
|
+
ar_running() { kill -0 "$1" 2>/dev/null && ! ps -o stat= -p "$1" 2>/dev/null | grep -q '^Z'; }
|
|
8103
|
+
ar_stopped() { ! ar_running "$1"; }
|
|
8104
|
+
ar_start() { # ar_start [VAR=val ...] -- <cmd> [args...]: the launch typed in the pane, backgrounded
|
|
8105
|
+
local pre=()
|
|
8106
|
+
while [ $# -gt 0 ] && [ "$1" != "--" ]; do pre+=("$1"); shift; done
|
|
8107
|
+
[ $# -gt 0 ] && shift
|
|
8108
|
+
( cd "$ARW" && exec env "${AREC[@]}" ${pre[@]+"${pre[@]}"} "$@" ) </dev/null >> "$ARB/tui.out" 2>&1 &
|
|
8109
|
+
AR_PID=$!
|
|
8110
|
+
# Not a job of this shell: the crash case SIGKILLs it, and bash would print a job notice.
|
|
8111
|
+
# bash still reaps it (waitpid(-1) on SIGCHLD); ar_reap kills it by pid.
|
|
8112
|
+
disown "$AR_PID" 2>/dev/null || true
|
|
8113
|
+
}
|
|
8114
|
+
ar_reap() { # ar_reap <pool>: stop every client and watcher the case started, then prove it
|
|
8115
|
+
local p n=0
|
|
8116
|
+
: > "$1/autoresume.off" 2>/dev/null || true # running watchers leave on their next tick
|
|
8117
|
+
if [ -f "$ARB/pids" ]; then
|
|
8118
|
+
while read -r p; do kill -TERM "$p" 2>/dev/null; done < "$ARB/pids"
|
|
8119
|
+
fi
|
|
8120
|
+
while ! ar_no_watcher "$1" && [ "$n" -lt 50 ]; do sleep 0.1; n=$((n + 1)); done
|
|
8121
|
+
pkill -KILL -f "$1/tmp/autoresume/" 2>/dev/null || true
|
|
8122
|
+
if [ -f "$ARB/pids" ]; then
|
|
8123
|
+
while read -r p; do kill -KILL "$p" 2>/dev/null; done < "$ARB/pids"
|
|
8124
|
+
fi
|
|
8125
|
+
[ -n "${AR_PID:-}" ] && kill -KILL "$AR_PID" 2>/dev/null
|
|
8126
|
+
AR_PID=""
|
|
8127
|
+
}
|
|
8128
|
+
AR_SID=5f0c1d2e-3a4b-4c5d-8e9f-a0b1c2d3e4f5 # the fake claude's default session id
|
|
8129
|
+
AR_CXSID=6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d # the fake codex's
|
|
8130
|
+
ar_typed_ok() { # ar_typed_ok <CLAUDE|CODEX> <sid> <ROOTVAR=root or ""> <shim>: exactly one line, exactly this
|
|
8131
|
+
local line tok
|
|
8132
|
+
[ -f "$ARB/tmux/typed" ] && [ "$(($(wc -l < "$ARB/tmux/typed")))" = 1 ] || return 1
|
|
8133
|
+
line="$(cat "$ARB/tmux/typed")"
|
|
8134
|
+
tok="${line#" $1_MULTIACC_AR="}"; tok="${tok%%:*}"
|
|
8135
|
+
printf '%s\n' "$tok" | grep -Eqx '[A-Za-z0-9]{16,64}' || return 1
|
|
8136
|
+
[ "$line" = " $1_MULTIACC_AR=$tok:$2 ${3:+$3 }$4" ]
|
|
8137
|
+
}
|
|
8138
|
+
ar_pty() { # ar_pty <both|stdin> <cmd...>: stdin on a pty; stdout on that pty too, or on a pipe
|
|
8139
|
+
python3 -c '
|
|
8140
|
+
import pty, subprocess, sys
|
|
8141
|
+
m, s = pty.openpty()
|
|
8142
|
+
r = subprocess.run(sys.argv[2:], stdin=s, stdout=s if sys.argv[1] == "both" else subprocess.PIPE)
|
|
8143
|
+
if r.stdout:
|
|
8144
|
+
sys.stdout.buffer.write(r.stdout)
|
|
8145
|
+
sys.exit(r.returncode)' "$@"
|
|
8146
|
+
}
|
|
8147
|
+
ar_plant() { # ar_plant <pool> <token> <provider> <class> <left acct> -- <argv...>: a watcher's relaunch files
|
|
8148
|
+
local pool="$1" tok="$2" prov="$3" cls="$4" acct="$5" sid="$AR_SID"
|
|
8149
|
+
shift 5; [ "${1:-}" = "--" ] && shift
|
|
8150
|
+
[ "$prov" = codex ] && sid="$AR_CXSID"
|
|
8151
|
+
mkdir -p "$pool/tmp/autoresume"
|
|
8152
|
+
printf '%s\n' v=1 "provider=$prov" "class=$cls" "acct=$acct" "sid=$sid" "cwd=$ARW" depth=1 chain=PlantTst \
|
|
8153
|
+
> "$pool/tmp/autoresume/r-$tok.relaunch"
|
|
8154
|
+
printf '%s\0' "$@" > "$pool/tmp/autoresume/r-$tok.argv"
|
|
8155
|
+
}
|
|
8156
|
+
# A typed relaunch whose token cannot be honoured names the session and exits 2: the line
|
|
8157
|
+
# has no argv of its own, so running on would open a FRESH session in the stopped one's place.
|
|
8158
|
+
ar_dead() { # ar_dead <claude|codex> <name> <expected output> [VAR=val ...]
|
|
8159
|
+
local prov="$1" name="$2" want="$3" out rc
|
|
8160
|
+
shift 3
|
|
8161
|
+
if [ "$prov" = claude ]; then
|
|
8162
|
+
out="$(cd "$ARH" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" "$@" claude </dev/null 2>&1)"; rc=$?
|
|
8163
|
+
else
|
|
8164
|
+
out="$(cd "$ARH" && env "${AREX[@]}" CODEX_MULTIACC_PYTHON="$ARB/stub-python" "$@" codex </dev/null 2>&1)"; rc=$?
|
|
8165
|
+
fi
|
|
8166
|
+
[ "$rc" = 2 ] && [ "$out" = "$want" ] \
|
|
8167
|
+
&& t_ok "AR dead token ($prov): $name -> the resume hint, exit 2, nothing started" \
|
|
8168
|
+
|| t_fail "AR dead token ($prov): $name" "rc=$rc out=$out"
|
|
8169
|
+
}
|
|
8170
|
+
AR_DEAD_MSG="auto-resume could not continue automatically (the resume token expired)."
|
|
8171
|
+
|
|
8172
|
+
# ---- AR1. claude: fixture pool, environment, and the gate --------------------------------
|
|
8173
|
+
ARC="$WORK/ar-claude"
|
|
8174
|
+
ar_claude_pool() { # a fresh two-account pool whose accounts share one projects tree
|
|
8175
|
+
local i
|
|
8176
|
+
rm -rf "$ARC" "$ARB/tmux" "$ARB/pids" "$ARB/tui.log" "$ARB/stub.log"
|
|
8177
|
+
mkdir -p "$ARC/tmp" "$ARC/shared-projects" "$ARB/tmux"
|
|
8178
|
+
printf '{"version": 1, "threshold": 90, "accounts": [%s, %s]}\n' \
|
|
8179
|
+
'{"id": "acct-01", "email": "a@ar", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8180
|
+
'{"id": "acct-02", "email": "b@ar", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8181
|
+
> "$ARC/accounts.json"
|
|
8182
|
+
for i in 01 02; do
|
|
8183
|
+
mkdir -p "$ARC/acct-$i"
|
|
8184
|
+
printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-ar%s","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999,"scopes":["user:inference"],"subscriptionType":"max"}}' "$i" \
|
|
8185
|
+
> "$ARC/acct-$i/.credentials.json"
|
|
8186
|
+
printf '{"fetched_at":%s,"weekly_percent":20,"session_percent":10,"max_percent":20,"buckets":[]}' "$(date +%s)" \
|
|
8187
|
+
> "$ARC/acct-$i/limits.json"
|
|
8188
|
+
ln -s "$ARC/shared-projects" "$ARC/acct-$i/projects"
|
|
8189
|
+
done
|
|
8190
|
+
: > "$ARC/.limits-kick"
|
|
8191
|
+
}
|
|
8192
|
+
AREC=(CLAUDE_ACCOUNTS_DIR="$ARC" HOME="$ARH" TMUX="$ARB/tmux-sock,4242,0" TMUX_PANE=%1
|
|
8193
|
+
CLAUDE_MULTIACC_AUTORESUME=1 CLAUDE_MULTIACC_PYTHON="$ARPY" CLAUDE_MULTIACC_AR_TEST_TTY=1
|
|
8194
|
+
CLAUDE_MULTIACC_AR_TEST_TMUX_ANYPANE=1 CLAUDE_MULTIACC_AR_POLL=0.1 CLAUDE_MULTIACC_AR_GRACE=0.5
|
|
8195
|
+
CLAUDE_MULTIACC_AR_TERM_GRACE=3 CLAUDE_MULTIACC_AR_PANE_WAIT=5 CLAUDE_MULTIACC_AR_PROBE_TIMEOUT=15
|
|
8196
|
+
CLAUDE_MULTIACC_AR_CRASH_MIN_RUNTIME=0 CLAUDE_MULTIACC_AUTORESUME_DEBUG=1
|
|
8197
|
+
FAKE_TMUX_DIR="$ARB/tmux" FAKE_TUI_LOG="$ARB/tui.log" FAKE_TUI_PIDS="$ARB/pids")
|
|
8198
|
+
|
|
8199
|
+
# The gate: only the plain interactive exec in tmux, with an argv the relaunch can rebuild,
|
|
8200
|
+
# gets a watcher; everything else is exactly today's exec (same argv, no state file).
|
|
8201
|
+
ar_claude_pool
|
|
8202
|
+
ar_gate() { # ar_gate <name> <watcher|none> [VAR=val ...] -- <claude args...>
|
|
8203
|
+
local name="$1" want="$2" pre=() out states
|
|
8204
|
+
shift 2
|
|
8205
|
+
while [ $# -gt 0 ] && [ "$1" != "--" ]; do pre+=("$1"); shift; done
|
|
8206
|
+
[ $# -gt 0 ] && shift
|
|
8207
|
+
rm -rf "$ARC/tmp/autoresume" "$ARB/stub.log"
|
|
8208
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" \
|
|
8209
|
+
FAKE_STUB_PY_LOG="$ARB/stub.log" ${pre[@]+"${pre[@]}"} claude "$@" </dev/null 2>&1)"
|
|
8210
|
+
states="$(ls "$ARC/tmp/autoresume/" 2>/dev/null | grep -c '\.state$')"
|
|
8211
|
+
case "$out" in
|
|
8212
|
+
*"ARGS=$*") ;;
|
|
8213
|
+
*) t_fail "auto-resume gate: $name execs the argv unchanged" "$out"; return ;;
|
|
8214
|
+
esac
|
|
8215
|
+
if [ "$want" = watcher ]; then
|
|
8216
|
+
if [ "$states" = 1 ] && ar_wait 5 grep -qs -- "-I .*lib/autoresume.py watch --state $ARC/tmp/autoresume/[0-9]*\.state" "$ARB/stub.log"; then
|
|
8217
|
+
t_ok "auto-resume gate: $name gets a watcher, exec unchanged"
|
|
8218
|
+
else
|
|
8219
|
+
t_fail "auto-resume gate: $name gets a watcher" "states=$states stub=$(cat "$ARB/stub.log" 2>/dev/null)"
|
|
8220
|
+
fi
|
|
8221
|
+
else
|
|
8222
|
+
[ "$states" = 0 ] && [ ! -s "$ARB/stub.log" ] \
|
|
8223
|
+
&& t_ok "auto-resume gate: $name gets no watcher, exec unchanged" \
|
|
8224
|
+
|| t_fail "auto-resume gate: $name gets no watcher" "states=$states"
|
|
8225
|
+
fi
|
|
8226
|
+
}
|
|
8227
|
+
ar_gate "a prompt with the permission flag" watcher -- --dangerously-skip-permissions "fix the bug please"
|
|
8228
|
+
ar_gate "--resume <uuid> with a model" watcher -- --model opus --resume "$AR_SID"
|
|
8229
|
+
ar_gate "-p" none -- -p "hi there"
|
|
8230
|
+
ar_gate "a pinned run" none CLAUDE_ACCOUNT=acct-01 -- --dangerously-skip-permissions
|
|
8231
|
+
ar_gate "outside tmux" none TMUX= -- --dangerously-skip-permissions
|
|
8232
|
+
ar_gate "piped stdio" none CLAUDE_MULTIACC_AR_TEST_TTY= -- --dangerously-skip-permissions
|
|
8233
|
+
ar_gate "an unknown flag" none -- --fork-session --dangerously-skip-permissions
|
|
8234
|
+
ar_gate "a single-word positional (maybe a subcommand)" none -- doctor
|
|
8235
|
+
ar_gate "CLAUDE_MULTIACC_AUTORESUME=0" none CLAUDE_MULTIACC_AUTORESUME=0 -- --dangerously-skip-permissions
|
|
8236
|
+
: > "$ARC/autoresume.off"
|
|
8237
|
+
ar_gate "the pool's autoresume.off" none -- --dangerously-skip-permissions
|
|
8238
|
+
rm -f "$ARC/autoresume.off"
|
|
8239
|
+
# the watcher's argv is the ORIGINAL one, before the fallback-model pin adds --model
|
|
8240
|
+
ar_gate "no argv at all" watcher --
|
|
8241
|
+
af="$(ls "$ARC/tmp/autoresume/"*.argv 2>/dev/null | head -1)"
|
|
8242
|
+
[ -n "$af" ] && [ ! -s "$af" ] \
|
|
8243
|
+
&& t_ok "auto-resume: a bare launch records an empty original argv" \
|
|
8244
|
+
|| t_fail "auto-resume: a bare launch records an empty original argv" "$af"
|
|
8245
|
+
# ...and it stays empty when the pin DOES fire: every account's Fable bucket is spent, so
|
|
8246
|
+
# the exec carries --model, but the watcher must rebuild from what the user typed (the
|
|
8247
|
+
# relaunch re-runs selection and pins again for the NEW pick).
|
|
8248
|
+
for i in 01 02; do
|
|
8249
|
+
printf '%s\nbucket=weekly_scoped:Fable percent=100 marked_at=x reason=limits\n' "$(( $(date +%s) + 3600 ))" \
|
|
8250
|
+
> "$ARC/acct-$i/.limited"
|
|
8251
|
+
printf '{"fetched_at":%s,"max_percent":100,"weekly_percent":20,"session_percent":10,"buckets":[{"name":"session","percent":10},{"name":"weekly_all","percent":20},{"name":"weekly_scoped:Fable","percent":100}]}' \
|
|
8252
|
+
"$(date +%s)" > "$ARC/acct-$i/limits.json"
|
|
8253
|
+
done
|
|
8254
|
+
rm -rf "$ARC/tmp/autoresume" "$ARB/stub.log"
|
|
8255
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" FAKE_STUB_PY_LOG="$ARB/stub.log" \
|
|
8256
|
+
claude </dev/null 2>&1)"
|
|
8257
|
+
af="$(ls "$ARC/tmp/autoresume/"*.argv 2>/dev/null | head -1)"
|
|
8258
|
+
case "$out" in
|
|
8259
|
+
*"ARGS=--model claude-opus-5")
|
|
8260
|
+
[ -n "$af" ] && [ -f "$af" ] && [ ! -s "$af" ] \
|
|
8261
|
+
&& t_ok "auto-resume: the watcher gets the argv from BEFORE the fallback-model pin" \
|
|
8262
|
+
|| t_fail "auto-resume: the .argv file carries the pin" "$(tr '\0' ' ' < "$af" 2>/dev/null)" ;;
|
|
8263
|
+
*) t_fail "auto-resume: the fallback-model pin fired" "$out" ;;
|
|
8264
|
+
esac
|
|
8265
|
+
ar_claude_pool
|
|
8266
|
+
ar_gate "an invalid pin (random fallback)" none CLAUDE_ACCOUNT=acct-99 -- --dangerously-skip-permissions
|
|
8267
|
+
# The relaunch line is typed into the shell unquoted: a pool root or a shim path that would
|
|
8268
|
+
# need quoting gets no watcher, and the launch itself is unchanged.
|
|
8269
|
+
ln -s "$ARC" "$WORK/ar claude root"
|
|
8270
|
+
ar_gate "a pool root that would need quoting" none CLAUDE_ACCOUNTS_DIR="$WORK/ar claude root" -- --dangerously-skip-permissions
|
|
8271
|
+
mkdir -p "$WORK/ar shim copy"
|
|
8272
|
+
cp -R "$REPO_DIR/bin" "$REPO_DIR/lib" "$WORK/ar shim copy/"
|
|
8273
|
+
rm -rf "$ARC/tmp/autoresume" "$ARB/stub.log"
|
|
8274
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" FAKE_STUB_PY_LOG="$ARB/stub.log" \
|
|
8275
|
+
"$WORK/ar shim copy/bin/claude" --dangerously-skip-permissions </dev/null 2>&1)"
|
|
8276
|
+
case "$out" in
|
|
8277
|
+
*"ARGS=--dangerously-skip-permissions")
|
|
8278
|
+
[ -z "$(ls "$ARC/tmp/autoresume/" 2>/dev/null | grep '\.state$')" ] && [ ! -s "$ARB/stub.log" ] \
|
|
8279
|
+
&& t_ok "auto-resume gate: a shim path that would need quoting gets no watcher, exec unchanged" \
|
|
8280
|
+
|| t_fail "auto-resume gate: a shim path that would need quoting gets no watcher" "$(ls "$ARC/tmp/autoresume/" 2>/dev/null)" ;;
|
|
8281
|
+
*) t_fail "auto-resume gate: a shim path that would need quoting execs the argv unchanged" "$out" ;;
|
|
8282
|
+
esac
|
|
8283
|
+
# stdin on a terminal but stdout piped (`claude "x y" | tee log`): no watcher. The same
|
|
8284
|
+
# launch with stdout on the terminal too does get one, so the pipe is what decided it.
|
|
8285
|
+
for mode in stdin both; do
|
|
8286
|
+
rm -rf "$ARC/tmp/autoresume" "$ARB/stub.log"
|
|
8287
|
+
out="$(cd "$ARW" && ar_pty "$mode" env "${AREC[@]}" CLAUDE_MULTIACC_AR_TEST_TTY= \
|
|
8288
|
+
CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" FAKE_STUB_PY_LOG="$ARB/stub.log" \
|
|
8289
|
+
claude --dangerously-skip-permissions 2>&1)"
|
|
8290
|
+
states="$(ls "$ARC/tmp/autoresume/" 2>/dev/null | grep -c '\.state$')"
|
|
8291
|
+
if [ "$mode" = stdin ]; then
|
|
8292
|
+
case "$out" in *"ARGS=--dangerously-skip-permissions") ;; *) states="exec:$out" ;; esac
|
|
8293
|
+
[ "$states" = 0 ] && [ ! -s "$ARB/stub.log" ] \
|
|
8294
|
+
&& t_ok "auto-resume gate: a terminal on stdin with stdout piped gets no watcher, exec unchanged" \
|
|
8295
|
+
|| t_fail "auto-resume gate: stdout piped gets no watcher" "states=$states"
|
|
8296
|
+
else
|
|
8297
|
+
[ "$states" = 1 ] && t_ok "auto-resume gate: stdin and stdout on a terminal get a watcher (control)" \
|
|
8298
|
+
|| t_fail "auto-resume gate: both on a terminal get a watcher" "states=$states"
|
|
8299
|
+
fi
|
|
8300
|
+
done
|
|
8301
|
+
# The client never sees an auto-resume variable (a nested claude must not inherit a probe).
|
|
8302
|
+
rm -rf "$ARC/tmp/autoresume"
|
|
8303
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" \
|
|
8304
|
+
CLAUDE_MULTIACC_AR_AVOID=acct-01:9999999999 FAKE_PRINT_ENV=1 \
|
|
8305
|
+
claude --dangerously-skip-permissions </dev/null 2>&1)"
|
|
8306
|
+
check "auto-resume: the client never sees an auto-resume variable" "AR_ENV=0" "$out"
|
|
8307
|
+
st="$(ls "$ARC/tmp/autoresume/"*.state 2>/dev/null | head -1)"
|
|
8308
|
+
grep -q "^pid=[0-9]" "$st" 2>/dev/null && grep -q "^tmux=$ARB/tmux-sock,4242,0\$" "$st" \
|
|
8309
|
+
&& grep -q '^pane=%1$' "$st" && grep -q "^acc_root=$ARC\$" "$st" && grep -q '^depth=0$' "$st" \
|
|
8310
|
+
&& t_ok "auto-resume: the state file names the pid, pane, pool and chain depth" \
|
|
8311
|
+
|| t_fail "auto-resume state file" "$(cat "$st" 2>/dev/null)"
|
|
8312
|
+
|
|
8313
|
+
# ---- AR2. claude probe mode: the real candidate loop, never an exec ----------------------
|
|
8314
|
+
ar_claude_pool
|
|
8315
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_AR_PROBE=1 claude --dangerously-skip-permissions </dev/null 2>&1)"; rc=$?
|
|
8316
|
+
case "$rc:$out" in
|
|
8317
|
+
0:pick=acct-0[12]" tier=eligible") t_ok "probe prints pick/tier and exits 0" ;;
|
|
8318
|
+
*) t_fail "probe prints pick/tier" "rc=$rc out=$out" ;;
|
|
8319
|
+
esac
|
|
8320
|
+
case "$out" in *CFG=*) t_fail "probe never execs the client" "$out" ;; *) t_ok "probe never execs the client" ;; esac
|
|
8321
|
+
all2=1
|
|
8322
|
+
for _ in 1 2 3 4 5; do
|
|
8323
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_AR_PROBE=1 \
|
|
8324
|
+
CLAUDE_MULTIACC_AR_AVOID="acct-01:$(( $(date +%s) + 3600 )),acct-02:$(( $(date +%s) - 5 ))" \
|
|
8325
|
+
claude --dangerously-skip-permissions </dev/null 2>&1)"
|
|
8326
|
+
[ "$out" = "pick=acct-02 tier=eligible" ] || all2=0
|
|
8327
|
+
done
|
|
8328
|
+
[ "$all2" = 1 ] && t_ok "probe honours AVOID (an expired entry is ignored)" \
|
|
8329
|
+
|| t_fail "probe AVOID" "$out"
|
|
8330
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_AR_PROBE=1 \
|
|
8331
|
+
CLAUDE_MULTIACC_AR_AVOID="acct-01:$(( $(date +%s) + 3600 )),acct-02:$(( $(date +%s) + 3600 ))" \
|
|
8332
|
+
claude --dangerously-skip-permissions </dev/null 2>&1)"
|
|
8333
|
+
case "$out" in "pick=acct-0"[12]" tier=soft") t_ok "probe with every account avoided answers tier=soft" ;;
|
|
8334
|
+
*) t_fail "probe tier=soft" "$out" ;; esac
|
|
8335
|
+
ar_logged "$ARC" 'all-limited fallback' \
|
|
8336
|
+
&& t_fail "a probe logs no fallback pick" "$(grep 'all-limited' "$ARC/selection.log")" \
|
|
8337
|
+
|| t_ok "a probe logs no fallback pick"
|
|
8338
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_AR_PROBE=1 CLAUDE_MULTIACC_DISABLE=1 claude </dev/null 2>&1)"; rc=$?
|
|
8339
|
+
[ "$rc:$out" = "3:pick= tier=none" ] && t_ok "probe on a passthrough answers tier=none (exit 3)" \
|
|
8340
|
+
|| t_fail "probe passthrough" "rc=$rc out=$out"
|
|
8341
|
+
[ ! -f "$ARC/.last-pick" ] && t_ok "a probe does not count as a pick (no rotation memory)" \
|
|
8342
|
+
|| t_fail "a probe does not count as a pick" "$(cat "$ARC/.last-pick")"
|
|
8343
|
+
# No valid login anywhere: a launch would fall back to the stock client — a probe must not.
|
|
8344
|
+
rm -f "$ARC"/acct-0*/.credentials.json
|
|
8345
|
+
out="$(cd "$ARW" && env "${AREC[@]}" CLAUDE_MULTIACC_AR_PROBE=1 claude --dangerously-skip-permissions </dev/null 2>&1)"; rc=$?
|
|
8346
|
+
[ "$rc:$out" = "3:pick= tier=none" ] && t_ok "probe on a pool with no valid login answers tier=none (exit 3), never execs" \
|
|
8347
|
+
|| t_fail "probe with no valid login" "rc=$rc out=$out"
|
|
8348
|
+
|
|
8349
|
+
# ---- AR3. claude quota: stop, relaunch on the other account, same session ----------------
|
|
8350
|
+
# No ANYPANE here: the pane's pid is this shell, which IS the launching shim's parent. The
|
|
8351
|
+
# transcript scan (12f) is off, so the old account's marker can only come from the relaunch.
|
|
8352
|
+
ar_claude_pool
|
|
8353
|
+
ar_start FAKE_TUI=quota CLAUDE_MULTIACC_AR_TEST_TMUX_ANYPANE= FAKE_TMUX_PANE_PID=$$ \
|
|
8354
|
+
CLAUDE_MULTIACC_CLIENT_LIMITS=0 -- claude --dangerously-skip-permissions "fix the bug please"
|
|
8355
|
+
first=$AR_PID
|
|
8356
|
+
if ar_wait 30 ar_lines 2 "$ARB/tui.log"; then
|
|
8357
|
+
l1="$(sed -n 1p "$ARB/tui.log")"; l2="$(sed -n 2p "$ARB/tui.log")"
|
|
8358
|
+
from="$(ar_acct_of "$l1")"; to="$(ar_acct_of "$l2")"
|
|
8359
|
+
check "AR quota: the first launch ran the user's argv" \
|
|
8360
|
+
"ARGS=--dangerously-skip-permissions fix the bug please" "$l1"
|
|
8361
|
+
[ -n "$from" ] && [ -n "$to" ] && [ "$from" != "$to" ] \
|
|
8362
|
+
&& t_ok "AR quota: relaunched on the OTHER account ($from -> $to)" \
|
|
8363
|
+
|| t_fail "AR quota: relaunched on the other account" "$l1 | $l2"
|
|
8364
|
+
check "AR quota: the relaunch resumes the same session with the auto-resume prompt" \
|
|
8365
|
+
"ARGS=--dangerously-skip-permissions --resume $AR_SID (claude-multiacc auto-resume) This session was restarted automatically on another account because the previous account hit its usage limit." "$l2"
|
|
8366
|
+
case "$l2" in *"fix the bug please"*) t_fail "AR quota: the original prompt is not replayed" "$l2" ;;
|
|
8367
|
+
*) t_ok "AR quota: the original prompt is not replayed" ;; esac
|
|
8368
|
+
ar_wait 5 ar_stopped "$first" && t_ok "AR quota: the stuck client was stopped" \
|
|
8369
|
+
|| t_fail "AR quota: the stuck client was stopped" "pid $first alive"
|
|
8370
|
+
grep -q 'bucket=client:five_hour percent=100' "$ARC/$from/.limited" 2>/dev/null \
|
|
8371
|
+
&& t_ok "AR quota: the old account carries a client:five_hour marker" \
|
|
8372
|
+
|| t_fail "AR quota: marker on the old account" "$(cat "$ARC/$from/.limited" 2>/dev/null)"
|
|
8373
|
+
ar_logged "$ARC" "autoresume switch provider=claude chain=[A-Za-z0-9]+ from=$from class=quota sid=$AR_SID depth=1 probe=$to" \
|
|
8374
|
+
&& t_ok "AR quota: the watcher logged the switch (field 2 = autoresume)" \
|
|
8375
|
+
|| t_fail "AR quota: switch log line" "$(grep autoresume "$ARC/selection.log")"
|
|
8376
|
+
chain="$(sed -n 's/.*autoresume watch provider=claude chain=\([A-Za-z0-9]*\) .*/\1/p' "$ARC/selection.log" | head -1)"
|
|
8377
|
+
ar_logged "$ARC" "autoresume relaunch chain=$chain from=$from class=quota depth=1\$" \
|
|
8378
|
+
&& t_ok "AR quota: the relaunched shim consumed the token and kept the chain" \
|
|
8379
|
+
|| t_fail "AR quota: relaunch log line" "$(grep autoresume "$ARC/selection.log")"
|
|
8380
|
+
ar_typed_ok CLAUDE "$AR_SID" "CLAUDE_ACCOUNTS_ROOT=$(cd "$ARC" && pwd)" "$REPO_DIR/bin/claude" \
|
|
8381
|
+
&& t_ok "AR quota: exactly one token line was typed: token:sid, the pool root, the shim's path" \
|
|
8382
|
+
|| t_fail "AR quota: typed line" "$(cat "$ARB/tmux/typed" 2>/dev/null)"
|
|
8383
|
+
seq_ok="$(grep -E 'display-message|send-keys' "$ARB/tmux/calls" | sed 's/ -l .*/ -l <line>/' | tr '\n' '|')"
|
|
8384
|
+
case "$seq_ok" in
|
|
8385
|
+
*"-S $ARB/tmux-sock display-message -p -t %1 #{pane_pid}"*"-S $ARB/tmux-sock send-keys -R -t %1|-S $ARB/tmux-sock send-keys -t %1 C-u|-S $ARB/tmux-sock send-keys -t %1 -l <line>|-S $ARB/tmux-sock send-keys -t %1 Enter|")
|
|
8386
|
+
t_ok "AR quota: pane check, then -R, C-u, the line, Enter — on the launching pane" ;;
|
|
8387
|
+
*) t_fail "AR quota: tmux sequence" "$seq_ok" ;;
|
|
8388
|
+
esac
|
|
8389
|
+
ls "$ARC/tmp/autoresume/" 2>/dev/null | grep -q '^r-' \
|
|
8390
|
+
&& t_fail "AR quota: the relaunch token is single use" "$(ls "$ARC/tmp/autoresume/")" \
|
|
8391
|
+
|| t_ok "AR quota: the relaunch token is single use (files consumed)"
|
|
8392
|
+
ar_wait 10 ar_logged "$ARC" "autoresume watch provider=claude chain=$chain pid=[0-9]+ acct=$to depth=1\$" \
|
|
8393
|
+
&& t_ok "AR quota: the relaunched session is supervised again (same chain, depth 1)" \
|
|
8394
|
+
|| t_fail "AR quota: second watcher" "$(grep autoresume "$ARC/selection.log")"
|
|
8395
|
+
else
|
|
8396
|
+
t_fail "AR quota: relaunched within 30 s" "tui=$(cat "$ARB/tui.log" 2>/dev/null) log=$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8397
|
+
fi
|
|
8398
|
+
ar_reap "$ARC"
|
|
8399
|
+
ar_no_watcher "$ARC" && t_ok "AR quota: no watcher left running" || t_fail "AR quota: watcher left running"
|
|
8400
|
+
|
|
8401
|
+
# ---- AR4. claude: a pane that is not the launching shell is never typed into -----------
|
|
8402
|
+
ar_claude_pool
|
|
8403
|
+
ar_start FAKE_TUI=quota CLAUDE_MULTIACC_AR_TEST_TMUX_ANYPANE= FAKE_TMUX_PANE_PID=1 \
|
|
8404
|
+
-- claude --dangerously-skip-permissions "fix the bug please"
|
|
8405
|
+
if ar_wait 20 ar_logged "$ARC" 'autoresume giveup provider=claude .*reason=pane'; then
|
|
8406
|
+
sleep 0.3
|
|
8407
|
+
ar_running "$AR_PID" && [ ! -s "$ARB/tmux/typed" ] \
|
|
8408
|
+
&& ! ls "$ARC/tmp/autoresume/" 2>/dev/null | grep -q '^r-' \
|
|
8409
|
+
&& t_ok "AR pane: another pane's process -> give up before stopping anything" \
|
|
8410
|
+
|| t_fail "AR pane: nothing stopped, nothing typed" "$(cat "$ARB/tmux/typed" 2>/dev/null)"
|
|
8411
|
+
else
|
|
8412
|
+
t_fail "AR pane: giveup logged" "$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8413
|
+
fi
|
|
8414
|
+
ar_reap "$ARC"
|
|
8415
|
+
|
|
8416
|
+
# ---- AR5. claude auth: the old login is parked, the session moves ------------------------
|
|
8417
|
+
ar_claude_pool
|
|
8418
|
+
ar_start FAKE_TUI=auth -- claude --dangerously-skip-permissions "fix the bug please"
|
|
8419
|
+
if ar_wait 30 ar_lines 2 "$ARB/tui.log"; then
|
|
8420
|
+
l1="$(sed -n 1p "$ARB/tui.log")"; l2="$(sed -n 2p "$ARB/tui.log")"
|
|
8421
|
+
from="$(ar_acct_of "$l1")"; to="$(ar_acct_of "$l2")"
|
|
8422
|
+
[ -n "$from" ] && [ "$from" != "$to" ] && [ -f "$ARC/$from/.expired" ] \
|
|
8423
|
+
&& t_ok "AR auth: the old account is parked (.expired) and the session moved ($from -> $to)" \
|
|
8424
|
+
|| t_fail "AR auth: park + rotate" "$l1 | $l2 | $(ls -a "$ARC/$from" 2>/dev/null | tr '\n' ' ')"
|
|
8425
|
+
check "AR auth: resumed with the login reason" \
|
|
8426
|
+
"--resume $AR_SID (claude-multiacc auto-resume) This session was restarted automatically on another account because the previous account's login failed." "$l2"
|
|
8427
|
+
else
|
|
8428
|
+
t_fail "AR auth: relaunched within 30 s" "$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8429
|
+
fi
|
|
8430
|
+
ar_reap "$ARC"
|
|
8431
|
+
|
|
8432
|
+
# ---- AR6. claude crash: the registry left behind means a crash; relaunch in place -------
|
|
8433
|
+
ar_claude_pool
|
|
8434
|
+
ar_start FAKE_TUI=crash -- claude --dangerously-skip-permissions "fix the bug please"
|
|
8435
|
+
if ar_wait 30 ar_lines 2 "$ARB/tui.log"; then
|
|
8436
|
+
l2="$(sed -n 2p "$ARB/tui.log")"
|
|
8437
|
+
check "AR crash: resumed with the crash reason, no 'another account'" \
|
|
8438
|
+
"--resume $AR_SID (claude-multiacc auto-resume) This session was restarted automatically because the previous process exited unexpectedly." "$l2"
|
|
8439
|
+
ar_logged "$ARC" 'autoresume crash provider=claude .* runtime=[0-9]+' \
|
|
8440
|
+
&& ar_logged "$ARC" 'autoresume relaunch chain=[A-Za-z0-9]+ from=acct-0[12] class=crash depth=1$' \
|
|
8441
|
+
&& t_ok "AR crash: logged as a crash and relaunched (no marker class)" \
|
|
8442
|
+
|| t_fail "AR crash: log lines" "$(grep autoresume "$ARC/selection.log")"
|
|
8443
|
+
ls "$ARC"/acct-0*/.limited >/dev/null 2>&1 \
|
|
8444
|
+
&& t_fail "AR crash: no account is marked" "$(ls "$ARC"/acct-0*/.limited)" \
|
|
8445
|
+
|| t_ok "AR crash: no account is marked"
|
|
8446
|
+
else
|
|
8447
|
+
t_fail "AR crash: relaunched within 30 s" "$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8448
|
+
fi
|
|
8449
|
+
ar_reap "$ARC"
|
|
8450
|
+
|
|
8451
|
+
# ---- AR7. claude: errors auto-resume must NOT act on ---------------------------------------
|
|
8452
|
+
# invalid_request: another account would fail the same way — logged once, left alone.
|
|
8453
|
+
ar_claude_pool
|
|
8454
|
+
ar_start FAKE_TUI=invalid -- claude --dangerously-skip-permissions "fix the bug please"
|
|
8455
|
+
if ar_wait 15 ar_logged "$ARC" 'autoresume never provider=claude .*code=invalid_request'; then
|
|
8456
|
+
sleep 1.5
|
|
8457
|
+
ar_running "$AR_PID" && [ ! -s "$ARB/tmux/typed" ] \
|
|
8458
|
+
&& t_ok "AR never: invalid_request is logged and the session left alone" \
|
|
8459
|
+
|| t_fail "AR never: the session was stopped"
|
|
8460
|
+
else
|
|
8461
|
+
t_fail "AR never: logged" "$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8462
|
+
fi
|
|
8463
|
+
ar_reap "$ARC"
|
|
8464
|
+
# An OLD rejection in a resumed transcript never fires: only records from this launch count.
|
|
8465
|
+
ar_claude_pool
|
|
8466
|
+
mkdir -p "$ARC/shared-projects/-fake-tui"
|
|
8467
|
+
printf '{"type":"assistant","isApiErrorMessage":true,"error":"rate_limit","quotaLimits":{"status":"rejected","resetsAt":%s,"rateLimitType":"five_hour"},"timestamp":"2026-01-01T00:00:00.000Z","sessionId":"%s","message":{"content":[{"type":"text","text":"limit"}]}}\n' \
|
|
8468
|
+
"$(( $(date +%s) + 3600 ))" "$AR_SID" > "$ARC/shared-projects/-fake-tui/$AR_SID.jsonl"
|
|
8469
|
+
ar_start FAKE_TUI=idle FAKE_TUI_QUIET=1 -- claude --dangerously-skip-permissions --resume "$AR_SID" "carry on please"
|
|
8470
|
+
if ar_wait 15 grep -qs "transcript .*$AR_SID.jsonl" "$ARC/tmp/autoresume/$AR_PID.log"; then
|
|
8471
|
+
sleep 1.5
|
|
8472
|
+
ar_logged "$ARC" 'autoresume (detect|switch)' || ! ar_running "$AR_PID" \
|
|
8473
|
+
&& t_fail "AR resumed transcript: an old rejection fired" "$(grep autoresume "$ARC/selection.log")" \
|
|
8474
|
+
|| t_ok "AR resumed transcript: an old rejection does not fire"
|
|
8475
|
+
else
|
|
8476
|
+
t_fail "AR resumed transcript: watcher followed the transcript" "$(cat "$ARC/tmp/autoresume/$AR_PID.log" 2>/dev/null)"
|
|
8477
|
+
fi
|
|
8478
|
+
ar_reap "$ARC"
|
|
8479
|
+
|
|
8480
|
+
# ---- AR8. claude: no account to move to -> HOLD, nothing is stopped ----------------------
|
|
8481
|
+
ar_claude_pool
|
|
8482
|
+
# acct-02 is out for the week (a weekly client marker outlives every telemetry pass).
|
|
8483
|
+
printf '%s\nbucket=client:seven_day percent=100 marked_at=%s reason=client-rate-limit\n' \
|
|
8484
|
+
"$(( $(date +%s) + 86400 ))" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$ARC/acct-02/.limited"
|
|
8485
|
+
ar_start FAKE_TUI=quota -- claude --dangerously-skip-permissions "fix the bug please"
|
|
8486
|
+
if ar_wait 20 ar_logged "$ARC" 'autoresume hold provider=claude .*class=quota .*reason=no-room'; then
|
|
8487
|
+
sleep 1
|
|
8488
|
+
ar_running "$AR_PID" && [ ! -s "$ARB/tmux/typed" ] \
|
|
8489
|
+
&& ! ls "$ARC/tmp/autoresume/" 2>/dev/null | grep -q '^r-' \
|
|
8490
|
+
&& t_ok "AR hold: no eligible account -> hold, no kill, nothing typed" \
|
|
8491
|
+
|| t_fail "AR hold: the session was stopped"
|
|
8492
|
+
grep -q 'CFG=acct-01' "$ARB/tui.log" && t_ok "AR hold: the first launch took the only eligible account" \
|
|
8493
|
+
|| t_fail "AR hold: first pick" "$(cat "$ARB/tui.log")"
|
|
8494
|
+
else
|
|
8495
|
+
t_fail "AR hold: logged" "$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8496
|
+
fi
|
|
8497
|
+
ar_reap "$ARC"
|
|
8498
|
+
|
|
8499
|
+
# ---- AR9. claude: autoresume.off stops a RUNNING watcher ---------------------------------
|
|
8500
|
+
ar_claude_pool
|
|
8501
|
+
ar_start FAKE_TUI=quota FAKE_TUI_DELAY=2.5 -- claude --dangerously-skip-permissions "fix the bug please"
|
|
8502
|
+
if ar_wait 15 ar_logged "$ARC" 'autoresume watch provider=claude'; then
|
|
8503
|
+
: > "$ARC/autoresume.off"
|
|
8504
|
+
ar_wait 5 ar_no_watcher "$ARC" && t_ok "AR off: the running watcher exits on autoresume.off" \
|
|
8505
|
+
|| t_fail "AR off: the running watcher exits on autoresume.off"
|
|
8506
|
+
ar_wait 10 grep -qs '"rate_limit"' "$ARC/shared-projects/-fake-tui/$AR_SID.jsonl"
|
|
8507
|
+
sleep 1.2
|
|
8508
|
+
ar_running "$AR_PID" && [ ! -s "$ARB/tmux/typed" ] && ! ar_logged "$ARC" 'autoresume (detect|switch)' \
|
|
8509
|
+
&& t_ok "AR off: the limit after the kill switch stops nothing" \
|
|
8510
|
+
|| t_fail "AR off: the session was stopped"
|
|
8511
|
+
else
|
|
8512
|
+
t_fail "AR off: watcher started" "$(grep autoresume "$ARC/selection.log" 2>/dev/null)"
|
|
8513
|
+
fi
|
|
8514
|
+
ar_reap "$ARC"
|
|
8515
|
+
|
|
8516
|
+
# ---- AR11. a relaunch re-entering through the token preflight keeps its chain -----------
|
|
8517
|
+
# The relaunch left acct-01 on a MODEL limit (no marker: the chain's AVOID list is all that
|
|
8518
|
+
# keeps the session off it). acct-02 leads on headroom but its portable token is revoked,
|
|
8519
|
+
# so the preflight parks it and re-enters the shim with `exec "$SELF"` — the token is spent
|
|
8520
|
+
# by then, so the chain rides along in the environment. Without it the re-entered selection
|
|
8521
|
+
# takes acct-01 (the best weekly left) and the next watcher restarts at depth 0.
|
|
8522
|
+
ar_claude_pool
|
|
8523
|
+
printf '{"version": 1, "threshold": 90, "accounts": [%s, %s, %s]}\n' \
|
|
8524
|
+
'{"id": "acct-01", "email": "a@ar", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8525
|
+
'{"id": "acct-02", "email": "b@ar", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8526
|
+
'{"id": "acct-03", "email": "c@ar", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8527
|
+
> "$ARC/accounts.json"
|
|
8528
|
+
mkdir -p "$ARC/acct-03"
|
|
8529
|
+
cp "$ARC/acct-01/.credentials.json" "$ARC/acct-03/.credentials.json"
|
|
8530
|
+
ln -s "$ARC/shared-projects" "$ARC/acct-03/projects"
|
|
8531
|
+
rm -f "$ARC/acct-02/.credentials.json"
|
|
8532
|
+
printf 'sk-ant-oat01-REVOKED-carry\n' > "$ARC/acct-02/server.token"
|
|
8533
|
+
nowc="$(date +%s)"
|
|
8534
|
+
for i in 01:5 02:10 03:20; do
|
|
8535
|
+
printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":10,"max_percent":%s,"buckets":[]}' \
|
|
8536
|
+
"$nowc" "${i#*:}" "${i#*:}" > "$ARC/acct-${i%%:*}/limits.json"
|
|
8537
|
+
done
|
|
8538
|
+
mkdir -p "$ARC/tmp/autoresume"
|
|
8539
|
+
tok=CarryTok0123456789
|
|
8540
|
+
printf '%s\n' v=1 provider=claude class=model acct=acct-01 reset= rtype= marked= "sid=$AR_SID" \
|
|
8541
|
+
"cwd=$ARW" depth=2 "avoid=acct-01:$((nowc + 18000))" "hist=model:$((nowc - 100))" chain=CarryTst \
|
|
8542
|
+
> "$ARC/tmp/autoresume/r-$tok.relaunch"
|
|
8543
|
+
printf '%s\0' --dangerously-skip-permissions --resume "$AR_SID" "(claude-multiacc auto-resume) carry on please" \
|
|
8544
|
+
> "$ARC/tmp/autoresume/r-$tok.argv"
|
|
8545
|
+
out="$(cd "$ARH" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" FAKE_STUB_PY_LOG="$ARB/stub.log" \
|
|
8546
|
+
CLAUDE_MULTIACC_HEADROOM_BAND=0 CLAUDE_MULTIACC_AR="$tok" claude </dev/null 2>&1)"
|
|
8547
|
+
check "AR carry: the re-entered selection still skips the account the chain left" \
|
|
8548
|
+
"CFG=acct-03 TOK=none ARGS=--dangerously-skip-permissions --resume $AR_SID (claude-multiacc auto-resume) carry on please" "$out"
|
|
8549
|
+
ar_logged "$ARC" 'acct-02 parked \(setup-token-invalid\)' \
|
|
8550
|
+
&& t_ok "AR carry: the revoked token was parked by the preflight (the re-entry happened)" \
|
|
8551
|
+
|| t_fail "AR carry: preflight park" "$(cat "$ARC/selection.log")"
|
|
8552
|
+
st="$(ls "$ARC/tmp/autoresume/"*.state 2>/dev/null | head -1)"
|
|
8553
|
+
grep -q '^depth=2$' "$st" 2>/dev/null && grep -q '^chain=CarryTst$' "$st" \
|
|
8554
|
+
&& grep -q "^avoid=acct-01:$((nowc + 18000))\$" "$st" && grep -q '^cwd=/.*/ar-work dir$' "$st" \
|
|
8555
|
+
&& t_ok "AR carry: the next watcher inherits depth, chain, AVOID and the session's cwd" \
|
|
8556
|
+
|| t_fail "AR carry: state file" "$(cat "$st" 2>/dev/null)"
|
|
8557
|
+
[ "$(grep -c 'autoresume relaunch chain=CarryTst from=acct-01 class=model depth=2$' "$ARC/selection.log")" = 1 ] \
|
|
8558
|
+
&& t_ok "AR carry: the relaunch is logged once, not again by the re-entry" \
|
|
8559
|
+
|| t_fail "AR carry: relaunch log" "$(grep autoresume "$ARC/selection.log")"
|
|
8560
|
+
ar_reap "$ARC"
|
|
8561
|
+
|
|
8562
|
+
# ---- AR12. claude relaunch tokens: the auth park, dead tokens, the trust mirror ------------
|
|
8563
|
+
# Planted directly (no watcher, no TUI), so each assertion can only come from the relaunch
|
|
8564
|
+
# entry itself. acct-01 leads on headroom and HEADROOM_BAND=0 makes the lead decisive.
|
|
8565
|
+
ar_claude_pool
|
|
8566
|
+
nowc="$(date +%s)"
|
|
8567
|
+
for i in 01:5 02:20; do
|
|
8568
|
+
printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":10,"max_percent":%s,"buckets":[]}' \
|
|
8569
|
+
"$nowc" "${i#*:}" "${i#*:}" > "$ARC/acct-${i%%:*}/limits.json"
|
|
8570
|
+
done
|
|
8571
|
+
# class=auth: no auth record in any transcript, so only the relaunch can park acct-01.
|
|
8572
|
+
ar_plant "$ARC" AuthTok0123456789 claude auth acct-01 -- \
|
|
8573
|
+
--dangerously-skip-permissions --resume "$AR_SID" "(claude-multiacc auto-resume) carry on please"
|
|
8574
|
+
out="$(cd "$ARH" && env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" CLAUDE_MULTIACC_HEADROOM_BAND=0 \
|
|
8575
|
+
CLAUDE_MULTIACC_AR="AuthTok0123456789:$AR_SID" claude </dev/null 2>&1)"
|
|
8576
|
+
[ -f "$ARC/acct-01/.expired" ] && ar_logged "$ARC" 'acct-01 parked \(' \
|
|
8577
|
+
&& t_ok "AR auth token: the relaunch parks the account it left (.expired)" \
|
|
8578
|
+
|| t_fail "AR auth token: park" "$(ls -a "$ARC/acct-01" | tr '\n' ' ')"
|
|
8579
|
+
check "AR auth token: ...and the run lands on the other account, resuming the session" \
|
|
8580
|
+
"CFG=acct-02 TOK=none ARGS=--dangerously-skip-permissions --resume $AR_SID (claude-multiacc auto-resume) carry on please" "$out"
|
|
8581
|
+
# Dead tokens: missing, expired (older than 10 min), malformed, a FIFO in its place.
|
|
8582
|
+
ar_dead claude "a missing relaunch file" "claude-multiacc: $AR_DEAD_MSG Resume with: claude --resume $AR_SID" \
|
|
8583
|
+
CLAUDE_MULTIACC_AR="MissingTok0123456:$AR_SID"
|
|
8584
|
+
ar_plant "$ARC" ExpiredTok0123456 claude quota acct-01 -- --resume "$AR_SID" "(claude-multiacc auto-resume) x y"
|
|
8585
|
+
touch -t 202601010000 "$ARC/tmp/autoresume/r-ExpiredTok0123456.relaunch"
|
|
8586
|
+
ar_dead claude "an expired relaunch file" "claude-multiacc: $AR_DEAD_MSG Resume with: claude --resume $AR_SID" \
|
|
8587
|
+
CLAUDE_MULTIACC_AR="ExpiredTok0123456:$AR_SID"
|
|
8588
|
+
ls "$ARC/tmp/autoresume/" 2>/dev/null | grep -q '^r-ExpiredTok' \
|
|
8589
|
+
&& t_fail "AR dead token (claude): an expired token is consumed" "$(ls "$ARC/tmp/autoresume/")" \
|
|
8590
|
+
|| t_ok "AR dead token (claude): an expired token is consumed"
|
|
8591
|
+
ar_dead claude "a malformed token" "claude-multiacc: $AR_DEAD_MSG Resume with: claude --resume $AR_SID" \
|
|
8592
|
+
CLAUDE_MULTIACC_AR="../bad/tok:$AR_SID"
|
|
8593
|
+
ar_dead claude "an unusable session id (no hint)" "claude-multiacc: $AR_DEAD_MSG" \
|
|
8594
|
+
CLAUDE_MULTIACC_AR='MissingTok0123456:$(id)'
|
|
8595
|
+
mkfifo "$ARC/tmp/autoresume/r-FifoTok0123456789.relaunch"
|
|
8596
|
+
( cd "$ARH" && exec env "${AREC[@]}" CLAUDE_MULTIACC_PYTHON="$ARB/stub-python" \
|
|
8597
|
+
CLAUDE_MULTIACC_AR="FifoTok0123456789:$AR_SID" claude ) </dev/null > "$ARB/fifo.out" 2>&1 &
|
|
8598
|
+
fp=$!
|
|
8599
|
+
if ar_wait 10 ar_stopped "$fp"; then
|
|
8600
|
+
wait "$fp"; rc=$?
|
|
8601
|
+
[ "$rc" = 2 ] && ! grep -q 'CFG=' "$ARB/fifo.out" \
|
|
8602
|
+
&& t_ok "AR dead token (claude): a FIFO named like a relaunch file never blocks the launch" \
|
|
8603
|
+
|| t_fail "AR dead token (claude): FIFO" "rc=$rc $(cat "$ARB/fifo.out")"
|
|
8604
|
+
else
|
|
8605
|
+
kill -KILL "$fp" 2>/dev/null; wait "$fp" 2>/dev/null
|
|
8606
|
+
t_fail "AR dead token (claude): a FIFO named like a relaunch file blocked the launch"
|
|
8607
|
+
fi
|
|
8608
|
+
rm -f "$ARC/tmp/autoresume/r-FifoTok0123456789.relaunch"
|
|
8609
|
+
# Trust: the relaunch marks its cwd trusted in the PICKED account's .claude.json (Claude
|
|
8610
|
+
# shows a blocking trust dialog in a new directory even with --dangerously-skip-permissions),
|
|
8611
|
+
# keeping every other key. The real python runs it; TMUX= keeps the watcher out of the way.
|
|
8612
|
+
# The key is the directory as the relaunch's own `cd` spells it, plus its physical path
|
|
8613
|
+
# when that differs (Claude may key projects by either; macOS TMPDIR is a symlink).
|
|
8614
|
+
ar_claude_pool
|
|
8615
|
+
nowc="$(date +%s)"
|
|
8616
|
+
arw_key="$(cd "$ARW" && pwd)"
|
|
8617
|
+
for i in 01:20 02:5; do
|
|
8618
|
+
printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":10,"max_percent":%s,"buckets":[]}' \
|
|
8619
|
+
"$nowc" "${i#*:}" "${i#*:}" > "$ARC/acct-${i%%:*}/limits.json"
|
|
8620
|
+
done
|
|
8621
|
+
printf '{"numStartups": 7, "projects": {"%s": {"allowedTools": ["Bash"]}, "/elsewhere": {"hasTrustDialogAccepted": false}}}\n' \
|
|
8622
|
+
"$arw_key" > "$ARC/acct-02/.claude.json"
|
|
8623
|
+
chmod 600 "$ARC/acct-02/.claude.json"
|
|
8624
|
+
printf '{"numStartups": 1}\n' > "$ARC/acct-01/.claude.json"
|
|
8625
|
+
cp "$ARC/acct-01/.claude.json" "$ARB/acct-01.claude.json"
|
|
8626
|
+
ar_plant "$ARC" TrustTok0123456789 claude crash acct-01 -- \
|
|
8627
|
+
--dangerously-skip-permissions --resume "$AR_SID" "(claude-multiacc auto-resume) carry on please"
|
|
8628
|
+
out="$(cd "$ARH" && env "${AREC[@]}" TMUX= CLAUDE_MULTIACC_HEADROOM_BAND=0 \
|
|
8629
|
+
CLAUDE_MULTIACC_AR="TrustTok0123456789:$AR_SID" claude </dev/null 2>&1)"
|
|
8630
|
+
check "AR trust: the relaunch ran on the picked account" "CFG=acct-02 " "$out"
|
|
8631
|
+
if python3 - "$ARC/acct-02/.claude.json" "$arw_key" <<'PYEOF'
|
|
8632
|
+
import json, os, stat, sys
|
|
8633
|
+
path, cwd = sys.argv[1], sys.argv[2]
|
|
8634
|
+
d = json.load(open(path))
|
|
8635
|
+
p = d["projects"]
|
|
8636
|
+
real = os.path.realpath(cwd)
|
|
8637
|
+
ok = (p[cwd] == {"allowedTools": ["Bash"], "hasTrustDialogAccepted": True}
|
|
8638
|
+
and (real == cwd or p.get(real) == {"hasTrustDialogAccepted": True})
|
|
8639
|
+
and len(p) == (2 if real == cwd else 3) and d.get("numStartups") == 7
|
|
8640
|
+
and p["/elsewhere"] == {"hasTrustDialogAccepted": False}
|
|
8641
|
+
and stat.S_IMODE(os.stat(path).st_mode) == 0o600)
|
|
8642
|
+
sys.exit(0 if ok else 1)
|
|
8643
|
+
PYEOF
|
|
8644
|
+
then
|
|
8645
|
+
t_ok "AR trust: projects[cwd].hasTrustDialogAccepted=true, every other key and the file mode kept"
|
|
8646
|
+
else
|
|
8647
|
+
t_fail "AR trust: .claude.json after the relaunch" "$(cat "$ARC/acct-02/.claude.json")"
|
|
8648
|
+
fi
|
|
8649
|
+
cmp -s "$ARC/acct-01/.claude.json" "$ARB/acct-01.claude.json" \
|
|
8650
|
+
&& t_ok "AR trust: the account the session left is not touched" \
|
|
8651
|
+
|| t_fail "AR trust: the left account's .claude.json changed" "$(cat "$ARC/acct-01/.claude.json")"
|
|
8652
|
+
# A plain launch (no token) never edits trust.
|
|
8653
|
+
printf '{"numStartups": 7}\n' > "$ARC/acct-02/.claude.json"
|
|
8654
|
+
cp "$ARC/acct-02/.claude.json" "$ARB/acct-02.claude.json"
|
|
8655
|
+
(cd "$ARW" && env "${AREC[@]}" TMUX= claude --dangerously-skip-permissions </dev/null >/dev/null 2>&1)
|
|
8656
|
+
cmp -s "$ARC/acct-01/.claude.json" "$ARB/acct-01.claude.json" \
|
|
8657
|
+
&& cmp -s "$ARC/acct-02/.claude.json" "$ARB/acct-02.claude.json" \
|
|
8658
|
+
&& t_ok "AR trust: a launch that is not a relaunch never edits .claude.json" \
|
|
8659
|
+
|| t_fail "AR trust: a plain launch edited .claude.json" "$(cat "$ARC"/acct-0*/.claude.json)"
|
|
8660
|
+
ar_reap "$ARC"
|
|
8661
|
+
|
|
8662
|
+
# ---- AR10. codex: the same machinery through bin/codex ------------------------------------
|
|
8663
|
+
ARX="$WORK/ar-codex"
|
|
8664
|
+
ar_codex_pool() { # a fresh two-account codex pool sharing one sessions tree (the installed layout)
|
|
8665
|
+
local i
|
|
8666
|
+
rm -rf "$ARX" "$ARB/tmux" "$ARB/pids" "$ARB/tui.log" "$ARB/stub.log"
|
|
8667
|
+
mkdir -p "$ARX/tmp" "$ARX/shared-sessions" "$ARB/tmux"
|
|
8668
|
+
printf '{"version": 1, "threshold": 90, "accounts": [%s, %s]}\n' \
|
|
8669
|
+
'{"id": "acct-01", "email": "a@arx", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8670
|
+
'{"id": "acct-02", "email": "b@arx", "home": "mac", "added_at": "2026-09-23T00:00:00Z"}' \
|
|
8671
|
+
> "$ARX/accounts.json"
|
|
8672
|
+
for i in 01 02; do
|
|
8673
|
+
mkdir -p "$ARX/acct-$i"
|
|
8674
|
+
mk_cx_auth "$ARX/acct-$i/auth.json" "$i@arx" "$FUTURE_EXP"
|
|
8675
|
+
printf '{"fetched_at":%s,"weekly_percent":20,"session_percent":10,"max_percent":20,"buckets":[]}' "$(date +%s)" \
|
|
8676
|
+
> "$ARX/acct-$i/limits.json"
|
|
8677
|
+
ln -s "$ARX/shared-sessions" "$ARX/acct-$i/sessions"
|
|
8678
|
+
done
|
|
8679
|
+
: > "$ARX/.limits-kick"
|
|
8680
|
+
}
|
|
8681
|
+
AREX=(CODEX_ACCOUNTS_DIR="$ARX" HOME="$ARH" TMUX="$ARB/tmux-sock,4243,0" TMUX_PANE=%2
|
|
8682
|
+
CODEX_MULTIACC_AUTORESUME=1 CODEX_MULTIACC_PYTHON="$ARPY" CODEX_MULTIACC_AR_TEST_TTY=1
|
|
8683
|
+
CODEX_MULTIACC_AR_TEST_TMUX_ANYPANE=1 CODEX_MULTIACC_AR_POLL=0.1 CODEX_MULTIACC_AR_GRACE=0.5
|
|
8684
|
+
CODEX_MULTIACC_AR_TERM_GRACE=3 CODEX_MULTIACC_AR_PANE_WAIT=5 CODEX_MULTIACC_AR_PROBE_TIMEOUT=15
|
|
8685
|
+
CODEX_MULTIACC_AR_DISCOVER_TIMEOUT=20 CODEX_MULTIACC_AUTORESUME_DEBUG=1
|
|
8686
|
+
FAKE_TMUX_DIR="$ARB/tmux" FAKE_TUI_LOG="$ARB/tui.log" FAKE_TUI_PIDS="$ARB/pids")
|
|
8687
|
+
ar_codex_pool
|
|
8688
|
+
cx_gate() { # cx_gate <name> <watcher|none> [VAR=val ...] -- <codex args...>
|
|
8689
|
+
local name="$1" want="$2" pre=() out states
|
|
8690
|
+
shift 2
|
|
8691
|
+
while [ $# -gt 0 ] && [ "$1" != "--" ]; do pre+=("$1"); shift; done
|
|
8692
|
+
[ $# -gt 0 ] && shift
|
|
8693
|
+
rm -rf "$ARX/tmp/autoresume" "$ARB/stub.log"
|
|
8694
|
+
out="$(cd "$ARW" && env "${AREX[@]}" CODEX_MULTIACC_PYTHON="$ARB/stub-python" \
|
|
8695
|
+
FAKE_STUB_PY_LOG="$ARB/stub.log" ${pre[@]+"${pre[@]}"} codex "$@" </dev/null 2>&1)"
|
|
8696
|
+
states="$(ls "$ARX/tmp/autoresume/" 2>/dev/null | grep -c '\.state$')"
|
|
8697
|
+
case "$out" in *CFG=acct-0*) ;; *) t_fail "codex auto-resume gate: $name execs" "$out"; return ;; esac
|
|
8698
|
+
if [ "$want" = watcher ]; then
|
|
8699
|
+
[ "$states" = 1 ] && ar_wait 5 grep -qs -- "-I .*lib/autoresume.py watch --state $ARX/tmp/autoresume/[0-9]*\.state" "$ARB/stub.log" \
|
|
8700
|
+
&& t_ok "codex auto-resume gate: $name gets a watcher" \
|
|
8701
|
+
|| t_fail "codex auto-resume gate: $name gets a watcher" "states=$states"
|
|
8702
|
+
else
|
|
8703
|
+
[ "$states" = 0 ] && [ ! -s "$ARB/stub.log" ] \
|
|
8704
|
+
&& t_ok "codex auto-resume gate: $name gets no watcher" \
|
|
8705
|
+
|| t_fail "codex auto-resume gate: $name gets no watcher" "states=$states"
|
|
8706
|
+
fi
|
|
8707
|
+
}
|
|
8708
|
+
cx_gate "a prompt with the bypass flag" watcher -- --dangerously-bypass-approvals-and-sandbox "fix the bug please"
|
|
8709
|
+
cx_gate "resume <uuid>" watcher -- resume "$AR_CXSID"
|
|
8710
|
+
cx_gate "exec" none -- exec "do it now"
|
|
8711
|
+
cx_gate "-p (profile)" none -- -p work
|
|
8712
|
+
cx_gate "-c overrides" none -- -c model=o3
|
|
8713
|
+
cx_gate "a pinned run" none CODEX_ACCOUNT=acct-01 --
|
|
8714
|
+
cx_gate "an invalid pin (random fallback)" none CODEX_ACCOUNT=acct-99 --
|
|
8715
|
+
cx_gate "outside tmux" none TMUX_PANE= --
|
|
8716
|
+
cx_gate "CODEX_MULTIACC_AUTORESUME=off" none CODEX_MULTIACC_AUTORESUME=off --
|
|
8717
|
+
ln -s "$ARX" "$WORK/ar codex root"
|
|
8718
|
+
cx_gate "a pool root that would need quoting" none CODEX_ACCOUNTS_DIR="$WORK/ar codex root" -- --yolo
|
|
8719
|
+
# The client never sees an auto-resume variable (AREX itself exports several).
|
|
8720
|
+
rm -rf "$ARX/tmp/autoresume"
|
|
8721
|
+
out="$(cd "$ARW" && env "${AREX[@]}" CODEX_MULTIACC_PYTHON="$ARB/stub-python" FAKE_PRINT_ENV=1 \
|
|
8722
|
+
codex --yolo </dev/null 2>&1)"
|
|
8723
|
+
case "$out" in
|
|
8724
|
+
*"AR_ENV=0"*) [ -n "$(ls "$ARX/tmp/autoresume/" 2>/dev/null | grep '\.state$')" ] \
|
|
8725
|
+
&& t_ok "codex auto-resume: the client never sees an auto-resume variable (watcher spawned)" \
|
|
8726
|
+
|| t_fail "codex auto-resume: env scrub case spawned no watcher" "$out" ;;
|
|
8727
|
+
*) t_fail "codex auto-resume: the client never sees an auto-resume variable" "$out" ;;
|
|
8728
|
+
esac
|
|
8729
|
+
out="$(cd "$ARW" && env "${AREX[@]}" CODEX_MULTIACC_AR_PROBE=1 codex --yolo </dev/null 2>&1)"; rc=$?
|
|
8730
|
+
case "$rc:$out" in 0:pick=acct-0[12]" tier=eligible") t_ok "codex probe prints pick/tier, never execs" ;;
|
|
8731
|
+
*) t_fail "codex probe" "rc=$rc out=$out" ;; esac
|
|
8732
|
+
all1=1
|
|
8733
|
+
for _ in 1 2 3 4 5; do
|
|
8734
|
+
out="$(cd "$ARW" && env "${AREX[@]}" CODEX_MULTIACC_AR_PROBE=1 \
|
|
8735
|
+
CODEX_MULTIACC_AR_AVOID="acct-02:$(( $(date +%s) + 3600 ))" codex --yolo </dev/null 2>&1)"
|
|
8736
|
+
[ "$out" = "pick=acct-01 tier=eligible" ] || all1=0
|
|
8737
|
+
done
|
|
8738
|
+
[ "$all1" = 1 ] && t_ok "codex probe honours AVOID (5 of 5)" || t_fail "codex probe AVOID" "$out"
|
|
8739
|
+
# codex relaunch tokens, planted directly: acct-01 leads, so only the relaunch's own park
|
|
8740
|
+
# keeps the run off it (the shared sessions tree turns the rollout scan off).
|
|
8741
|
+
nowx="$(date +%s)"
|
|
8742
|
+
for i in 01:5 02:20; do
|
|
8743
|
+
printf '{"fetched_at":%s,"weekly_percent":%s,"session_percent":10,"max_percent":%s,"buckets":[]}' \
|
|
8744
|
+
"$nowx" "${i#*:}" "${i#*:}" > "$ARX/acct-${i%%:*}/limits.json"
|
|
8745
|
+
done
|
|
8746
|
+
rm -f "$ARX/.last-pick"
|
|
8747
|
+
ar_plant "$ARX" CxAuthTok01234567 codex auth acct-01 -- \
|
|
8748
|
+
resume --yolo "$AR_CXSID" "(claude-multiacc auto-resume) carry on please"
|
|
8749
|
+
out="$(cd "$ARH" && env "${AREX[@]}" CODEX_MULTIACC_PYTHON="$ARB/stub-python" CODEX_MULTIACC_HEADROOM_BAND=0 \
|
|
8750
|
+
CODEX_MULTIACC_AR="CxAuthTok01234567:$AR_CXSID" codex </dev/null 2>&1)"
|
|
8751
|
+
grep -q 'reason=auth-error soft_until=' "$ARX/acct-01/.expired" 2>/dev/null \
|
|
8752
|
+
&& t_ok "AR codex auth token: the relaunch parks the account it left (auth-error, soft)" \
|
|
8753
|
+
|| t_fail "AR codex auth token: park" "$(cat "$ARX/acct-01/.expired" 2>/dev/null)"
|
|
8754
|
+
check "AR codex auth token: ...and the run lands on the other account" "CFG=acct-02" "$out"
|
|
8755
|
+
ar_dead codex "a missing relaunch file" "codex-multiacc: $AR_DEAD_MSG Resume with: codex resume $AR_CXSID" \
|
|
8756
|
+
CODEX_MULTIACC_AR="MissingTok0123456:$AR_CXSID"
|
|
8757
|
+
ar_plant "$ARX" ExpiredTok0123456 codex quota acct-01 -- resume "$AR_CXSID" "(claude-multiacc auto-resume) x y"
|
|
8758
|
+
touch -t 202601010000 "$ARX/tmp/autoresume/r-ExpiredTok0123456.relaunch"
|
|
8759
|
+
ar_dead codex "an expired relaunch file" "codex-multiacc: $AR_DEAD_MSG Resume with: codex resume $AR_CXSID" \
|
|
8760
|
+
CODEX_MULTIACC_AR="ExpiredTok0123456:$AR_CXSID"
|
|
8761
|
+
ls "$ARX/tmp/autoresume/" 2>/dev/null | grep -q '^r-ExpiredTok' \
|
|
8762
|
+
&& t_fail "AR dead token (codex): an expired token is consumed" "$(ls "$ARX/tmp/autoresume/")" \
|
|
8763
|
+
|| t_ok "AR dead token (codex): an expired token is consumed"
|
|
8764
|
+
ar_dead codex "a malformed token" "codex-multiacc: $AR_DEAD_MSG Resume with: codex resume $AR_CXSID" \
|
|
8765
|
+
CODEX_MULTIACC_AR="../bad/tok:$AR_CXSID"
|
|
8766
|
+
ar_dead codex "an unusable session id (no hint)" "codex-multiacc: $AR_DEAD_MSG" \
|
|
8767
|
+
CODEX_MULTIACC_AR='MissingTok0123456:$(id)'
|
|
8768
|
+
# No valid login anywhere: a launch would fall back to the stock client — a probe must not.
|
|
8769
|
+
ar_codex_pool
|
|
8770
|
+
rm -f "$ARX"/acct-0*/auth.json
|
|
8771
|
+
out="$(cd "$ARW" && env "${AREX[@]}" CODEX_MULTIACC_AR_PROBE=1 codex --yolo </dev/null 2>&1)"; rc=$?
|
|
8772
|
+
[ "$rc:$out" = "3:pick= tier=none" ] && t_ok "codex probe on a pool with no valid login answers tier=none (exit 3), never execs" \
|
|
8773
|
+
|| t_fail "codex probe with no valid login" "rc=$rc out=$out"
|
|
8774
|
+
|
|
8775
|
+
ar_codex_pool
|
|
8776
|
+
AREC=("${AREX[@]}") # ar_start launches with AREC
|
|
8777
|
+
ar_start FAKE_TUI=quota -- codex --dangerously-bypass-approvals-and-sandbox "fix the bug please"
|
|
8778
|
+
first=$AR_PID
|
|
8779
|
+
if ar_wait 30 ar_lines 2 "$ARB/tui.log"; then
|
|
8780
|
+
l1="$(sed -n 1p "$ARB/tui.log")"; l2="$(sed -n 2p "$ARB/tui.log")"
|
|
8781
|
+
from="$(ar_acct_of "$l1")"; to="$(ar_acct_of "$l2")"
|
|
8782
|
+
[ -n "$from" ] && [ -n "$to" ] && [ "$from" != "$to" ] \
|
|
8783
|
+
&& t_ok "AR codex quota: relaunched on the OTHER account ($from -> $to)" \
|
|
8784
|
+
|| t_fail "AR codex quota: relaunched on the other account" "$l1 | $l2"
|
|
8785
|
+
check "AR codex quota: \`resume <opts> <sid> <prompt>\`" \
|
|
8786
|
+
"ARGS=resume --dangerously-bypass-approvals-and-sandbox $AR_CXSID (claude-multiacc auto-resume) This session was restarted automatically on another account because the previous account hit its usage limit." "$l2"
|
|
8787
|
+
ar_wait 5 ar_stopped "$first" && t_ok "AR codex quota: the stuck client was stopped" \
|
|
8788
|
+
|| t_fail "AR codex quota: the stuck client was stopped" "pid $first alive"
|
|
8789
|
+
m1="$(sed -n 1p "$ARX/$from/.limited" 2>/dev/null)"; m2="$(sed -n 2p "$ARX/$from/.limited" 2>/dev/null)"
|
|
8790
|
+
nowx="$(date +%s)"
|
|
8791
|
+
case "$m1" in ''|*[!0-9]*) m1=0 ;; esac
|
|
8792
|
+
[ "$m1" -gt "$((nowx + 500))" ] && [ "$m1" -le "$((nowx + 600))" ] \
|
|
8793
|
+
&& case "$m2" in "bucket=error-cooldown percent=? marked_at="*" reason=error-cooldown") true ;; *) false ;; esac \
|
|
8794
|
+
&& t_ok "AR codex quota: the old account gets the 10-minute error-cooldown, never client:7d" \
|
|
8795
|
+
|| t_fail "AR codex quota: cooldown marker" "$m1 / $m2"
|
|
8796
|
+
ar_logged "$ARX" "autoresume relaunch chain=[A-Za-z0-9]+ from=$from class=quota depth=1\$" \
|
|
8797
|
+
&& ar_logged "$ARX" "autoresume switch provider=codex chain=[A-Za-z0-9]+ from=$from class=quota sid=$AR_CXSID depth=1 probe=$to" \
|
|
8798
|
+
&& t_ok "AR codex quota: switch + relaunch logged" \
|
|
8799
|
+
|| t_fail "AR codex quota: log lines" "$(grep autoresume "$ARX/selection.log")"
|
|
8800
|
+
ar_typed_ok CODEX "$AR_CXSID" "CODEX_ACCOUNTS_ROOT=$(cd "$ARX" && pwd)" "$REPO_DIR/bin/codex" \
|
|
8801
|
+
&& grep -q -- "send-keys -R -t %2" "$ARB/tmux/calls" \
|
|
8802
|
+
&& t_ok "AR codex quota: terminal reset, then the token line typed" \
|
|
8803
|
+
|| t_fail "AR codex quota: typed line" "$(cat "$ARB/tmux/typed" 2>/dev/null)"
|
|
8804
|
+
ar_wait 10 ar_logged "$ARX" "autoresume watch provider=codex chain=[A-Za-z0-9]+ pid=[0-9]+ acct=$to depth=1\$" \
|
|
8805
|
+
&& t_ok "AR codex quota: the resumed session is supervised again" \
|
|
8806
|
+
|| t_fail "AR codex quota: second watcher" "$(grep autoresume "$ARX/selection.log")"
|
|
8807
|
+
else
|
|
8808
|
+
t_fail "AR codex quota: relaunched within 30 s" "tui=$(cat "$ARB/tui.log" 2>/dev/null) log=$(grep autoresume "$ARX/selection.log" 2>/dev/null)"
|
|
8809
|
+
fi
|
|
8810
|
+
ar_reap "$ARX"
|
|
8811
|
+
ar_no_watcher "$ARX" && t_ok "AR codex: no watcher left running" || t_fail "AR codex: watcher left running"
|
|
8812
|
+
|
|
7888
8813
|
# The reset-credit contract is easier to prove against a stateful local HTTP server
|
|
7889
8814
|
# than file:// fixtures: it pins thresholding, credit ordering and idempotent POST retry.
|
|
7890
8815
|
if CODEX_MULTIACC_AUTO_RESET=1 python3 -m unittest discover -s "$REPO_DIR/tests" -p 'test_codex_reset*.py'; then
|
|
@@ -7909,6 +8834,13 @@ if CLAUDE_MULTIACC_AUTO_RESET=1 python3 -m unittest discover -s "$REPO_DIR/tests
|
|
|
7909
8834
|
else
|
|
7910
8835
|
t_fail "claude automatic reset suite" "see unittest output above"
|
|
7911
8836
|
fi
|
|
8837
|
+
# ...and the auto-resume watcher, unit by unit: classifiers, argv sanitizer, relaunch
|
|
8838
|
+
# files, budgets, tmux commands and a fake-runner walk through every stop/relaunch branch.
|
|
8839
|
+
if python3 -m unittest discover -s "$REPO_DIR/tests" -p 'test_autoresume.py'; then
|
|
8840
|
+
t_ok "auto-resume watcher unit suite"
|
|
8841
|
+
else
|
|
8842
|
+
t_fail "auto-resume watcher unit suite" "see unittest output above"
|
|
8843
|
+
fi
|
|
7912
8844
|
|
|
7913
8845
|
# ---- summary ---------------------------------------------------------------------
|
|
7914
8846
|
echo
|