tokenmaxxing 1.166.0 → 1.168.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/package.json +2 -3
- package/src/entries/pisupervisor.ts +0 -1
- package/src/entries/supervisor.ts +8 -2
- package/DESIGN.md +0 -152
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.168.0",
|
|
4
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",
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"src",
|
|
18
|
-
"README.md"
|
|
19
|
-
"DESIGN.md"
|
|
18
|
+
"README.md"
|
|
20
19
|
],
|
|
21
20
|
"engines": {
|
|
22
21
|
"bun": ">=1.4.0"
|
|
@@ -436,7 +436,6 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
|
436
436
|
log("pisupervisor.depleted_exit", { id, pool, account: target.id.slice(0, 8), waitUntil });
|
|
437
437
|
say(`\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit, so this session stopped. The next reset is at ${clockFmt.format(waitUntil)} on ${target.label}. Start pi again after that to continue.\x1b[0m\n`);
|
|
438
438
|
clearPresence({ dir: presenceDir, id });
|
|
439
|
-
await releaseClaim();
|
|
440
439
|
return 1;
|
|
441
440
|
}
|
|
442
441
|
say(`\n\x1b[36m↻ tokenmaxxing: moving pi to ${target.label} - resuming...\x1b[0m\n`);
|
|
@@ -17,7 +17,7 @@ import { teeObservation } from "../lib/sample.ts";
|
|
|
17
17
|
import { childStdout, clockFmt, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, relayStdout, runPassthrough, SEAT_POLL_MS, SEAT_RETRY_MS, type Say } from "../lib/supervise.ts";
|
|
18
18
|
import { saveTermios } from "../lib/tty.ts";
|
|
19
19
|
import { liveSessionId, loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
20
|
-
import { loadAccounts, loadConfig, readJsonFile, releaseWaitClaim, writeUsage } from "../lib/state.ts";
|
|
20
|
+
import { loadAccounts, loadConfig, readJsonFile, releaseWaitClaim, replaceWaitClaim, writeUsage } from "../lib/state.ts";
|
|
21
21
|
import { gatedFamilies, modelFromFlag, parseStreamLine, scrubCredEnv } from "../lib/usage.ts";
|
|
22
22
|
import { CostLineSchema, CostStateRowSchema, JsonTextSchema, RespawnMarkerSchema, type Account, type ApiKey, type Config, type ModelInfo, type SessionCost, type UsageWindows, type Window } from "../lib/types.ts";
|
|
23
23
|
import { errorMessage, log } from "../lib/log.ts";
|
|
@@ -728,8 +728,15 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
728
728
|
const launched = await withLock(claudePool.lockFile, async () => {
|
|
729
729
|
if (terminating) return null;
|
|
730
730
|
releaseClaimLocked(sid);
|
|
731
|
+
if (noticeSid !== sid) releaseClaimLocked(noticeSid);
|
|
731
732
|
const target = resolveLaunch(wanted, gate.launchedAt, model, refused);
|
|
732
733
|
if (target?.account && target.depletedUntil !== undefined && target.depletedUntil > gate.launchedAt) {
|
|
734
|
+
try {
|
|
735
|
+
replaceWaitClaim({ sessionId: noticeSid, accountId: target.account.id, at: gate.launchedAt, waitUntil: target.depletedUntil });
|
|
736
|
+
claimSid = null;
|
|
737
|
+
} catch (e) {
|
|
738
|
+
log("supervisor.claim_write_failed", { err: errorMessage(e) });
|
|
739
|
+
}
|
|
733
740
|
return { waitOn: target.account, waitUntil: target.depletedUntil };
|
|
734
741
|
}
|
|
735
742
|
const picked = target?.account ?? null;
|
|
@@ -784,7 +791,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
784
791
|
log("supervisor.depleted_exit", { sid: sid.slice(0, 8), account: launched.waitOn.id.slice(0, 8), waitUntil: launched.waitUntil });
|
|
785
792
|
say(`\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit, so this session stopped. The next reset is at ${clockFmt.format(launched.waitUntil)} on ${launched.waitOn.label}. Run \`claude --resume ${noticeSid}\` after that to continue.\x1b[0m\n`, text);
|
|
786
793
|
if (stream) process.stdout.write(depletedLines(noticeSid, launched.waitUntil, text));
|
|
787
|
-
await releaseClaim();
|
|
788
794
|
return 1;
|
|
789
795
|
}
|
|
790
796
|
|
package/DESIGN.md
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# tokenmaxxing - design
|
|
2
|
-
|
|
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 by default), 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 assigns sessions first come first served to reset targets (at most eight sessions per account; overflow takes the next reset with room up to twice `policy.maxWaitMs` out, joining the least-crowded reset only when the horizon is full), stops each session with its assigned reset named, and holds no process open until that reset, so a harness closes the run and the session resumes when it is started again after the reset (further out than twice `policy.maxWaitMs`, a running session stays put).
|
|
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 and supervisor; since 1.35.0 both run on one engine. Codex's verified internals live in `.claude/rules/codex.md`.
|
|
6
|
-
>
|
|
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
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. Why there is a thin supervisor (and why that's the whole trick)
|
|
12
|
-
|
|
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
|
-
|
|
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 reset the wait queue assigned (at most eight waiters per account, first come first served, overflow on the next reset with room). **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). The default projection margin is 0, so the configured thresholds and the effective bars read the same.
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
```
|
|
20
|
-
supervisor (you type `claude`) → real claude, CLAUDE_SECURESTORAGE_CONFIG_DIR=stores/<uuid8> → Stop hook
|
|
21
|
-
▲______________ relaunch --resume <sid> under another store ______________|
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
It is a process manager only - spawn with inherited stdio (stdin through a relay pipe for a stream-json session, see §3.3) 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
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 2. What tokenmaxxing installs
|
|
29
|
-
|
|
30
|
-
- A `claude` **supervisor** on your PATH ahead of the real binary (`~/.config/tokenmaxxing/bin/claude`) - you invoke it identically; a `claude` shell function or alias would shadow it, and `doctor` warns about one.
|
|
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
|
-
- **`~/.config/tokenmaxxing/`** - the single home for config and state:
|
|
33
|
-
- `config.json` - SPARSE overrides only (thresholds.session/weekly/accounts, claudeBin/codexBin/grokBin/museBin/piBin 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, addedAt, windows, lastUsageAt, lastProbeAt, probeFails, storeFails, usageRetryAt, 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`). Neither index names an active account: the schema is `{version, accounts}`, and an older file that still carries `activeId` loads and drops the key on the next save.
|
|
35
|
-
- `stores/<uuid8>/` - one Claude Code credential store per pooled account (see below).
|
|
36
|
-
- `codex-stores/<uuid8>/` - one Codex credential store per pooled account: its `auth.json`, with everything else symlinked to the shared `~/.codex` so sessions, history, and config stay shared and `codex resume` works across stores.
|
|
37
|
-
- `usage/<uuid8>.json` - the statusline tee per account, written by the sessions running on it.
|
|
38
|
-
- `live/<session-id>` - PID-validated presence per supervised session, naming its account; placement counts them.
|
|
39
|
-
- `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal): the target account, the session to resume, a `waitUntil`, a `compact` flag, the `origin` (the hook or the seat watch that wrote it), the launch time the marker was written for, and after a usage-credits refusal the accounts that have refused the session.
|
|
40
|
-
- `wait-queue.json` - the claims of supervised sessions that are moving or waiting (`{sessionId, accountId, at, waitUntil}`), naming the account each relaunches on: placement counts a claimed session there, and at most eight sessions wait on one account; a session that arrives when every reset within `policy.maxWaitMs` is full waits for the next reset with room up to twice `policy.maxWaitMs` out, joining the least-crowded reset when the horizon is full.
|
|
41
|
-
- `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
|
|
42
|
-
- `pi-stores/claude/<uuid8>/` and `pi-stores/codex/<uuid8>/` - one pi store per pooled account that has a pi login: the account's own pi grant in `auth.json`, everything else symlinked to the shared `~/.pi/agent` ([pi support](docs/content/docs/pi.mdx)).
|
|
43
|
-
- `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-respawn/` and `codex-live/`, and the `grok-*` and `muse-*` state of the status-only grok and Muse Code pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
|
|
44
|
-
- 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.
|
|
45
|
-
|
|
46
|
-
- 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 runs the decision with no seat (nothing moves), folds fresh tees into the index, samples up to three accounts whose last sample attempt is oldest (the direct no-spend usage GET), deletes leftover atomic-write temp files under the state root, and on a Bun global install checks npm for a newer version once a day, so the cached figures every placement reads stay fresh.
|
|
47
|
-
- A usage hub service (`com.tokenmaxxing.hub` launchd agent on macOS, `tokenmaxxing-hub.service` systemd user service on Linux) running `tokenmaxxing serve`, so the [usage hub](docs/content/docs/hub.mdx) is up in the background beside the check timer.
|
|
48
|
-
|
|
49
|
-
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 in the switching path. The [usage hub](docs/content/docs/hub.mdx) that answers the CLIProxyAPI management subset for a dashboard from the cached figures runs outside the switching path, as a background service beside the check timer (`tokenmaxxing serve` runs the same server in the foreground). The `claude` binary, `~/.claude.json`, and Claude Code's default credential store are untouched; inside `~/.claude` only `settings.json` gets the five entries.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## 3. How a move happens
|
|
54
|
-
|
|
55
|
-
### 3.1 Usage feed (free, push-based)
|
|
56
|
-
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. A stream-json session runs no statusLine, so its supervisor writes the same tee from the child's stdout: it relays every chunk unchanged, tees each `rate_limit_event` line's `unifiedWindows` (the same response-header figures, pushed on every one-point move), and restamps the tee on each `assistant` line. The seat is sampled when its tee is older than `policy.usagePollTtlMs`, and for the per-model rows when the active model is a gated family, at most once per the account's sample interval (§5): the direct no-spend usage read with the seat's stored token. The compact child still sets `TOKENMAXXING_PROBE=1` so the hook entries return at once. The sample takes one attempt per interval; a failed sample doubles the interval up to 30 minutes, and a success resets it. A sample that finds the stored access token expired has Claude Code refresh it first (§5).
|
|
57
|
-
|
|
58
|
-
### 3.2 Detect + decide + signal (Stop hook, per turn)
|
|
59
|
-
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.
|
|
60
|
-
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).
|
|
61
|
-
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).
|
|
62
|
-
4. Write `respawn/<session-id>` (atomic temp+rename) naming the target, with `waitUntil` now for a plain move or the assigned reset when the pool is depleted. No credential is written.
|
|
63
|
-
|
|
64
|
-
### 3.3 Move and depleted-pool stop (supervisor)
|
|
65
|
-
The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, compacts the conversation when the marker asks for it, the old seat's record carries no live `enforcedUntil`, and `waitUntil` is not in the future (`compact: true` from the Stop and SessionStart hooks and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <live-id> /compact` under the old seat's store, bounded at five minutes; the compaction counts only when a `compact_boundary` row lands in the transcript, because a refused compaction still exits 0, and a failure only prints why), and (when `waitUntil` is in the future, which means no pooled account has quota) skips the compaction and hands the session to the launch gate, which every launch goes through: with no account that has quota and no API key, it starts no child, writes one notice naming the assigned reset, releases the session's claim, and exits, and a later resume places the session again; with an account that has quota it relaunches `claude --resume <live-id>` (`--session-id <live-id>` when that id has no transcript yet, because `--resume` exits with `No conversation found` for it) with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The marker file is keyed by the launch id `<sid>`; the live id inside it is the session id the supervised process runs now, which `/clear` and an in-session `/resume` change. A hook adopts a differing stdin id as the live id (`current` in `sessions/<sid>.json`) only when its own process descends, through shell processes alone, from the presence record's pid; any other differing id is a nested session and writes nothing. The resumed process reads that store cold → runs on the target account, same conversation. The `--session-id` path is the exception: the conversation has no prior turn (a `/clear` with no turn since, or a move before the first turn), so it starts with no compaction and no first prompt, and in a stream-json session the relay replays the harness lines the killed child received: `user` lines and the `initialize` and session-state control requests in `REPLAYED_REQUESTS`; every other control request gets an error `control_response` on stdout, because a repeat could run its effect twice, and the `control_response` answers to the killed child's own requests are dropped. A first launch with `--resume <id>` takes the same path when the supervisor holds `sessions/<id>.json` for that id and no project directory holds its transcript. On the `--resume` path the relaunch submits a first prompt that names the move (and the compaction, when it landed). After a Stop move the previous turn had finished, so the prompt asks Claude to end the turn without restating it, and to relaunch any background Bash task, Monitor, or Workflow run that turn waited on, because the move ended it. After any other move the prompt asks Claude to continue the task, says the restart ended every background Bash task, Monitor, and Workflow run the session started, and asks Claude to relaunch what the task still needs; a SessionStart move is in that group because a prompt sent inside the marker poll window can already have started on the old seat. The conversation goes on without a keystroke: in a terminal session it is the positional prompt after the session id; in a stream-json session (`--input-format stream-json`, where the binary ignores a positional prompt) the supervisor owns stdin, relays the harness's lines to the child through a pipe, sends the new process the latest `initialize` and session-state control requests the harness sent, because the Agent SDK sends `initialize` once per query and it carries the appended system prompt, the hooks, and the SDK MCP servers, and then writes the prompt as the first `user` line. In a terminal session the supervisor's own notices go to stderr. In a stream-json session (`--output-format stream-json`, read from the effective flags like the relay) there is no terminal and no statusline, so the supervisor writes each notice (the compaction and its failure, the move, the depleted-pool stop with its reset time, the resume) to stdout as a `system` line of subtype `informational` and level `warning`, the shape Claude Code emits for its own print-mode notices, which the harness renders in the conversation while the stream stays parseable; the supervisor writes only while no child is running, except a fatal that ends the session. Before the first notice of a move it writes a `task_notification` line with `status: "stopped"` and `reason: "worker_restart"` for each task the old child started on stdout (`task_started`) and did not end there (a `task_notification`, or a `task_updated` to `completed`, `failed`, or `killed`), the line Claude Code writes when a restarted process finds a task orphaned, because the move ended those tasks and a harness that tracks them from the stream, as T3 Code does, otherwise shows them running until the session exits. The `SessionStart` hook (every source) 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. The supervisor also watches its own seat every ten seconds while the child runs (account record plus tee, the same bars) and writes the marker itself when the seat is at or over a bar, so a move never depends on a hook firing: a harness session between turns whose background workflow burns the window, or a subagent refusal that stamped the wall, is moved within seconds. SIGTERM to the supervisor is forwarded to the child and the supervisor exits after it, so a harness that stops the wrapper (the Agent SDK does) never leaves an orphaned claude on the old seat. A marker whose `launchedAt` does not match the running child is discarded. A marker that fails to parse is never deleted and never treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path. In a stream-json session that fatal, the wrap-depth abort, the wrapper-chain abort, and a failed presence write also go to stdout as the same `informational`/`warning` line.
|
|
66
|
-
|
|
67
|
-
### 3.4 Placement (supervisor, under the pool lock)
|
|
68
|
-
1. Fold every account's tee into the index.
|
|
69
|
-
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 (still supervised, on whatever login the environment names). Print mode, every subcommand, the bare resume picker, an invalid `--session-id`, `--fork-session` with a resume, and `--no-session-persistence` (no transcript, so a move could not resume the run) are passed through unmanaged. One exception: an invocation that carries T3 Code's capability probe shape (the stream-json flag set with `--strict-mcp-config`, `--setting-sources=user,project,local`, `--settings {"disableAllHooks":true}`, `--no-session-persistence`, and the probe environment `ENABLE_CLAUDEAI_MCP_SERVERS=false`, `CLAUDE_CODE_AUTO_CONNECT_IDE=0`, and `CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL=1`) is placed on the account placement picks, or the soonest-recovering account when none is usable, and its `get_usage` control request is answered from that account's sampled windows instead of reaching the child, so the probe sends no usage read. It writes no presence file and no session flags, and no other `--no-session-persistence` run is seated.
|
|
70
|
-
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.
|
|
71
|
-
|
|
72
|
-
### 3.5 Multiple concurrent sessions
|
|
73
|
-
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; the tick's usage GET runs outside it, while a live `status` samples under it.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 4. Onboarding (no `adopt`)
|
|
78
|
-
|
|
79
|
-
- **`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.
|
|
80
|
-
- **`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; a session's store is always set through `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
|
|
81
|
-
- **`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 account that is flagged or whose store holds no usable credential, one by one.
|
|
82
|
-
- 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.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## 5. Placement and rotation policy
|
|
87
|
-
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, assign first come first served to a reset target (at most eight sessions per account; overflow takes the next reset with room), stop the session, and name that reset. The same ranking places a session at launch; a launch onto a depleted pool takes the same target selection, counting running sessions as well as assigned ones, and stops before it starts a `claude` process.
|
|
88
|
-
|
|
89
|
-
- 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 pace pressure (remaining percent of the binding gated per-model cap, or of the weekly aggregate for an account with no gated row, over time to its reset), then soonest weekly reset, then lowest weekly usage; unknown usage ranks last. Organization membership is not an input. Codex drops the headroom key, keeps the pace ordering and the weekly tie-breaks, and excludes accounts running in another codex session, because codex has no cross-process refresh lock and two sessions must never share a login.
|
|
90
|
-
- Selection reads the cached figures only, after folding every account's tee. Each periodic check tick samples up to three accounts whose last sample attempt is oldest, skipping any attempted within the account's sample interval, over the direct no-spend usage read with the account's stored token. The interval is 15 minutes times the unused fraction of the bar closest to being reached, never under `policy.usagePollTtlMs`, and 15 minutes for an account at or over an aggregate bar or with no session or borrower on this host. Every sampler claims a read under the pool lock before it sends it, so concurrent hooks and `status` runs send an account one read. The windows are the session and weekly aggregates plus the `policy.switchModels` caps that are still under their bar, and an account with no stored aggregate gets `policy.usagePollTtlMs`. Every account is attempted about once per three ticks per pooled account, or once per its sample interval 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 GET. The usage endpoint rate limits each account on its own and names the time to come back in the `retry-after` of its HTTP 429; that time is stored as `usageRetryAt`, and no sampler (the tick, `status`, the seat's in-decision sample) reads the account before it. No sampler sends a read with an expired stored access token, because the endpoint answers it with 401 and, after a few, with the same 429. A sample that finds the token expired first runs `claude -p /usage --no-session-persistence --safe-mode` with the account's store set and a throwaway `CLAUDE_CONFIG_DIR`, so Claude Code refreshes the token under its own lock, and then reads with the new token; the child's output is not read. A refresh that leaves the token expired fails the sample with no read, and one that clears the store flags the account `needsReauth`.
|
|
91
|
-
- The depleted stop happens at the bar, not at 100; Codex has no stop and rides its account until the server refuses it, because a running sibling cannot adopt another account without restarting.
|
|
92
|
-
- Neither pool has a post-swap cooldown or a 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.
|
|
93
|
-
|
|
94
|
-
The [profile](docs/content/docs/switching-profile.mdx) records observed cache rewrites across swaps and does not infer subscription quota savings.
|
|
95
|
-
|
|
96
|
-
**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 session's model is in `policy.switchModels`, or the seat's tee is older than the TTL, we read it from the `limits[]` rows of the direct no-spend usage read for the seat (free, 0 tokens, one attempt with backoff) 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. A session's model is the `--model` value it was launched with: the supervisor reads it from the launch flags, uses it for launch placement and the seat watch, and hands it to the session's hooks in `TOKENMAXXING_MODEL`, which the hooks read ahead of the per-account tee. With the default `policy.switchModels`, a session launched as Opus, Sonnet, or Haiku gates no per-model cap, so it runs on an account whose Fable cap is spent while the session and weekly windows have headroom. A launch without `--model`, or with a value that names no model family (`best`, `opusplan`), is unknown: launch placement and the seat watch gate every configured family for it, and its hooks fall back to the model in the account's tee. A move off a walled seat, or one that follows a subagent's five-hour or weekly refusal, screens candidates on every configured family whatever the launch model.
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## 6. Honest papercuts
|
|
101
|
-
- **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). A bar-triggered move compacts on the old account first so that upload is the summary, not the transcript; a move after a refusal cannot, and re-uploads the full context.
|
|
102
|
-
- **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 relaunch continues the conversation on the fresh account from the supervisor's resume prompt. A nonzero static session-bar margin (`policy.projectionMargin`, default 0) reduces this.
|
|
103
|
-
- **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.
|
|
104
|
-
- **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.
|
|
105
|
-
- **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.
|
|
106
|
-
- **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.
|
|
107
|
-
- **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, so it takes the ps-safe stdin write; a blob past the 4000-byte line fails the onboarding write, because a credential never goes on argv) and never rewrites a store afterwards.
|
|
108
|
-
- **settings.json is user-owned.** Install by merge; ship `tokenmaxxing doctor` to verify the supervisor + 5 entries survive a `/config` edit or update.
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
## 7. Scope
|
|
113
|
-
**v1:** `tokenmaxxing init` / `add` / `auth` / `status` (bare default; `ls` removed in 1.30.0, `status --cached` covers it) / `check` / `config` / `doctor` / `rm` / `rename` / `uninstall`; 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.
|
|
114
|
-
|
|
115
|
-
**v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
|
|
116
|
-
|
|
117
|
-
**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.
|
|
118
|
-
|
|
119
|
-
**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.
|
|
120
|
-
|
|
121
|
-
**Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, per-account `codex-stores/`, 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 stay 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 store per pooled account with `CODEX_HOME` set per session (everything but `auth.json` symlinked to the shared home, so resume works across stores), 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 (persisted into the account's own store the instant it returns, never refreshed for an account running in another session), and the codex Stop hook drives the auto-switch through a codex supervisor shim that respawns `codex resume <session-id>` under the target's store (hooks must be trusted once via `/hooks`). The status-only grok pool shares the engine's account commands and state layout with `statusOnly: true` and no supervisor or hooks ([Status-only pools](docs/content/docs/status-only-pools.mdx)). grok `status` reads the weekly subscription credit window from the credits billing GET.
|
|
122
|
-
|
|
123
|
-
**Removed (issue #173):** the Cursor Cloud relay and its setup tokens: `tokenmaxxing setup-token`, `tokenmaxxing cursor init`, `tokenmaxxing cloud run`, `setup-tokens.json`, `TOKENMAXXING_TOKENS`, and the `cloud/` state.
|
|
124
|
-
|
|
125
|
-
**Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the read-only identity check at onboarding; Slack as a message bus; soft concurrency caps.
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## 8. Stack
|
|
130
|
-
TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` and `codex` supervisors, the statusLine and subagent statusLine shims, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry with `--no-env-file`, so a `.env` in the working directory is not imported into the supervisor or the session (a longer `/bin/sh` shim under Nix that drops its own directory from PATH; the Stop path runs every turn, and bun's start-up stays low-millisecond). A Bun global install refreshes itself from npm on the check tick, once a day. Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a merge that changes a packed file releases itself: CI assigns the next minor version on main, publishes, and tags it (details in `.claude/rules/ci.md` and the `release` skill). The supervisor spawns claude with inherited stdio (a relay pipe on stdin for a stream-json session) and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## 9. What the acceptance gates showed
|
|
135
|
-
|
|
136
|
-
### 2026-09-12 (per-account stores, 1.37.0)
|
|
137
|
-
|
|
138
|
-
Hermetic run on Linux under a throwaway `HOME` and `TOKENMAXXING_HOME`, a stub `claude` that records its environment, and a synthetic three-account version 2 pool with equal headroom on two accounts and the third over its session bar. Usage comes from a stub usage endpoint named by `TOKENMAXXING_OAUTH_USAGE_URL` that answers per access token. No `/usage` output is parsed.
|
|
139
|
-
|
|
140
|
-
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.
|
|
141
|
-
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.
|
|
142
|
-
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.
|
|
143
|
-
4. **Sampling - ✅.** The check tick sampled the stalest account over the direct usage GET with the stored token; live `status` sampled the accounts without a fresh tee the same way and read the one with a fresh tee from the tee.
|
|
144
|
-
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.
|
|
145
|
-
|
|
146
|
-
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.
|
|
147
|
-
|
|
148
|
-
### 2026-07-09 (v0.1.0)
|
|
149
|
-
|
|
150
|
-
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.
|
|
151
|
-
|
|
152
|
-
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.
|