tokenmaxxing 1.2.1 → 1.3.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
@@ -79,9 +79,11 @@ Each terminal ran the supervisor, so each has its own child `claude` and its own
79
79
  ---
80
80
 
81
81
  ## 5. Rotation policy
82
- The decision engages at `five_hour >= 50%` (policy.greedySessionFloor): from there it greedily converges on the usable account furthest behind its weekly pace, staying put whenever the current account wins or ties. The hard bars - `five_hour >= 95%` OR `seven_day >= 98%`, per org - always force a switch and also screen candidates. "Exhausted" is a **timestamped state** (`resets_at`), not a flag - an account is a candidate again after it resets. Optional projected threshold (`bar - policy.projectionMargin`, a fixed configured margin) so a single large turn is less likely to blow past 100% before the next Stop hook.
82
+ The decision engages at `five_hour >= 50%` (policy.greedySessionFloor): from there it greedily converges on the usable account furthest behind its weekly pace, staying put whenever the current account wins or ties. The **Layer 1 screening bars** - `five_hour >= 95%` OR `seven_day >= 98%`, per org (`thresholds`) - force a switch onto a fresher account and also screen candidates. "Exhausted" is a **timestamped state** (`resets_at`), not a flag - an account is a candidate again after it resets. Optional projected threshold (`bar - policy.projectionMargin`, a fixed configured margin) so a single large turn is less likely to blow past 100% before the next Stop hook.
83
83
 
84
- **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.
84
+ **Two layers - pump the last drops.** The screening bars deliberately leave headroom, so when *every* account is over them Layer 1 alone would park the pool with 2-5% of each account's quota still unspent. **Layer 2 - the wall bars** (`hardThresholds`, default `100/100`, the server's own limit) - is the fallback reached only at that point: the session **holds its seat and squeezes** while it is under the wall, else swaps onto the best still-under-wall account (the same pace-pressure ranking as every other swap - squeeze the account whose weekly quota is most about to be forfeited first), and only parks (depleted-wait) once every account has truly walled. Recovery is then measured against the wall, not the screening bar, so an account whose 5h window drops below 100 is squeezable again even while its weekly window still sits above the Layer 1 bar. The wall reading is the statusLine's own `rate_limits` feed - the same server-side figure claude's `/rate-limit-options` renders - so when an account genuinely maxes out the tee shows 100 and Layer 2 moves on; a single-turn overshoot is caught one boundary later (the periodic `check` timer, or the next Stop hook) without needing to sniff assistant text. The serve/SDK path additionally stamps an account walled the instant an *errored* turn result reports a limit (`recordObservedLimit`, gated on `is_error`), because it has no statusLine tee. Set `hardThresholds` equal to `thresholds` to disable Layer 2. **Layer 2 is Claude-only:** a swap on Claude is a hot, in-place credential adoption every concurrent session follows automatically, whereas a running Codex refuses another account's credential (restart is the switch), so a last-drop-swap there would strand any sibling still on the walled account - Codex instead keeps riding its current account to the wall (its existing all-exhausted stay-put already squeezes it).
85
+
86
+ **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. Both layers apply the per-model gate: a burnt Fable cap screens an account out of a Layer 1 switch, and a Fable cap at the wall screens it out of the Layer 2 squeeze too.
85
87
 
86
88
  ---
87
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.2.1",
3
+ "version": "1.3.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/config.ts CHANGED
@@ -20,6 +20,8 @@ import { c } from "./render.ts";
20
20
  export const KNOWN_KEYS = [
21
21
  "thresholds.session",
22
22
  "thresholds.weekly",
23
+ "hardThresholds.session",
24
+ "hardThresholds.weekly",
23
25
  "claudeBin",
24
26
  "codexBin",
25
27
  "policy.projectionMargin",
package/src/cli/switch.ts CHANGED
@@ -113,7 +113,9 @@ export async function cmdSwitch(selector?: string): Promise<number> {
113
113
  // No usable target swapped in: everything is depleted, or the remaining
114
114
  // candidates' refresh tokens just died (performSwap persists needs-reauth
115
115
  // before throwing, hence the reload). Stay on / switch to whichever
116
- // recovers soonest.
116
+ // recovers soonest. (Layer 2 - the wall squeeze - is deliberately confined
117
+ // to the automatic decision path in decide.ts, which decides off the live
118
+ // statusLine tee; bare `xx switch` stays cache-only and simply parks here.)
117
119
  const fresh = loadAccounts();
118
120
  const earliest = pickEarliestReset(fresh.accounts, everyone);
119
121
  if (!earliest) {
@@ -260,7 +260,13 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
260
260
 
261
261
  // Hard path: a bar is crossed. Land on the best usable candidate, walking
262
262
  // past dead grants; a fully depleted pool stays put (no pre-park: nothing
263
- // can pause a codex session for a countdown yet).
263
+ // can pause a codex session for a countdown yet). Layer 2 (the wall) is
264
+ // deliberately claude-only: a running codex cannot hot-adopt a swapped
265
+ // credential (restart IS the switch), so a last-drop-swap onto a still-
266
+ // under-wall account would strand any concurrent sibling on the departed
267
+ // account (the reconcile can only signal siblings onto a Layer-1-usable
268
+ // seat), and a hold-only Layer 2 is identical to codex already staying put
269
+ // here - so codex just rides the current account to its wall.
264
270
  const tried = new Set<string>();
265
271
  while (true) {
266
272
  const current = loadCodexAccounts();
package/src/lib/decide.ts CHANGED
@@ -30,7 +30,7 @@ import { paths } from "./paths.ts";
30
30
  import { loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsage, loadModelUsage, saveAccounts, saveDepletedWait, saveModelUsage, usageTeeAt, writeUsage } from "./state.ts";
31
31
  import { readOAuthAccount } from "./claudejson.ts";
32
32
  import { chooseAndSwap, performSwap } from "./swap.ts";
33
- import { currentWins, effectiveBars, pickBest, pickEarliestReset, usableAt } from "./picker.ts";
33
+ import { currentWins, effectiveBars, hardBars, isExhausted, pickBest, pickEarliestReset, usableAt } from "./picker.ts";
34
34
  import { InvalidGrantError } from "./oauth.ts";
35
35
  import { familyTokens, gatedFamilies, probeUsage } from "./usage.ts";
36
36
  import { log } from "./log.ts";
@@ -307,15 +307,44 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
307
307
  const landed = await chooseAndSwap({ now, thresholds: effectiveBars(cfg), switchFamilies, currentAccountUuid: seatOf(loadAccounts())?.accountUuid ?? null });
308
308
  if (landed) return { swapped: true, account: landed, reason: "swapped" };
309
309
 
310
- // Every account is depleted. Wait for whichever recovers soonest (including
311
- // the current one), if that reset is within the auto-wait window. A dead
312
- // grant on the chosen pre-park target must not abort the wait: performSwap
313
- // persists needs-reauth before throwing, so each retry re-ranks without the
314
- // dead account and the loop terminates (mirrors the greedy loop above).
310
+ // ── LAYER 2 (the wall). Every account is exhausted at the Layer 1
311
+ // screening bars, so Layer 1 alone would park the pool right here with
312
+ // quota still unspent on every account. Before parking, pump the last drops
313
+ // against the hard wall bars (default the server's own 100% limit, the same
314
+ // figure /rate-limit-options reads): hold the seat while it is still under
315
+ // its wall, else move onto the best still-under-wall account (chooseAndSwap
316
+ // keeps the usual pace-pressure ranking - squeeze the account whose weekly
317
+ // quota is most about to be forfeited first). Only when EVERY account has
318
+ // truly walled do we fall through to the depleted-wait park below. The wall
319
+ // reading is the statusLine's authoritative rate_limits tee (the same data
320
+ // /rate-limit-options renders); a single-turn overshoot is caught one
321
+ // boundary later by the check timer or the next Stop hook, and the serve/SDK
322
+ // path additionally stamps observed limits on errored results.
323
+ const hardCtx = { now, thresholds: hardBars(cfg), currentAccountUuid: null, switchFamilies };
324
+ const seat = seatOf(loadAccounts());
325
+ if (seat && !seat.needsReauth && !isExhausted(seat, hardCtx)) {
326
+ log("decide.last_drop_hold", { account: seat.accountUuid.slice(0, 8) });
327
+ return { swapped: false, account: null, reason: "last-drop-hold" };
328
+ }
329
+ const squeezed = await chooseAndSwap({ ...hardCtx, currentAccountUuid: seat?.accountUuid ?? null });
330
+ if (squeezed) {
331
+ log("decide.last_drop_swap", { account: squeezed.accountUuid.slice(0, 8) });
332
+ return { swapped: true, account: squeezed, reason: "last-drop-swap" };
333
+ }
334
+
335
+ // Every account is walled. Wait for whichever drops below its wall soonest
336
+ // (including the current one), if that reset is within the auto-wait window.
337
+ // Recovery is measured against the WALL, not the screening bars: an account
338
+ // whose session window resets below 100 is squeezable again even while its
339
+ // weekly window still sits above the Layer 1 bar, so waiting on the Layer 1
340
+ // reset would over-park. A dead grant on the chosen pre-park target must not
341
+ // abort the wait: performSwap persists needs-reauth before throwing, so each
342
+ // retry re-ranks without the dead account and the loop terminates (mirrors
343
+ // the greedy loop above).
315
344
  while (true) {
316
345
  const fresh = loadAccounts();
317
346
  const current = seatOf(fresh);
318
- const ctx = { now, thresholds: effectiveBars(cfg), currentAccountUuid: current?.accountUuid ?? null, switchFamilies };
347
+ const ctx = { now, thresholds: hardBars(cfg), currentAccountUuid: current?.accountUuid ?? null, switchFamilies };
319
348
  const currentAt = current ? usableAt(current, ctx) : Number.POSITIVE_INFINITY;
320
349
  const other = pickEarliestReset(fresh.accounts, ctx);
321
350
 
package/src/lib/picker.ts CHANGED
@@ -27,6 +27,23 @@ export function effectiveBars(cfg: Config): Thresholds {
27
27
  };
28
28
  }
29
29
 
30
+ /** LAYER 2 - the wall bars. The projection margin is subtracted the SAME way
31
+ * effectiveBars does it, for two reasons: (1) it keeps the documented disable
32
+ * contract honest - `hardThresholds == thresholds` then yields hardBars ==
33
+ * effectiveBars, so Layer 2 has no band to act in and is truly off (without
34
+ * the margin here a nonzero margin left a live band between the two, review
35
+ * catch PR #47); (2) at the default margin 0 the wall is still the literal 100
36
+ * (the server's own figure /rate-limit-options reads). Used only in the
37
+ * all-Layer-1-exhausted fallback, where an account under its wall is still
38
+ * worth squeezing. Config's refine (hardThresholds >= thresholds) guarantees
39
+ * hardBars >= effectiveBars, so Layer 2 is never stricter than Layer 1. */
40
+ export function hardBars(cfg: Config): Thresholds {
41
+ return {
42
+ session: cfg.hardThresholds.session - cfg.policy.projectionMargin,
43
+ weekly: cfg.hardThresholds.weekly - cfg.policy.projectionMargin,
44
+ };
45
+ }
46
+
30
47
  const PickCtxSchema = z.object({
31
48
  now: z.number(),
32
49
  thresholds: ThresholdsSchema,
package/src/lib/state.ts CHANGED
@@ -20,9 +20,14 @@ import {
20
20
  // ---- config.json (minimal, fixed schema) ---------------------------------
21
21
 
22
22
  const DEFAULT_CONFIG: Config = {
23
- // Screening bars, split per window (user 2026-07-16): a session reset is
24
- // cheap to sit out, weekly quota is use-it-or-lose-it so it drains to 98.
23
+ // LAYER 1 - screening bars, split per window (user 2026-07-16): a session
24
+ // reset is cheap to sit out, weekly quota is use-it-or-lose-it so it drains
25
+ // to 98. These drive normal account-to-account switching with headroom.
25
26
  thresholds: { session: 95, weekly: 98 },
27
+ // LAYER 2 - the wall bars (default the server's own 100% limit). Reached only
28
+ // once every account is over its Layer 1 bar: from there a session pumps the
29
+ // last drops up to the wall instead of parking with quota unspent.
30
+ hardThresholds: { session: 100, weekly: 100 },
26
31
  claudeBin: "",
27
32
  codexBin: "",
28
33
  // per-model weekly caps exist only for Sonnet and Fable (no Opus-only quota,
@@ -42,6 +47,7 @@ const PercentSchema = z.number().min(0).max(100);
42
47
  export const ConfigFileSchema = z
43
48
  .object({
44
49
  thresholds: z.object({ session: PercentSchema, weekly: PercentSchema }).partial(),
50
+ hardThresholds: z.object({ session: PercentSchema, weekly: PercentSchema }).partial(),
45
51
  claudeBin: z.string(),
46
52
  codexBin: z.string(),
47
53
  policy: z
@@ -69,9 +75,16 @@ export type MergeOutcome = z.infer<typeof MergeOutcomeSchema>;
69
75
  * throw, silently disabling status/switch/hooks/statusline until the file is
70
76
  * hand-repaired (closing-review catch). */
71
77
  export function mergeConfigFile(p: z.infer<typeof ConfigFileSchema>): MergeOutcome {
72
- const cfg: Config = { ...DEFAULT_CONFIG, thresholds: { ...DEFAULT_CONFIG.thresholds }, policy: { ...DEFAULT_CONFIG.policy } };
78
+ const cfg: Config = {
79
+ ...DEFAULT_CONFIG,
80
+ thresholds: { ...DEFAULT_CONFIG.thresholds },
81
+ hardThresholds: { ...DEFAULT_CONFIG.hardThresholds },
82
+ policy: { ...DEFAULT_CONFIG.policy },
83
+ };
73
84
  cfg.thresholds.session = p.thresholds?.session ?? cfg.thresholds.session;
74
85
  cfg.thresholds.weekly = p.thresholds?.weekly ?? cfg.thresholds.weekly;
86
+ cfg.hardThresholds.session = p.hardThresholds?.session ?? cfg.hardThresholds.session;
87
+ cfg.hardThresholds.weekly = p.hardThresholds?.weekly ?? cfg.hardThresholds.weekly;
75
88
  cfg.claudeBin = p.claudeBin ?? cfg.claudeBin;
76
89
  cfg.codexBin = p.codexBin ?? cfg.codexBin;
77
90
  cfg.policy.projectionMargin = p.policy?.projectionMargin ?? cfg.policy.projectionMargin;
package/src/lib/types.ts CHANGED
@@ -142,6 +142,18 @@ export type Thresholds = z.infer<typeof ThresholdsSchema>;
142
142
  export const ConfigSchema = z
143
143
  .object({
144
144
  thresholds: ThresholdsSchema,
145
+ /** LAYER 2 - the wall bars. `thresholds` (Layer 1) screen normal
146
+ * account-to-account switching and deliberately leave headroom; these are
147
+ * the true-wall bars the decision falls back to ONLY once every account is
148
+ * exhausted at Layer 1. Below the wall a session holds its seat and pumps
149
+ * the last drops; a window at/over the wall (default 100 = the server's own
150
+ * limit, the same figure /rate-limit-options reads) is genuinely spent and
151
+ * the pool moves to the next account, parking only when all are walled.
152
+ * Set equal to `thresholds` to disable Layer 2 (hardBars subtracts the same
153
+ * projectionMargin as effectiveBars, so equal thresholds collapse to one
154
+ * effective bar and Layer 2 has no band to act in). At the default margin 0
155
+ * the wall is the literal 100. */
156
+ hardThresholds: ThresholdsSchema,
145
157
  claudeBin: z.string(),
146
158
  /** the real codex binary (empty = resolve from PATH); pinned by `init --codex`. */
147
159
  codexBin: z.string(),
@@ -168,6 +180,14 @@ export const ConfigSchema = z
168
180
  // switch path churns. Per-field bounds alone cannot see this.
169
181
  .refine((cfg) => cfg.policy.projectionMargin < Math.min(cfg.thresholds.session, cfg.thresholds.weekly), {
170
182
  message: "policy.projectionMargin must be strictly below both thresholds (effectiveBars would hit zero and every account would read as exhausted)",
183
+ })
184
+ // The wall must sit at or above each screening bar. A wall BELOW its
185
+ // screening bar would make Layer 2 "usable" a stricter test than Layer 1
186
+ // screening - the pool could reach the wall fallback and find every account
187
+ // already over the (lower) wall, parking earlier than Layer 1 alone would.
188
+ // Equal is allowed and simply disables Layer 2 for that window.
189
+ .refine((cfg) => cfg.hardThresholds.session >= cfg.thresholds.session && cfg.hardThresholds.weekly >= cfg.thresholds.weekly, {
190
+ message: "hardThresholds (the Layer 2 wall) must be at or above thresholds (the Layer 1 screening bars) for both windows",
171
191
  });
172
192
  export type Config = z.infer<typeof ConfigSchema>;
173
193