tokenmaxxing 1.38.0 → 1.40.0

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/DESIGN.md CHANGED
@@ -1,121 +1,123 @@
1
1
  # tokenmaxxing - design
2
2
 
3
- Automatic Claude Code account switching. You run `claude` exactly as always; when the active account crosses its swap threshold (**90%** of the 5h session window or **98%** of a weekly window), tokenmaxxing swaps the credential to a fresh account at a safe turn boundary and **your running session adopts it in place - no restart**. Works across many concurrent sessions at once; a fully depleted pool pauses with a countdown and auto-resumes at the soonest reset when it lands within `policy.maxWaitMs` (default 1h; further out, the session stays put rather than parking for hours).
3
+ Automatic Claude Code account switching. You run `claude` exactly as always; every session starts on the pooled account with the most session-window headroom per running session, and when the account a session runs on crosses its bar (**90%** of the 5h session window or **98%** of a weekly window), tokenmaxxing resumes that session on a fresher account at a safe turn boundary. Works across many concurrent sessions at once, each on its own account; a fully depleted pool pauses a session with a countdown and auto-resumes at the soonest reset when it lands within `policy.maxWaitMs` (default 1h; further out, the session stays put rather than parking for hours).
4
4
 
5
- > Scope: **Claude Code first (macOS + Linux, the latter since 2026-07-09).** Codex support landed in 0.13.0 (2026-07-16) with its own parallel state, decision engine, and supervisor; its verified internals live in AGENTS.md's Codex sections.
5
+ > Scope: **Claude Code first (macOS + Linux, the latter since 2026-07-09).** Codex support landed in 0.13.0 (2026-07-16) with its own parallel state and supervisor; since 1.35.0 both run on one engine. Codex's verified internals live in AGENTS.md's Codex sections.
6
6
  >
7
- > Status: **implemented** (v0.1.0, 2026-07-09). TypeScript on Bun, shipped as SOURCE with a bun-shebang bin (the compiled-binary distribution was deleted in 0.2.1); Zod validates every external-boundary payload, JSON config, es-toolkit for utilities, `flock(2)` via `bun:ffi`. All load-bearing external facts were adversarially verified against the `2.1.204` binary + docs (OAuth token endpoint is `platform.claude.com/v1/oauth/token`, client_id `9d1c250a-...`, JSON body). What the acceptance gate actually shows is in §9.
7
+ > Status: **implemented** (v0.1.0, 2026-07-09; per-account stores since 1.37.0, 2026-09-12). TypeScript on Bun, shipped as SOURCE with a bun-shebang bin (the compiled-binary distribution was deleted in 0.2.1); Zod validates every external-boundary payload, JSON config, es-toolkit for utilities, `flock(2)` via `bun:ffi`. Load-bearing external facts were adversarially verified against the `2.1.204` binary + docs when first shipped and re-verified against `2.1.269` for the store model (§2). What the acceptance gates showed is in §9.
8
8
 
9
9
  ---
10
10
 
11
11
  ## 1. Why there is a thin supervisor (and why that's the whole trick)
12
12
 
13
- A **running** `claude` DOES adopt an externally swapped credential (verified live 2026-07-10, correcting this document's original claim): an ensure-fresh poll re-reads the credential store around every request, so a swap lands within ~30s on macOS (raw keychain cache) and on the next request on Linux. A plain swap therefore needs no process management at all (since 0.15.0, 2026-07-16; earlier versions respawned on every swap): the Stop hook swaps the credential and the session keeps running. What adoption cannot give you is the depleted case: when every account is over its bar the session must be PAUSED until something resets, and a live `claude` cannot pause itself.
13
+ Claude Code namespaces its credential by `CLAUDE_SECURESTORAGE_CONFIG_DIR` (verified in the 2.1.269 binary, `xSe`/`y_`): the variable moves only the credential store (a `.credentials.json` inside that directory on Linux, a keychain item named `Claude Code-credentials-<sha256(dir)[0:8]>` on macOS), while `~/.claude` (settings, transcripts, plugins) and `~/.claude.json` stay shared, so `--resume` works across stores. An empty namespaced store never falls back to the default login. Hooks, the statusline, and MCP servers inherit the variable. That is the whole mechanism: give every pooled account its own store, set the variable per session, and each session runs on its own account with no credential ever moving.
14
14
 
15
- So the supervisor's job is narrow: on a depleted pool it **replaces the process at a salvageable moment** - after a turn completes, the conversation is fully written to the transcript JSONL and `claude` is idle at the prompt, so killing it there loses nothing - shows an interruptible countdown to the soonest reset, and relaunches `claude --resume <session-id>` when it passes. **Thresholds still sit below 100%: the headroom is the budget to reach a clean turn boundary (plus up to one turn of adoption lag on macOS) before the account actually hits its real limit.** The session window swaps at 90 (a 5h reset is cheap to sit out), while the weekly windows drain to 98 (weekly allowance is use-it-or-lose-it).
15
+ What the variable cannot give you is a change of account for a running process: the store is fixed at spawn. So the supervisor's job is placement and moves: on launch it **picks the seat** (see §5) and spawns claude under that account's store; when the session's Stop hook decides the seat is over its bar, the supervisor **replaces the process at a salvageable moment** - after a turn completes, the conversation is fully written to the transcript JSONL and `claude` is idle at the prompt, so killing it there loses nothing - and relaunches `claude --resume <session-id>` under the target's store. When the whole pool is over its bars it shows an interruptible countdown to the soonest reset first. **Thresholds sit below 100: the headroom is the budget to reach a clean turn boundary before the account actually hits its real limit.** The session window moves at 90 (a 5h reset is cheap to sit out), while the weekly windows drain to 98 (weekly allowance is use-it-or-lose-it).
16
16
 
17
- A hook can't do the pause-and-relaunch - when `claude` exits, the shell owns the terminal. So tokenmaxxing installs a **supervisor** (aliased to `claude`) that owns the process lifecycle:
17
+ A hook can't do the placement or the relaunch - when `claude` exits, the shell owns the terminal. So tokenmaxxing installs a **supervisor** (aliased to `claude`) that owns the process lifecycle:
18
18
 
19
19
  ```
20
- supervisor (you type `claude`) → real claude (inherited stdio) → Stop hook
21
- ▲_______________ relaunch --resume <sid> ______________|
20
+ supervisor (you type `claude`) → real claude, CLAUDE_SECURESTORAGE_CONFIG_DIR=stores/<uuid8> → Stop hook
21
+ ▲______________ relaunch --resume <sid> under another store ______________|
22
22
  ```
23
23
 
24
- It is a process manager only - spawn with inherited stdio plus saved `stty -g` termios (not a PTY copy), wait, restore the terminal, relaunch. It never proxies API traffic or handles tokens. Everything else about `claude` is unchanged.
24
+ It is a process manager only - spawn with inherited stdio plus saved `stty -g` termios (not a PTY copy), wait, restore the terminal, relaunch. It never proxies API traffic and never handles tokens. Everything else about `claude` is unchanged.
25
25
 
26
26
  ---
27
27
 
28
28
  ## 2. What tokenmaxxing installs
29
29
 
30
30
  - A `claude` **supervisor** on your PATH ahead of the real binary (`~/.config/tokenmaxxing/bin/claude`), or a shell function - you invoke it identically.
31
- - Four `~/.claude/settings.json` entries (merged - other settings keys are preserved, but the `statusLine` slot is taken over): the tokenmaxxing `statusLine` renderer (native since 2026-07-11; it also tees usage), a `subagentStatusLine` (per-subagent rows in the agents panel), a `Stop` hook, a `SessionStart` hook.
31
+ - Five `~/.claude/settings.json` entries (merged - other settings keys are preserved, but the `statusLine` slot is taken over): the tokenmaxxing `statusLine` renderer (native since 2026-07-11; it also tees usage), a `subagentStatusLine` (per-subagent rows in the agents panel), a `Stop` hook, a `StopFailure` hook, a `SessionStart` hook.
32
32
  - **`~/.config/tokenmaxxing/`** - the single home for config and state:
33
33
  - `config.json` - SPARSE overrides only (thresholds.session/weekly, claudeBin/codexBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
34
- - `accounts.json` - non-secret index `{id, label, email, tier, windows, lastUsageAt, needsReauth, oauthAccount, ...}` (each cached window carries its name, duration, reset, and sample time; the same shape serves the Codex pool as `codex-accounts.json`).
35
- - `usage.json` - live usage, written by the statusLine shim.
36
- - `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal, depleted-pool waits only).
34
+ - `accounts.json` - non-secret index `{id, label, email, tier, windows, lastUsageAt, lastProbeAt, enforcedUntil, needsReauth, oauthAccount}` (each cached window carries its name, duration, reset, and sample time; the same shape serves the Codex pool as `codex-accounts.json`). `activeId` is `null` for Claude: there is no pool-wide active account.
35
+ - `stores/<uuid8>/` - one Claude Code credential store per pooled account (see below).
36
+ - `usage/<uuid8>.json` - the statusline tee per account, written by the sessions running on it.
37
+ - `live/<session-id>` - PID-validated presence per supervised session, naming its account; placement counts them.
38
+ - `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal): the target account plus a `waitUntil`.
37
39
  - `bin/claude` - the supervisor.
38
- - Per-account **credentials** follow the platform's Claude Code store: macOS = login-keychain items `tokenmaxxing-cred-<accountUuid[:8]>` (never plaintext on disk); Linux = 0600 files `creds/tokenmaxxing-cred-<accountUuid[:8]>.json` (the same plaintext model claude itself uses - its Linux build has no keyring path at all, binary-verified 2.1.205). One `credstore` facade dispatches on a `{kind: keychain|file}` target; call sites never branch on platform.
40
+ - Per-account **credential stores** follow the platform's Claude Code store: macOS = a login-keychain item keyed by the store path (never plaintext on disk); Linux = a 0600 `.credentials.json` inside the 0700 store directory (the same plaintext model claude itself uses - its Linux build has no keyring path at all, binary-verified 2.1.205). One `credstore` facade dispatches on a `{kind: keychain|file}` target; call sites never branch on platform. tokenmaxxing writes a store once, at onboarding; Claude Code's own refresh is the only writer afterwards.
39
41
 
40
- - A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): hooks alone miss long agentic turns, so the timer is the backstop that keeps switching engaged mid-turn.
42
+ - A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): it folds fresh tees into the index and samples the account whose `/usage` figure is oldest, so the cached figures every placement reads stay fresh.
41
43
 
42
- The switching path runs no long-lived daemon. The statusline pushes usage. Hooks and the supervisor react. The periodic check job above is the only recurring process. The `claude` binary and `~/.claude` layout are untouched.
44
+ The switching path runs no long-lived daemon. The statusline pushes usage. Hooks and the supervisor react. The periodic check job above is the only recurring process. The `claude` binary, `~/.claude`, `~/.claude.json`, and Claude Code's default credential store are untouched.
43
45
 
44
46
  ---
45
47
 
46
- ## 3. How a switch happens
48
+ ## 3. How a move happens
47
49
 
48
50
  ### 3.1 Usage feed (free, push-based)
49
- The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}`, after every turn, 300ms debounce, zero cost). tokenmaxxing's statusLine tees that to `usage.json` (write-on-change, O(ms)) and renders its own native line (it replaced the earlier pass-through delegation on 2026-07-11: install takes the statusLine slot outright, so a pre-existing custom statusline command is overwritten). Cold-start fallback if `usage.json` is absent: `TOKENMAXXING_PROBE=1 claude -p '/usage'`, with `[ -n "$TOKENMAXXING_PROBE" ] && exit 0` as the hook's first line to stop the nested process recursing (hooks fire in `-p` too). The probe scrubs every ambient credential override claude reads before the keychain (`CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, etc.) so it can only meter the credential in the keychain item, and retries the transient empty-footer case (claude prints local stats with no percentages when its own usage fetch throttles).
51
+ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}`, after every turn, 300ms debounce, zero cost). tokenmaxxing's statusLine tees that to `usage/<uuid8>.json` for the session's seat, read from the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` (write-on-change, O(ms)), and renders its own native line (it replaced the earlier pass-through delegation on 2026-07-11: install takes the statusLine slot outright, so a pre-existing custom statusline command is overwritten). A session with no pooled seat writes no tee. Cold-start fallback when the seat has no tee: `TOKENMAXXING_PROBE=1 claude -p '/usage'` under the seat's store, with a throwaway `CLAUDE_CONFIG_DIR` so no hooks load and `[ -n "$TOKENMAXXING_PROBE" ] && exit 0` as every hook's first line to stop the nested process recursing (hooks fire in `-p` too). The probe scrubs every ambient credential override claude reads before the store (`CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, etc.) and then sets the store it means to read, and retries the transient empty-footer case (claude prints local stats with no percentages when its own usage fetch throttles).
50
52
 
51
- ### 3.2 Detect + swap + signal (Stop hook, per turn)
52
- 1. Read `usage.json` and exit while the live account is under both bars, as defined in the [switching policy](docs/content/docs/switching.mdx).
53
- 2. Take the pool lock, re-check state, select a candidate under the [switching policy](docs/content/docs/switching.mdx) from the cached usage figures, and swap the credential.
54
- 3. Done - the running session adopts the new credential on its own within a request or two. Only when the pool is depleted (the decision returned a `waitUntil`: pre-parked on the soonest-recovering account, or staying on the current one when it recovers first) does the hook write `respawn/<session_id>` (atomic temp+rename).
53
+ ### 3.2 Detect + decide + signal (Stop hook, per turn)
54
+ 1. Resolve the seat: the pooled account whose store the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` names. No seat (a session started outside the supervisor) means nothing to move.
55
+ 2. Read the seat's tee and exit while it is under both bars, as defined in the [switching policy](docs/content/docs/switching.mdx).
56
+ 3. Take the pool lock, re-check, fold every account's tee into the index, select a candidate under the switching policy from the cached figures, and verify that the target's store holds a usable credential (a store Claude Code dead-cleared flags the account for re-auth and the next candidate is tried).
57
+ 4. Write `respawn/<session-id>` (atomic temp+rename) naming the target, with `waitUntil` now for a plain move or the soonest reset when the pool is depleted. No credential is written.
55
58
 
56
- ### 3.3 Depleted-pool pause (supervisor)
57
- The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, shows an interruptible countdown to the reset, and relaunches `claude --resume <sid>` when it passes. The resumed process reads the keychain cold → runs on the recovered account, same conversation. The `SessionStart` hook (source `resume`) re-checks the account before the first turn as a backstop. A marker that fails to parse is never deleted and treated as absent. The supervisor stops the child, restores the terminal, and exits naming the marker path.
59
+ ### 3.3 Move and depleted-pool pause (supervisor)
60
+ The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <sid>` with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The resumed process reads that store cold → runs on the target account, same conversation. The `SessionStart` hook (source `resume`) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. A marker that fails to parse is never deleted and treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path.
58
61
 
59
- ### 3.4 Swap sequence (under the lock)
60
- 1. **Harvest the live credential into its TRUE owner's backup** - read the current `Claude Code-credentials` blob and resolve which account it actually belongs to via the profile endpoint (`GET /api/oauth/profile`, matched on `account.uuid`), NOT the `accounts.json` active label and NOT the organization (team seats share one). The label drifts from the live blob (a kill mid-swap, a manual `/login`), and harvesting by label once overwrote another account's backup and destroyed its only credential. Mandatory anyway: Claude rotates the refresh token in place, so older backups are dead. Refuse the swap if the live credential belongs to no pooled account.
61
- 2. **Refresh B** - a blob Claude Code dead-cleared (empty tokens) is dead without a request. Resolve the owner of B's parked token through the profile endpoint first; a token that belongs to another account is never refreshed, because a refresh rotation revokes that account's live sessions, and B is marked `needs_reauth` instead. Then OAuth refresh-grant with B's parked refresh token → fresh access token; persist the rotated refresh token. On `invalid_grant`, mark B `needs_reauth`, notify, try the next account. On any other 4xx, skip B for this evaluation without marking it and try the next account.
62
- 3. **Install B** - `security add-generic-password -U ... 'Claude Code-credentials' ...` with B's fresh (non-expired) `claudeAiOauth` JSON.
63
- 4. **Swap identity + mark B active** - atomically rewrite only the `oauthAccount` object in `~/.claude.json` (temp+rename) to B's, and write `activeId = B` in the SAME critical section, so a crash can't leave the installed credential and the active label pointing at different accounts.
64
- 5. Do steps 1, 3, 4 inside Claude's own refresh locks - the primary `<credDir>/.oauth_refresh.lock` plus the legacy sibling `<realpath(credDir)>.lock` (`~/.claude.lock` by default), both mkdir-based proper-lockfile locks, binary-verified against 2.1.214 - so the writes can't collide with a token refresh. Contention fails the swap fast (claude itself gives up with `lock_timeout` rather than refreshing unlocked); the next check retries.
62
+ ### 3.4 Placement (supervisor, under the pool lock)
63
+ 1. Fold every account's tee into the index.
64
+ 2. Rank the usable accounts (§5) with the live session counts from `live/`, or the soonest-recovering account when none is usable; with no pooled account or every account needing re-auth, launch without a store (unmanaged).
65
+ 3. Spawn claude with the store env set, then write `live/<sid>` with the child's pid and start time. The lock covers the pick and the presence write, so concurrent launches see each other.
65
66
 
66
67
  ### 3.5 Multiple concurrent sessions
67
- Each terminal ran the supervisor, so each has its own child `claude` and its own `--session-id`. When the shared account hits a threshold, the first Stop hook to win the `flock` performs the one swap; every running session then adopts the new credential in place - no restarts. (They share one credential, so they always move together - consistent with "one current account, many windows.") Only a depleted pool fans out: each supervised session's Stop hook writes its own `respawn/<sid>` marker, and each supervisor independently pauses and later relaunches `claude --resume <its-own-sid>`.
68
+ Each terminal ran the supervisor, so each has its own child `claude`, its own `--session-id`, its own seat, and its own presence file. Sessions spread over the pool at launch (the second launch never lands on the first's account while another fresh account exists), sessions on one account share its store and coordinate refreshes through Claude Code's own store-rooted refresh lock, and each session moves on its own seat's figures. A move never touches another session. Only a shared state read is serialized: the pool lock covers decisions, placement, and the sampler's reservation, never a `/usage` child.
68
69
 
69
70
  ---
70
71
 
71
72
  ## 4. Onboarding (no `adopt`)
72
73
 
73
- - **`tokenmaxxing init` imports the account you're already on - automatically, no prompts, no re-login.** It reads the live `Claude Code-credentials` keychain blob plus the `oauthAccount` object in `~/.claude.json` (email, `organizationUuid`, `accountUuid`, plan tier) and writes them as **account #1** into tokenmaxxing's store (`tokenmaxxing-cred-<accountUuid[:8]>` + an `accounts.json` index entry). Nothing about your current session changes - that account stays active; it's now just also a registered pool member. After this one command you already have a working (single-account) pool. `init` also installs the supervisor + the four settings entries.
74
- - If the current auth is API-key mode (`ANTHROPIC_API_KEY`/`apiKeyHelper`) rather than a subscription `/login`, there's no quota-poolable subscription credential to import - `init` says so and points you to `/login` first (per-token API billing isn't what tokenmaxxing pools).
75
- - **`tokenmaxxing add`** - registers *additional* accounts: logs one in via a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard` (your primary login untouched), harvests it into the store, deletes the temp dir + its namespaced item. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway isolated stores like this one: the same harvest serves `auth`, and parked-account `/usage` sampling probes under `sample/<item>` the same way; an AMBIENT `CLAUDE_CONFIG_DIR` is refused by every CLI command.
76
- - **`tokenmaxxing auth [sel | --all]`** - reauthenticates an *existing* pool member whose refresh token died (a needs-reauth account can never heal through a swap: the dead token is exactly what a swap would need). Same isolated-login harvest as `add`, but it states which email to sign in with and **requires the login to land on the target account** (harvested `accountUuid` must match, else nothing changes) - the credential write and the needs-reauth clear happen in one flock critical section so a concurrent swap's harvest cannot clobber the fresh backup. Bare `auth` lists the pool with emails and asks which; `--all` walks every flagged account one by one.
74
+ - **`tokenmaxxing init` logs your first pooled account in through an isolated browser sign-in, even if you are already signed in.** The login runs in a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard`, its token's owner is verified through the profile endpoint (`GET /api/oauth/profile`, matched on `account.uuid`), and the credential is harvested once into that account's store; the throwaway home and its credential item are deleted. The login you already had keeps its own grant in Claude Code's default store for sessions started outside the supervisor; it is never copied, because a copy of one grant in two stores dies at the first refresh. `init` also installs the supervisor + the five settings entries.
75
+ - **`tokenmaxxing add`** - registers *additional* accounts the same way, one store each. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway homes like this one and the probe homes under `sample/`; a session's store is always set through `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
76
+ - **`tokenmaxxing auth [sel | --all]`** - reauthenticates an *existing* pool member whose store died (a needs-reauth account can never heal on its own: the dead grant is exactly what Claude Code's refresh would need). Same isolated-login harvest as `add`, but it states which email to sign in with and **requires the login to land on the target account** (verified `accountUuid` must match, else nothing changes). Bare `auth` lists the pool with emails and asks which; `--all` walks every flagged account one by one.
77
77
  - Both commands exercise `security` reads/writes interactively (where a macOS keychain ACL prompt is acceptable), so the first access never happens cold inside a headless hook.
78
78
 
79
79
  ---
80
80
 
81
- ## 5. Rotation policy
82
- The [switching policy](docs/content/docs/switching.mdx) is one rule: under both bars, hold; at or over a bar, move to the usable account with the highest pace pressure; nothing usable, wait for the soonest reset.
81
+ ## 5. Placement and rotation policy
82
+ The [switching policy](docs/content/docs/switching.mdx) is one rule: under both bars, hold; at or over a bar, move to the usable account that ranks first; nothing usable, wait for the soonest reset. The same ranking places a session at launch.
83
83
 
84
- - Automatic and manual switching rank by pace pressure alone; organization membership is not an input.
85
- - Selection reads the cached figures only. Each periodic check tick samples the parked account whose last `/usage` attempt is oldest, skipping any attempted within `policy.usagePollTtlMs`, so every parked account is attempted about once per tick per pooled account, or once per `policy.usagePollTtlMs` plus a tick when that is longer, and its cached figure is as fresh as its last successful attempt. No sample runs inside the post-swap cooldown. The pool lock covers only the reservation (its owner lookups and refresh under one 20-second deadline) and the result write, never the `/usage` child.
84
+ - Ranking (decided by consult for #79): greatest remaining headroom to the session bar divided by the number of sessions after placement, `(bar - used) / (sessions + 1)`, then highest weekly pace pressure (remaining weekly percent over time to the weekly reset), then soonest weekly reset, then lowest weekly usage; unknown usage ranks last. Organization membership is not an input. Codex keeps the pure pace ordering and excludes accounts running in another codex session, because a codex session cannot share a login.
85
+ - Selection reads the cached figures only, after folding every account's tee. Each periodic check tick samples the account whose last `/usage` attempt is oldest, skipping any attempted within `policy.usagePollTtlMs`, under that account's store, so every account is attempted about once per tick per pooled account, or once per `policy.usagePollTtlMs` plus a tick when that is longer, and its cached figure is as fresh as its last successful attempt or its sessions' latest push. The pool lock covers only the reservation and the result write, never the `/usage` child.
86
86
  - The depleted pause happens at the bar, not at 100; Codex has no pause and rides its account until the server refuses it, because a running sibling cannot adopt another account without restarting.
87
+ - The Claude pool has no post-swap cooldown and no manual `switch`: a move is a respawn of one session under a fixed store, so nothing another session reads is invalidated by it, and placement at launch replaces the manual pick. Codex keeps both.
87
88
 
88
89
  The [profile](docs/content/docs/switching-profile.mdx) records observed cache rewrites across swaps and does not infer subscription quota savings.
89
90
 
90
- **Model-aware trigger.** Claude subscriptions also enforce **per-model weekly caps** - currently only for Sonnet and Fable (there is no Opus-only quota), and Fable's tighter limit binds *before* the aggregate (e.g. 80% week-Fable at only 50% week-all-models). This cap isn't in statusLine stdin, so when the active model is in `policy.switchModels` we read it from `claude -p '/usage'` (free, 0 tokens, TTL-cached) and add `week(<activeModel>) >= threshold` to the trigger. A Fable session switches on the Fable cap; a Sonnet session rides the aggregate. Candidate screening applies the same per-model gate: a burnt Fable cap screens an account out of a switch.
91
+ **Model-aware trigger.** Claude subscriptions also enforce **per-model weekly caps** - currently only for Sonnet and Fable (there is no Opus-only quota), and Fable's tighter limit binds *before* the aggregate (e.g. 80% week-Fable at only 50% week-all-models). This cap isn't in statusLine stdin, so when the active model is in `policy.switchModels` we read it from `claude -p '/usage'` under the seat's store (free, 0 tokens, TTL-cached) and add `week(<activeModel>) >= threshold` to the trigger. A Fable session moves on the Fable cap; a Sonnet session rides the aggregate. Candidate screening applies the same per-model gate: a burnt Fable cap screens an account out of a move, and a launch, whose model is unknown, gates every configured family.
91
92
 
92
93
  ---
93
94
 
94
95
  ## 6. Honest papercuts
95
- - **Respawn hiccup (depleted pause only).** Plain swaps never restart the session. When the whole pool is depleted you see `claude` stop, a countdown, and a resume; anything typed in the split second before the SIGTERM is lost, and the supervisor resets terminal mode so nothing is left garbled.
96
- - **Adoption lag.** macOS reads the keychain through a raw 30s cache, so at most the first turn after a swap can still meter the old account. The bars' headroom absorbs it.
97
- - **One cold turn.** Prompt cache is org-scoped: the first turn on B re-uploads context once (bigger on long transcripts).
98
- - **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn can end rate-limited before the Stop hook swaps; the swap then still recovers the session (its next turn adopts the fresh account). Projected threshold reduces this.
99
- - **Shared blast radius.** All default-profile sessions share one keychain item, so a swap moves them all (each adopts in place). The `flock` + re-check is mandatory or racing hooks burn two accounts at once.
100
- - **Refresh-token rotation / parked-token rot.** Step 1 re-harvest is mandatory; a parked refresh token can be invalidated by logging in elsewhere → picker must catch `invalid_grant`, mark `needs_reauth`, fall through.
96
+ - **A move restarts the process.** Every move is a `claude` stop and `--resume`; anything typed in the split second before the SIGTERM is lost, the supervisor resets terminal mode so nothing is left garbled, and the first turn on the new account re-uploads context once (prompt cache is org-scoped; bigger on long transcripts).
97
+ - **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn ends rate-limited; the StopFailure hook walls the seat and moves the session, and the next prompt continues the conversation on the fresh account. Projected threshold reduces this.
98
+ - **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on whatever login its environment names, by default Claude Code's default store, which tokenmaxxing never writes.
99
+ - **One shared identity file.** `~/.claude.json` keeps a single `oauthAccount`, which Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email. The store decides which credential a session uses.
100
+ - **Dead stores.** A refresh Claude Code cannot complete dead-clears the store; the sampler and the decision recognize the empty tokens without a request, flag the account, and `auth` repairs it in place.
101
101
  - **statusLine fragility.** The native statusline is the most visible surface - a bug flickers or blanks the line for every session (and install takes the slot outright, replacing any custom statusline you had). Keep it O(ms), write-on-change.
102
- - **Keychain blob size & ps-safety.** The live `Claude Code-credentials` item also holds per-MCP-server OAuth state, so it can exceed `security -i`'s ~4KB interactive line buffer (verified on a real machine - a 4.3KB blob truncated). tokenmaxxing therefore stores parked backups as **`claudeAiOauth`-only** (small → always the ps-safe stdin write) and, on a swap, **merges** the fresh `claudeAiOauth` into the *current* live blob so MCP tokens survive the switch - using the argv write path (secret briefly visible in `ps` on your own machine) only for that one oversized live write.
103
- - **settings.json is user-owned.** Install by merge; ship `tokenmaxxing doctor` to verify the supervisor + 4 entries survive a `/config` edit or update.
102
+ - **Keychain blob size & ps-safety.** A store's keychain item also holds per-MCP-server OAuth state once Claude Code writes it, so it can exceed `security -i`'s ~4KB interactive line buffer (verified on a real machine - a 4.3KB blob truncated). tokenmaxxing's one write, the onboarding harvest, stores the **`claudeAiOauth`-only** object (small → always the ps-safe stdin write) and never rewrites a store afterwards.
103
+ - **settings.json is user-owned.** Install by merge; ship `tokenmaxxing doctor` to verify the supervisor + 5 entries survive a `/config` edit or update.
104
104
 
105
105
  ---
106
106
 
107
107
  ## 7. Scope
108
- **v1:** `tokenmaxxing init` / `add` / `ls` (removed in 1.30.0, `status --cached` covers it) / `status` / `doctor`; the supervisor; statusLine shim + Stop/SessionStart hooks; threshold swap with `flock` + reset-aware picker; platform credential store (macOS keychain / Linux 0600 files, one facade); auto-respawn across concurrent sessions. macOS + Linux.
108
+ **v1:** `tokenmaxxing init` / `add` / `ls` (removed in 1.30.0, `status --cached` covers it) / `status` / `doctor`; the supervisor; statusLine shim + Stop/StopFailure/SessionStart hooks; per-session placement and moves by respawn with `flock` + reset-aware picker; platform credential stores (macOS keychain / Linux 0600 files, one facade). macOS + Linux.
109
109
 
110
110
  **v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
111
111
 
112
112
  **Removed (1.23.0):** the programmatic SDK surface, the stdio MCP server, and the Agent Plugin that shipped from 0.11.0 through 1.22.0. The CLI, the hooks, and the supervisor are the only surfaces.
113
113
 
114
- **Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, `codex-creds/`, own flock). Since 1.35.0 the engine is one: a generic account record (`id`, `label`, `email`, `tier`, `windows`), one picker, one decision, and one set of account commands run against a `Provider` (`src/lib/provider.ts`); `src/lib/claude.ts` and `src/lib/codex.ts` hold only what differs per product (credential store, identity, usage source, presence, login and install flows). Codex differences that shaped it: restart IS the switch (a running codex refuses another account's credential), usage is a free direct GET with epoch resets and a duration-classified window set (the weekly window is primary on current plans), the refresh token rotates with reuse punished (harvest-by-true-owner, persist every rotation), and codex's new Stop-hook system drives the auto-swap through a codex supervisor shim that respawns `codex resume <session-id>` (hooks must be trusted once via `/hooks`). Sibling sessions left on ANY non-live account are reconciled cross-session (owner decisions 2026-07-20; a non-live session cannot refresh cross-account and would wedge at token expiry, healthy or not): the deciding actor drops a reconcile marker addressed to the sibling's supervisor, and the sibling's own Stop hook promotes it into a respawn onto the live account at its next turn boundary - the only safe respawn point - while a blocked live seat never receives a signal.
114
+ **Removed (1.37.0, issue #79):** the shared live credential and everything that served it: the swap-time harvest, park, and install, tokenmaxxing's own OAuth refresh grant and its interlock with Claude Code's refresh lock, the `~/.claude.json` `oauthAccount` rewrite, the 45-second post-swap cooldown, the pool-wide depleted-wait record, the parked-credential slots (`creds/`, `tokenmaxxing-cred-*`), the ambient `CLAUDE_CONFIG_DIR` refusal, and manual `switch` for the Claude pool.
115
115
 
116
- **Shipped since (1.29.0):** a Cursor Cloud relay. A cloud VM has no keychain, no pool state, and no hot swap, so the path uses what it does have (environment variables, a shell, repo files) and a different credential primitive, one-year inference-only setup tokens, minted once per pooled account by `tokenmaxxing setup-token` on the owner's machine and carried to the VM as one user-scoped Runtime Secret (`TOKENMAXXING_TOKENS`). `tokenmaxxing cursor init` writes a `claude` project subagent (`.cursor/agents/claude.md`) that forwards each task to `tokenmaxxing cloud run`, which spawns `claude -p --dangerously-skip-permissions --output-format json` under `CLAUDE_CODE_OAUTH_TOKEN` and pins the thread to that token (the API caches by exact prefix, so a rotation busts one thread, not all). A setup token carries no usage signal, so rotation is reactive. A run whose transcript gained a non-transient rate-limit row during that run (the structured row the StopFailure hook reads) walls the label until the reset the row names (five hours without one) and resumes the same session on the next token; the result's `api_error_status` alone never walls, so a transient 429 or an older row from a previous token does not cascade across the pool. The local switching engine never reads a setup token; `/usage` prints no percentages under one. Details in [Cursor Cloud](docs/content/docs/cursor-cloud.mdx).
116
+ **Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, `codex-creds/`, own flock). Since 1.35.0 the engine is one: a generic account record (`id`, `label`, `email`, `tier`, `windows`), one picker, one decision, and one set of account commands run against a `Provider` (`src/lib/provider.ts`); `src/lib/claude.ts` and `src/lib/codex.ts` hold only what differs per product (credential store, identity, usage source, presence, login and install flows), and the provider's `seats` capability (`shared` for Claude, `live` for Codex) is how the engine knows whether present accounts remain candidates and whether a move needs the caller's respawn. Codex differences that shaped it: restart IS the switch (a running codex refuses another account's credential), one live `auth.json` for every codex session, usage is a free direct GET with epoch resets and a duration-classified window set (the weekly window is primary on current plans), the refresh token rotates with reuse punished (harvest-by-true-owner, persist every rotation), and codex's Stop-hook system drives the auto-swap through a codex supervisor shim that respawns `codex resume <session-id>` (hooks must be trusted once via `/hooks`). Sibling sessions left on ANY non-live account are reconciled cross-session (owner decisions 2026-07-20): the deciding actor drops a reconcile marker addressed to the sibling's supervisor, and the sibling's own Stop hook promotes it into a respawn onto the live account at its next turn boundary - the only safe respawn point - while a blocked live seat never receives a signal.
117
117
 
118
- **Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the single refresh-grant call in the swap; Slack as a message bus; soft concurrency caps.
118
+ **Shipped since (1.29.0):** a Cursor Cloud relay. A cloud VM has no keychain, no pool state, and no supervisor, so the path uses what it does have (environment variables, a shell, repo files) and a different credential primitive, one-year inference-only setup tokens, minted once per pooled account by `tokenmaxxing setup-token` on the owner's machine and carried to the VM as one user-scoped Runtime Secret (`TOKENMAXXING_TOKENS`). `tokenmaxxing cursor init` writes a `claude` project subagent (`.cursor/agents/claude.md`) that forwards each task to `tokenmaxxing cloud run`, which spawns `claude -p --dangerously-skip-permissions --output-format json` under `CLAUDE_CODE_OAUTH_TOKEN` and pins the thread to that token (the API caches by exact prefix, so a rotation busts one thread, not all). A setup token carries no usage signal, so rotation is reactive. A run whose transcript gained a non-transient rate-limit row during that run (the structured row the StopFailure hook reads) walls the label until the reset the row names (five hours without one) and resumes the same session on the next token; the result's `api_error_status` alone never walls, so a transient 429 or an older row from a previous token does not cascade across the pool. The local switching engine never reads a setup token; `/usage` prints no percentages under one. Details in [Cursor Cloud](docs/content/docs/cursor-cloud.mdx).
119
+
120
+ **Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the read-only identity check at onboarding; Slack as a message bus; soft concurrency caps.
119
121
 
120
122
  ---
121
123
 
@@ -124,16 +126,22 @@ TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/u
124
126
 
125
127
  ---
126
128
 
127
- ## 9. What the acceptance gate showed (2026-07-09)
129
+ ## 9. What the acceptance gates showed
128
130
 
129
- Unit suite: **32 pass**. Hermetic swap+concurrency+model-aware E2E: **all pass**. CLI init/doctor/uninstall: **pass** (re-verified init/doctor 2026-07-09 through the npm-installed bun shim on linux-arm64 after the switch to source packaging; full suite 47 pass / 0 fail there).
131
+ ### 2026-09-12 (per-account stores, 1.37.0)
130
132
 
131
- 1. **Transcript continuity across a process boundary - ✅ proven on real claude/real account.** `claude --session-id X -p ...` committed a clean 12-line transcript; `claude --resume X -p ...` recalled the earlier turn's codeword. The supervisor's kill→restore-termios→respawn→`--resume` loop is proven with a mock claude. The one step not run live is the abrupt SIGTERM of an *idle interactive* real claude (structurally safe - the transcript is fully committed+fsynced before idle and nothing writes while idle).
132
- 2. **Terminal restoration - ✅ mechanism proven.** The supervisor saves `stty -g` and restores it between kill and respawn; the path executes in the mock-supervisor run. Visual raw-mode/​resize confirmation wants a live interactive terminal.
133
- 3. **SessionStart swap - ✅ swap logic proven; ⚠️ "adopted by first turn" needs a 2nd real account.** The hook's decision+swap path is covered by the E2E; the claude-internal timing (SessionStart before first `getToken`) needs a second subscription to observe end-to-end.
134
- 4. **Keychain writes from a headless hook - ✅ proven.** The swap E2E's 4 concurrent subprocesses each wrote the live item via `security -i` (secret on stdin, never argv) with no prompt/hang; `init` parked backups headlessly too. (Residual: the live `Claude Code-credentials` ACL for the *fresh resumed claude* is mitigated by onboarding touching `security` interactively.)
135
- 5. **Concurrent flocked swap - ✅ proven.** 4 concurrent processes racing the trigger → exactly **one** `flock`ed swap, one OAuth refresh, all end on B. Per-supervisor respawn is proven with the mock; N *real* supervisors respawning together needs real accounts + PTYs.
136
- 6. **Model-aware trigger - ✅ proven.** Fable at 96% per-model cap (aggregate only 30/50) → swaps; Sonnet at 96% → no swap.
133
+ Hermetic run on Linux under a throwaway `HOME` and `TOKENMAXXING_HOME`, a stub `claude` that records its environment and answers `-p /usage` from a per-store fixture, and a synthetic three-account version 2 pool with equal headroom on two accounts and the third over its session bar:
137
134
 
138
- Owed before fully trusting in the wild: the live interactive-PTY SIGTERM/terminal test, and the two items that require a **second real subscription account** (SessionStart adoption, N real supervisors).
139
- ```
135
+ 1. **Placement - ✅.** The first supervised launch spawned the stub with `CLAUDE_SECURESTORAGE_CONFIG_DIR=stores/<first account>` and wrote its presence; the second launch, with equal headroom, landed on the other account.
136
+ 2. **Plain move - ✅.** A 95% session tee for the second session's account made its Stop hook write the marker; the supervisor relaunched `--resume <sid>` under the first account's store within a second and rewrote the presence file.
137
+ 3. **Wall move - ✅.** A five-hour refusal row fed to the first session's StopFailure hook stamped `enforcedUntil` on its seat and moved the session to the account that had just been freed; a Stop hook on the walled seat moved the remaining session off it too.
138
+ 4. **Sampling - ✅.** The check tick probed the stalest account under its store with the `-tick` probe home; live `status` probed the accounts without a fresh tee under their stores and read the one with a fresh tee from the tee.
139
+ 5. **Stores never move - ✅.** SHA-256 of every store's `.credentials.json` was identical before and after; no `creds/` directory and no root `usage.json` appeared; the SessionStart hook on a seat under its bars wrote no marker; stopping the children cleared every presence file.
140
+
141
+ The `pooledSpawnEnv` helper the issue names left with the SDK surface in 1.23.0; the supervisor's launch environment is the one spawn-env site.
142
+
143
+ ### 2026-07-09 (v0.1.0)
144
+
145
+ Unit suite: **32 pass**. Hermetic swap+concurrency+model-aware E2E: **all pass**. CLI init/doctor/uninstall: **pass** (re-verified init/doctor 2026-07-09 through the npm-installed bun shim on linux-arm64 after the switch to source packaging; full suite 47 pass / 0 fail there). Transcript continuity across a process boundary was proven on a real account (`claude --session-id X -p ...` then `claude --resume X -p ...` recalled the earlier turn), the kill→restore-termios→respawn→`--resume` loop with a mock claude, headless keychain writes through `security -i`, and a concurrent flocked decision with 4 racing processes. The test suite itself was deleted on 2026-09-02 (owner ruling); verification since is the typecheck plus hermetic CLI runs.
146
+
147
+ Owed before fully trusting in the wild: the live interactive-PTY SIGTERM/terminal test (the owner declined it once), and a live run with two real subscription accounts observing placement and a move end to end.
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # tokenmaxxing
2
2
 
3
- **Automatic Claude Code account switching.** Run `claude` exactly as you always do; when the active account nears its usage limit, tokenmaxxing swaps the credential to a fresher account at a safe turn boundary and your session keeps running on it - no restart, same conversation. Works across many concurrent sessions. Only when the whole pool is at its limit does anything visible happen: a countdown that auto-resumes at the soonest reset, when that reset falls within `policy.maxWaitMs` (default 1h - a longer wait stays put rather than parking your terminal for hours).
3
+ **Automatic Claude Code account switching.** Run `claude` exactly as you always do; each session starts on the pooled account with the most headroom per running session, and when that account nears its usage limit, tokenmaxxing resumes the session on a fresher account at a safe turn boundary - same conversation, restarted under another account's credential store. Works across many concurrent sessions, which spread over the pool instead of draining one account together. When the whole pool is at its limit, a session pauses with a countdown and auto-resumes at the soonest reset, when that reset falls within `policy.maxWaitMs` (default 1h - a longer wait stays put rather than parking your terminal for hours).
4
4
 
5
5
  > **Scope:** Claude Code only, macOS and Linux. It pools **subscription** accounts (Pro/Max), not API keys.
6
6
 
7
7
  ```
8
8
  $ claude
9
- ...you work normally; swaps are invisible (watch the statusline account flip)...
9
+ ...you work normally; the statusline's ◆ marks this session's account...
10
+ ↻ tokenmaxxing: moving to work@acme.com - resuming...
10
11
  ⏳ tokenmaxxing: all accounts at their limit. Resuming on work@acme.com when it resets (Ctrl-C to resume now).
11
12
  ```
12
13
 
13
14
  ## Why
14
15
 
15
- A running `claude` re-checks the credential store between requests, so a swapped credential is adopted in-place (within ~30s on macOS, the next request on Linux) - a swap never restarts your session. The one case that still needs process management is a fully depleted pool: a session cannot pause itself, so a thin `claude` supervisor on your PATH stops it at a committed turn boundary (the transcript is already on disk, nothing is lost), shows a countdown, and auto-resumes `claude --resume <id>` at the soonest reset. Everything else about `claude` is unchanged - all flags, MCP, hooks, and skills pass through.
16
+ Every pooled account owns one Claude Code credential store under `~/.config/tokenmaxxing/stores/`. A thin `claude` supervisor on your PATH picks an account for each session and points `CLAUDE_SECURESTORAGE_CONFIG_DIR` at its store, so sessions on different accounts never share a credential, while sessions on the same account share its store and Claude Code's own refresh keeps them in step. A session moves only by respawn: the supervisor stops it at a committed turn boundary (the transcript is already on disk, nothing is lost) and runs `claude --resume <id>` under another store. tokenmaxxing writes a store once at onboarding and never again; it never copies a credential between stores, because a refresh rotation revokes the previous access token and two copies of one grant break each other. Everything else about `claude` is unchanged - all flags, MCP, hooks, and skills pass through.
16
17
 
17
18
  ## Install
18
19
 
@@ -23,7 +24,7 @@ bun add -g tokenmaxxing
23
24
  tokenmaxxing init
24
25
  ```
25
26
 
26
- Or with Nix (same source-run-by-Bun package; `init` still owns credentials, the `claude` shim, and settings merges). Install onto PATH first, then init — `nix run ... -- init` alone leaves supervisor shims without a stable `tokenmaxxing` on PATH after the ephemeral run exits:
27
+ Or with Nix (same source-run-by-Bun package; `init` still owns the stores, the `claude` shim, and settings merges). Install onto PATH first, then init: `nix run ... -- init` alone leaves supervisor shims without a stable `tokenmaxxing` on PATH after the ephemeral run exits.
27
28
 
28
29
  ```sh
29
30
  nix profile install github:anaclumos/tokenmaxxing
@@ -50,29 +51,28 @@ programs.tokenmaxxing.package = inputs.tokenmaxxing.packages.${pkgs.system}.defa
50
51
 
51
52
  With either module, run `tokenmaxxing init` afterwards.
52
53
 
53
- `init` imports the account you're already on, installs the `claude` supervisor + five `settings.json` entries (the tokenmaxxing statusLine, a subagentStatusLine, a Stop hook, a StopFailure hook, a SessionStart hook), and adds the supervisor's bin dir to PATH in your shell rc (idempotent; it must sit ahead of the real `claude` to intercept it). Restart your shell, then add more accounts and go:
54
+ `init` opens a browser sign-in for your first pooled account (even if you are already signed in; the login you already have stays for sessions started outside the supervisor), installs the `claude` supervisor + five `settings.json` entries (the tokenmaxxing statusLine, a subagentStatusLine, a Stop hook, a StopFailure hook, a SessionStart hook), and adds the supervisor's bin dir to PATH in your shell rc (idempotent; it must sit ahead of the real `claude` to intercept it). Restart your shell, then add more accounts and go:
54
55
 
55
56
  ```sh
56
- tokenmaxxing add # logs one in, in isolation, and pools it
57
- claude # use claude as always
57
+ tokenmaxxing add # logs one in, in isolation, into its own store
58
+ claude # use claude as always; each session gets its own account
58
59
  ```
59
60
 
60
61
  ## Commands
61
62
 
62
63
  | command | what it does |
63
64
  |---|---|
64
- | `tokenmaxxing init` | import the current account + install supervisor & hooks |
65
+ | `tokenmaxxing init` | log the first account in (isolated) + install supervisor & hooks |
65
66
  | `tokenmaxxing init --codex` | same for codex: import login, install codex supervisor + Stop hook |
66
- | `tokenmaxxing add` | register an additional account (isolated login, harvested into the pool) |
67
+ | `tokenmaxxing add` | register an additional account (isolated login, harvested once into its own store) |
67
68
  | `tokenmaxxing add --codex` | register an additional codex account (isolated login) |
68
69
  | `tokenmaxxing auth [--codex] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every needs-reauth account one by one; `--codex` does the same for the codex pool |
69
- | `tokenmaxxing switch [sel]` | switch the claude pool: bare picks the best account (no-op when the current one wins), a selector targets one |
70
- | `tokenmaxxing switch --codex [sel]` | switch the codex pool (takes effect on the next codex start) |
71
- | `tokenmaxxing status [--cached]` | accounts with 5h / weekly usage bars, active + exhausted-until-reset; `--cached` renders the stored figures without sampling |
70
+ | `tokenmaxxing switch --codex [sel]` | switch the codex pool (takes effect on the next codex start); the claude pool has no manual switch, each session is placed at launch |
71
+ | `tokenmaxxing status [--cached]` | accounts with 5h / weekly usage bars, live session counts, exhausted-until-reset; `--cached` renders the stored figures without sampling |
72
72
  | `tokenmaxxing config` | the config path and the effective values; edit the file in an editor, a bad value fails the next load with the field name |
73
- | `tokenmaxxing doctor` | verify the supervisor + settings entries survived |
73
+ | `tokenmaxxing doctor` | verify the supervisor + settings entries survived and every store holds its account's credential |
74
74
  | `tokenmaxxing rename [--codex] <sel> <label>` / `rm [--codex] <sel>` | manage the pool (`--codex` targets the codex pool: one email can hold both a claude and a codex account) |
75
- | `tokenmaxxing uninstall` | remove supervisor + settings entries (accounts/credentials kept) |
75
+ | `tokenmaxxing uninstall` | remove supervisor + settings entries (accounts/stores kept) |
76
76
  | `tokenmaxxing setup-token [--print \| rm <label\|uuid>]` | Cursor Cloud only: mint one `claude setup-token` per pooled account (a browser sign-in each) and print the `TOKENMAXXING_TOKENS` secret value; `--print` prints the stored set, `rm` drops one |
77
77
  | `tokenmaxxing cursor init [dir]` | write the Claude relay subagent and `.cursor/environment.json` into a repo |
78
78
  | `tokenmaxxing cloud run [--session <id>] [--max-turns <n>] "<prompt>"` | on a Cursor Cloud VM: run `claude -p` on a setup token, rotate to the next token on a usage limit |
@@ -80,16 +80,16 @@ claude # use claude as always
80
80
 
81
81
  ## How switching decides
82
82
 
83
- One rule: while the active account is under its bars, nothing happens; once it is at or over a bar, the decision moves to the usable account **furthest behind its own weekly pace**. The session bar is **90%** of the 5-hour window and the weekly bar is **98%**. The bars also screen candidates on any of:
83
+ Each session runs on its own seat. One rule: while the seat is under its bars, nothing happens; once it is at or over a bar, the session moves to the usable account that ranks first. The session bar is **90%** of the 5-hour window and the weekly bar is **98%**. The bars also screen candidates on any of:
84
84
 
85
- - **Session** (5-hour) or **week (all models)** - the aggregate windows, fed free/push-based by the statusLine.
86
- - **Per-model weekly cap** - the most capable model (Fable) has its own tighter weekly limit that binds *before* the aggregate (per-model caps currently exist only for Sonnet and Fable, and Sonnet's is generous). tokenmaxxing reads it from `claude -p '/usage'` (free, 0 tokens, TTL-cached) whenever the active model is one of `policy.switchModels`, so a Fable session switches on the Fable cap while a Sonnet session rides the aggregate.
85
+ - **Session** (5-hour) or **week (all models)** - the aggregate windows, fed free/push-based by the statusLine into one tee per account.
86
+ - **Per-model weekly cap** - the most capable model (Fable) has its own tighter weekly limit that binds *before* the aggregate (per-model caps currently exist only for Sonnet and Fable, and Sonnet's is generous). tokenmaxxing reads it from `claude -p '/usage'` under the seat's store (free, 0 tokens, TTL-cached) whenever the active model is one of `policy.switchModels`, so a Fable session moves on the Fable cap while a Sonnet session rides the aggregate.
87
87
 
88
- The bars' headroom is deliberate: it's the budget to reach a clean turn boundary (plus up to one turn of adoption lag on macOS) before the account's real limit. The session bar sits lower (90) because a 5-hour reset is cheap to sit out; weekly quota is use-it-or-lose-it, so it drains closer to the limit (98).
88
+ The bars' headroom is deliberate: it's the budget to reach a clean turn boundary before the account's real limit. The session bar sits lower (90) because a 5-hour reset is cheap to sit out; weekly quota is use-it-or-lose-it, so it drains closer to the limit (98).
89
89
 
90
- Selection ranks usable candidates by remaining weekly percentage divided by time to reset, highest first; organization membership is not an input, and manual `tokenmaxxing switch` uses the same ranking.
90
+ Launches and moves rank usable accounts by session-window headroom per running session, `(bar - used) / (sessions + 1)`, so two fresh accounts alternate and a third session goes to whichever has more headroom left per session; ties break by weekly pace pressure (remaining weekly percent divided by time to reset, highest first). Organization membership is not an input.
91
91
 
92
- - Every periodic check tick samples the parked account whose last `/usage` attempt is oldest, skipping any attempted within `policy.usagePollTtlMs`, so every parked account is attempted about once per tick per pooled account, or once per `usagePollTtlMs` plus a tick when that is longer; the swap reads the cached figures, which are as fresh as each account's last successful attempt. No sample runs inside the 45-second post-swap cooldown.
92
+ - Every periodic check tick folds fresh statusline tees into the index and samples the account whose last `/usage` attempt is oldest, under that account's store, skipping any attempted within `policy.usagePollTtlMs`; a move reads the cached figures, which are as fresh as each account's last successful attempt or its sessions' latest push.
93
93
  - When no account is usable, the session pauses until the soonest reset if that lands within `policy.maxWaitMs`, and otherwise stays put.
94
94
 
95
95
  See [How switching decides](docs/content/docs/switching.mdx) for the policy and the [cache-cost profile](docs/content/docs/switching-profile.mdx) for measurements and their limits.
@@ -110,9 +110,9 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
110
110
  }
111
111
  ```
112
112
 
113
- `thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from each threshold bar (effective bar = threshold - margin), so a large turn is less likely to blow past a bar between checks; `switchModels` names the models whose per-model cap triggers a switch; `usagePollTtlMs` is how long a `/usage` attempt stays fresh, for the live per-model poll and for the parked account each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a soonest reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick.
113
+ `thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from each threshold bar (effective bar = threshold - margin), so a large turn is less likely to blow past a bar between checks; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is how long a `/usage` attempt stays fresh, for a seat's per-model poll and for the account each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a soonest reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick.
114
114
 
115
- State lives entirely in `~/.config/tokenmaxxing/`. Per-account credentials follow the platform's Claude Code store: the login keychain on macOS (`tokenmaxxing-cred-<uuid8>` items, never plaintext on disk), 0600 files under `~/.config/tokenmaxxing/creds/` on Linux (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
115
+ State lives entirely in `~/.config/tokenmaxxing/`. Each account's credential store is `stores/<uuid8>/`, which Claude Code reads and refreshes as its own: on macOS the credential is a login-keychain item keyed by the store path (never plaintext on disk), on Linux a 0600 `.credentials.json` inside the store (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
116
116
 
117
117
  ## Codex support
118
118
 
@@ -124,26 +124,26 @@ tokenmaxxing add --codex # log in another account, isolated - your primary lo
124
124
  codex # use codex as always
125
125
  ```
126
126
 
127
- Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, so **a restart is the switch**. The installed Stop hook runs the same pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, weekly aggregate and per-model caps alike); when it swaps, the supervisor relaunches `codex resume <session-id>` on the fresh account with the transcript intact. `tokenmaxxing switch --codex [sel]` does it manually, `status` (and `status --cached`) shows both pools.
127
+ Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, and codex has no per-session credential store, so **a restart is the switch** and every codex session shares one live login. The installed Stop hook runs the same pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, weekly aggregate and per-model caps alike); when it swaps, the supervisor relaunches `codex resume <session-id>` on the fresh account with the transcript intact. `tokenmaxxing switch --codex [sel]` does it manually, `status` (and `status --cached`) shows both pools.
128
128
 
129
129
  Two codex-specific facts worth knowing: codex does not run hooks it has not been told to trust, so after `init --codex` you must open codex once and trust the tokenmaxxing Stop hook via `/hooks` (auto-switching is inert until then); and codex has no cross-process lock on `auth.json`, so tokenmaxxing serializes all of its own credential writes behind its own lock and swaps only at idle turn boundaries.
130
130
 
131
131
  ## Cursor Cloud
132
132
 
133
- A Cursor Cloud Agent can hand substantial work to Claude Code running on your own accounts. The VM has no keychain and no hot swap, so this path uses setup tokens instead. `tokenmaxxing setup-token` mints one `claude setup-token` per pooled account on your machine and prints the value for a user-scoped Runtime Secret named `TOKENMAXXING_TOKENS`; `tokenmaxxing cursor init` writes a `claude` project subagent and an `environment.json` install line into the repo; on the VM, the subagent forwards each task to `tokenmaxxing cloud run`, which runs `claude -p` on one token per thread and moves the thread to the next token when a run ends on a usage limit. Setup tokens are inference-only and report no usage percentages, so cloud rotation is reactive and the local switching engine never reads one. Details and limits in [Cursor Cloud](docs/content/docs/cursor-cloud.mdx).
133
+ A Cursor Cloud Agent can hand substantial work to Claude Code running on your own accounts. The VM has no keychain and no supervisor, so this path uses setup tokens instead. `tokenmaxxing setup-token` mints one `claude setup-token` per pooled account on your machine and prints the value for a user-scoped Runtime Secret named `TOKENMAXXING_TOKENS`; `tokenmaxxing cursor init` writes a `claude` project subagent and an `environment.json` install line into the repo; on the VM, the subagent forwards each task to `tokenmaxxing cloud run`, which runs `claude -p` on one token per thread and moves the thread to the next token when a run ends on a usage limit. Setup tokens are inference-only and report no usage percentages, so cloud rotation is reactive and the local switching engine never reads one. Details and limits in [Cursor Cloud](docs/content/docs/cursor-cloud.mdx).
134
134
 
135
135
  ## Honest limitations
136
136
 
137
- - **One cold turn.** The first turn on a new account re-uploads context once (prompt cache is org-scoped).
138
- - **Depleted-pause hiccup.** Plain swaps never restart the session. Only when the whole pool is at its limit does `claude` stop for the countdown; anything typed in that split second is lost.
139
- - **Adoption lag.** On macOS the first turn within ~30s of a swap can still meter the old account; the bars' headroom absorbs it.
140
- - **Shared blast radius.** All default-profile sessions share one live credential, so a swap moves them all together (each adopts in place). A `flock` + re-check keeps racing hooks from burning two accounts.
137
+ - **A move restarts the process.** The session resumes the same transcript under the new account, but the process restarts and the first turn on the new account re-uploads context once (prompt cache is org-scoped).
138
+ - **Depleted-pause hiccup.** When the whole pool is at its limit, `claude` stops for the countdown; anything typed in that split second is lost.
139
+ - **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on Claude Code's own login, which tokenmaxxing never writes.
140
+ - **One shared identity file.** `~/.claude.json` holds one `oauthAccount` that Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email; the statusline's `◆` marks the account a session actually uses.
141
141
  - **Keychain ACL (macOS).** `init`/`add` touch the keychain interactively so the first `security` access isn't cold inside a headless hook.
142
- - **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; tokenmaxxing's parked copies follow the same model.
142
+ - **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
143
143
 
144
144
  ## How it's built
145
145
 
146
- TypeScript on Bun. One multi-call entry (`src/main.ts`) runs the CLI, the `claude` supervisor, and the hook/statusLine shims. [Zod](https://zod.dev) validates every external-boundary payload (credential blobs, hook/statusLine stdin, OAuth responses, config). [es-toolkit](https://es-toolkit.dev) for utilities. The supervisor is process/terminal-only. It never proxies API traffic or touches tokens in flight. Cross-process coordination uses `flock(2)` via `bun:ffi` (macOS has no `flock(1)`; one codepath for both platforms). Credential I/O goes through one platform-selected store: `security(1)` generic-passwords on macOS, atomic 0600 file writes on Linux.
146
+ TypeScript on Bun. One multi-call entry (`src/main.ts`) runs the CLI, the `claude` supervisor, and the hook/statusLine shims. [Zod](https://zod.dev) validates every external-boundary payload (credential blobs, hook/statusLine stdin, identity responses, config). [es-toolkit](https://es-toolkit.dev) for utilities. The supervisor is process/terminal-only. It never proxies API traffic or touches tokens in flight. Cross-process coordination uses `flock(2)` via `bun:ffi` (macOS has no `flock(1)`; one codepath for both platforms). Credential I/O goes through one platform-selected store: `security(1)` generic-passwords on macOS, atomic 0600 file writes on Linux.
147
147
 
148
148
  ## License
149
149
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.38.0",
4
- "description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
3
+ "version": "1.40.0",
4
+ "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "anaclumos",
package/src/cli/check.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { claude } from "../lib/claude.ts";
2
2
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
3
- import { sampleOldestParked } from "../lib/sample.ts";
3
+ import { sampleOldest } from "../lib/sample.ts";
4
4
  import { loadConfig } from "../lib/state.ts";
5
+ import { maybeAutoUpdate } from "../lib/update.ts";
5
6
  import { log } from "../lib/log.ts";
6
7
  import { c, emitError, emitJson, fmtReset } from "./render.ts";
7
8
 
@@ -10,7 +11,8 @@ export async function cmdCheck(json = false): Promise<number> {
10
11
  let d;
11
12
  try {
12
13
  d = await evaluateAndMaybeSwap(claude, now);
13
- await sampleOldestParked(loadConfig());
14
+ await sampleOldest(loadConfig());
15
+ await maybeAutoUpdate();
14
16
  } catch (e) {
15
17
  const detail = e instanceof Error ? e.message : String(e);
16
18
  log("check.error", { err: detail });