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 +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/switching-policy/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/cli/config.ts +1 -0
- package/src/lib/decide.ts +4 -4
- package/src/lib/picker.ts +4 -0
- package/src/lib/state.ts +3 -1
- package/src/lib/types.ts +1 -0
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
|
|
package/agent-plugin/plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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
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(),
|