tokenmaxxing 1.12.0 → 1.13.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,7 +79,7 @@ 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 >= 80%` (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 >= <active rung>` (the 5h ladder `thresholds.session`, default `[90]`, a single rung: the lowest rung some pooled account still clears, the current one included) 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.
82
+ The decision engages at `five_hour >= 80%` (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 automatic greedy path adds **incumbent hysteresis** (`policy.greedySwapMargin`, default 0.15): the seat holds unless a parked account's pace pressure beats it by more than that fraction, so near-tied accounts stop trading the seat back and forth and busting the per-org prompt cache (each such swap forces a full uncached re-read next turn). The margin only gates lateral under-bar swaps, never a crossed bar or the wall; bare `tokenmaxxing switch` sets it to zero and still converges on the strictly best account. The **Layer 1 screening bars** - `five_hour >= <active rung>` (the 5h ladder `thresholds.session`, default `[90]`, a single rung: the lowest rung some pooled account still clears, the current one included) 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
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. 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
85
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "tokenmaxxing",
4
- "version": "1.12.0",
4
+ "version": "1.13.0",
5
5
  "description": "Pool Claude Code and Codex logins, switch on pace pressure, and expose safe ops tools to agent clients.",
6
6
  "author": {
7
7
  "name": "anaclumos",
@@ -8,7 +8,7 @@ description: Explain and apply tokenmaxxing switch policy (greedy vs hard path,
8
8
  ## Vocabulary
9
9
 
10
10
  - **Engaged**: session used >= `policy.greedySessionFloor` (default 80) or any screening bar crossed.
11
- - **GREEDY path**: engaged but under every bar. Rank all accounts by pace pressure; keep seat on best-or-tie (`currentWins`); else swap to strictly better. Never depleted-waits or pre-parks.
11
+ - **GREEDY path**: engaged but under every bar. Rank all accounts by pace pressure; keep seat on best-or-tie (`currentWins`); else swap to strictly better. Never depleted-waits or pre-parks. The automatic path adds incumbent hysteresis (`policy.greedySwapMargin`, default 0.15): hold the seat unless a parked account's pace pressure exceeds the current one's by more than that fraction, so near-tied accounts stop trading the seat and busting the per-org prompt cache. The margin gates lateral under-bar swaps only, never a crossed bar or the wall; bare `tokenmaxxing switch` uses margin 0 and always takes the strictly best account.
12
12
  - **HARD path**: a screening bar crossed. Swap to best usable target; if none, Layer 2 wall logic (Claude only).
13
13
  - **Pace pressure**: remaining weekly percent / time to weekly reset (highest first). Not most-remaining.
14
14
  - **Effective bars**: `effectiveBars(cfg, pool)` = the active rung of the 5h ladder (`thresholds.session`, default `[90]`, a single rung: the lowest rung some pooled account, the current one included, still clears) and the weekly bar, each minus `policy.projectionMargin`. Trigger and screening must share these bars or swaps ping-pong. Codex reads `terminalBars(cfg)`, the top rung only. The check cadence is capped one band per rung climbed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.12.0",
3
+ "version": "1.13.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
@@ -16,6 +16,7 @@ export const KNOWN_KEYS = [
16
16
  "codexBin",
17
17
  "policy.projectionMargin",
18
18
  "policy.greedySessionFloor",
19
+ "policy.greedySwapMargin",
19
20
  "policy.switchModels",
20
21
  "policy.usagePollTtlMs",
21
22
  "policy.maxWaitMs",
package/src/lib/decide.ts CHANGED
@@ -184,11 +184,11 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
184
184
  idx2.accounts.find((a) => a.accountUuid === idx2.activeAccountUuid) ??
185
185
  null;
186
186
 
187
- const greedy = async (): Promise<SwapDecision> => {
187
+ const greedy = async (holdMargin: number): Promise<SwapDecision> => {
188
188
  while (true) {
189
189
  const cur = loadAccounts();
190
190
  const active = seatOf(cur);
191
- const ctxAll = { now, thresholds: barsOf(cur.accounts), currentAccountUuid: null, switchFamilies };
191
+ const ctxAll = { now, thresholds: barsOf(cur.accounts), currentAccountUuid: null, switchFamilies, holdMargin };
192
192
  if (currentWins(active, cur.accounts, ctxAll)) {
193
193
  return { swapped: false, account: null, reason: "current-best" };
194
194
  }
@@ -204,13 +204,13 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
204
204
  return { swapped: true, account: best, reason: "swapped" };
205
205
  }
206
206
  };
207
- if (!enforced2 && !isOver(u2, mu2, org2, bars, cfg, now)) return greedy();
207
+ if (!enforced2 && !isOver(u2, mu2, org2, bars, cfg, now)) return greedy(cfg.policy.greedySwapMargin);
208
208
 
209
209
  while (true) {
210
210
  const cur = loadAccounts();
211
211
  const seat = seatOf(cur);
212
212
  const thresholds = barsOf(cur.accounts);
213
- if (!enforced2 && seat && !seat.needsReauth && !isExhausted(seat, { now, thresholds, currentAccountUuid: null, switchFamilies })) return greedy();
213
+ if (!enforced2 && seat && !seat.needsReauth && !isExhausted(seat, { now, thresholds, currentAccountUuid: null, switchFamilies })) return greedy(0);
214
214
  const best = pickBest(cur.accounts, { now, thresholds, currentAccountUuid: seat?.accountUuid ?? null, switchFamilies });
215
215
  if (!best) break;
216
216
  try {
package/src/lib/picker.ts CHANGED
@@ -35,6 +35,7 @@ const PickCtxSchema = z.object({
35
35
  thresholds: ThresholdsSchema,
36
36
  currentAccountUuid: z.string().nullable(),
37
37
  switchFamilies: z.array(z.string()),
38
+ holdMargin: z.number().min(0).optional(),
38
39
  });
39
40
  export type PickCtx = z.infer<typeof PickCtxSchema>;
40
41
 
@@ -110,6 +111,9 @@ export function currentWins(active: Account | null, accounts: Account[], ctx: Pi
110
111
  if (!active || active.needsReauth || isExhausted(active, ctx)) return false;
111
112
  const best = pickBest(accounts, { ...ctx, currentAccountUuid: null });
112
113
  if (best == null || best.accountUuid === active.accountUuid) return true;
114
+ const margin = ctx.holdMargin ?? 0;
115
+ const bestPace = pacePressure(best, ctx.now);
116
+ if (margin > 0 && bestPace > 0 && bestPace <= pacePressure(active, ctx.now) * (1 + margin)) return true;
113
117
  return swapPreference(ctx.now).every((k) => k(active) === k(best));
114
118
  }
115
119
 
package/src/lib/state.ts CHANGED
@@ -22,7 +22,7 @@ const DEFAULT_CONFIG: Config = {
22
22
  hardThresholds: { session: 100, weekly: 100 },
23
23
  claudeBin: "",
24
24
  codexBin: "",
25
- policy: { projectionMargin: 0, greedySessionFloor: 80, switchModels: ["fable"], usagePollTtlMs: 90_000, maxWaitMs: 3_600_000 },
25
+ policy: { projectionMargin: 0, greedySessionFloor: 80, greedySwapMargin: 0.15, switchModels: ["fable"], usagePollTtlMs: 90_000, maxWaitMs: 3_600_000 },
26
26
  };
27
27
 
28
28
  const PercentSchema = z.number().min(0).max(100);
@@ -37,6 +37,7 @@ export const ConfigFileSchema = z
37
37
  .object({
38
38
  projectionMargin: PercentSchema,
39
39
  greedySessionFloor: PercentSchema,
40
+ greedySwapMargin: z.number().min(0).max(1),
40
41
  switchModels: z.array(z.string()),
41
42
  usagePollTtlMs: z.number().int().positive(),
42
43
  maxWaitMs: z.number().int().positive(),
@@ -66,6 +67,7 @@ export function mergeConfigFile(p: z.infer<typeof ConfigFileSchema>): MergeOutco
66
67
  cfg.codexBin = p.codexBin ?? cfg.codexBin;
67
68
  cfg.policy.projectionMargin = p.policy?.projectionMargin ?? cfg.policy.projectionMargin;
68
69
  cfg.policy.greedySessionFloor = p.policy?.greedySessionFloor ?? cfg.policy.greedySessionFloor;
70
+ cfg.policy.greedySwapMargin = p.policy?.greedySwapMargin ?? cfg.policy.greedySwapMargin;
69
71
  cfg.policy.usagePollTtlMs = p.policy?.usagePollTtlMs ?? cfg.policy.usagePollTtlMs;
70
72
  cfg.policy.maxWaitMs = p.policy?.maxWaitMs ?? cfg.policy.maxWaitMs;
71
73
  if (p.policy?.switchModels) {
package/src/lib/types.ts CHANGED
@@ -121,6 +121,7 @@ export const ConfigSchema = z
121
121
  policy: z.object({
122
122
  projectionMargin: z.number().min(0).max(100),
123
123
  greedySessionFloor: z.number().min(0).max(100),
124
+ greedySwapMargin: z.number().min(0).max(1),
124
125
  switchModels: z.array(z.string()),
125
126
  usagePollTtlMs: z.number().int().positive(),
126
127
  maxWaitMs: z.number().int().positive(),