tokenmaxxing 1.119.0 → 1.121.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/README.md +1 -1
- package/package.json +1 -1
- package/src/entries/pisupervisor.ts +1 -1
- package/src/entries/stophook.ts +2 -2
- package/src/entries/supervisor.ts +4 -4
- package/src/lib/decide.ts +7 -3
- package/src/main.ts +6 -0
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick. `claudeBin`, `codexBin`, `grokBin`, `opencodeBin`, and `piBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, `TOKENMAXXING_OPENCODE_BIN`, and `TOKENMAXXING_PI_BIN` override them for one process, except that `init` pins the binary it resolved into `config.json`.
|
|
115
|
+
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left, and a session on it past its bars moves to such an account; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick. `claudeBin`, `codexBin`, `grokBin`, `opencodeBin`, and `piBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, `TOKENMAXXING_OPENCODE_BIN`, and `TOKENMAXXING_PI_BIN` override them for one process, except that `init` pins the binary it resolved into `config.json`.
|
|
116
116
|
|
|
117
117
|
State lives in `~/.config/tokenmaxxing/`; outside it the install touches Claude Code's `settings.json`, codex's `hooks.json`, the timer and usage hub units, and your shell rc. Each account's credential store is `stores/<uuid8>/`, which Claude Code reads and refreshes as its own: on macOS the credential is a login-keychain item keyed by the store path (never plaintext on disk), on Linux a 0600 `.credentials.json` inside the store (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
|
|
118
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.121.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -286,7 +286,7 @@ async function watchSeat(input: { pool: PiPool; seatId: string; id: string; fami
|
|
|
286
286
|
const unmeasured = input.pool === "codex" && (observed == null || now - observed.at > cfg.policy.usagePollTtlMs);
|
|
287
287
|
if (unmeasured) log("pisupervisor.seat_unmeasured", { seat: account.id.slice(0, 8), usageAt: observed?.at });
|
|
288
288
|
const current = observed ? { ...account, windows: observed.windows } : account;
|
|
289
|
-
const until = usableAt(current, { now, thresholds: thresholdBars(cfg), currentId: account.id, families: input.families, seats: null });
|
|
289
|
+
const until = usableAt({ ...current, hasCredits: false }, { now, thresholds: thresholdBars(cfg), currentId: account.id, families: input.families, seats: null });
|
|
290
290
|
if (until <= now || until <= input.overriddenUntil) return { decided: unmeasured, move: null };
|
|
291
291
|
const decision = await evaluateAndMaybeSwap(mover, now, true, null, { seatId: account.id, ...(input.families ? { sessionFamilies: input.families } : {}), waiterId: input.id });
|
|
292
292
|
log("pisupervisor.seat_exhausted", { pool: input.pool, seat: account.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
package/src/entries/stophook.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
4
|
import { readStdin } from "../lib/proc.ts";
|
|
5
|
-
import { adoptLiveSession, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
6
6
|
import { JsonTextSchema } from "../lib/types.ts";
|
|
7
7
|
import { errorMessage, log } from "../lib/log.ts";
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ export async function runBoundaryHook(event: "stop" | "sessionstart"): Promise<n
|
|
|
24
24
|
log(`${event}.nested_session`, { stdin: stdin.session_id?.slice(0, 8), supervised: supervised.live.slice(0, 8), source: stdin.source });
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
|
-
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid });
|
|
27
|
+
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts() });
|
|
28
28
|
if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
29
29
|
writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now(), compact: true, origin: event });
|
|
30
30
|
log(`${event}.${decision.waitUntil !== undefined ? "wait" : "move"}`, { source: stdin.source, account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil, session: session.sid.slice(0, 8), live: session.live.slice(0, 8) });
|
|
@@ -228,18 +228,18 @@ function seatBlockedUntil(seatId: string, now: number, families: string[], cfg:
|
|
|
228
228
|
if (!account) return null;
|
|
229
229
|
const observed = teeObservation(account);
|
|
230
230
|
const current = observed ? { ...account, windows: observed.windows } : account;
|
|
231
|
-
const until = usableAt(current, { now, thresholds: thresholdBars(cfg), currentId: seatId, families, seats: null });
|
|
231
|
+
const until = usableAt({ ...current, hasCredits: false }, { now, thresholds: thresholdBars(cfg), currentId: seatId, families, seats: null });
|
|
232
232
|
return until > now ? until : null;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, model: ModelInfo | null): Promise<boolean> {
|
|
235
|
+
async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, model: ModelInfo | null, refused: string[]): Promise<boolean> {
|
|
236
236
|
try {
|
|
237
237
|
const now = Date.now();
|
|
238
238
|
const cfg = loadConfig();
|
|
239
239
|
const families = gatedFamilies(model, cfg.policy.switchModels);
|
|
240
240
|
const until = seatBlockedUntil(seat.id, now, families, cfg);
|
|
241
241
|
if (until == null || until <= gate.overriddenUntil) return false;
|
|
242
|
-
const decision = await evaluateAndMaybeSwap(claude, now, true, null, { seatId: seat.id, sessionFamilies: families, waiterId: sid });
|
|
242
|
+
const decision = await evaluateAndMaybeSwap(claude, now, true, null, { seatId: seat.id, sessionFamilies: families, waiterId: sid, refused });
|
|
243
243
|
log("supervisor.seat_exhausted", { seat: seat.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
244
244
|
if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
245
245
|
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt, live: liveSessionId(sid) }, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: true, origin: "seatwatch" });
|
|
@@ -535,7 +535,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
535
535
|
tick: async () => {
|
|
536
536
|
if (existsSync(marker) && (await consumableMarker(marker, gate)) != null) return true;
|
|
537
537
|
if (seat && !terminating && Date.now() >= seatCheckAt) {
|
|
538
|
-
const decided = await moveExhaustedSeat(seat, sid, gate, model);
|
|
538
|
+
const decided = await moveExhaustedSeat(seat, sid, gate, model, refused);
|
|
539
539
|
seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
540
540
|
}
|
|
541
541
|
return false;
|
package/src/lib/decide.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { countBy } from "es-toolkit";
|
|
|
2
2
|
import { CLAUDE_COMPACT_KILL_MS } from "./compact.ts";
|
|
3
3
|
import { withLock } from "./lock.ts";
|
|
4
4
|
import { loadAccounts, loadConfig, liveWaitClaims, releaseWaitClaim, replaceWaitClaim, saveAccounts } from "./state.ts";
|
|
5
|
-
import { MAX_WAITERS_PER_ACCOUNT, isExhausted, landWindows, limitWindows, liveUsed, nextWeeklyReset, pickBest, pickWaitTarget, screensUntilReset, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
|
+
import { MAX_WAITERS_PER_ACCOUNT, isExhausted, landWindows, limitWindows, liveUsed, nextWeeklyReset, onCredits, pickBest, pickWaitTarget, screensUntilReset, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
6
6
|
import { familyTokens } from "./usage.ts";
|
|
7
7
|
import { errorMessage, log } from "./log.ts";
|
|
8
8
|
import type { Observation, Provider } from "./provider.ts";
|
|
@@ -15,6 +15,7 @@ export type EvalOpts = {
|
|
|
15
15
|
sessionFamilies?: string[];
|
|
16
16
|
waiterId?: string;
|
|
17
17
|
exclude?: string[];
|
|
18
|
+
refused?: string[];
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const MOVE_CLAIM_MS = 2 * CLAUDE_COMPACT_KILL_MS;
|
|
@@ -27,7 +28,8 @@ function isOver(account: Account | undefined, observed: Observation | null, ctx:
|
|
|
27
28
|
if (account.needsReauth === true) return true;
|
|
28
29
|
if (account.enforcedUntil != null && account.enforcedUntil > ctx.now) return true;
|
|
29
30
|
if (!observed) return false;
|
|
30
|
-
|
|
31
|
+
const seen = { ...account, windows: observed.windows };
|
|
32
|
+
return isExhausted(seen, ctx) || onCredits(seen, ctx);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function modelKindCovered(limit: EnforcedLimit, account: Account, now: number, bars: Bars): boolean {
|
|
@@ -140,7 +142,9 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
140
142
|
const cur = loadAccounts(p.pool);
|
|
141
143
|
const seat = seatOf(cur);
|
|
142
144
|
const ctx: PickCtx = { now, thresholds: bars, currentId: seat?.id ?? null, families: switchFamilies, seats };
|
|
143
|
-
const
|
|
145
|
+
const credited = !enforced2 && seat != null && !seat.needsReauth && !isExhausted(seat, ctx) && onCredits(seat, ctx);
|
|
146
|
+
const best = pickBest(usable(cur.accounts).filter((a) => !(credited && opts.refused?.includes(a.id))), ctx);
|
|
147
|
+
if (credited && (best == null || onCredits(best, ctx))) return { swapped: false, account: null, reason: "no-plan-headroom" };
|
|
144
148
|
if (!best) break;
|
|
145
149
|
try {
|
|
146
150
|
await p.swap(best);
|
package/src/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { basename } from "node:path";
|
|
4
|
+
import pkg from "../package.json" with { type: "json" };
|
|
4
5
|
import { runSupervisor } from "./entries/supervisor.ts";
|
|
5
6
|
import { runStatusline } from "./entries/statusline.ts";
|
|
6
7
|
import { runSubagentStatusline } from "./entries/subagentstatusline.ts";
|
|
@@ -67,6 +68,7 @@ function printHelp(): void {
|
|
|
67
68
|
${c.cyan("tokenmaxxing uninstall")} [--yes] print the targets, then remove supervisor + settings entries (refused without ${c.cyan("--yes")} when HOME is the login home)
|
|
68
69
|
|
|
69
70
|
${c.cyan("--json")} print one JSON document on stdout instead of text (status, config, check); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
|
|
71
|
+
${c.cyan("--version")} print the running package's version and exit (${c.cyan("-v")})
|
|
70
72
|
|
|
71
73
|
${c.dim("(aliased as")} ${c.cyan("xx")}${c.dim(")")} - then just run ${c.bold("claude")} as always; it switches accounts near quota automatically.`);
|
|
72
74
|
}
|
|
@@ -206,6 +208,10 @@ async function main(): Promise<number> {
|
|
|
206
208
|
case "--help":
|
|
207
209
|
printHelp();
|
|
208
210
|
return 0;
|
|
211
|
+
case "-v":
|
|
212
|
+
case "--version":
|
|
213
|
+
console.log(pkg.version);
|
|
214
|
+
return 0;
|
|
209
215
|
default:
|
|
210
216
|
emitError({ message: `unknown command: ${sub}` });
|
|
211
217
|
printHelp();
|