tokenmaxxing 0.7.0 → 0.8.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 +7 -7
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/lib/claudelock.ts +1 -1
- package/src/lib/state.ts +1 -1
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; when the active account crosses **
|
|
3
|
+
Automatic Claude Code account switching. You run `claude` exactly as always; when the active account crosses **98%** usage, tokenmaxxing swaps to a fresh account and - at the next safe turn boundary - **restarts your session resumed on it, automatically**. Works across many concurrent sessions at once.
|
|
4
4
|
|
|
5
5
|
> Scope: **Claude Code only, macOS first.** Codex and other CLIs deferred (see `.memory/cc-codex-auth-mechanics.md`).
|
|
6
6
|
>
|
|
@@ -12,7 +12,7 @@ Automatic Claude Code account switching. You run `claude` exactly as always; whe
|
|
|
12
12
|
|
|
13
13
|
A **running** `claude` cannot adopt a swapped credential mid-flight - binary-verified: it holds its OAuth token in memory and a 429 (the limit event) does **not** invalidate it, so it never re-reads the keychain on the event we care about. A keychain swap is only picked up by a **fresh** `claude` process.
|
|
14
14
|
|
|
15
|
-
The clean way to exploit that is not to fight the live process but to **replace it at a salvageable moment**: after a turn completes, the conversation is fully written to the transcript JSONL and `claude` is idle at the prompt - killing it there loses nothing, and a `claude --resume <session-id>` comes back cold on the new account and continues exactly where it left off. **This is why we swap at
|
|
15
|
+
The clean way to exploit that is not to fight the live process but to **replace it at a salvageable moment**: after a turn completes, the conversation is fully written to the transcript JSONL and `claude` is idle at the prompt - killing it there loses nothing, and a `claude --resume <session-id>` comes back cold on the new account and continues exactly where it left off. **This is why we swap at 98%: the 2% headroom is the budget to reach a clean turn boundary and respawn before the account actually hits the wall.**
|
|
16
16
|
|
|
17
17
|
A hook can't do the respawn - when `claude` exits, the shell owns the terminal. So tokenmaxxing installs a **supervisor** (aliased to `claude`) that owns the process lifecycle:
|
|
18
18
|
|
|
@@ -47,7 +47,7 @@ No background daemon - it's event-driven (statusline pushes usage; hooks + super
|
|
|
47
47
|
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 shim tees that to `usage.json` (write-on-change, O(ms)) and passes your real statusline through unchanged. 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).
|
|
48
48
|
|
|
49
49
|
### 3.2 Detect + swap + signal (Stop hook, per turn)
|
|
50
|
-
1. Read `usage.json`; `exit 0` fast if both windows `<
|
|
50
|
+
1. Read `usage.json`; `exit 0` fast if both windows `< 98%` (metered per `organizationUuid`).
|
|
51
51
|
2. Else take a `flock` on `~/.config/tokenmaxxing/lock`, re-check under it (parallel sessions race - first winner already swapped), pick the best parked account (not rate-limited, furthest behind its own weekly pace first: highest remaining% / time-to-weekly-reset, since unused allowance is forfeited at the fixed per-account reset; tiebreak soonest expiry then lowest 7-day usage), and **swap the credential** (§3.4).
|
|
52
52
|
3. Write `respawn/<session_id>` (atomic temp+rename) and `SIGTERM` the parent `claude` (`kill -TERM $PPID`). The turn is already committed, so this is a clean stop.
|
|
53
53
|
|
|
@@ -62,7 +62,7 @@ The supervisor's `claude` call returns; it sees `respawn/<sid>`, deletes it, res
|
|
|
62
62
|
5. Do steps 1, 3, 4 inside Claude's own `~/.claude.lock` so the writes can't collide with a token refresh.
|
|
63
63
|
|
|
64
64
|
### 3.5 Multiple concurrent sessions
|
|
65
|
-
Each terminal ran the supervisor, so each has its own child `claude`, its own `--session-id`, and its own respawn marker. When the shared account hits
|
|
65
|
+
Each terminal ran the supervisor, so each has its own child `claude`, its own `--session-id`, and its own respawn marker. When the shared account hits 98%, the first Stop hook to win the `flock` performs the one swap; **every** session's Stop hook writes its own respawn marker and SIGTERMs its own `claude`; **every** supervisor independently relaunches `claude --resume <its-own-sid>`. All of them come back on the new account, each continuing its own conversation. (They share one credential, so they always move together - consistent with "one current account, many windows.")
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
@@ -76,7 +76,7 @@ Each terminal ran the supervisor, so each has its own child `claude`, its own `-
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
78
|
## 5. Rotation policy
|
|
79
|
-
Trigger at `five_hour >=
|
|
79
|
+
Trigger at `five_hour >= 98%` OR `seven_day >= 98%`, per org. "Exhausted" is a **timestamped state** (`resets_at`), not a flag - an account is a candidate again after it resets. Optional projected threshold (`98 − EMA(per-turn Δ%)`) so a single large turn can't blow past 100% before the next Stop hook.
|
|
80
80
|
|
|
81
81
|
**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.
|
|
82
82
|
|
|
@@ -85,7 +85,7 @@ Trigger at `five_hour >= 95%` OR `seven_day >= 95%`, per org. "Exhausted" is a *
|
|
|
85
85
|
## 6. Honest papercuts
|
|
86
86
|
- **Respawn hiccup.** At the swap turn you see `claude` restart (~1–2s) and resume. Anything you typed in the split second before respawn is lost - mitigate by respawning fast and showing a clear "switching" state; the supervisor resets terminal mode so nothing is left garbled.
|
|
87
87
|
- **One cold turn.** Prompt cache is org-scoped: the first turn after resuming on B re-uploads context once (bigger on long transcripts).
|
|
88
|
-
- **Single-turn overshoot.** If one turn jumps from <
|
|
88
|
+
- **Single-turn overshoot.** If one turn jumps from <98% straight past the wall, that turn can end rate-limited before the Stop hook swaps; the respawn then still recovers it. Projected threshold reduces this.
|
|
89
89
|
- **Shared blast radius.** All default-profile sessions share one keychain item, so a swap moves them all (each via its own respawn). The `flock` + re-check is mandatory or racing hooks burn two accounts at once.
|
|
90
90
|
- **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.
|
|
91
91
|
- **statusLine fragility.** The shim is the most visible surface - a bug flickers or breaks your real status line. Keep it O(ms), write-on-change.
|
|
@@ -95,7 +95,7 @@ Trigger at `five_hour >= 95%` OR `seven_day >= 95%`, per org. "Exhausted" is a *
|
|
|
95
95
|
---
|
|
96
96
|
|
|
97
97
|
## 7. Scope
|
|
98
|
-
**v1:** `tokenmaxxing init` / `add` / `ls` / `status` / `doctor`; the supervisor; statusLine shim + Stop/SessionStart hooks;
|
|
98
|
+
**v1:** `tokenmaxxing init` / `add` / `ls` / `status` / `doctor`; the supervisor; statusLine shim + Stop/SessionStart hooks; 98% swap with `flock` + reset-aware picker; platform credential store (macOS keychain / Linux 0600 files, one facade); auto-respawn across concurrent sessions. macOS + Linux.
|
|
99
99
|
|
|
100
100
|
**v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
|
|
101
101
|
|
package/README.md
CHANGED
|
@@ -45,12 +45,12 @@ claude # use claude as always
|
|
|
45
45
|
|
|
46
46
|
## How switching decides
|
|
47
47
|
|
|
48
|
-
Switching triggers at **
|
|
48
|
+
Switching triggers at **98%** (configurable) on any of:
|
|
49
49
|
|
|
50
50
|
- **Session** (5-hour) or **week (all models)** - the aggregate windows, fed free/push-based by the statusLine.
|
|
51
51
|
- **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.
|
|
52
52
|
|
|
53
|
-
The
|
|
53
|
+
The 2% headroom is deliberate: it's the budget to reach a clean turn boundary and respawn before the wall.
|
|
54
54
|
|
|
55
55
|
The **target** is chosen greedily off each account's cached windows: the usable account (session and week under threshold, or past their reset) whose weekly window **expires soonest** - unused weekly allowance is forfeited at the fixed per-account reset. Cached resets are absolute UTC epochs, so a stale snapshot still resolves correctly: a weekly reset that has passed extrapolates forward in 7-day steps, and a session window past its reset counts as empty. `tokenmaxxing switch` ranks the current account too and does nothing when it already wins, so it is idempotent - running it periodically converges on the right account.
|
|
56
56
|
|
|
@@ -60,7 +60,7 @@ The **target** is chosen greedily off each account's cached windows: the usable
|
|
|
60
60
|
|
|
61
61
|
```json
|
|
62
62
|
{
|
|
63
|
-
"threshold":
|
|
63
|
+
"threshold": 98,
|
|
64
64
|
"policy": {
|
|
65
65
|
"projectionMargin": 0,
|
|
66
66
|
"switchModels": ["fable"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/lib/claudelock.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// npm `proper-lockfile` (mkdir-based) at <claudeDir>/.oauth_refresh.lock - the
|
|
4
4
|
// lock is the DIRECTORY itself. We mkdir it; on contention we wait briefly, then
|
|
5
5
|
// proceed anyway (our own flock already serializes tokenmaxxing swaps, and claude
|
|
6
|
-
// only refreshes near token expiry / on 401, rarely at the
|
|
6
|
+
// only refreshes near token expiry / on 401, rarely at the 98% usage trigger).
|
|
7
7
|
|
|
8
8
|
import { mkdirSync, rmdirSync, statSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
package/src/lib/state.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
// ---- config.json (minimal, fixed schema) ---------------------------------
|
|
21
21
|
|
|
22
22
|
const DEFAULT_CONFIG: Config = {
|
|
23
|
-
threshold:
|
|
23
|
+
threshold: 98,
|
|
24
24
|
claudeBin: "",
|
|
25
25
|
// per-model weekly caps exist only for Sonnet and Fable (no Opus-only quota,
|
|
26
26
|
// per the user 2026-07-12), and only Fable's is worth switching on.
|