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.
Files changed (150) hide show
  1. package/README.md +111 -3
  2. package/bin/claude +323 -84
  3. package/bin/claude-accounts +273 -42
  4. package/bin/codex +15 -0
  5. package/bin/codex-accounts +31 -4
  6. package/docs/ACCOUNT_OPERATIONS.md +62 -2
  7. package/docs/AUTORESUME-HANDOFF.md +30 -3
  8. package/docs/AUTORESUME.md +275 -18
  9. package/docs/CODEX.md +1 -0
  10. package/docs/INSTALLATION.md +7 -1
  11. package/docs/VERIFICATION.md +4 -0
  12. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  45. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  46. package/lib/audit.py +88 -22
  47. package/lib/autoresume.py +1197 -82
  48. package/lib/autoresume_cross.py +3 -1
  49. package/lib/autoresume_handoff.py +82 -10
  50. package/lib/autoresume_probe.py +2 -2
  51. package/lib/claude_client_error.py +9 -4
  52. package/lib/codex_trust.py +263 -0
  53. package/lib/common.sh +131 -3
  54. package/lib/credential.py +5 -2
  55. package/lib/install_actions.sh +19 -0
  56. package/lib/keychain.py +180 -36
  57. package/lib/mcp_oauth.py +1748 -0
  58. package/lib/model_catalog.json +404 -0
  59. package/lib/model_catalog.py +201 -0
  60. package/lib/model_match.py +104 -0
  61. package/lib/report.py +8 -5
  62. package/lib/session_settings.py +123 -0
  63. package/package.json +2 -2
  64. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  65. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  121. package/tests/fake_codex_tui.py +25 -0
  122. package/tests/fake_security.sh +24 -2
  123. package/tests/fixtures/cross_provider_cli.py +111 -1
  124. package/tests/run-tests.sh +1028 -23
  125. package/tests/test_autoresume.py +215 -8
  126. package/tests/test_autoresume_catalog.py +226 -0
  127. package/tests/test_autoresume_catalog_cli.py +185 -0
  128. package/tests/test_autoresume_codex_records.py +16 -0
  129. package/tests/test_autoresume_codex_trust.py +247 -0
  130. package/tests/test_autoresume_cross_guards.py +28 -0
  131. package/tests/test_autoresume_cross_origin.py +70 -0
  132. package/tests/test_autoresume_cross_tmux.py +27 -2
  133. package/tests/test_autoresume_cross_tokens.py +32 -2
  134. package/tests/test_autoresume_handoff_argv.py +120 -16
  135. package/tests/test_autoresume_handoff_origins.py +67 -0
  136. package/tests/test_autoresume_memory.py +1141 -0
  137. package/tests/test_autoresume_memory_tmux.py +94 -0
  138. package/tests/test_autoresume_probe.py +2 -1
  139. package/tests/test_autoresume_relaunch_settings.py +317 -0
  140. package/tests/test_autoresume_settings.py +135 -0
  141. package/tests/test_autoresume_sidechain.py +1518 -0
  142. package/tests/test_autoresume_sidechain_tmux.py +79 -0
  143. package/tests/test_autoresume_trust_tmux.py +63 -0
  144. package/tests/test_claude_client_error.py +10 -7
  145. package/tests/test_claude_org_block.py +1 -0
  146. package/tests/test_claude_pool_prepare.py +4 -1
  147. package/tests/test_claude_reset.py +3 -0
  148. package/tests/test_keychain_nologin.py +340 -0
  149. package/tests/test_mcp_oauth.py +1414 -0
  150. package/tests/test_packaged_commands.py +41 -0
@@ -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
- ulog "self-update: npm $cur -> ${lat:-latest} (prefix $prefix, npm $npm_bin)"
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. Source model names, effort settings, session
44
- selectors and the original launch prompt are not replayed as destination CLI arguments.
45
- The destination uses its own configured model.
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
@@ -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 is restarted automatically on another pooled account
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. For codex, a new user message,
89
- `task_started` or `turn_aborted` cancels it.
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
- - An explicit `--model` from the original launch is kept. No model is added otherwise.
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 crash restarts** drop "on another account".
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
- Over budget, the watcher leaves the session alone.
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>`, `resume <uuid>|--last`, and at most one prompt that contains a
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, so nothing is stopped |
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
  ```