claude-multiacc 2.0.57 → 2.0.59
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 +111 -3
- package/bin/claude +323 -84
- package/bin/claude-accounts +273 -42
- package/bin/codex +15 -0
- package/bin/codex-accounts +31 -4
- package/docs/ACCOUNT_OPERATIONS.md +62 -2
- package/docs/AUTORESUME-HANDOFF.md +30 -3
- package/docs/AUTORESUME.md +275 -18
- package/docs/CODEX.md +1 -0
- package/docs/INSTALLATION.md +7 -1
- package/docs/VERIFICATION.md +4 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.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__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/audit.py +88 -22
- package/lib/autoresume.py +1197 -82
- package/lib/autoresume_cross.py +3 -1
- package/lib/autoresume_handoff.py +82 -10
- package/lib/autoresume_probe.py +2 -2
- package/lib/claude_client_error.py +9 -4
- package/lib/codex_trust.py +263 -0
- package/lib/common.sh +131 -3
- package/lib/credential.py +5 -2
- package/lib/install_actions.sh +19 -0
- package/lib/keychain.py +180 -36
- package/lib/mcp_oauth.py +1748 -0
- package/lib/model_catalog.json +404 -0
- package/lib/model_catalog.py +201 -0
- package/lib/model_match.py +104 -0
- package/lib/report.py +8 -5
- package/lib/session_settings.py +123 -0
- package/package.json +2 -2
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- 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_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.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/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/fake_codex_tui.py +25 -0
- package/tests/fake_security.sh +24 -2
- package/tests/fixtures/cross_provider_cli.py +111 -1
- package/tests/run-tests.sh +1028 -23
- package/tests/test_autoresume.py +215 -8
- package/tests/test_autoresume_catalog.py +226 -0
- package/tests/test_autoresume_catalog_cli.py +185 -0
- package/tests/test_autoresume_codex_records.py +16 -0
- package/tests/test_autoresume_codex_trust.py +247 -0
- package/tests/test_autoresume_cross_guards.py +28 -0
- package/tests/test_autoresume_cross_origin.py +70 -0
- package/tests/test_autoresume_cross_tmux.py +27 -2
- package/tests/test_autoresume_cross_tokens.py +32 -2
- package/tests/test_autoresume_handoff_argv.py +120 -16
- package/tests/test_autoresume_handoff_origins.py +67 -0
- package/tests/test_autoresume_memory.py +1141 -0
- package/tests/test_autoresume_memory_tmux.py +94 -0
- package/tests/test_autoresume_probe.py +2 -1
- package/tests/test_autoresume_relaunch_settings.py +317 -0
- package/tests/test_autoresume_settings.py +135 -0
- package/tests/test_autoresume_sidechain.py +1518 -0
- package/tests/test_autoresume_sidechain_tmux.py +79 -0
- package/tests/test_autoresume_trust_tmux.py +63 -0
- package/tests/test_claude_client_error.py +10 -7
- package/tests/test_claude_org_block.py +1 -0
- package/tests/test_claude_pool_prepare.py +4 -1
- package/tests/test_claude_reset.py +3 -0
- package/tests/test_keychain_nologin.py +340 -0
- package/tests/test_mcp_oauth.py +1414 -0
- package/tests/test_packaged_commands.py +41 -0
package/bin/codex-accounts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# codex-accounts — manage the codex-multiacc account pool (OpenAI Codex CLI).
|
|
3
3
|
# Subcommands: list status add import adopt dedupe remove login expired relogin
|
|
4
4
|
# sync verify limits post-sync health self-update
|
|
5
|
-
# export-credential import-credential
|
|
5
|
+
# export-credential import-credential model-catalog
|
|
6
6
|
set -u
|
|
7
7
|
# lib/codex_audit.py is imported by several subcommands; keep the install tree free
|
|
8
8
|
# of __pycache__ (it may be root-owned, read-only, or an npm global prefix).
|
|
@@ -98,7 +98,14 @@ USAGE
|
|
|
98
98
|
stay selectable. Skips accounts fetched in the last 45s and honors
|
|
99
99
|
429/refresh backoff; --force ignores all three.
|
|
100
100
|
codex-accounts health limits + full verify; logs to health.log
|
|
101
|
-
codex-accounts self-update update this npm/git install; logs to update.log
|
|
101
|
+
codex-accounts self-update update this npm/git install (never to an older version); logs to update.log
|
|
102
|
+
codex-accounts model-catalog [refresh|show] [--if-stale] [--quiet]
|
|
103
|
+
the model/effort catalog auto-resume relaunches a session with. refresh (the default)
|
|
104
|
+
merges the catalog this release ships with the model caches the CLIs keep in their
|
|
105
|
+
account dirs (no request, no credential) into model-catalog.json in BOTH pool roots;
|
|
106
|
+
every install and self-update runs it, and each limits pass refreshes its own pool's
|
|
107
|
+
copy once it is an hour old. --if-stale leaves a younger copy alone; --quiet prints
|
|
108
|
+
nothing. show prints the merged catalog.
|
|
102
109
|
codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
103
110
|
codex-accounts mcp add <name> [--provider claude|codex|both] [--scope user]
|
|
104
111
|
[-e KEY=VAL]... [-H 'Header: v']... [--transport stdio|http]
|
|
@@ -139,6 +146,8 @@ ENV
|
|
|
139
146
|
CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
|
|
140
147
|
CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
|
|
141
148
|
CODEX_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
|
|
149
|
+
CODEX_MULTIACC_MODEL_CATALOG=0 never refresh the codex pool's model-catalog.json
|
|
150
|
+
(MULTIACC_MODEL_CATALOG=0: neither pool's)
|
|
142
151
|
EOF
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -1837,6 +1846,8 @@ PYEOF
|
|
|
1837
1846
|
# lock while a consumer reads its output.
|
|
1838
1847
|
codex_limits_lock_release
|
|
1839
1848
|
trap - EXIT
|
|
1849
|
+
# This pool's model catalog, once its copy is an hour old (lib/common.sh).
|
|
1850
|
+
model_catalog_backstop
|
|
1840
1851
|
# The refresher fails open per account; a NON-zero status means the pass itself
|
|
1841
1852
|
# broke (unreadable manifest, dead python). Report the state anyway — stale data
|
|
1842
1853
|
# beats silence — but hand the caller the failure, exactly as before --json existed.
|
|
@@ -2251,6 +2262,10 @@ cmd_mcp() {
|
|
|
2251
2262
|
local args=()
|
|
2252
2263
|
case "$sub" in
|
|
2253
2264
|
add|add-json|remove|list|apply|import-local) shift ;;
|
|
2265
|
+
# Sharing MCP OAuth logins across accounts is a claude-pool feature (lib/mcp_oauth.py):
|
|
2266
|
+
# Codex keeps an MCP server's OAuth state in its own per-account store.
|
|
2267
|
+
auth-status|auth-sync|auth-forget)
|
|
2268
|
+
die "mcp $sub is not supported for the codex pool — MCP OAuth logins are shared across the claude pool only (claude-accounts mcp $sub)" ;;
|
|
2254
2269
|
"") usage; return 1 ;;
|
|
2255
2270
|
*) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
|
|
2256
2271
|
esac
|
|
@@ -2369,7 +2384,17 @@ cmd_self_update() {
|
|
|
2369
2384
|
ulog "self-update: already latest ($cur)"
|
|
2370
2385
|
return 0
|
|
2371
2386
|
fi
|
|
2372
|
-
|
|
2387
|
+
# Only ever UP, to a version known to be newer (claude-accounts says why): the same
|
|
2388
|
+
# package, so either CLI could otherwise install `latest` over a newer hand install.
|
|
2389
|
+
case "$lat" in [0-9]*.*[0-9]) ;; *)
|
|
2390
|
+
ulog "self-update: could not read the registry's version (npm view); not updating"
|
|
2391
|
+
return 1 ;;
|
|
2392
|
+
esac
|
|
2393
|
+
if ver_newer "$cur" "$lat"; then
|
|
2394
|
+
ulog "self-update: this install ($cur) is newer than the registry's $lat; not downgrading"
|
|
2395
|
+
return 0
|
|
2396
|
+
fi
|
|
2397
|
+
ulog "self-update: npm $cur -> $lat (prefix $prefix, npm $npm_bin)"
|
|
2373
2398
|
if PATH="$npm_path" "$npm_bin" install -g --prefix "$prefix" claude-multiacc@latest \
|
|
2374
2399
|
>>"$ACC_ROOT/update.log" 2>&1; then
|
|
2375
2400
|
after="$(pkg_version_at "$REPO_DIR")"
|
|
@@ -2378,6 +2403,7 @@ cmd_self_update() {
|
|
|
2378
2403
|
return 1
|
|
2379
2404
|
fi
|
|
2380
2405
|
ulog "self-update: npm update ok ($after)"
|
|
2406
|
+
model_catalog_after_update "$quiet"
|
|
2381
2407
|
else
|
|
2382
2408
|
ulog "self-update: npm update FAILED (see update.log)"
|
|
2383
2409
|
return 1
|
|
@@ -2441,7 +2467,7 @@ PYEOF
|
|
|
2441
2467
|
# distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
|
|
2442
2468
|
# FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
|
|
2443
2469
|
# the dispatcher below implements, and a test pins that both ways.
|
|
2444
|
-
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp"
|
|
2470
|
+
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp model-catalog"
|
|
2445
2471
|
case " $_KNOWN_VERBS " in
|
|
2446
2472
|
*" ${1:-help} "*)
|
|
2447
2473
|
# ...but never past a literal `--`: from there on the words belong to an MCP
|
|
@@ -2473,6 +2499,7 @@ case "${1:-help}" in
|
|
|
2473
2499
|
self-update) shift; cmd_self_update "$@" ;;
|
|
2474
2500
|
init-pool) shift; cmd_init_pool "$@" ;;
|
|
2475
2501
|
mcp) shift; cmd_mcp "$@" ;;
|
|
2502
|
+
model-catalog) shift; cmd_model_catalog "$@" ;;
|
|
2476
2503
|
help|--help|-h) usage ;;
|
|
2477
2504
|
*) usage; exit 1 ;;
|
|
2478
2505
|
esac
|
|
@@ -59,7 +59,9 @@ claude-accounts adopt acct-01
|
|
|
59
59
|
`adopt` makes the account dir a **symlink to `~/.claude`** — the machine's existing
|
|
60
60
|
login joins the pool with a single credential file (no copy of the OAuth grant exists
|
|
61
61
|
anywhere else, so refreshes can never race). This is how the two current accounts run:
|
|
62
|
-
acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).
|
|
62
|
+
acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side). It refuses
|
|
63
|
+
an account dir that already holds a credential of its own — any `.credentials.json`, even
|
|
64
|
+
one with only MCP server sign-ins in it, or a `server.token` — rather than delete it.
|
|
63
65
|
|
|
64
66
|
### Add a fresh account (~1 min, one browser sign-in)
|
|
65
67
|
|
|
@@ -212,6 +214,62 @@ is applied on the Mac and never synced. `--help` on `mcp` lists every option;
|
|
|
212
214
|
`CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at seed and launch and the
|
|
213
215
|
shim's mirror (the CLI, sync and fan-out keep working).
|
|
214
216
|
|
|
217
|
+
### MCP OAuth logins (per machine, shared by every account)
|
|
218
|
+
|
|
219
|
+
The registry carries a server's CONFIG; an OAuth server's LOGIN lives in each account's
|
|
220
|
+
credential store (`mcpOAuth` in `.credentials.json`, or the account's Keychain item). The
|
|
221
|
+
claude pool shares those logins across every account on the machine through
|
|
222
|
+
`<pool>/mcp-oauth.json` (lib/mcp_oauth.py): authenticate a server once in any account
|
|
223
|
+
(`/mcp` in a session) and every account has it from its next launch. It goes into the store
|
|
224
|
+
each account already has — its `.credentials.json`, or its Keychain item — and an account that
|
|
225
|
+
runs on its setup token with no file gets one created, holding the MCP logins alone (0600):
|
|
226
|
+
in an ssh/tmux session that file is all its client reads them from, and it is no Claude login
|
|
227
|
+
to the shim, the limits pass, the audit or verify (`list` keeps showing `auth=token`). No file
|
|
228
|
+
is created beside a Keychain login, in a parked account (`.expired`) or an adopted dir.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
claude-accounts mcp auth-status # server, expiry, source account, refresh state, per-account stores
|
|
232
|
+
claude-accounts mcp auth-sync # what the limits pass does every 15 minutes, now
|
|
233
|
+
claude-accounts mcp auth-sync --force # ...retrying a refresh that is backing off
|
|
234
|
+
claude-accounts mcp auth-forget higgsfield # deliberate logout, every account on this machine
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
What to expect, and how to read `auth-status`:
|
|
238
|
+
|
|
239
|
+
- **fresh / stale / own / stub / missing / absent** per account file or Keychain item. `stale`
|
|
240
|
+
(a provably older copy), `stub` and `missing` turn `fresh` at that account's next launch (the
|
|
241
|
+
shim merges before exec) or at the next limits pass; `own` is a login of another grant whose
|
|
242
|
+
age the expiries cannot settle — left where it is; `locked` means the item exists but this
|
|
243
|
+
session (ssh, tmux) cannot open it — a Terminal on the Mac, or the scheduled pass, reaches it.
|
|
244
|
+
An `absent` file turns `fresh` only for a setup-token account (the file is created for it);
|
|
245
|
+
an absent Keychain item is never created.
|
|
246
|
+
- **NOT shared: …** / **sharing paused: …**: one login in many accounts needs ONE refresher —
|
|
247
|
+
the pool, ahead of every client — so nothing is shared with
|
|
248
|
+
`CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0`, by a launch when no refreshing limits pass ran in the
|
|
249
|
+
last hour (`mcp auth-sync` runs one now), or for a login inside the clients' own 5-minute
|
|
250
|
+
window, refused by its server, or whose failing refresh would next try too late. Each
|
|
251
|
+
account keeps what it holds meanwhile.
|
|
252
|
+
- **NEEDS RE-AUTH**: the authorization server refused the refresh token. Sign in again in any
|
|
253
|
+
one account (`/mcp`); the new login replaces the old one everywhere. A backoff (`N
|
|
254
|
+
failure(s), last http 503`) retries by itself, 15 minutes doubling to 6 hours; `refresh: due
|
|
255
|
+
… — deferred: a copy sits in acct-NN keychain` waits for a pass that can open that item,
|
|
256
|
+
until 20 minutes before expiry.
|
|
257
|
+
- **Never synced**: each Mac keeps its own grants. A runner Mac, a replica and the source
|
|
258
|
+
each sign in once; `sync` and the limits fan-out push explicit file lists without it.
|
|
259
|
+
- **Logging out**: a `/mcp` "Clear authentication", `claude mcp login <server>` or a
|
|
260
|
+
re-authentication in ANY one account revokes the shared login at the server, so the pool
|
|
261
|
+
retires it in every account (`login: none — retired … (cleared in acct-NN file)`); one
|
|
262
|
+
sign-in in any account shares the new one. `mcp auth-forget NAME` is the same logout from
|
|
263
|
+
the operator's side. `codex-accounts mcp auth-*` is not supported (Codex keeps MCP OAuth in
|
|
264
|
+
its own store).
|
|
265
|
+
- **Rolling it out**: the first share runs at install time (the limits agent starts on
|
|
266
|
+
install), and a build older than this one reads a shared MCP-only file as a live login:
|
|
267
|
+
every setup-token account would then say "Not logged in". Install it only where no older
|
|
268
|
+
build can come back: from the registry once it is published, or by hand at or above the
|
|
269
|
+
registry's version (npm `self-update` never installs an older one). Until then, hold the
|
|
270
|
+
sharing with `CLAUDE_MULTIACC_MCP_OAUTH=0` (`launchctl setenv` before the install for the
|
|
271
|
+
scheduled agents).
|
|
272
|
+
|
|
215
273
|
### Pointing sync somewhere else (or nowhere)
|
|
216
274
|
|
|
217
275
|
The target is no longer baked in. In precedence order:
|
|
@@ -332,7 +390,7 @@ emitted for diagnostics.
|
|
|
332
390
|
| Credential | Where it lives | Class | Can it be copied to another machine? |
|
|
333
391
|
| --- | --- | --- | --- |
|
|
334
392
|
| claude setup-token | `<acct>/server.token` | **portable** | **Yes**; the fleet-distributed credential |
|
|
335
|
-
| claude OAuth login | `<acct>/.credentials.json`, or a macOS Keychain item | machine-local | **No**; refresh tokens rotate |
|
|
393
|
+
| claude OAuth login | `<acct>/.credentials.json`, or a macOS Keychain item, carrying `claudeAiOauth` (MCP OAuth state alone is no login) | machine-local | **No**; refresh tokens rotate |
|
|
336
394
|
| codex ChatGPT login | `<acct>/auth.json` | machine-local | **No**; Codex has no portable credential |
|
|
337
395
|
|
|
338
396
|
Non-portable accounts are not a dead end: they show up as `credential_class:
|
|
@@ -422,6 +480,8 @@ derived from the pool root. Uninstalling an instance removes only that instance'
|
|
|
422
480
|
| `CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY=<s>` | five-hour recovery grace; default 300s (weekly never clears early) |
|
|
423
481
|
| `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
|
|
424
482
|
| `CLAUDE_MULTIACC_AUTORESUME=0` | no [auto-resume](AUTORESUME.md) watcher for launches from this environment. `touch <pool>/autoresume.off` also stops running watchers |
|
|
483
|
+
| `CLAUDE_MULTIACC_MCP_OAUTH=0` | stop sharing MCP OAuth logins across accounts (no merge before exec, no sync in the limits pass); `auth-status` / `auth-forget` still run |
|
|
484
|
+
| `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0` | never refresh MCP OAuth logins — and so share none: a login in many accounts needs ONE refresher, or their clients refresh one rotating grant at once (each account keeps what it holds) |
|
|
425
485
|
| `CLAUDE_ACCOUNTS_ROOT=...` | relocate the pool; legacy spelling is `CLAUDE_ACCOUNTS_DIR` |
|
|
426
486
|
| `CLAUDE_MULTIACC_SYNC_TARGET=...` | sync target, overriding the manifest; `none` = local-only |
|
|
427
487
|
| `CLAUDE_MULTIACC_SYNC_ROOT=... / _SYNC_REPO=...` | remote pool root / addon repo that goes with it |
|
|
@@ -28,6 +28,8 @@ available; a Codex ID is never passed as a Claude native `--resume` ID, or vice
|
|
|
28
28
|
The watcher uses the history already bound to the source client. It does not guess from
|
|
29
29
|
the newest transcript or directory name. Invalid, ambiguous or unavailable source
|
|
30
30
|
history prevents the handoff before the source client is stopped.
|
|
31
|
+
Codex main conversations marked `source=cli` or `source=vscode` are supported when
|
|
32
|
+
resumed in a supervised TUI; their exact conversation and file identity still must match.
|
|
31
33
|
|
|
32
34
|
If another live client holds the same source conversation, the watcher holds to avoid
|
|
33
35
|
creating duplicate destination conversations. Stale process records do not count.
|
|
@@ -40,9 +42,34 @@ Account avoidance is separate for each provider, so Claude's `acct-01` and Codex
|
|
|
40
42
|
|
|
41
43
|
## Permissions and arguments
|
|
42
44
|
|
|
43
|
-
The working directory is preserved.
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
The working directory is preserved. Session selectors, the original launch prompt and the
|
|
46
|
+
source's own model and effort flags are not replayed as destination CLI arguments. The
|
|
47
|
+
destination starts on the model and effort of the **same level** instead. Both are read from
|
|
48
|
+
the validated source history, up to its snapshot boundary, and matched through the
|
|
49
|
+
[model catalog](../README.md#auto-resume):
|
|
50
|
+
|
|
51
|
+
- **Models match by level** (operator's map, 2026-10-14):
|
|
52
|
+
|
|
53
|
+
| Codex source | Claude destination | | Claude source | Codex destination |
|
|
54
|
+
|---|---|---|---|---|
|
|
55
|
+
| astra | opus | | fable, mythos | astra |
|
|
56
|
+
| sol | sonnet | | opus | astra |
|
|
57
|
+
| terra | haiku | | sonnet | sol |
|
|
58
|
+
| luna | haiku | | haiku | terra |
|
|
59
|
+
|
|
60
|
+
The catalog encodes it as levels: Claude haiku 2, sonnet 3, opus 4, fable 5 (mythos counts
|
|
61
|
+
as fable); Codex luna 1, terra 2, sol 3, astra 4. Equal levels match; a level with no
|
|
62
|
+
family on the other side (luna, fable) takes the nearest one, the higher on a tie. The
|
|
63
|
+
match is that family's newest listed model, whatever its version (launches are staggered,
|
|
64
|
+
so a family a release behind is still its own level). A retiring model counts as the model
|
|
65
|
+
the catalog upgrades it to.
|
|
66
|
+
- **Efforts match by name.** low, medium, high, xhigh and max carry over unchanged. Codex's
|
|
67
|
+
`ultra` becomes `max`, and `minimal` or `none` becomes `low`. The result is clamped to the
|
|
68
|
+
destination model: the nearest supported level below, else its lowest.
|
|
69
|
+
- **Unknown means the destination's default.** A model or effort the history does not
|
|
70
|
+
record, or the catalog cannot place, is left out, and the destination CLI applies its own
|
|
71
|
+
default. Claude takes either flag alone. Codex takes both or neither, with the matched
|
|
72
|
+
model's default effort when only the model is known.
|
|
46
73
|
|
|
47
74
|
An explicit permission bypass maps to the equivalent destination bypass. Merely allowing
|
|
48
75
|
a bypass flag is not treated as enabling it. Ordinary launches use explicit conservative
|
package/docs/AUTORESUME.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Auto-resume (interactive sessions in tmux)
|
|
2
2
|
|
|
3
3
|
An interactive `claude` or `codex` session that stops on a usage limit, a failed login, an
|
|
4
|
-
API error or (claude only) a crash
|
|
4
|
+
API error or (claude only) a crash or [critical memory usage](#claude-critical-memory) is
|
|
5
|
+
restarted automatically on another pooled account
|
|
5
6
|
with headroom. Within the same provider it keeps the **same session id, directory and
|
|
6
7
|
flags**, and you press no keys. When a quota recovery has no usable account left in that
|
|
7
8
|
provider, it can continue in the other provider using the source conversation's history.
|
|
@@ -69,7 +70,12 @@ shell: runs the shim again → token → marker on the old account → normal
|
|
|
69
70
|
2. **The watcher reads what the client already writes.**
|
|
70
71
|
- **Claude:** `<acct>/sessions/<pid>.json` names the session. The watcher re-reads it
|
|
71
72
|
every tick, so it follows `/clear` and `/resume` inside the TUI. The transcript
|
|
72
|
-
under `projects/` records every API error.
|
|
73
|
+
under `projects/` records every API error of the main thread. Subagents and workflow
|
|
74
|
+
agents keep their own transcripts beside it; the watcher follows those too, but only
|
|
75
|
+
for the account's own verdicts (see
|
|
76
|
+
[subagent transcripts](#claude-subagent-and-workflow-transcripts)). The client's
|
|
77
|
+
critical-memory warning reaches no transcript, so for that one the watcher reads its
|
|
78
|
+
own pane (see [critical memory](#claude-critical-memory)).
|
|
73
79
|
- **Codex:** the rollout under `sessions/` is found in one of three ways, in order:
|
|
74
80
|
- the `resume` id;
|
|
75
81
|
- the native process's open files;
|
|
@@ -81,16 +87,23 @@ shell: runs the shim again → token → marker on the old account → normal
|
|
|
81
87
|
transcript never fires. An automatic Codex restart also carries the previous
|
|
82
88
|
rollout's inode and byte offset: the new watcher starts after those bytes, so a
|
|
83
89
|
quota event from the same second cannot trigger another rotation.
|
|
84
|
-
- Subagent (sidechain) records are ignored.
|
|
90
|
+
- Subagent (sidechain) records in the main transcript are ignored.
|
|
85
91
|
3. **It waits for the error to settle.**
|
|
86
92
|
- **Grace period:** a verdict has to stand for 5 s.
|
|
87
93
|
- **Cancellation:** a message you type cancels it. So does the client's own
|
|
88
|
-
auto-continue producing a normal reply.
|
|
89
|
-
|
|
94
|
+
auto-continue producing a normal reply. A slash command that only shows or
|
|
95
|
+
configures the session (`/model`, `/effort`, `/usage`, `/status`, `/mcp` …) cancels
|
|
96
|
+
only a model-limit or transient verdict: `/model` is how you handle one model's
|
|
97
|
+
limit, but a usage-limited, logged-out or refused account is just as stuck after it.
|
|
98
|
+
Any other command counts as typing: `/login` and `/limit-reset` are how you fix the
|
|
99
|
+
account itself. A verdict a subagent recorded is also cancelled by a newer reply in
|
|
100
|
+
any subagent transcript. For codex, a new user message, `task_started` or
|
|
101
|
+
`turn_aborted` cancels it.
|
|
90
102
|
- **Transient errors** wait longer: 30 s, then 60 s, then 120 s on repeats.
|
|
91
103
|
- **Model-limit and transient errors** also wait until no subagent transcript has
|
|
92
104
|
changed for 20 s, for up to 15 min. That background work may be healthy, or running
|
|
93
|
-
on another model.
|
|
105
|
+
on another model. An agent meeting the account's own limit meanwhile ends the wait:
|
|
106
|
+
its verdict outranks the main thread's (see [subagents](#claude-subagent-and-workflow-transcripts)).
|
|
94
107
|
4. **It asks before it acts.** The watcher runs the shim in *probe mode*
|
|
95
108
|
(`CLAUDE_MULTIACC_AR_PROBE=1`). This is the same candidate loop and the same argv the
|
|
96
109
|
relaunch will use. It prints `pick=acct-NN tier=…` and never execs.
|
|
@@ -159,6 +172,16 @@ shell: runs the shim again → token → marker on the old account → normal
|
|
|
159
172
|
(`projects[<dir>].hasTrustDialogAccepted`). Claude asks "Is this a project you
|
|
160
173
|
trust?" in a directory an account has never opened, even with
|
|
161
174
|
`--dangerously-skip-permissions`, and nobody is there to answer.
|
|
175
|
+
**codex:** it saves `projects[<dir>].trust_level = "trusted"` in the selected
|
|
176
|
+
account's `config.toml` before startup, so the **Folder access / Trust this
|
|
177
|
+
folder?** screen cannot block the continuation. This also covers a handoff from
|
|
178
|
+
Claude. Only a consumed automatic relaunch token enables this step; normal
|
|
179
|
+
launches keep their existing trust decisions. The logical, physical and on-disk
|
|
180
|
+
spellings of the directory are covered (Codex looks the canonical one up first), an
|
|
181
|
+
explicit `untrusted` answer is overridden (nobody is there to answer again), and
|
|
182
|
+
unrelated settings and file permissions are preserved. An unreadable, invalid or
|
|
183
|
+
concurrently changed config is left untouched and logs `autoresume trust-failed`;
|
|
184
|
+
Codex then asks on its own screen and the watcher gives up on the missing prompt.
|
|
162
185
|
6. The new pick gets a watcher of its own.
|
|
163
186
|
7. For Codex, the old watcher waits up to 30 seconds for the exact continuation
|
|
164
187
|
prompt to appear as a new user message in the same rollout. Only then does it log
|
|
@@ -180,7 +203,8 @@ The relaunch starts from the original argv:
|
|
|
180
203
|
`resume <id>|--last`) and the original prompt are removed.
|
|
181
204
|
- `--resume <sid> "<prompt>"` is appended. For codex the command becomes
|
|
182
205
|
`resume <options> <sid> "<prompt>"`.
|
|
183
|
-
-
|
|
206
|
+
- The session's own model and effort are carried over, as described below, in place of
|
|
207
|
+
launch options that a `/model` or `/effort` switch made stale.
|
|
184
208
|
- Codex's explicit or automatically selected `--no-daemon` is kept.
|
|
185
209
|
|
|
186
210
|
The prompt is submitted automatically:
|
|
@@ -191,11 +215,45 @@ The prompt is submitted automatically:
|
|
|
191
215
|
> may still be running. Inspect its state before relying on it or restarting it, and do
|
|
192
216
|
> not duplicate active or completed work.
|
|
193
217
|
|
|
194
|
-
- **The reason** follows the class.
|
|
195
|
-
- **Transient and
|
|
218
|
+
- **The reason** follows the class; a memory restart's names the size the banner showed.
|
|
219
|
+
- **Transient, crash and memory restarts** drop "on another account".
|
|
196
220
|
- **To replace the whole text**, set `CLAUDE_MULTIACC_AUTORESUME_PROMPT` or
|
|
197
221
|
`CODEX_MULTIACC_AUTORESUME_PROMPT`.
|
|
198
222
|
|
|
223
|
+
### Model and effort
|
|
224
|
+
|
|
225
|
+
The watcher reads the model and effort from the session's own history when it decides to
|
|
226
|
+
relaunch, so a `/model` or `/effort` switch made after the launch is honoured:
|
|
227
|
+
|
|
228
|
+
- **Claude:** the last main-thread assistant record, never a subagent's or a meta record.
|
|
229
|
+
The model is its `requestedModel`, else `message.model`. The effort is its `effort`, else
|
|
230
|
+
`perTurnEffort`. A `/model` switch with no reply after it is not visible yet, because only
|
|
231
|
+
assistant records carry the model.
|
|
232
|
+
- **Codex:** whichever comes last of a `turn_context` (`model`, `effort`) and a
|
|
233
|
+
`thread_settings_applied` event for the rollout's own thread (`model`,
|
|
234
|
+
`reasoning_effort`). This is the rule Codex's own state database follows.
|
|
235
|
+
|
|
236
|
+
They become flags placed before the session id:
|
|
237
|
+
|
|
238
|
+
- **Claude** gets `--effort E`, because `--resume` never restores the effort. It gets no
|
|
239
|
+
model flag: `--resume` restores the session's model itself, and an explicit `--model`
|
|
240
|
+
would turn that restore off and put the shim in explicit-model mode, where its
|
|
241
|
+
model-scoped fallback (the Fable bucket) no longer applies. A launch `--model` is replayed
|
|
242
|
+
as given until the history shows the session ran on another model; then it is dropped and
|
|
243
|
+
`--resume` takes over. The relaunch file names that model (`model=`), so the shim pins its
|
|
244
|
+
fallback model only for a session that is on the limited model itself: an Opus session
|
|
245
|
+
relaunched onto an account whose Fable bucket is full resumes on Opus, not the fallback.
|
|
246
|
+
- **Codex** gets `-m M -c model_reasoning_effort=E`, always both or neither: a model or an
|
|
247
|
+
effort flag alone makes Codex drop the thread's other saved value. An effort the history
|
|
248
|
+
does not name becomes that model's default from the catalog; with no default either, the
|
|
249
|
+
launch's own model and effort options are dropped too, and Codex restores both from the
|
|
250
|
+
thread.
|
|
251
|
+
|
|
252
|
+
The [model catalog](../README.md#auto-resume) also clamps an effort to the levels that
|
|
253
|
+
model supports. A value the history does not record is left out, and a history that cannot
|
|
254
|
+
be read relaunches with the launch's own options, as before. The `switch` line records
|
|
255
|
+
what the relaunched client was given (`model=`, `effort=`).
|
|
256
|
+
|
|
199
257
|
## What counts as what
|
|
200
258
|
|
|
201
259
|
**Claude**, from the transcript's API-error records:
|
|
@@ -207,10 +265,18 @@ The prompt is submitted automatically:
|
|
|
207
265
|
| auth | `authentication_failed` | `.expired`, the shim's client-reported auth park | 1 h |
|
|
208
266
|
| blocked | `oauth_org_not_allowed` | persistent `.expired` `org-blocked` park | 6 h in this chain |
|
|
209
267
|
| blocked | `account_on_hold`, `billing_error`, `verification_required` | nothing: only this chain avoids it | 6 h |
|
|
210
|
-
| transient | `overloaded`, `server_error`, `unknown`, any other `rate_limit` | nothing | none, so the same account may continue |
|
|
268
|
+
| transient | `overloaded`, `server_error`, `unknown`, `rateLimitType` `concurrents` (too many requests in flight at once; the account serves the next one), any other `rate_limit` | nothing | none, so the same account may continue |
|
|
211
269
|
| crash | the process ended on its own after ≥ 60 s, leaving its session registry behind | nothing | none, so normal selection applies |
|
|
270
|
+
| memory | not a record: the client's `Critical memory usage (<size>)` banner in its own pane ([critical memory](#claude-critical-memory)) | nothing | none, so normal selection applies |
|
|
212
271
|
| never | anything else (`invalid_request`, `max_output_tokens`, `model_not_found`, …) | nothing | not restarted; logged once |
|
|
213
272
|
|
|
273
|
+
An auth verdict parks the credential the stopped run carried: the launching shim writes
|
|
274
|
+
`via=token|login` into the watcher's state, and the relaunch file carries it (`via=`), so
|
|
275
|
+
a portable token that failed is retired as `setup-token-invalid` (its proof dropped) and a
|
|
276
|
+
login gets `auth-error`, even in a session the client switched the run to with `/clear`
|
|
277
|
+
or `/resume`, which no session-index line names. A relaunch file without `via=` (an older
|
|
278
|
+
watcher) falls back to the session's index line.
|
|
279
|
+
|
|
214
280
|
An organization refusal carries `rtype=oauth_org_not_allowed` into the relaunch file,
|
|
215
281
|
so the shim parks the old account even if its earlier session-index capture missed it.
|
|
216
282
|
An admin must enable access, then full `claude-accounts verify` must prove it; a PASS
|
|
@@ -247,9 +313,188 @@ Codex differs from claude in two ways:
|
|
|
247
313
|
- at most 20 relaunches;
|
|
248
314
|
- 8 rotations per hour;
|
|
249
315
|
- 3 transient restarts per hour;
|
|
250
|
-
- 2 crash restarts per 10 minutes
|
|
316
|
+
- 2 crash restarts per 10 minutes;
|
|
317
|
+
- 2 memory restarts per hour.
|
|
318
|
+
|
|
319
|
+
Over budget, the watcher leaves the session alone. Over the memory budget it gives up on
|
|
320
|
+
that process's memory for good (logged once) and goes on watching for everything else.
|
|
321
|
+
|
|
322
|
+
### Claude subagent and workflow transcripts
|
|
323
|
+
|
|
324
|
+
An account limit can reach only the background agents. On 2026-10-19 two sessions sat
|
|
325
|
+
idle behind paused workflows: the workflow agents had recorded the five-hour limit, the
|
|
326
|
+
client knew it and sent no further main-thread request, and the main transcript showed
|
|
327
|
+
nothing to act on. So the watcher also follows the session's
|
|
328
|
+
`<sid>/subagents/agent-*.jsonl` (Agent tool subagents) and
|
|
329
|
+
`<sid>/subagents/workflows/wf_*/agent-*.jsonl` (workflow agents) beside the transcript.
|
|
330
|
+
|
|
331
|
+
- **Only the account's own verdicts count there:** quota (`five_hour` / `seven_day`
|
|
332
|
+
rejected), auth (`authentication_failed`) and blocked (the codes in the table above).
|
|
333
|
+
A subagent's model-scoped limit, transient error or any other error is its own and is
|
|
334
|
+
ignored, and so are its user records: they are its parent's prompts, never a human.
|
|
335
|
+
- **From this launch only:** a record needs a timestamp from this launch, and a
|
|
336
|
+
rejection one from at least a second after it. The main thread's two seconds of slack
|
|
337
|
+
before the launch would admit the last rejections of the process a relaunch replaced:
|
|
338
|
+
its workflow agents meet a limit in bursts that span seconds and run on until it is
|
|
339
|
+
stopped, a second or two before the relaunch. A transcript last written before the
|
|
340
|
+
launch is followed from its end.
|
|
341
|
+
- **The main thread comes first, unless the account is out.** A main-thread quota, auth
|
|
342
|
+
or blocked verdict stands over any subagent's. Several agents meeting the same limit make
|
|
343
|
+
one verdict. An account verdict, though, outranks a model-limit or transient one,
|
|
344
|
+
whichever thread met it first: the account serves the main thread no better, and the
|
|
345
|
+
model/transient verdict would wait on agents that are only being refused. On 2026-10-20 a
|
|
346
|
+
session sat six minutes behind its main thread's `concurrents` rejection while all its
|
|
347
|
+
agents met the five-hour limit. The outranked verdict waits behind it and stands
|
|
348
|
+
again only if that one goes without the main thread having moved on: an agent's newer
|
|
349
|
+
reply answers the account, or a quota reset passes. Anything the main thread does next
|
|
350
|
+
(typing, `/model`, a reply, another rejection) ends it.
|
|
351
|
+
- **The latest reset is the one marked.** Rejections of one class — main thread and
|
|
352
|
+
agents alike — are one verdict, marked with the latest reset any of them named and that
|
|
353
|
+
reset's type: a five-hour limit in one transcript and the weekly one in another keep the
|
|
354
|
+
account out until the weekly one lifts.
|
|
355
|
+
- **A limit that has reset is no verdict.** A quota rejection whose `resetsAt` has passed
|
|
356
|
+
is dropped when it is read, and a pending one whose reset passes while it waits (the
|
|
357
|
+
grace, a hold for want of room) is dropped then, never acted on; a rejection without a
|
|
358
|
+
`resetsAt` stands as before. This holds for the main thread too.
|
|
359
|
+
- **Cancellation:** a real reply to a request sent after the verdict, in the main thread
|
|
360
|
+
or in any subagent transcript, or a message you type. A reply counts from when its
|
|
361
|
+
request was sent: the `api-request` record the client writes right before each request,
|
|
362
|
+
which the reply names by `requestRef` (the reply's own timestamp when there is none). A
|
|
363
|
+
stream the account accepted before the limit can end many minutes after it, and an
|
|
364
|
+
agent's final answer streamed that way, the last record a paused workflow writes, must
|
|
365
|
+
not stand for the account serving again. A slash command that only shows or configures
|
|
366
|
+
the session never counts, any other one (`/login`, `/limit-reset` …) does, and a
|
|
367
|
+
`<synthetic>` record (the client's own words, not the API's) never does. Records older
|
|
368
|
+
than the verdict cancel nothing, whatever order the transcripts are read in.
|
|
369
|
+
- **A restarted watcher replays safely.** A watcher started onto a session launched hours
|
|
370
|
+
ago reads everything written since that launch. While nothing is pending it skips the
|
|
371
|
+
main thread's lines unparsed (its request records excepted), but the latest real reply
|
|
372
|
+
or typed message among them still counts: an agent's rejection older than it never
|
|
373
|
+
becomes a verdict. An old workflow limit cannot stop a session whose main thread has
|
|
374
|
+
been served since. Nor can one whose agents have been: a subagent's verdict is acted on
|
|
375
|
+
only once every followed transcript is read to its end, which can take a minute of
|
|
376
|
+
ticks for a session with hundreds of megabytes of agent transcripts.
|
|
377
|
+
- **It waits for the main thread's work in flight.** A main-thread verdict comes from the
|
|
378
|
+
main thread's own rejected request, so whatever it was doing has ended by then; an
|
|
379
|
+
agent's verdict can come mid-tool or mid-stream. So it is not acted on while the
|
|
380
|
+
main thread's newest request (`querySource` `repl_main_thread`) has nothing back yet,
|
|
381
|
+
while that request's reply is still streaming (its newest block has `"stop_reason":
|
|
382
|
+
null`, or says `tool_use` before the tool_use block is written), or while a tool the
|
|
383
|
+
reply started has no `tool_result` yet. Stopping the client would kill the tool's
|
|
384
|
+
processes with it. This is keyed by request, never by the file's order: the client
|
|
385
|
+
writes the request record as it sends, sometimes before the prompt that led to it and
|
|
386
|
+
before the previous reply's last blocks. An API error after the request, the client's
|
|
387
|
+
`[Request interrupted by user` record, or a finished reply ends the wait. So does a
|
|
388
|
+
main-thread rejection of its own, which then stands in the agent's verdict's place. The
|
|
389
|
+
wait is capped at `CLAUDE_MULTIACC_AR_SUBAGENT_CAP` (15 min) from when the verdict
|
|
390
|
+
became pending; a tool waiting for a permission nobody gives waits that long. The
|
|
391
|
+
watcher keeps the main transcript's newest 4 MB of lines for this and reads the file no
|
|
392
|
+
second time; the wait is logged once, as `hold reason=main-<request|stream|tool>`. An
|
|
393
|
+
idle main thread (a finished reply, queued task notices, `/model`) moves at the grace as
|
|
394
|
+
before. The agents' own streams and tools are not waited for: the paused workflow ends
|
|
395
|
+
with the session either way.
|
|
396
|
+
- **Everything after that is the main thread's path:** grace, budgets, probe, hold,
|
|
397
|
+
cross-provider continuation, the old account's marker and the `switch` line. The
|
|
398
|
+
`detect` line adds `src=sidechain agent=<agentId>`.
|
|
399
|
+
- **Bounds:** new transcripts are looked for every 5 s
|
|
400
|
+
(`CLAUDE_MULTIACC_AR_SUBAGENT_SCAN`), in a walk of at most 5000 entries. The 128 most
|
|
401
|
+
recently written are followed and share 16 MB of reading per tick; they are read only
|
|
402
|
+
once the main transcript is caught up. Only their API-error and request lines and each
|
|
403
|
+
read's latest reply are parsed, the reply found from the read's end. A workflow's
|
|
404
|
+
`journal.jsonl` and an agent's `*.meta.json` are not transcripts.
|
|
405
|
+
|
|
406
|
+
### Claude critical memory
|
|
407
|
+
|
|
408
|
+
Claude Code checks its own heap every 10 s (2.1.304: `process.memoryUsage().heapUsed`). From
|
|
409
|
+
2.5 GiB on it shows, in red, in the notification column at the right of the prompt's footer:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
⏵⏵ bypass permissions on · 3 shells Critical memory usage (8.3GB) — restart and resume with claude --continue, or run /compact
|
|
413
|
+
```
|
|
251
414
|
|
|
252
|
-
|
|
415
|
+
It writes nothing of this to the transcript. On 2026-10-20 a session running eight
|
|
416
|
+
background workflows reached 8.3GB. The fix is what the banner says, so the operator's rule
|
|
417
|
+
is the usage-limit one: the session is restarted and resumed automatically.
|
|
418
|
+
|
|
419
|
+
- **Where it is read:** the watcher reads its own pane, read-only, with `tmux capture-pane
|
|
420
|
+
-p -J` on the socket and pane from its state file. It never sends a key. It reads only
|
|
421
|
+
while the pane is still the launching shell's and the client runs in the foreground of its
|
|
422
|
+
terminal (not suspended), and only for a session it can name to `--resume`.
|
|
423
|
+
- **What counts:** only the footer of the prompt's box. The box is found by its shape:
|
|
424
|
+
a rule across the pane from its first column, right under it the prompt's row (the mode
|
|
425
|
+
indicator in the first column, `❯`, `!` in shell mode or the agent a teammate view names,
|
|
426
|
+
then the client's no-break space), the draft's other rows indented past the indicator, and
|
|
427
|
+
the rule that closes it. The footer is the rows under that rule, down to the first blank
|
|
428
|
+
row. The conversation is above the box and the draft inside it, so the banner's words in a
|
|
429
|
+
reply, a quoted log line, a tool's output or the prompt never count. Neither do the
|
|
430
|
+
background tasks, listed after that blank row. A dialog drawn in the box's place (a
|
|
431
|
+
permission request, a plan to approve) has a top rule only, its title and body indented
|
|
432
|
+
under it. It opens no box, so nothing it quotes counts, and the same goes for a picker or
|
|
433
|
+
a question. The match is the client's
|
|
434
|
+
own shape: `Critical memory usage (<size>)`, with the size in the client's format (`8.3GB`,
|
|
435
|
+
`12GB`, `900MB`), then ` — restart and resume with claude --continue` with its em dash,
|
|
436
|
+
whole or cut at an ellipsis. A hyphen or an en dash after the size is not the banner. The
|
|
437
|
+
banner is the notification column's last line, so it ends its row: only the client's own
|
|
438
|
+
pills may follow it, a stashed prompt's (`› stashed`) and an active goal's
|
|
439
|
+
(`◎ /goal active (5m)`). The banner can be on the mode line's row or, when the pane is too
|
|
440
|
+
narrow, on a footer row of its own (the notification column wraps). It can also directly
|
|
441
|
+
follow the mode line, which is then cut short too. A row of the background-task list never
|
|
442
|
+
counts, even with no blank row above it: it starts with the selection pointer or two spaces,
|
|
443
|
+
an agent's tree connector, then the task's bullet (`◯`, `⏺` or `●` for the task viewed,
|
|
444
|
+
`⏸` for a paused workflow), and a task's name and description are the model's own words.
|
|
445
|
+
Escape sequences are ignored, and wrapped rows are joined. A banner cut before its size
|
|
446
|
+
closes goes unseen.
|
|
447
|
+
- **When:** every `CLAUDE_MULTIACC_AR_MEMORY_SCAN` seconds (30; `0` turns the memory watch
|
|
448
|
+
off), and never before the launch is `CLAUDE_MULTIACC_AR_MEMORY_MIN_UPTIME` old (900 s).
|
|
449
|
+
A resumed session can come back as heavy as it left, and this keeps it out of a restart
|
|
450
|
+
loop.
|
|
451
|
+
- **Then the main thread's path:** grace, budgets, probe, hold for want of room, the
|
|
452
|
+
relaunch file, the stop and the typed relaunch, with these differences:
|
|
453
|
+
- **Any account may take the session**, the one it is on included, exactly as after a
|
|
454
|
+
crash. Nothing is marked on the old account and the chain avoids nothing new: the shim
|
|
455
|
+
runs ordinary selection.
|
|
456
|
+
- **The transcript's own verdict goes first.** A usage limit, login failure or API error
|
|
457
|
+
pending at the same time moves the session instead, which also ends the heavy process.
|
|
458
|
+
- **The main thread's work in flight is waited for:** a request with nothing back, a reply
|
|
459
|
+
still streaming, a tool with no result (the same rules as for an agent's verdict), and a
|
|
460
|
+
compaction under way. `/compact` is the banner's other remedy, and the transcript shows
|
|
461
|
+
nothing of it until it is done, so the pane's spinner above the prompt box counts:
|
|
462
|
+
`Compacting conversation…`. The wait is logged once as
|
|
463
|
+
`hold reason=main-<request|stream|tool|compact>`. Each wait is capped at
|
|
464
|
+
`CLAUDE_MULTIACC_AR_MEMORY_BUSY_CAP` (600 s), since the process may not last much
|
|
465
|
+
longer. The cap counts from the first reading that found that work, not from the
|
|
466
|
+
detection. Time the verdict spent held for want of room, or behind a transcript verdict,
|
|
467
|
+
uses none of it, and work started later gets a full cap of its own. A main thread found
|
|
468
|
+
idle ends its wait. Background agents and workflows are not waited for: they run
|
|
469
|
+
continuously, and they end with the session either way.
|
|
470
|
+
- **The banner going away ends the verdict.** After `/compact` the heap can fall back under
|
|
471
|
+
the line. The pane is read again on each scan and once more right before the stop. A
|
|
472
|
+
banner that comes and goes (a heap near the line, a pane read that fails) changes only the
|
|
473
|
+
memory verdict: a transcript verdict pending meanwhile keeps its own probes and hold lines.
|
|
474
|
+
- **A check that refuses the stop drops only the memory verdict.** The checks before any
|
|
475
|
+
stop still apply: the pane and its shell, the client in the foreground and leading its
|
|
476
|
+
process group, no other client holding the session, its directory, no synchronized panes
|
|
477
|
+
or stuck mode, and the relaunch file written. When one refuses a usage limit, a login
|
|
478
|
+
failure or a crash, the watcher gives up, as always. When one refuses a memory restart,
|
|
479
|
+
the watcher logs `giveup reason=memory-<why> class=memory` once and drops the verdict.
|
|
480
|
+
It then does not read the pane for `CLAUDE_MULTIACC_AR_MEMORY_BACKOFF` seconds (600), and
|
|
481
|
+
keeps watching the session for everything else.
|
|
482
|
+
- **At most `CLAUDE_MULTIACC_AR_MEMORY_PER_HOUR` (2) memory restarts per chain per hour.**
|
|
483
|
+
Beyond that the watcher logs `giveup reason=memory-budget class=memory` once and leaves
|
|
484
|
+
that process's memory alone, but keeps watching for every other verdict.
|
|
485
|
+
- **The prompt says why:** "…restarted automatically because the previous process reached
|
|
486
|
+
critical memory usage (8.3GB). …", without "on another account".
|
|
487
|
+
- **A client that ignores SIGTERM** (the 8.3GB one did, for over 15 s) gets SIGKILL after
|
|
488
|
+
`CLAUDE_MULTIACC_AR_TERM_GRACE` (10 s), like any stop. The relaunch is typed once the
|
|
489
|
+
client and the processes recorded with it are gone and the shell is back. Tearing a huge
|
|
490
|
+
process down can take the kernel a few seconds; `CLAUDE_MULTIACC_AR_PANE_WAIT` (15 s)
|
|
491
|
+
allows for that.
|
|
492
|
+
- **An optional backstop,** off by default: with `CLAUDE_MULTIACC_AR_MEMORY_LIMIT_MB=<MB>`
|
|
493
|
+
set, the client's own memory at or over that many megabytes counts too
|
|
494
|
+
(`code=memory_limit src=process`), read without a child process: its phys_footprint
|
|
495
|
+
(Activity Monitor's Memory) through libproc on macOS, VmRSS + VmSwap on Linux. That is
|
|
496
|
+
the whole process: more than the heap the banner measures, so set the limit well above
|
|
497
|
+
2.5 GiB.
|
|
253
498
|
|
|
254
499
|
## What gets a watcher
|
|
255
500
|
|
|
@@ -266,7 +511,8 @@ launch.
|
|
|
266
511
|
`--effort <e>`, `--permission-mode <m>`, and at most one prompt argument that
|
|
267
512
|
contains a space.
|
|
268
513
|
- **codex:** `--dangerously-bypass-approvals-and-sandbox`, `--yolo`, `--no-daemon`,
|
|
269
|
-
`-m` / `--model <m>`,
|
|
514
|
+
`-m` / `--model <m>`, `-c model_reasoning_effort=<level>` (the only `-c` accepted,
|
|
515
|
+
with a lowercase level), `resume <uuid>|--last`, and at most one prompt that contains a
|
|
270
516
|
space. The conservative pair `--sandbox read-only --ask-for-approval on-request`
|
|
271
517
|
is also supported; partial, conflicting or other sandbox/approval combinations are not.
|
|
272
518
|
- **Anything else means no watcher.** That includes `-p` (for codex, `-p` is
|
|
@@ -293,13 +539,14 @@ tokens.
|
|
|
293
539
|
| Event | Written by | Meaning |
|
|
294
540
|
| --- | --- | --- |
|
|
295
541
|
| `watch` | watcher | supervision started for a pid |
|
|
296
|
-
| `detect` | watcher | a classified error is pending |
|
|
542
|
+
| `detect` | watcher | a classified error is pending (`src=sidechain agent=<id>` when a subagent's transcript recorded it); `class=memory code=critical_memory size=<size> src=pane` for the client's memory banner (`code=memory_limit src=process` for the opt-in backstop) |
|
|
297
543
|
| `never` | watcher | an error auto-resume does not handle; ignored |
|
|
298
|
-
| `hold` | watcher | no other account has headroom,
|
|
299
|
-
| `switch` | watcher | stopped the session and typed the relaunch (`from=`, `class=`, `sid=`, `depth=`) |
|
|
544
|
+
| `hold` | watcher | the verdict stands but nothing is stopped yet: no other account has headroom (`reason=no-room`), a budget is spent, or an agent's or a memory verdict waits for the main thread's own work (`reason=main-request`, `main-stream`, `main-tool`; a memory verdict also `main-compact`) |
|
|
545
|
+
| `switch` | watcher | stopped the session and typed the relaunch (`from=`, `class=`, `sid=`, `depth=`; `model=` / `effort=` when the relaunch pins them; `size=` for a memory restart) |
|
|
300
546
|
| `crash` | watcher | claude ended unexpectedly; relaunching |
|
|
301
|
-
| `giveup` | watcher | could not relaunch safely (`reason=` pane, shell, pgrp, holder, tree, relaunch, …); with `stopped=1` the session was stopped and `resume=` names the command to run |
|
|
547
|
+
| `giveup` | watcher | could not relaunch safely (`reason=` pane, shell, pgrp, holder, tree, relaunch, …); with `stopped=1` the session was stopped and `resume=` names the command to run. `reason=memory-budget class=memory`: memory restarts are over for that process, and the watcher goes on with every other verdict. `reason=memory-<pane, pane-shell, background, pgrp, holder, cwd, sync, mode, write> class=memory`: that check refused a memory restart before anything was stopped; the verdict is dropped, the pane is not read for `MEMORY_BACKOFF` s, and the watcher goes on |
|
|
302
548
|
| `relaunch` | the relaunched shim | consumed the token (`chain=`, `from=`, `class=`, `depth=`) |
|
|
549
|
+
| `trust-failed` | the relaunched codex shim | could not mark its cwd trusted in the picked account (`chain=`, `acct=`); the TUI may ask for folder access |
|
|
303
550
|
|
|
304
551
|
Cross-provider `switch` records also include `to_provider` and the newly confirmed
|
|
305
552
|
destination conversation's `to_sid`; `sid` continues to identify the source conversation.
|
|
@@ -349,12 +596,22 @@ CLAUDE_MULTIACC_AR_PROBE=1 CLAUDE_MULTIACC_AR_AVOID=acct-07:1790000000 claude
|
|
|
349
596
|
descendants in its process group. Detached work and a pre-existing shared Codex daemon
|
|
350
597
|
may survive; the continuation prompt requires checking before relying on or restarting
|
|
351
598
|
that work. For Claude model-limit and transient errors, the watcher first waits up to
|
|
352
|
-
15 min for subagent transcripts to go quiet.
|
|
599
|
+
15 min for subagent transcripts to go quiet. An account verdict a subagent recorded
|
|
600
|
+
moves the session after the usual 5 s even while the main thread is still running a
|
|
601
|
+
tool: the account would refuse that thread's next request, and the tool is stopped.
|
|
353
602
|
- **Only the pane's own shell is typed into.** The relaunch goes only to the shell that
|
|
354
603
|
launched the session. A session started from a nested shell or a script, or with
|
|
355
604
|
`exec claude`, gets a watcher that gives up instead of relaunching.
|
|
356
605
|
- **Typeahead is discarded.** Text sitting at the shell prompt when the relaunch is typed
|
|
357
606
|
is cleared.
|
|
607
|
+
- **Critical memory is seen only on screen.** The banner is read from the pane, so it goes
|
|
608
|
+
unseen while the prompt box is not drawn (the transcript view, a dialog), when the pane is
|
|
609
|
+
too narrow to show its size, or in a client that words or places it differently. A
|
|
610
|
+
fullscreen client (`"tui": "fullscreen"`, `CLAUDE_CODE_NO_FLICKER=1`; the pool's accounts
|
|
611
|
+
run `"tui": "default"`) draws its notifications over the row above the prompt box. That
|
|
612
|
+
row is among the conversation's, where the banner cannot be told from a reply quoting it,
|
|
613
|
+
so it is not read. The session then stays as it is; `CLAUDE_MULTIACC_AR_MEMORY_LIMIT_MB`
|
|
614
|
+
is the backstop for that.
|
|
358
615
|
- **Codex goals do not follow yet.** Codex keeps `/goal` state per account
|
|
359
616
|
(`goals_1.sqlite`), so set the goal again after a codex rotation. Claude restores a goal
|
|
360
617
|
from the transcript on resume.
|
package/docs/CODEX.md
CHANGED
|
@@ -208,6 +208,7 @@ codex-accounts list
|
|
|
208
208
|
codex-accounts status
|
|
209
209
|
codex-accounts verify --quick # check local auth without inference
|
|
210
210
|
codex-accounts verify # real codex exec call per account
|
|
211
|
+
codex-accounts model-catalog show # the model/effort catalog auto-resume matches with
|
|
211
212
|
codex-accounts --help # all account commands and options
|
|
212
213
|
claude-multiacc codex status # same status command via the npm wrapper
|
|
213
214
|
```
|