tokenmaxxing 0.6.2 → 0.7.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
@@ -48,7 +48,7 @@ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limi
48
48
 
49
49
  ### 3.2 Detect + swap + signal (Stop hook, per turn)
50
50
  1. Read `usage.json`; `exit 0` fast if both windows `< 95%` (metered per `organizationUuid`).
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, soonest-expiring weekly window first since unused allowance is forfeited at the fixed per-account reset, lowest 7-day usage tiebreak), and **swap the credential** (§3.4).
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
 
54
54
  ### 3.3 Respawn (supervisor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "0.6.2",
3
+ "version": "0.7.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/cli/switch.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // `tokenmaxxing switch [selector]`.
2
- // No selector → greedy: rank EVERY account (current included) by soonest weekly
3
- // expiry among those with session/week under threshold, off the cached windows.
2
+ // No selector → greedy: rank EVERY account (current included) by pace pressure
3
+ // (furthest behind its own weekly pace first, see picker.ts) among those with
4
+ // session/week under threshold, off the cached windows.
4
5
  // When the current account already wins (or ties - swapping between equals buys
5
6
  // nothing), do nothing: the command is idempotent, so running it periodically
6
7
  // converges on the right account. With a selector → switch to that one. Runs
package/src/lib/picker.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  // Choose the account to switch TO. Greedy policy: among usable accounts (no
2
- // reauth, no window >= threshold that hasn't reset yet), take the one whose
3
- // weekly window expires soonest - weekly limits reset at a fixed per-account
4
- // time and unused allowance is forfeited at reset, so quota nearest its reset
5
- // is use-it-or-lose-it and should be drained first. Runs entirely off each
6
- // account's cached windows (absolute UTC epochs, so a stale snapshot still
7
- // resolves to the correct upcoming reset), which makes the pick deterministic
8
- // and idempotent: re-running lands on the same account.
2
+ // reauth, no window >= threshold that hasn't reset yet), take the one furthest
3
+ // behind its own weekly pace - highest pacePressure, the burn rate its
4
+ // remaining weekly quota demands to be consumed at before the fixed
5
+ // per-account reset forfeits it (weekly allowance is use-it-or-lose-it).
6
+ // This refines the older soonest-expiry policy in both directions: equal
7
+ // remaining reduces to soonest expiry first, equal expiry to most remaining
8
+ // first. Runs entirely off each account's cached windows (absolute UTC
9
+ // epochs, so a stale snapshot still resolves to the correct upcoming reset),
10
+ // which makes the pick deterministic and idempotent: re-running lands on the
11
+ // same account.
9
12
 
10
13
  import { minBy, sortBy } from "es-toolkit";
11
14
  import { z } from "zod";
@@ -75,10 +78,28 @@ export function weeklyExpiry(a: Account, now: number): number {
75
78
  return nextWeeklyReset(a.lastUsage?.sevenDay.resetsAt ?? null, now) ?? Number.POSITIVE_INFINITY;
76
79
  }
77
80
 
78
- /** The switch preference: soonest weekly expiry first, tiebreak lowest 7-day
79
- * usage. Shared with the statusLine pool ordering so the display order IS the
81
+ /** How far behind its own weekly pace the account is, measured forward: the
82
+ * burn rate (percent per ms) its remaining weekly quota must be consumed at
83
+ * to beat the reset that forfeits it. A backward-looking used/expected ratio
84
+ * blows up right after a reset (expected ~0) and ignores how much quota is
85
+ * at risk; the required forward rate has neither problem. A window past its
86
+ * cached reset counts as empty (the account is fresh again); an account with
87
+ * no sampled reset anchor has nothing to forfeit on any known clock and
88
+ * ranks last (0). */
89
+ export function pacePressure(a: Account, now: number): number {
90
+ const cached = a.lastUsage?.sevenDay;
91
+ const reset = nextWeeklyReset(cached?.resetsAt ?? null, now);
92
+ if (cached == null || reset == null) return 0;
93
+ const used = cached.resetsAt != null && cached.resetsAt <= now ? 0 : cached.usedPercentage;
94
+ return Math.max(0, 100 - used) / Math.max(1, reset - now);
95
+ }
96
+
97
+ /** The switch preference: furthest behind its own weekly pace first (highest
98
+ * pacePressure), tiebreak soonest weekly expiry then lowest 7-day usage.
99
+ * Shared with the statusLine pool ordering so the display order IS the
80
100
  * swap order. */
81
101
  export const swapPreference = (now: number) => [
102
+ (a: Account) => -pacePressure(a, now),
82
103
  (a: Account) => weeklyExpiry(a, now),
83
104
  (a: Account) => a.lastUsage?.sevenDay.usedPercentage ?? 0,
84
105
  ];