tokenmaxxing 1.109.0 → 1.111.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/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/seat.ts +2 -2
- package/src/cli/serve.ts +2 -2
- package/src/cli/status.ts +11 -7
- package/src/entries/codexstophook.ts +1 -2
- package/src/entries/codexsupervisor.ts +1 -3
- package/src/entries/pisupervisor.ts +2 -2
- package/src/lib/codex.ts +13 -6
- package/src/lib/decide.ts +5 -5
- package/src/lib/picker.ts +37 -15
- package/src/lib/presence.ts +2 -0
- package/src/lib/sample.ts +4 -4
- package/src/lib/types.ts +15 -4
- package/src/main.ts +1 -1
package/DESIGN.md
CHANGED
|
@@ -30,7 +30,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
|
|
|
30
30
|
- A `claude` **supervisor** on your PATH ahead of the real binary (`~/.config/tokenmaxxing/bin/claude`) - you invoke it identically; a `claude` shell function or alias would shadow it, and `doctor` warns about one.
|
|
31
31
|
- Five `~/.claude/settings.json` entries (merged - other settings keys are preserved, but the `statusLine` slot is taken over): the tokenmaxxing `statusLine` renderer (native since 2026-07-11; it also tees usage), a `subagentStatusLine` (per-subagent rows in the agents panel), a `Stop` hook, a `StopFailure` hook, a `SessionStart` hook.
|
|
32
32
|
- **`~/.config/tokenmaxxing/`** - the single home for config and state:
|
|
33
|
-
- `config.json` - SPARSE overrides only (thresholds.session/weekly, claudeBin/codexBin/grokBin/opencodeBin/piBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
|
|
33
|
+
- `config.json` - SPARSE overrides only (thresholds.session/weekly/accounts, claudeBin/codexBin/grokBin/opencodeBin/piBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
|
|
34
34
|
- `accounts.json` - non-secret index `{id, label, email, tier, addedAt, windows, lastUsageAt, lastProbeAt, probeFails, storeFails, usageRetryAt, enforcedUntil, needsReauth, oauthAccount}` (each cached window carries its name, duration, reset, and sample time; the same shape serves the Codex pool as `codex-accounts.json`). Neither index names an active account: the schema is `{version, accounts}`, and an older file that still carries `activeId` loads and drops the key on the next save.
|
|
35
35
|
- `stores/<uuid8>/` - one Claude Code credential store per pooled account (see below).
|
|
36
36
|
- `codex-stores/<uuid8>/` - one Codex credential store per pooled account: its `auth.json`, with everything else symlinked to the shared `~/.codex` so sessions, history, and config stay shared and `codex resume` works across stores.
|
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 3), 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; `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 3), 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 `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.111.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/seat.ts
CHANGED
|
@@ -11,8 +11,8 @@ const POOLS = {
|
|
|
11
11
|
},
|
|
12
12
|
codex: {
|
|
13
13
|
borrow: borrowCodexSeat,
|
|
14
|
-
usage: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account,
|
|
15
|
-
none: "no usable codex account (pool empty, every account
|
|
14
|
+
usage: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account, lent to <pid> until it exits and shared with codex sessions and other borrowers; codex processes that share one auth.json can lose a refresh race that signs the account out until `tokenmaxxing auth --codex` runs again",
|
|
15
|
+
none: "no usable codex account (pool empty, or every account at a limit, flagged for reauthentication, held by a pi session, or held by the supervised codex session this runs in) - use the ambient codex login",
|
|
16
16
|
},
|
|
17
17
|
"opencode-go": {
|
|
18
18
|
borrow: borrowOpencodeGoSeat,
|
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,
|
|
17
|
+
import type { Account, Bars, Config } 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, thresholds:
|
|
27
|
+
fold: (account: Account, thresholds: Bars) => void;
|
|
28
28
|
read: (account: Account, cfg: Config, now: number) => Promise<Account>;
|
|
29
29
|
body: (usage: UsageReport, account: Account) => unknown;
|
|
30
30
|
};
|
package/src/cli/status.ts
CHANGED
|
@@ -6,10 +6,10 @@ 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, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
9
|
+
import { barFor, 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
|
-
import type { Account, Config, Thresholds, Window } from "../lib/types.ts";
|
|
12
|
+
import type { Account, Bars, Config, Thresholds, Window } from "../lib/types.ts";
|
|
13
13
|
|
|
14
14
|
export type WindowReport = { usedPercentage: number; resetsAt: number | null; windowSeconds: number | null };
|
|
15
15
|
|
|
@@ -24,6 +24,7 @@ type StatusAccount = {
|
|
|
24
24
|
sessions: number;
|
|
25
25
|
needsReauth: boolean;
|
|
26
26
|
exhausted: boolean;
|
|
27
|
+
thresholds: Thresholds | null;
|
|
27
28
|
usage: UsageReport | null;
|
|
28
29
|
usageAt: number | null;
|
|
29
30
|
limitsAt: number | null;
|
|
@@ -60,7 +61,7 @@ export function usageReport(a: Account, now: number): UsageReport | null {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function gatedNote(accounts: Account[], families: string[] | null,
|
|
64
|
+
function gatedNote(accounts: Account[], families: string[] | null, bars: Bars, now: number): string | null {
|
|
64
65
|
const groups = new Map<string, { carriers: number; capped: number }>();
|
|
65
66
|
for (const a of accounts) {
|
|
66
67
|
if (a.needsReauth === true) continue;
|
|
@@ -68,7 +69,7 @@ function gatedNote(accounts: Account[], families: string[] | null, weeklyBar: nu
|
|
|
68
69
|
const key = (w.name ?? "").toLowerCase();
|
|
69
70
|
const g = groups.get(key) ?? { carriers: 0, capped: 0 };
|
|
70
71
|
g.carriers++;
|
|
71
|
-
if (liveUsed(w, now) >=
|
|
72
|
+
if (liveUsed(w, now) >= barFor(a, w, bars, now)) g.capped++;
|
|
72
73
|
groups.set(key, g);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -77,7 +78,7 @@ function gatedNote(accounts: Account[], families: string[] | null, weeklyBar: nu
|
|
|
77
78
|
const headroom = accounts.filter((a) => {
|
|
78
79
|
if (a.needsReauth === true) return false;
|
|
79
80
|
const w = weeklyWindow(a);
|
|
80
|
-
return w != null && liveUsed(w, now) <
|
|
81
|
+
return w != null && liveUsed(w, now) < barFor(a, w, bars, now);
|
|
81
82
|
}).length;
|
|
82
83
|
if (headroom === 0) return null;
|
|
83
84
|
return `every ${capped.join(", ")} cap is at the weekly bar, but ${count({ n: headroom, noun: "account" })} still ha${headroom === 1 ? "s" : "ve"} weekly aggregate headroom for other models`;
|
|
@@ -130,6 +131,7 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
130
131
|
|
|
131
132
|
const present = p.presence();
|
|
132
133
|
const ctx = { now, thresholds: bars, currentId: null, families: p.gatedFamilies(cfg), seats: null };
|
|
134
|
+
const own = new Map(Object.entries(cfg.thresholds.accounts));
|
|
133
135
|
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
134
136
|
const accounts = ordered.map((a): StatusAccount => {
|
|
135
137
|
const limits = limitWindows(a);
|
|
@@ -142,6 +144,7 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
142
144
|
sessions: present.get(a.id) ?? 0,
|
|
143
145
|
needsReauth: a.needsReauth === true,
|
|
144
146
|
exhausted: isExhausted(a, ctx),
|
|
147
|
+
thresholds: own.get(a.label) ?? null,
|
|
145
148
|
usage: usageReport(a, now),
|
|
146
149
|
usageAt: a.lastUsageAt ?? null,
|
|
147
150
|
limitsAt: limits.length > 0 ? Math.max(...limits.map((w) => w.sampledAt)) : null,
|
|
@@ -150,9 +153,9 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
150
153
|
});
|
|
151
154
|
return {
|
|
152
155
|
thresholds: { session: cfg.thresholds.session, weekly: cfg.thresholds.weekly },
|
|
153
|
-
bars,
|
|
156
|
+
bars: { session: bars.session, weekly: bars.weekly },
|
|
154
157
|
projectionMargin: cfg.policy.projectionMargin,
|
|
155
|
-
gatedNote: gatedNote(idx.accounts, ctx.families, bars
|
|
158
|
+
gatedNote: gatedNote(idx.accounts, ctx.families, bars, now),
|
|
156
159
|
accounts,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
@@ -218,6 +221,7 @@ function card(p: Provider, a: StatusAccount, now: number, staleAfterMs: number):
|
|
|
218
221
|
for (const w of a.usage.limits) lines.push(usageRow(p.windowLabel(w.name), w));
|
|
219
222
|
}
|
|
220
223
|
const notes: Note[] = [];
|
|
224
|
+
if (a.thresholds) notes.push({ paint: c.dim, text: `thresholds 5h ${a.thresholds.session}% weekly ${a.thresholds.weekly}%` });
|
|
221
225
|
if (a.sample.ok && a.sample.source === "statusline") {
|
|
222
226
|
const stale = a.usageAt == null || now - a.usageAt > staleAfterMs;
|
|
223
227
|
const age = a.usageAt != null ? fmtAgo(a.usageAt, now) : "age unknown";
|
|
@@ -4,14 +4,13 @@ import type { z } from "zod";
|
|
|
4
4
|
import { codexPaths, optionalEnv } from "../lib/paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
6
6
|
import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin } from "../lib/claudebin.ts";
|
|
7
|
-
import { codex, codexPickCtx } from "../lib/codex.ts";
|
|
7
|
+
import { CODEX_SUPERVISOR_ID_ENV, codex, codexPickCtx } from "../lib/codex.ts";
|
|
8
8
|
import { compactCodexThread } from "../lib/compact.ts";
|
|
9
9
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
10
10
|
import { isExhausted } from "../lib/picker.ts";
|
|
11
11
|
import { livingPresences } from "../lib/presence.ts";
|
|
12
12
|
import { readStdin } from "../lib/proc.ts";
|
|
13
13
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
14
|
-
import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
|
|
15
14
|
import { CodexRespawnMarkerSchema, CodexStopStdinSchema, JsonTextSchema } from "../lib/types.ts";
|
|
16
15
|
import { errorMessage, log } from "../lib/log.ts";
|
|
17
16
|
|
|
@@ -7,7 +7,7 @@ import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
|
7
7
|
import { withLock } from "../lib/lock.ts";
|
|
8
8
|
import { CODEX_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
9
9
|
import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
|
|
10
|
-
import { borrowCodexSeat, codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
|
|
10
|
+
import { CODEX_SUPERVISOR_ID_ENV, borrowCodexSeat, codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
|
|
11
11
|
import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
12
12
|
import { isExhausted } from "../lib/picker.ts";
|
|
13
13
|
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
@@ -16,8 +16,6 @@ import { loadAccounts, readJsonFile } from "../lib/state.ts";
|
|
|
16
16
|
import { CodexRespawnMarkerSchema, type Account } from "../lib/types.ts";
|
|
17
17
|
import { errorMessage, log } from "../lib/log.ts";
|
|
18
18
|
|
|
19
|
-
export const CODEX_SUPERVISOR_ID_ENV = "TOKENMAXXING_CODEX_SUPERVISOR_ID";
|
|
20
|
-
|
|
21
19
|
const NONINTERACTIVE_SUBCMDS = new Set([
|
|
22
20
|
"exec", "e", "review", "login", "logout", "mcp", "plugin", "mcp-server", "app-server",
|
|
23
21
|
"remote-control", "app", "completion", "update", "doctor", "sandbox", "debug",
|
|
@@ -12,7 +12,7 @@ import { claudePool, codexPaths, expandTilde, optionalEnv, paths, piPaths, type
|
|
|
12
12
|
import { pickPiSeat, piMovers } from "../lib/pi.ts";
|
|
13
13
|
import { ensurePiStoreHome, piStoreUsable } from "../lib/piauth.ts";
|
|
14
14
|
import { thresholdBars, usableAt } from "../lib/picker.ts";
|
|
15
|
-
import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
15
|
+
import { PI_PRESENCE_PREFIX, clearPresence, livingPresences } from "../lib/presence.ts";
|
|
16
16
|
import { teeObservation } from "../lib/sample.ts";
|
|
17
17
|
import { countdownWait, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough, SEAT_POLL_MS, SEAT_RETRY_MS } from "../lib/supervise.ts";
|
|
18
18
|
import { saveTermios } from "../lib/tty.ts";
|
|
@@ -327,7 +327,7 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
|
327
327
|
if (launch == null) return runPassthrough({ real, argv, env: childEnv, onSpawn: (p) => { child = p; } });
|
|
328
328
|
const { args, pool, dirs, sid } = launch;
|
|
329
329
|
|
|
330
|
-
const id =
|
|
330
|
+
const id = `${PI_PRESENCE_PREFIX}${crypto.randomUUID()}`;
|
|
331
331
|
const mover = piMovers[pool];
|
|
332
332
|
const presenceDir = pool === "claude" ? paths.presenceDir : codexPaths.presenceDir;
|
|
333
333
|
const releaseClaim = async (): Promise<void> => {
|
package/src/lib/codex.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { existsSync, 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 { writeFileAtomic } from "./atomic.ts";
|
|
5
6
|
import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
|
|
6
7
|
import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, ensureCodexStoreHome, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
|
|
7
8
|
import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
|
|
8
9
|
import { deletePiStore } from "./piauth.ts";
|
|
9
|
-
import { livingPresences, seatCounts, writePresence } from "./presence.ts";
|
|
10
|
+
import { PI_PRESENCE_PREFIX, livingPresences, seatCounts, writePresence } from "./presence.ts";
|
|
10
11
|
import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
|
|
11
12
|
import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
|
|
12
13
|
import { withLock } from "./lock.ts";
|
|
13
14
|
import { errorMessage, log } from "./log.ts";
|
|
14
|
-
import { codexPaths, codexPool, codexSeatFromEnv } from "./paths.ts";
|
|
15
|
+
import { codexPaths, codexPool, codexSeatFromEnv, optionalEnv } from "./paths.ts";
|
|
15
16
|
import { isExhausted, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
16
17
|
import { StoreUnusableError, type Observation, type Provider, type SampleReport, type SeatBorrow } from "./provider.ts";
|
|
17
18
|
import { loadAccounts, loadConfig, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
|
|
@@ -19,6 +20,8 @@ import { restoreTermios, saveTermios } from "./tty.ts";
|
|
|
19
20
|
import type { Account, CodexAuthJson, CodexUsage, Config } from "./types.ts";
|
|
20
21
|
import { c } from "../cli/render.ts";
|
|
21
22
|
|
|
23
|
+
export const CODEX_SUPERVISOR_ID_ENV = "TOKENMAXXING_CODEX_SUPERVISOR_ID";
|
|
24
|
+
|
|
22
25
|
function liveId(): string | null {
|
|
23
26
|
return codexSeatFromEnv(loadAccounts(codexPool).accounts.map((a) => a.id));
|
|
24
27
|
}
|
|
@@ -251,7 +254,8 @@ export async function borrowCodexSeat(pid: number): Promise<SeatBorrow> {
|
|
|
251
254
|
const granted = await withLock(codexPool.lockFile, (): SeatBorrow => {
|
|
252
255
|
const ctx = codexPickCtx(now, null);
|
|
253
256
|
const idx = loadAccounts(codexPool);
|
|
254
|
-
const
|
|
257
|
+
const living = livingPresences(codexPaths.presenceDir);
|
|
258
|
+
const held = living.find((p) => p.id === seatId);
|
|
255
259
|
const heldAccount = held ? (idx.accounts.find((x) => x.id === held.accountId) ?? null) : null;
|
|
256
260
|
if (heldAccount) {
|
|
257
261
|
if (heldAccount.needsReauth === true) {
|
|
@@ -262,11 +266,14 @@ export async function borrowCodexSeat(pid: number): Promise<SeatBorrow> {
|
|
|
262
266
|
}
|
|
263
267
|
return { store: ensureCodexStoreHome(heldAccount.id), id: heldAccount.id, reused: true };
|
|
264
268
|
}
|
|
265
|
-
const
|
|
269
|
+
const parent = optionalEnv(CODEX_SUPERVISOR_ID_ENV);
|
|
270
|
+
const closed = new Set(living.filter((p) => p.id === parent || p.id.startsWith(PI_PRESENCE_PREFIX)).map((p) => p.accountId));
|
|
271
|
+
const lent = countBy(living, (p) => p.accountId);
|
|
266
272
|
const usable = idx.accounts.filter(
|
|
267
|
-
(a) => a.needsReauth !== true && !isExhausted(a, ctx) &&
|
|
273
|
+
(a) => !closed.has(a.id) && a.needsReauth !== true && !isExhausted(a, ctx) && codexStoreUsable(a.id)
|
|
268
274
|
);
|
|
269
|
-
const
|
|
275
|
+
const fewest = Math.min(...usable.map((a) => lent[a.id] ?? 0));
|
|
276
|
+
const picked = pickBest(usable.filter((a) => (lent[a.id] ?? 0) === fewest), ctx);
|
|
270
277
|
if (!picked) return null;
|
|
271
278
|
const store = ensureCodexStoreHome(picked.id);
|
|
272
279
|
writePresence({ dir: codexPaths.presenceDir, id: seatId, accountId: picked.id, pid });
|
package/src/lib/decide.ts
CHANGED
|
@@ -2,11 +2,11 @@ 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 { isExhausted, landWindows, limitWindows,
|
|
5
|
+
import { isExhausted, landWindows, limitWindows, nextWeeklyReset, 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";
|
|
9
|
-
import type { Account, EnforcedLimit } from "./types.ts";
|
|
9
|
+
import type { Account, Bars, EnforcedLimit } from "./types.ts";
|
|
10
10
|
|
|
11
11
|
export type SwapDecision = { swapped: boolean; account: Account | null; reason: string; waitUntil?: number };
|
|
12
12
|
|
|
@@ -31,10 +31,10 @@ function isOver(account: Account | undefined, observed: Observation | null, ctx:
|
|
|
31
31
|
return isExhausted({ ...account, windows: observed.windows }, ctx);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function modelKindCovered(limit: EnforcedLimit, account: Account, now: number,
|
|
34
|
+
function modelKindCovered(limit: EnforcedLimit, account: Account, now: number, bars: Bars): boolean {
|
|
35
35
|
if (limit.kind !== "model" || limit.family == null) return false;
|
|
36
36
|
const family = limit.family;
|
|
37
|
-
return limitWindows(account).some((w) => familyTokens(w.name ?? "").includes(family) &&
|
|
37
|
+
return limitWindows(account).some((w) => familyTokens(w.name ?? "").includes(family) && screensUntilReset(account, w, bars, now));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function enforcedWall(limit: EnforcedLimit, account: Account, now: number): number {
|
|
@@ -86,7 +86,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
86
86
|
let stamped = false;
|
|
87
87
|
const credits = enforced?.kind === "credits";
|
|
88
88
|
if (enforced && origin && !credits) {
|
|
89
|
-
const covered = modelKindCovered(enforced, origin, now, bars
|
|
89
|
+
const covered = modelKindCovered(enforced, origin, now, bars);
|
|
90
90
|
if (!covered || prior) {
|
|
91
91
|
origin.enforcedUntil = Math.max(origin.enforcedUntil ?? 0, enforcedWall(enforced, origin, now));
|
|
92
92
|
stamped = true;
|
package/src/lib/picker.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { maxBy, minBy, sortBy } from "es-toolkit";
|
|
2
2
|
import { familyTokens } from "./usage.ts";
|
|
3
|
-
import type { Account, Config, Thresholds, Window } from "./types.ts";
|
|
3
|
+
import type { Account, Bars, Config, Thresholds, Window } from "./types.ts";
|
|
4
4
|
|
|
5
|
-
export function thresholdBars(cfg: Config):
|
|
5
|
+
export function thresholdBars(cfg: Config): Bars {
|
|
6
|
+
const bars = (t: Thresholds): Thresholds => ({ session: t.session - cfg.policy.projectionMargin, weekly: t.weekly });
|
|
6
7
|
return {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
...bars(cfg.thresholds),
|
|
9
|
+
accounts: new Map(Object.entries(cfg.thresholds.accounts).map(([label, t]) => [label, bars(t)])),
|
|
10
|
+
accountReleaseMs: cfg.policy.accountReleaseMs,
|
|
9
11
|
};
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export type PickCtx = {
|
|
13
15
|
now: number;
|
|
14
|
-
thresholds:
|
|
16
|
+
thresholds: Bars;
|
|
15
17
|
currentId: string | null;
|
|
16
18
|
families: string[] | null;
|
|
17
19
|
seats: Map<string, number> | null;
|
|
@@ -49,20 +51,40 @@ export function liveUsed(w: Window, now: number): number {
|
|
|
49
51
|
return w.usedPercentage;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
return isSessionWindow(w) ?
|
|
54
|
+
function releaseMs(w: Window, bars: Bars): number {
|
|
55
|
+
return isSessionWindow(w) ? bars.accountReleaseMs.session : bars.accountReleaseMs.weekly;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
export function barFor(a: Account, w: Window, bars: Bars, now: number): number {
|
|
59
|
+
const pick = (t: Thresholds) => (isSessionWindow(w) ? t.session : t.weekly);
|
|
60
|
+
const own = bars.accounts.get(a.label);
|
|
61
|
+
if (own == null) return pick(bars);
|
|
62
|
+
const reset = windowResetAt(w, now);
|
|
63
|
+
const released = reset != null && reset > now && reset - now <= releaseMs(w, bars);
|
|
64
|
+
return released ? Math.max(pick(own), pick(bars)) : pick(own);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function windowEnd(w: Window): number {
|
|
68
|
+
return w.resetsAt ?? w.sampledAt + (w.windowSeconds ?? 0) * 1000;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function blockedUntil(a: Account, w: Window, bars: Bars, now: number): number {
|
|
72
|
+
const used = liveUsed(w, now);
|
|
73
|
+
if (used < barFor(a, w, bars, now)) return 0;
|
|
74
|
+
const reset = windowEnd(w);
|
|
75
|
+
const release = reset - releaseMs(w, bars);
|
|
76
|
+
return release > now && used < barFor(a, w, bars, release) ? release : reset;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function screensUntilReset(a: Account, w: Window, bars: Bars, now: number): boolean {
|
|
80
|
+
const reset = windowEnd(w);
|
|
81
|
+
return reset > now && blockedUntil(a, w, bars, now) === reset;
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
function blockingUntil(a: Account, ctx: PickCtx): number[] {
|
|
63
85
|
return [
|
|
64
|
-
...a.windows.filter((w) => w.name == null).map((w) => blockedUntil(
|
|
65
|
-
...gatedWindows(a, ctx.families).map((w) => blockedUntil(
|
|
86
|
+
...a.windows.filter((w) => w.name == null).map((w) => blockedUntil(a, w, ctx.thresholds, ctx.now)),
|
|
87
|
+
...gatedWindows(a, ctx.families).map((w) => blockedUntil(a, w, ctx.thresholds, ctx.now)),
|
|
66
88
|
...(a.enforcedUntil != null ? [a.enforcedUntil] : []),
|
|
67
89
|
];
|
|
68
90
|
}
|
|
@@ -71,7 +93,7 @@ export function isExhausted(a: Account, ctx: PickCtx): boolean {
|
|
|
71
93
|
return blockingUntil(a, ctx).some((t) => t > ctx.now);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
export function landWindows(a: Account, windows: Window[], at: number, thresholds:
|
|
96
|
+
export function landWindows(a: Account, windows: Window[], at: number, thresholds: Bars): void {
|
|
75
97
|
const ctx: PickCtx = { now: at, thresholds, currentId: null, families: [], seats: null };
|
|
76
98
|
const blocked = a.enforcedUntil != null && isExhausted({ ...a, enforcedUntil: undefined }, ctx);
|
|
77
99
|
a.windows = windows;
|
|
@@ -114,7 +136,7 @@ export function pacePressure(a: Account, ctx: PickCtx): number {
|
|
|
114
136
|
export function seatHeadroom(a: Account, ctx: PickCtx): number {
|
|
115
137
|
const session = sessionWindow(a);
|
|
116
138
|
if (session == null) return Number.NEGATIVE_INFINITY;
|
|
117
|
-
return (ctx.thresholds.
|
|
139
|
+
return (barFor(a, session, ctx.thresholds, ctx.now) - liveUsed(session, ctx.now)) / ((ctx.seats?.get(a.id) ?? 0) + 1);
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
const swapPreference = (ctx: PickCtx) => [
|
package/src/lib/presence.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { pidExists, pidStartTime } from "./proc.ts";
|
|
|
7
7
|
import { readJsonFile } from "./state.ts";
|
|
8
8
|
import { ErrnoSchema, JsonTextSchema } from "./types.ts";
|
|
9
9
|
|
|
10
|
+
export const PI_PRESENCE_PREFIX = "pi-";
|
|
11
|
+
|
|
10
12
|
const PresenceSchema = z.object({
|
|
11
13
|
accountId: z.string(),
|
|
12
14
|
pid: z.number(),
|
package/src/lib/sample.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { seatCounts } from "./presence.ts";
|
|
|
12
12
|
import type { Observation } from "./provider.ts";
|
|
13
13
|
import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
|
|
14
14
|
import { fetchUsageDirect, mergeWindows, scrubCredEnv, windowsOf } from "./usage.ts";
|
|
15
|
-
import type { Account,
|
|
15
|
+
import type { Account, Bars, Config, UsageWindows, Window } from "./types.ts";
|
|
16
16
|
|
|
17
17
|
export type SampleOutcome = { ok: true; usage: UsageWindows; via: "get" } | { ok: false; reason: string; retryAt?: number };
|
|
18
18
|
|
|
@@ -25,10 +25,10 @@ export function sampleIntervalMs(account: Account, cfg: Config, now: number): nu
|
|
|
25
25
|
const bars = thresholdBars(cfg);
|
|
26
26
|
const aggregates = account.windows.filter((w) => w.name == null);
|
|
27
27
|
if (aggregates.length === 0) return floor;
|
|
28
|
-
const blocked = (w: Window) => liveUsed(w, now) >= barFor(w, bars);
|
|
28
|
+
const blocked = (w: Window) => liveUsed(w, now) >= barFor(account, w, bars, now);
|
|
29
29
|
if (aggregates.some(blocked)) return Math.max(floor, SAMPLE_INTERVAL_MAX_MS);
|
|
30
30
|
const open = [...aggregates, ...gatedWindows(account, cfg.policy.switchModels).filter((w) => !blocked(w))];
|
|
31
|
-
const spare = Math.min(...open.map((w) => 1 - liveUsed(w, now) / barFor(w, bars)));
|
|
31
|
+
const spare = Math.min(...open.map((w) => 1 - liveUsed(w, now) / barFor(account, w, bars, now)));
|
|
32
32
|
return Math.max(floor, SAMPLE_INTERVAL_MAX_MS * spare);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -48,7 +48,7 @@ export function teeObservation(account: Account): Observation | null {
|
|
|
48
48
|
return { windows: mergeWindows(aggregate, account.windows), at };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function foldTee(account: Account, thresholds:
|
|
51
|
+
export function foldTee(account: Account, thresholds: Bars): boolean {
|
|
52
52
|
const observed = teeObservation(account);
|
|
53
53
|
if (!observed || (account.lastUsageAt != null && observed.at <= account.lastUsageAt)) return false;
|
|
54
54
|
landWindows(account, observed.windows, observed.at, thresholds);
|
package/src/lib/types.ts
CHANGED
|
@@ -92,12 +92,17 @@ export type EnforcedLimit = {
|
|
|
92
92
|
|
|
93
93
|
export type Thresholds = { session: number; weekly: number };
|
|
94
94
|
|
|
95
|
+
export type Bars = Thresholds & { accounts: Map<string, Thresholds>; accountReleaseMs: Thresholds };
|
|
96
|
+
|
|
97
|
+
const PercentSchema = z.number().min(0).max(100);
|
|
98
|
+
|
|
95
99
|
export const ConfigSchema = z
|
|
96
100
|
.object({
|
|
97
101
|
thresholds: z
|
|
98
102
|
.object({
|
|
99
|
-
session:
|
|
100
|
-
weekly:
|
|
103
|
+
session: PercentSchema.default(90),
|
|
104
|
+
weekly: PercentSchema.default(98),
|
|
105
|
+
accounts: z.record(z.string(), z.object({ session: PercentSchema, weekly: PercentSchema })).default({}),
|
|
101
106
|
})
|
|
102
107
|
.prefault({}),
|
|
103
108
|
claudeBin: z.string().default(""),
|
|
@@ -115,6 +120,12 @@ export const ConfigSchema = z
|
|
|
115
120
|
usagePollTtlMs: z.number().int().positive().default(90_000),
|
|
116
121
|
maxWaitMs: z.number().int().positive().default(3_600_000),
|
|
117
122
|
checkIntervalMs: z.number().int().min(10_000).default(60_000),
|
|
123
|
+
accountReleaseMs: z
|
|
124
|
+
.object({
|
|
125
|
+
session: z.number().int().min(0).default(1_800_000),
|
|
126
|
+
weekly: z.number().int().min(0).default(18_000_000),
|
|
127
|
+
})
|
|
128
|
+
.prefault({}),
|
|
118
129
|
})
|
|
119
130
|
.prefault({}),
|
|
120
131
|
hub: z
|
|
@@ -123,9 +134,9 @@ export const ConfigSchema = z
|
|
|
123
134
|
})
|
|
124
135
|
.prefault({}),
|
|
125
136
|
})
|
|
126
|
-
.refine((cfg) => cfg.policy.projectionMargin <
|
|
137
|
+
.refine((cfg) => [cfg.thresholds, ...Object.values(cfg.thresholds.accounts)].every((t) => cfg.policy.projectionMargin < t.session), {
|
|
127
138
|
path: ["policy", "projectionMargin"],
|
|
128
|
-
message: "must be strictly below
|
|
139
|
+
message: "must be strictly below every session threshold (the session bar would hit zero and every account it covers would read as exhausted)",
|
|
129
140
|
});
|
|
130
141
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
131
142
|
|
package/src/main.ts
CHANGED
|
@@ -61,7 +61,7 @@ function printHelp(): void {
|
|
|
61
61
|
${c.cyan("tokenmaxxing rename")} [--codex | --grok | --opencode-go] <sel> <label>
|
|
62
62
|
${c.cyan("tokenmaxxing rm")} [--codex | --grok | --opencode-go] <sel>
|
|
63
63
|
${c.cyan("tokenmaxxing seat")} <pid> lend one pooled Claude account to an unattended consumer until <pid> exits: prints the store directory to set as CLAUDE_SECURESTORAGE_CONFIG_DIR (the directory itself, as its owner, never a copy of its credential); host sessions and other borrowers share the account; exit 1 = none usable
|
|
64
|
-
${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set,
|
|
64
|
+
${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set, lent to <pid> until it exits; codex sessions and other borrowers share the account, and a refresh race between them can sign it out until auth --codex runs again; exit 1 = none usable, fall back to the ambient login
|
|
65
65
|
${c.cyan("tokenmaxxing seat --opencode-go")} <pid> lend one pooled opencode-go key to an unattended consumer until <pid> exits: prints the store directory, whose auth.json is an opencode auth file (mount it at $XDG_DATA_HOME/opencode/auth.json, then run a model such as opencode-go/mimo-v2.6-pro); other borrowers share the key; exit 1 = none usable
|
|
66
66
|
${c.cyan("tokenmaxxing serve")} serve the CLIProxyAPI-compatible usage API on http://localhost:<hub.port> (default 8317) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows every pooled Claude and Codex account's quota; the management key is the contents of hub-key in the state directory
|
|
67
67
|
${c.cyan("tokenmaxxing uninstall")} [--yes] print the targets, then remove supervisor + settings entries (refused without ${c.cyan("--yes")} when HOME is the login home)
|