tokenmaxxing 1.86.0 → 1.88.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/package.json +1 -1
- package/src/cli/serve.ts +6 -5
- package/src/cli/status.ts +36 -15
- package/src/entries/pisupervisor.ts +7 -1
- package/src/entries/stopfailurehook.ts +2 -2
- package/src/entries/supervisor.ts +23 -10
- package/src/lib/claude.ts +14 -11
- package/src/lib/codex.ts +41 -23
- package/src/lib/decide.ts +5 -5
- package/src/lib/picker.ts +13 -5
- package/src/lib/sample.ts +6 -7
package/DESIGN.md
CHANGED
|
@@ -37,7 +37,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
|
|
|
37
37
|
- `usage/<uuid8>.json` - the statusline tee per account, written by the sessions running on it.
|
|
38
38
|
- `live/<session-id>` - PID-validated presence per supervised session, naming its account; placement counts them.
|
|
39
39
|
- `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal): the target account, the session to resume, a `waitUntil`, a `compact` flag, the `origin` (the hook or the seat watch that wrote it), and the launch time the marker was written for.
|
|
40
|
-
- `wait-queue.json` - the
|
|
40
|
+
- `wait-queue.json` - the claims of supervised sessions that are moving or waiting (`{sessionId, accountId, at, waitUntil}`), naming the account each relaunches on: placement counts a claimed session there, and at most four sessions wait on one account; a session that arrives when every account already has four waiters takes the soonest reset anyway.
|
|
41
41
|
- `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
|
|
42
42
|
- `pi-stores/claude/<uuid8>/` and `pi-stores/codex/<uuid8>/` - one pi store per pooled account that has a pi login: the account's own pi grant in `auth.json`, everything else symlinked to the shared `~/.pi/agent` ([pi support](docs/content/docs/pi.mdx)).
|
|
43
43
|
- `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` and `opencode-go-*` state of the status-only pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.88.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",
|
package/src/cli/serve.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { Provider } from "../lib/provider.ts";
|
|
|
14
14
|
import { foldTee } from "../lib/sample.ts";
|
|
15
15
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
16
16
|
import { OAUTH_USAGE_URL } from "../lib/usage.ts";
|
|
17
|
-
import type { Account, Config } from "../lib/types.ts";
|
|
17
|
+
import type { Account, Config, Thresholds } from "../lib/types.ts";
|
|
18
18
|
import { c, emitError } from "./render.ts";
|
|
19
19
|
import { usageReport, type UsageReport, type WindowReport } from "./status.ts";
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ const STALE_AFTER_MS = 2 * 60 * 60_000;
|
|
|
24
24
|
type HubPool = {
|
|
25
25
|
provider: Provider;
|
|
26
26
|
usageUrl: string;
|
|
27
|
-
fold: (account: Account) => void;
|
|
27
|
+
fold: (account: Account, thresholds: Thresholds) => void;
|
|
28
28
|
read: (account: Account, cfg: Config, now: number) => Promise<Account>;
|
|
29
29
|
body: (usage: UsageReport, account: Account) => unknown;
|
|
30
30
|
};
|
|
@@ -96,7 +96,7 @@ function authFiles(cfg: Config, now: number): unknown {
|
|
|
96
96
|
const files = POOLS.flatMap((pool) => {
|
|
97
97
|
const ctx = { now, thresholds: thresholdBars(cfg), currentId: null, families: pool.provider.gatedFamilies(cfg), seats: null };
|
|
98
98
|
return loadAccounts(pool.provider.pool).accounts.map((a) => {
|
|
99
|
-
pool.fold(a);
|
|
99
|
+
pool.fold(a, ctx.thresholds);
|
|
100
100
|
return {
|
|
101
101
|
id: a.id,
|
|
102
102
|
auth_index: authIndex(pool, a),
|
|
@@ -140,8 +140,9 @@ async function apiCall(req: Request): Promise<Response> {
|
|
|
140
140
|
const account = loadAccounts(pool.provider.pool).accounts.find((a) => authIndex(pool, a) === index);
|
|
141
141
|
if (!account) return json({ error: "auth credential not found for auth_index" }, 400);
|
|
142
142
|
const now = Date.now();
|
|
143
|
-
const
|
|
144
|
-
pool.
|
|
143
|
+
const cfg = loadConfig();
|
|
144
|
+
const current = await pool.read(account, cfg, now);
|
|
145
|
+
pool.fold(current, thresholdBars(cfg));
|
|
145
146
|
const usage = usageReport({ ...current, windows: current.windows.filter((w) => now - w.sampledAt <= STALE_AFTER_MS) }, now);
|
|
146
147
|
if (!usage || (!usage.fiveHour && !usage.week)) return json({ error: `no usage figure newer than ${STALE_AFTER_MS / 3_600_000}h for this account` }, 502);
|
|
147
148
|
return json({ status_code: 200, header: { "Content-Type": ["application/json"] }, body: JSON.stringify(pool.body(usage, current)) });
|
package/src/cli/status.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { opencodeGo } from "../lib/opencodego.ts";
|
|
|
6
6
|
import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
|
|
7
7
|
import { withLock } from "../lib/lock.ts";
|
|
8
8
|
import { codexPool, grokPool, opencodeGoPool } from "../lib/paths.ts";
|
|
9
|
-
import { earliestReset, gatedWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
9
|
+
import { earliestReset, gatedWindows, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
10
10
|
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
11
11
|
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
12
12
|
import type { Account, Config, Thresholds, Window } from "../lib/types.ts";
|
|
@@ -84,25 +84,46 @@ function gatedNote(accounts: Account[], families: string[] | null, weeklyBar: nu
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async function collect(p: Provider, cfg: Config, now: number, cached: boolean): Promise<PoolReport> {
|
|
87
|
+
const bars = thresholdBars(cfg);
|
|
87
88
|
let idx = loadAccounts(p.pool);
|
|
88
89
|
let reports = new Map<string, SampleReport>();
|
|
89
90
|
let liveId: string | null = null;
|
|
90
91
|
if (!cached && idx.accounts.length > 0) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
const notice = process.stderr.isTTY;
|
|
93
|
+
if (notice) process.stderr.write(c.dim(`sampling ${p.name} usage...`));
|
|
94
|
+
try {
|
|
95
|
+
liveId = p.liveId();
|
|
96
|
+
const retries = new Map(idx.accounts.map((a) => [a.id, a.usageRetryAt ?? null]));
|
|
97
|
+
reports = await p.samplePool(idx.accounts, liveId, now);
|
|
98
|
+
await withLock(p.pool.lockFile, () => {
|
|
99
|
+
const fresh = loadAccounts(p.pool);
|
|
100
|
+
let dirty = false;
|
|
101
|
+
for (const a of idx.accounts) {
|
|
102
|
+
const stored = fresh.accounts.find((x) => x.id === a.id);
|
|
103
|
+
if (!stored) continue;
|
|
104
|
+
if (a.needsReauth === true && stored.needsReauth !== true) {
|
|
105
|
+
stored.needsReauth = true;
|
|
106
|
+
dirty = true;
|
|
107
|
+
}
|
|
108
|
+
if ((a.usageRetryAt ?? null) !== retries.get(a.id)) {
|
|
109
|
+
stored.usageRetryAt = a.usageRetryAt;
|
|
110
|
+
dirty = true;
|
|
111
|
+
}
|
|
112
|
+
if (a.lastUsageAt != null && (stored.lastUsageAt == null || a.lastUsageAt > stored.lastUsageAt)) {
|
|
113
|
+
landWindows(stored, p.mergeWindows(a.windows, stored.windows), a.lastUsageAt, bars);
|
|
114
|
+
if (a.email != null) stored.email = a.email;
|
|
115
|
+
if (a.tier != null) stored.tier = a.tier;
|
|
116
|
+
dirty = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (dirty) saveAccounts(p.pool, fresh);
|
|
120
|
+
idx = fresh;
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
if (notice) process.stderr.write("\r\x1b[2K");
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
|
|
105
|
-
const bars = thresholdBars(cfg);
|
|
106
127
|
const present = p.presence();
|
|
107
128
|
const ctx = { now, thresholds: bars, currentId: null, families: p.gatedFamilies(cfg), seats: null };
|
|
108
129
|
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
@@ -235,7 +256,7 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
235
256
|
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
236
257
|
console.log();
|
|
237
258
|
} else {
|
|
238
|
-
const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
|
|
259
|
+
const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% bars 5h ${claudeReport.bars.session}% weekly ${claudeReport.bars.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
|
|
239
260
|
renderPool(claude, claudeReport, header, Date.now(), cfg.policy.usagePollTtlMs);
|
|
240
261
|
}
|
|
241
262
|
}
|
|
@@ -386,6 +386,13 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
|
386
386
|
savedTermios,
|
|
387
387
|
});
|
|
388
388
|
}
|
|
389
|
+
if (pool === "claude") {
|
|
390
|
+
try {
|
|
391
|
+
releaseWaitClaim(id);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
log("pisupervisor.claim_release_failed", { err: errorMessage(e) });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
389
396
|
return { proc: spawned, seat: picked };
|
|
390
397
|
});
|
|
391
398
|
if (launched == null) {
|
|
@@ -422,7 +429,6 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
|
422
429
|
if (waitUntil > Date.now()) {
|
|
423
430
|
if (await countdownWait(target.label, waitUntil, { stream: false, say })) overriddenUntil = waitUntil;
|
|
424
431
|
} else say(`\n\x1b[36m↻ tokenmaxxing: moving pi to ${target.label} - resuming...\x1b[0m\n`);
|
|
425
|
-
await releaseClaim();
|
|
426
432
|
wanted = target.id;
|
|
427
433
|
launchArgs = sessionExists(dirs, sid) ? ["--session-id", sid, ...args.flags, "--", RESUME_PROMPT] : firstArgs;
|
|
428
434
|
continue;
|
|
@@ -80,8 +80,8 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn
|
|
84
|
-
if (
|
|
83
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined });
|
|
84
|
+
if (session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
85
85
|
writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: false, origin: "stopfailure" });
|
|
86
86
|
log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil ?? now });
|
|
87
87
|
} else {
|
|
@@ -180,7 +180,24 @@ type MarkerGate = {
|
|
|
180
180
|
overriddenUntil: number;
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
-
function
|
|
183
|
+
function releaseClaimLocked(sid: string): void {
|
|
184
|
+
try {
|
|
185
|
+
releaseWaitClaim(sid);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
log("supervisor.claim_release_failed", { err: errorMessage(e) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function discardMarker(marker: string, event: string, fields: Record<string, unknown>): Promise<null> {
|
|
192
|
+
await withLock(claudePool.lockFile, () => {
|
|
193
|
+
rmSync(marker, { force: true });
|
|
194
|
+
releaseClaimLocked(basename(marker));
|
|
195
|
+
});
|
|
196
|
+
log(event, fields);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function consumableMarker(marker: string, gate: MarkerGate): Promise<z.infer<typeof RespawnMarkerSchema> | null> {
|
|
184
201
|
let m: z.infer<typeof RespawnMarkerSchema>;
|
|
185
202
|
try {
|
|
186
203
|
m = readJsonFile(marker, RespawnMarkerSchema);
|
|
@@ -191,14 +208,10 @@ function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof Resp
|
|
|
191
208
|
);
|
|
192
209
|
}
|
|
193
210
|
if (m.launchedAt !== undefined && m.launchedAt !== gate.launchedAt) {
|
|
194
|
-
|
|
195
|
-
log("supervisor.marker_stale", { markerLaunch: m.launchedAt, childLaunch: gate.launchedAt });
|
|
196
|
-
return null;
|
|
211
|
+
return discardMarker(marker, "supervisor.marker_stale", { markerLaunch: m.launchedAt, childLaunch: gate.launchedAt });
|
|
197
212
|
}
|
|
198
213
|
if (m.waitUntil > Date.now() && m.waitUntil <= gate.overriddenUntil) {
|
|
199
|
-
|
|
200
|
-
log("supervisor.marker_overridden", { waitUntil: m.waitUntil });
|
|
201
|
-
return null;
|
|
214
|
+
return discardMarker(marker, "supervisor.marker_overridden", { waitUntil: m.waitUntil });
|
|
202
215
|
}
|
|
203
216
|
return m;
|
|
204
217
|
}
|
|
@@ -450,6 +463,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
450
463
|
savedTermios,
|
|
451
464
|
});
|
|
452
465
|
}
|
|
466
|
+
releaseClaimLocked(sid);
|
|
453
467
|
return { child: spawned, seat: picked };
|
|
454
468
|
});
|
|
455
469
|
if (launched == null) {
|
|
@@ -462,7 +476,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
462
476
|
await raceMarkerOrExit({
|
|
463
477
|
child: proc,
|
|
464
478
|
tick: async () => {
|
|
465
|
-
if (existsSync(marker) && consumableMarker(marker, gate) != null) return true;
|
|
479
|
+
if (existsSync(marker) && (await consumableMarker(marker, gate)) != null) return true;
|
|
466
480
|
if (seat && !terminating && Date.now() >= seatCheckAt) {
|
|
467
481
|
const decided = await moveExhaustedSeat(seat, sid, gate, model);
|
|
468
482
|
seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
@@ -478,7 +492,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
478
492
|
},
|
|
479
493
|
});
|
|
480
494
|
|
|
481
|
-
const m = !terminating && existsSync(marker) ? consumableMarker(marker, gate) : null;
|
|
495
|
+
const m = !terminating && existsSync(marker) ? await consumableMarker(marker, gate) : null;
|
|
482
496
|
if (m) {
|
|
483
497
|
rmSync(marker, { force: true });
|
|
484
498
|
respawns++;
|
|
@@ -511,7 +525,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
511
525
|
if (m.waitUntil > Date.now()) {
|
|
512
526
|
if (await countdownWait(label, m.waitUntil, { stream, say })) overriddenUntil = m.waitUntil;
|
|
513
527
|
} else say(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`, `tokenmaxxing: moving to ${label} and resuming.`);
|
|
514
|
-
await releaseClaim();
|
|
515
528
|
wanted = m.accountId;
|
|
516
529
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
517
530
|
const prompt = resumable ? resumePrompt({ compacted, origin: m.origin }) : null;
|
package/src/lib/claude.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { countBy } from "es-toolkit";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
|
|
5
6
|
import { CLAUDE_BIN, resolveRealBin, resolveVerifiedClaude } from "./claudebin.ts";
|
|
@@ -8,12 +9,12 @@ import { withLock } from "./lock.ts";
|
|
|
8
9
|
import { errorMessage, log } from "./log.ts";
|
|
9
10
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
10
11
|
import { claudePool, env, paths, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
11
|
-
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
12
|
+
import { landWindows, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
12
13
|
import { deletePiStore } from "./piauth.ts";
|
|
13
|
-
import {
|
|
14
|
+
import { livingPresences } from "./presence.ts";
|
|
14
15
|
import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
|
|
15
16
|
import { foldTee, sampleAccountUsage, teeObservation, usageBlockedUntil } from "./sample.ts";
|
|
16
|
-
import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
|
|
17
|
+
import { clearUsageSnapshot, liveWaitClaims, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
|
|
17
18
|
import { saveTermios, restoreTermios } from "./tty.ts";
|
|
18
19
|
import { fetchUsageDirect, gatedFamilies, mergeWindows, modelFromFlag, scrubCredentialEnv, windowsOf } from "./usage.ts";
|
|
19
20
|
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config, type ModelInfo } from "./types.ts";
|
|
@@ -24,7 +25,9 @@ function liveId(): string | null {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function presence(): Map<string, number> {
|
|
27
|
-
|
|
28
|
+
const seats = new Map(livingPresences(paths.presenceDir).map((s) => [s.id, s.accountId]));
|
|
29
|
+
for (const claim of liveWaitClaims(Date.now())) seats.set(claim.sessionId, claim.accountId);
|
|
30
|
+
return new Map(Object.entries(countBy([...seats.values()], (id) => id)));
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
const PROBE_BACKOFF_CAP_MS = 30 * 60 * 1000;
|
|
@@ -48,8 +51,7 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
|
|
|
48
51
|
if (outcome.ok) {
|
|
49
52
|
a.probeFails = 0;
|
|
50
53
|
if (a.lastUsageAt == null || startedAt > a.lastUsageAt) {
|
|
51
|
-
a
|
|
52
|
-
a.lastUsageAt = startedAt;
|
|
54
|
+
landWindows(a, mergeWindows(windowsOf(outcome.usage, startedAt), a.windows), startedAt, thresholdBars(cfg));
|
|
53
55
|
}
|
|
54
56
|
} else {
|
|
55
57
|
a.probeFails = (a.probeFails ?? 0) + 1;
|
|
@@ -64,12 +66,13 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
|
|
|
64
66
|
|
|
65
67
|
async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
|
|
66
68
|
const reports = new Map<string, SampleReport>();
|
|
69
|
+
const bars = thresholdBars(loadConfig());
|
|
67
70
|
await Promise.all(
|
|
68
71
|
accounts.map(async (a) => {
|
|
69
72
|
const tee = loadUsageSnapshot(a.id);
|
|
70
73
|
const teeAt = tee == null ? null : (tee.state.sampledAt ?? tee.state.ts);
|
|
71
74
|
if (teeAt != null && (a.lastUsageAt == null || teeAt >= a.lastUsageAt)) {
|
|
72
|
-
foldTee(a);
|
|
75
|
+
foldTee(a, bars);
|
|
73
76
|
reports.set(a.id, { ok: true, source: "statusline" });
|
|
74
77
|
return;
|
|
75
78
|
}
|
|
@@ -80,8 +83,7 @@ async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport
|
|
|
80
83
|
return;
|
|
81
84
|
}
|
|
82
85
|
const at = Date.now();
|
|
83
|
-
a.
|
|
84
|
-
a.windows = mergeWindows(windowsOf(outcome.usage, at), a.windows);
|
|
86
|
+
landWindows(a, mergeWindows(windowsOf(outcome.usage, at), a.windows), at, bars);
|
|
85
87
|
reports.set(a.id, { ok: true, source: "probe" });
|
|
86
88
|
}),
|
|
87
89
|
);
|
|
@@ -120,11 +122,12 @@ async function storeUsable(a: Account): Promise<boolean> {
|
|
|
120
122
|
|
|
121
123
|
export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Account) => boolean = () => true): Account | null {
|
|
122
124
|
const cfg = loadConfig();
|
|
125
|
+
const bars = thresholdBars(cfg);
|
|
123
126
|
const idx = loadAccounts(claudePool);
|
|
124
127
|
let dirty = false;
|
|
125
|
-
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
128
|
+
for (const a of idx.accounts) dirty = foldTee(a, bars) || dirty;
|
|
126
129
|
if (dirty) saveAccounts(claudePool, idx);
|
|
127
|
-
const ctx: PickCtx = { now, thresholds:
|
|
130
|
+
const ctx: PickCtx = { now, thresholds: bars, currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats: presence() };
|
|
128
131
|
const candidates = idx.accounts.filter(eligible);
|
|
129
132
|
return pickBest(candidates, ctx) ?? pickEarliestReset(candidates, ctx)?.account ?? null;
|
|
130
133
|
}
|
package/src/lib/codex.ts
CHANGED
|
@@ -33,9 +33,10 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
|
|
|
33
33
|
if (usage.planType != null) account.tier = usage.planType;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type
|
|
36
|
+
type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean };
|
|
37
|
+
type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | CodexReadFailure;
|
|
37
38
|
|
|
38
|
-
async function
|
|
39
|
+
async function freshCodexAuth(account: Account, now: number, refresh: boolean, holder?: string): Promise<{ ok: true; auth: CodexAuthJson } | CodexReadFailure> {
|
|
39
40
|
let auth: CodexAuthJson | null;
|
|
40
41
|
try {
|
|
41
42
|
auth = readCodexStoreAuth(account.id);
|
|
@@ -43,22 +44,30 @@ async function readCodexUsage(account: Account, now: number, refresh: boolean, h
|
|
|
43
44
|
return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
|
|
44
45
|
}
|
|
45
46
|
if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
|
|
47
|
+
if (!isCodexAccessExpiring({ auth, now })) return { ok: true, auth };
|
|
48
|
+
if (!refresh) return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false, expiring: true };
|
|
49
|
+
if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
|
|
50
|
+
return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
|
|
51
|
+
}
|
|
46
52
|
try {
|
|
47
|
-
|
|
48
|
-
if (!refresh) {
|
|
49
|
-
return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false };
|
|
50
|
-
}
|
|
51
|
-
if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
|
|
52
|
-
return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
|
|
53
|
-
}
|
|
54
|
-
auth = await refreshCodexAuth({ auth, now });
|
|
55
|
-
writeCodexStoreAuth(account.id, auth);
|
|
56
|
-
}
|
|
57
|
-
const at = Date.now();
|
|
58
|
-
return { ok: true, usage: await fetchCodexUsage({ auth, at }), at };
|
|
53
|
+
auth = await refreshCodexAuth({ auth, now });
|
|
59
54
|
} catch (e) {
|
|
60
55
|
if (e instanceof CodexInvalidGrantError) return { ok: false, reason: e.message, deadGrant: true };
|
|
61
|
-
if (e instanceof CodexRefreshFailedError
|
|
56
|
+
if (e instanceof CodexRefreshFailedError) return { ok: false, reason: e.message, deadGrant: false };
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
writeCodexStoreAuth(account.id, auth);
|
|
60
|
+
return { ok: true, auth };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
|
|
64
|
+
const fresh = await freshCodexAuth(account, now, refresh, holder);
|
|
65
|
+
if (!fresh.ok) return fresh;
|
|
66
|
+
try {
|
|
67
|
+
const at = Date.now();
|
|
68
|
+
return { ok: true, usage: await fetchCodexUsage({ auth: fresh.auth, at }), at };
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false };
|
|
62
71
|
throw e;
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -86,18 +95,27 @@ export async function observeCodex(account: Account, cfg: Config, now: number, o
|
|
|
86
95
|
|
|
87
96
|
async function samplePool(accounts: Account[], _liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
|
|
88
97
|
const reports = new Map<string, SampleReport>();
|
|
98
|
+
const record = (account: Account, outcome: CodexReadOutcome): void => {
|
|
99
|
+
if (outcome.ok) {
|
|
100
|
+
applyUsage(account, outcome.usage, outcome.at);
|
|
101
|
+
reports.set(account.id, { ok: true, source: "probe" });
|
|
102
|
+
} else {
|
|
103
|
+
if (outcome.deadGrant) account.needsReauth = true;
|
|
104
|
+
reports.set(account.id, { ok: false, reason: outcome.reason });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const expiring: Account[] = [];
|
|
89
108
|
await Promise.all(
|
|
90
109
|
accounts.map(async (account) => {
|
|
91
|
-
const outcome = await readCodexUsage(account, now,
|
|
92
|
-
if (outcome.ok)
|
|
93
|
-
|
|
94
|
-
reports.set(account.id, { ok: true, source: "probe" });
|
|
95
|
-
} else {
|
|
96
|
-
if (outcome.deadGrant) account.needsReauth = true;
|
|
97
|
-
reports.set(account.id, { ok: false, reason: outcome.reason });
|
|
98
|
-
}
|
|
110
|
+
const outcome = await readCodexUsage(account, now, false);
|
|
111
|
+
if (!outcome.ok && outcome.expiring === true) expiring.push(account);
|
|
112
|
+
else record(account, outcome);
|
|
99
113
|
}),
|
|
100
114
|
);
|
|
115
|
+
if (expiring.length > 0) {
|
|
116
|
+
const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async (account) => ({ account, fresh: await freshCodexAuth(account, now, true) }))));
|
|
117
|
+
await Promise.all(rotated.map(async ({ account, fresh }) => record(account, fresh.ok ? await readCodexUsage(account, now, false) : fresh)));
|
|
118
|
+
}
|
|
101
119
|
return reports;
|
|
102
120
|
}
|
|
103
121
|
|
package/src/lib/decide.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { countBy } from "es-toolkit";
|
|
2
|
+
import { CLAUDE_COMPACT_KILL_MS } from "./compact.ts";
|
|
2
3
|
import { withLock } from "./lock.ts";
|
|
3
4
|
import { loadAccounts, loadConfig, liveWaitClaims, releaseWaitClaim, replaceWaitClaim, saveAccounts } from "./state.ts";
|
|
4
|
-
import { isExhausted, limitWindows, liveUsed, nextWeeklyReset, pickBest, pickWaitTarget, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
|
+
import { isExhausted, landWindows, limitWindows, liveUsed, nextWeeklyReset, pickBest, pickWaitTarget, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
6
|
import { familyTokens } from "./usage.ts";
|
|
6
7
|
import { errorMessage, log } from "./log.ts";
|
|
7
8
|
import type { Observation, Provider } from "./provider.ts";
|
|
@@ -16,6 +17,7 @@ export type EvalOpts = {
|
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const MAX_WAITERS_PER_ACCOUNT = 4;
|
|
20
|
+
const MOVE_CLAIM_MS = 2 * CLAUDE_COMPACT_KILL_MS;
|
|
19
21
|
|
|
20
22
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
23
|
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
@@ -100,10 +102,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
100
102
|
for (const a of idx.accounts) {
|
|
101
103
|
const obs = await p.observeLive(a, cfg, now, { probe: false, perModel: false });
|
|
102
104
|
if (a === active) obs2 = obs;
|
|
103
|
-
if (obs && (a.lastUsageAt == null || obs.at > a.lastUsageAt))
|
|
104
|
-
a.windows = p.mergeWindows(obs.windows, a.windows);
|
|
105
|
-
a.lastUsageAt = obs.at;
|
|
106
|
-
}
|
|
105
|
+
if (obs && (a.lastUsageAt == null || obs.at > a.lastUsageAt)) landWindows(a, p.mergeWindows(obs.windows, a.windows), obs.at, bars);
|
|
107
106
|
}
|
|
108
107
|
saveAccounts(p.pool, idx);
|
|
109
108
|
|
|
@@ -146,6 +145,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
146
145
|
continue;
|
|
147
146
|
}
|
|
148
147
|
log("decide.swap", { account: best.id.slice(0, 8), enforced: enforced2 != null });
|
|
148
|
+
if (waiterId != null && p.seats === "shared") replaceWaitClaim({ sessionId: waiterId, accountId: best.id, at: now, waitUntil: now + MOVE_CLAIM_MS });
|
|
149
149
|
return { swapped: true, account: best, reason: "swapped" };
|
|
150
150
|
}
|
|
151
151
|
|
package/src/lib/picker.ts
CHANGED
|
@@ -53,16 +53,16 @@ function barFor(w: Window, thresholds: Thresholds): number {
|
|
|
53
53
|
return isSessionWindow(w) ? thresholds.session : thresholds.weekly;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function blockedUntil(w: Window, bar: number): number {
|
|
57
|
-
if (w
|
|
56
|
+
function blockedUntil(w: Window, bar: number, now: number): number {
|
|
57
|
+
if (liveUsed(w, now) < bar) return 0;
|
|
58
58
|
if (w.resetsAt != null) return w.resetsAt;
|
|
59
|
-
return w.
|
|
59
|
+
return w.sampledAt + (w.windowSeconds ?? 0) * 1000;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function blockingUntil(a: Account, ctx: PickCtx): number[] {
|
|
63
63
|
return [
|
|
64
|
-
...a.windows.filter((w) => w.name == null).map((w) => blockedUntil(w, barFor(w, ctx.thresholds))),
|
|
65
|
-
...gatedWindows(a, ctx.families).map((w) => blockedUntil(w, barFor(w, ctx.thresholds))),
|
|
64
|
+
...a.windows.filter((w) => w.name == null).map((w) => blockedUntil(w, barFor(w, ctx.thresholds), ctx.now)),
|
|
65
|
+
...gatedWindows(a, ctx.families).map((w) => blockedUntil(w, barFor(w, ctx.thresholds), ctx.now)),
|
|
66
66
|
...(a.enforcedUntil != null ? [a.enforcedUntil] : []),
|
|
67
67
|
];
|
|
68
68
|
}
|
|
@@ -71,6 +71,14 @@ export function isExhausted(a: Account, ctx: PickCtx): boolean {
|
|
|
71
71
|
return blockingUntil(a, ctx).some((t) => t > ctx.now);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function landWindows(a: Account, windows: Window[], at: number, thresholds: Thresholds): void {
|
|
75
|
+
const ctx: PickCtx = { now: at, thresholds, currentId: null, families: [], seats: null };
|
|
76
|
+
const blocked = a.enforcedUntil != null && isExhausted({ ...a, enforcedUntil: undefined }, ctx);
|
|
77
|
+
a.windows = windows;
|
|
78
|
+
a.lastUsageAt = at;
|
|
79
|
+
if (blocked && !isExhausted({ ...a, enforcedUntil: undefined }, ctx)) a.enforcedUntil = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
export function nextWeeklyReset(resetsAt: number | null, now: number): number | null {
|
|
75
83
|
if (resetsAt == null || resetsAt > now) return resetsAt;
|
|
76
84
|
return resetsAt + (Math.floor((now - resetsAt) / WEEK_MS) + 1) * WEEK_MS;
|
package/src/lib/sample.ts
CHANGED
|
@@ -3,11 +3,12 @@ import { withLock } from "./lock.ts";
|
|
|
3
3
|
import { errorMessage, log } from "./log.ts";
|
|
4
4
|
import { claudeTierLabel, isDeadCredential } from "./oauth.ts";
|
|
5
5
|
import { claudePool, paths } from "./paths.ts";
|
|
6
|
+
import { landWindows, thresholdBars } from "./picker.ts";
|
|
6
7
|
import { seatCounts } from "./presence.ts";
|
|
7
8
|
import type { Observation } from "./provider.ts";
|
|
8
9
|
import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
|
|
9
10
|
import { fetchUsageDirect, mergeWindows, windowsOf } from "./usage.ts";
|
|
10
|
-
import type { Account, Config, UsageWindows } from "./types.ts";
|
|
11
|
+
import type { Account, Config, Thresholds, UsageWindows } from "./types.ts";
|
|
11
12
|
|
|
12
13
|
export type SampleOutcome = { ok: true; usage: UsageWindows; via: "get" } | { ok: false; reason: string; retryAt?: number };
|
|
13
14
|
|
|
@@ -23,11 +24,10 @@ export function teeObservation(account: Account): Observation | null {
|
|
|
23
24
|
return { windows: mergeWindows(aggregate, account.windows), at };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function foldTee(account: Account): boolean {
|
|
27
|
+
export function foldTee(account: Account, thresholds: Thresholds): boolean {
|
|
27
28
|
const observed = teeObservation(account);
|
|
28
29
|
if (!observed || (account.lastUsageAt != null && observed.at <= account.lastUsageAt)) return false;
|
|
29
|
-
account.windows
|
|
30
|
-
account.lastUsageAt = observed.at;
|
|
30
|
+
landWindows(account, observed.windows, observed.at, thresholds);
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -79,7 +79,7 @@ export async function sampleOldest(cfg: Config): Promise<void> {
|
|
|
79
79
|
const now = Date.now();
|
|
80
80
|
const idx = loadAccounts(claudePool);
|
|
81
81
|
let dirty = false;
|
|
82
|
-
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
82
|
+
for (const a of idx.accounts) dirty = foldTee(a, thresholdBars(cfg)) || dirty;
|
|
83
83
|
const sampledAt = (a: Account) => Math.max(a.lastUsageAt ?? 0, a.lastProbeAt ?? 0);
|
|
84
84
|
const seats = seatCounts(paths.presenceDir);
|
|
85
85
|
const stale = idx.accounts
|
|
@@ -123,8 +123,7 @@ export async function sampleOldest(cfg: Config): Promise<void> {
|
|
|
123
123
|
dirty = true;
|
|
124
124
|
}
|
|
125
125
|
if (stored && outcome.ok && (stored.lastUsageAt == null || startedAt > stored.lastUsageAt)) {
|
|
126
|
-
stored
|
|
127
|
-
stored.lastUsageAt = startedAt;
|
|
126
|
+
landWindows(stored, mergeWindows(windowsOf(outcome.usage, startedAt), stored.windows), startedAt, thresholdBars(cfg));
|
|
128
127
|
dirty = true;
|
|
129
128
|
}
|
|
130
129
|
if (dirty) saveAccounts(claudePool, idx);
|