tokenmaxxing 1.116.0 → 1.117.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 +3 -3
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/lib/types.ts +2 -2
package/DESIGN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tokenmaxxing - design
|
|
2
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 (**
|
|
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 (**98%** of the 5h session window or 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 queues sessions first come first served onto reset targets (at most four waiters per account, overflow takes the soonest anyway), pauses each one with a countdown, and auto-resumes at its assigned reset when that lands within `policy.maxWaitMs` (default 1h; further out, the session stays put rather than parking for hours).
|
|
4
4
|
|
|
5
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
|
>
|
|
@@ -12,7 +12,7 @@ Automatic Claude Code account switching. You run `claude` exactly as always; eve
|
|
|
12
12
|
|
|
13
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
|
-
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 four waiters per account, first come first served, overflow on the soonest). **Thresholds sit below 100: the headroom is the budget to reach a clean turn boundary before the account actually hits its real limit.**
|
|
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 four waiters per account, first come first served, overflow on the soonest). **Thresholds sit below 100: the headroom is the budget to reach a clean turn boundary before the account actually hits its real limit.** Both windows move at 98 (weekly allowance is use-it-or-lose-it, so it drains close to the limit, and the session bar matches it, so the configured thresholds and the effective bars read the same).
|
|
16
16
|
|
|
17
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
|
|
|
@@ -99,7 +99,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
|
|
|
99
99
|
|
|
100
100
|
## 6. Honest papercuts
|
|
101
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.
|
|
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
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
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
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.
|
package/README.md
CHANGED
|
@@ -81,12 +81,12 @@ claude # use claude as always; each session gets its own accoun
|
|
|
81
81
|
|
|
82
82
|
## How switching decides
|
|
83
83
|
|
|
84
|
-
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 `thresholds.session` (
|
|
84
|
+
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 `thresholds.session` (98) minus `policy.projectionMargin` (0), so **98%** of the 5-hour window by default, and the weekly bar is **98%**. The bars also screen candidates on any of:
|
|
85
85
|
|
|
86
86
|
- **Session** (5-hour) or **week (all models)** - the aggregate windows, fed free/push-based by the statusLine into one tee per account.
|
|
87
87
|
- **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 the direct no-spend usage read for the seat (free, 0 tokens, one attempt per interval) whenever the seat's tee is older than `policy.usagePollTtlMs` or the session's model is one of `policy.switchModels`, backing off after a failed sample, so a Fable session moves on the Fable cap while a Sonnet session rides the aggregate. A session launched with `--model` on another family (`claude --model opus`) ignores the Fable cap from launch on: it is placed on, and moves to, an account whose Fable cap is spent as long as that account's session and weekly windows have headroom. The exception is a move off an account the server already refused (an enforced-limit wall), which screens every configured family. A refusal for usage credits, which the usage read cannot predict, walls nothing: the refused session moves with the Fable cap screened, and a later credits move never returns it to an account that already refused it.
|
|
88
88
|
|
|
89
|
-
The bars' headroom is deliberate: it's the budget to reach a clean turn boundary before the account's real limit.
|
|
89
|
+
The bars' headroom is deliberate: it's the budget to reach a clean turn boundary before the account's real limit. Both bars sit at 98: weekly quota is use-it-or-lose-it, so it drains close to the limit, and the session bar matches it, so the configured thresholds and the effective bars read the same.
|
|
90
90
|
|
|
91
91
|
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 pace pressure (remaining percent of the binding gated per-model cap, or of the weekly aggregate for an account with no gated row, divided by time to its reset, highest first), then soonest weekly reset, then lowest weekly use. Organization membership is not an input.
|
|
92
92
|
|
|
@@ -101,9 +101,9 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
101
101
|
|
|
102
102
|
```json
|
|
103
103
|
{
|
|
104
|
-
"thresholds": { "session":
|
|
104
|
+
"thresholds": { "session": 98, "weekly": 98 },
|
|
105
105
|
"policy": {
|
|
106
|
-
"projectionMargin":
|
|
106
|
+
"projectionMargin": 0,
|
|
107
107
|
"switchModels": ["fable"],
|
|
108
108
|
"usagePollTtlMs": 90000,
|
|
109
109
|
"maxWaitMs": 3600000,
|
|
@@ -112,7 +112,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
`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 the session bar only (default
|
|
115
|
+
`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 the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a 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. `claudeBin`, `codexBin`, `grokBin`, `opencodeBin`, and `piBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, `TOKENMAXXING_OPENCODE_BIN`, and `TOKENMAXXING_PI_BIN` override them for one process, except that `init` pins the binary it resolved into `config.json`.
|
|
116
116
|
|
|
117
117
|
State lives in `~/.config/tokenmaxxing/`; outside it the install touches Claude Code's `settings.json`, codex's `hooks.json`, the timer and usage hub units, and your shell rc. 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`).
|
|
118
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.117.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",
|
package/src/lib/types.ts
CHANGED
|
@@ -103,7 +103,7 @@ export const ConfigSchema = z
|
|
|
103
103
|
.object({
|
|
104
104
|
thresholds: z
|
|
105
105
|
.object({
|
|
106
|
-
session: PercentSchema.default(
|
|
106
|
+
session: PercentSchema.default(98),
|
|
107
107
|
weekly: PercentSchema.default(98),
|
|
108
108
|
accounts: z.record(z.string(), z.object({ session: PercentSchema, weekly: PercentSchema, credits: z.boolean().default(false) })).default({}),
|
|
109
109
|
})
|
|
@@ -115,7 +115,7 @@ export const ConfigSchema = z
|
|
|
115
115
|
piBin: z.string().default(""),
|
|
116
116
|
policy: z
|
|
117
117
|
.object({
|
|
118
|
-
projectionMargin: z.number().min(0).max(100).default(
|
|
118
|
+
projectionMargin: z.number().min(0).max(100).default(0),
|
|
119
119
|
switchModels: z
|
|
120
120
|
.array(z.string())
|
|
121
121
|
.default(["fable"])
|