tokenmaxxing 1.90.0 → 1.92.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 +3 -3
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/seat.ts +2 -41
- package/src/entries/codexsupervisor.ts +54 -11
- package/src/lib/codex.ts +41 -2
- package/src/lib/grokusage.ts +1 -1
- package/src/lib/sample.ts +46 -9
- package/src/lib/supervise.ts +2 -2
package/DESIGN.md
CHANGED
|
@@ -53,7 +53,7 @@ The switching path runs no long-lived daemon. The statusline pushes usage. Hooks
|
|
|
53
53
|
## 3. How a move happens
|
|
54
54
|
|
|
55
55
|
### 3.1 Usage feed (free, push-based)
|
|
56
|
-
The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}`, after every turn, 300ms debounce, zero cost). tokenmaxxing's statusLine tees that to `usage/<uuid8>.json` for the session's seat, read from the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` (write-on-change, O(ms)), and renders its own native line (it replaced the earlier pass-through delegation on 2026-07-11: install takes the statusLine slot outright, so a pre-existing custom statusline command is overwritten). A session with no pooled seat writes no tee. The seat is sampled when its tee is older than `policy.usagePollTtlMs`, and for the per-model rows when the active model is a gated family, at most once per the account's sample interval (§5): the direct no-spend usage read with the seat's stored token. The compact child still sets `TOKENMAXXING_PROBE=1` so the hook entries return at once. The sample takes one attempt per interval; a failed sample doubles the interval up to 30 minutes, and a success resets it.
|
|
56
|
+
The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}`, after every turn, 300ms debounce, zero cost). tokenmaxxing's statusLine tees that to `usage/<uuid8>.json` for the session's seat, read from the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` (write-on-change, O(ms)), and renders its own native line (it replaced the earlier pass-through delegation on 2026-07-11: install takes the statusLine slot outright, so a pre-existing custom statusline command is overwritten). A session with no pooled seat writes no tee. The seat is sampled when its tee is older than `policy.usagePollTtlMs`, and for the per-model rows when the active model is a gated family, at most once per the account's sample interval (§5): the direct no-spend usage read with the seat's stored token. The compact child still sets `TOKENMAXXING_PROBE=1` so the hook entries return at once. The sample takes one attempt per interval; a failed sample doubles the interval up to 30 minutes, and a success resets it. A sample that finds the stored access token expired has Claude Code refresh it first (§5).
|
|
57
57
|
|
|
58
58
|
### 3.2 Detect + decide + signal (Stop hook, per turn)
|
|
59
59
|
1. Resolve the seat: the pooled account whose store the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` names. No seat (a session started outside the supervisor) means nothing to move.
|
|
@@ -87,7 +87,7 @@ Each terminal ran the supervisor, so each has its own child `claude`, its own `-
|
|
|
87
87
|
The [switching policy](docs/content/docs/switching.mdx) is one rule: under both bars, hold; at or over a bar, move to the usable account that ranks first; nothing usable, queue first come first served onto a reset target (at most four waiters per account, overflow takes the soonest anyway) and wait for that reset. The same ranking places a session at launch.
|
|
88
88
|
|
|
89
89
|
- Ranking (decided by consult for #79): greatest remaining headroom to the session bar divided by the number of sessions after placement, `(bar - used) / (sessions + 1)`, then highest pace pressure (remaining percent of the binding gated per-model cap, or of the weekly aggregate for an account with no gated row, over time to its reset), then soonest weekly reset, then lowest weekly usage; unknown usage ranks last. Organization membership is not an input. Codex drops the headroom key, keeps the pace ordering and the weekly tie-breaks, and excludes accounts running in another codex session, because codex has no cross-process refresh lock and two sessions must never share a login.
|
|
90
|
-
- Selection reads the cached figures only, after folding every account's tee. Each periodic check tick samples up to three accounts whose last sample attempt is oldest, skipping any attempted within the account's sample interval, over the direct no-spend usage read with the account's stored token. The interval is 15 minutes times the unused fraction of the bar closest to being reached, never under `policy.usagePollTtlMs`, and 15 minutes for an account at or over an aggregate bar. The windows are the session and weekly aggregates plus the `policy.switchModels` caps that are still under their bar, and an account with no stored aggregate gets `policy.usagePollTtlMs`. Every account is attempted about once per three ticks per pooled account, or once per its sample interval plus a tick when that is longer, and its cached figure is as fresh as its last successful attempt or its sessions' latest push. The pool lock covers only the reservation and the result write, never the usage GET. The usage endpoint rate limits each account on its own and names the time to come back in the `retry-after` of its HTTP 429; that time is stored as `usageRetryAt`, and no sampler (the tick, `status`, the seat's in-decision sample) reads the account before it.
|
|
90
|
+
- Selection reads the cached figures only, after folding every account's tee. Each periodic check tick samples up to three accounts whose last sample attempt is oldest, skipping any attempted within the account's sample interval, over the direct no-spend usage read with the account's stored token. The interval is 15 minutes times the unused fraction of the bar closest to being reached, never under `policy.usagePollTtlMs`, and 15 minutes for an account at or over an aggregate bar. The windows are the session and weekly aggregates plus the `policy.switchModels` caps that are still under their bar, and an account with no stored aggregate gets `policy.usagePollTtlMs`. Every account is attempted about once per three ticks per pooled account, or once per its sample interval plus a tick when that is longer, and its cached figure is as fresh as its last successful attempt or its sessions' latest push. The pool lock covers only the reservation and the result write, never the usage GET. The usage endpoint rate limits each account on its own and names the time to come back in the `retry-after` of its HTTP 429; that time is stored as `usageRetryAt`, and no sampler (the tick, `status`, the seat's in-decision sample) reads the account before it. No sampler sends a read with an expired stored access token, because the endpoint answers it with 401 and, after a few, with the same 429. A sample that finds the token expired first runs `claude -p /usage --no-session-persistence --safe-mode` with the account's store set and a throwaway `CLAUDE_CONFIG_DIR`, so Claude Code refreshes the token under its own lock, and then reads with the new token; the child's output is not read. A refresh that leaves the token expired fails the sample with no read, and one that clears the store flags the account `needsReauth`.
|
|
91
91
|
- The depleted pause happens at the bar, not at 100; Codex has no pause and rides its account until the server refuses it, because a running sibling cannot adopt another account without restarting.
|
|
92
92
|
- Neither pool has a post-swap cooldown or a manual `switch`: a move is a respawn of one session under a fixed store, so nothing another session reads is invalidated by it, and placement at launch replaces the manual pick.
|
|
93
93
|
|
|
@@ -135,7 +135,7 @@ TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/u
|
|
|
135
135
|
|
|
136
136
|
### 2026-09-12 (per-account stores, 1.37.0)
|
|
137
137
|
|
|
138
|
-
Hermetic run on Linux under a throwaway `HOME` and `TOKENMAXXING_HOME`, a stub `claude` that records its environment, and a synthetic three-account version 2 pool with equal headroom on two accounts and the third over its session bar. Usage comes from a stub usage endpoint named by `TOKENMAXXING_OAUTH_USAGE_URL` that answers per access token.
|
|
138
|
+
Hermetic run on Linux under a throwaway `HOME` and `TOKENMAXXING_HOME`, a stub `claude` that records its environment, and a synthetic three-account version 2 pool with equal headroom on two accounts and the third over its session bar. Usage comes from a stub usage endpoint named by `TOKENMAXXING_OAUTH_USAGE_URL` that answers per access token. No `/usage` output is parsed.
|
|
139
139
|
|
|
140
140
|
1. **Placement - ✅.** The first supervised launch spawned the stub with `CLAUDE_SECURESTORAGE_CONFIG_DIR=stores/<first account>` and wrote its presence; the second launch, with equal headroom, landed on the other account.
|
|
141
141
|
2. **Plain move - ✅.** A 95% session tee for the second session's account made its Stop hook write the marker; the supervisor relaunched `--resume <sid>` under the first account's store within a second and rewrote the presence file.
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ The bars' headroom is deliberate: it's the budget to reach a clean turn boundary
|
|
|
90
90
|
|
|
91
91
|
Launches and moves rank usable accounts by session-window headroom per running session, `(bar - used) / (sessions + 1)`, so two fresh accounts alternate and a third session goes to whichever has more headroom left per session; ties break by pace pressure (remaining percent of the binding gated per-model cap, or of the weekly aggregate for an account with no gated row, divided by time to its reset, highest first), then soonest weekly reset, then lowest weekly use. Organization membership is not an input.
|
|
92
92
|
|
|
93
|
-
- Every periodic check tick folds fresh statusline tees into the index and samples up to three accounts whose last sample attempt is oldest, skipping any attempted within the account's sample interval (15 minutes for an empty account, shrinking to `policy.usagePollTtlMs` as it nears a bar, and 15 minutes again at or over an aggregate bar), with the direct no-spend usage read; a move reads the cached figures, which are as fresh as each account's last successful attempt or its sessions' latest push.
|
|
93
|
+
- Every periodic check tick folds fresh statusline tees into the index and samples up to three accounts whose last sample attempt is oldest, skipping any attempted within the account's sample interval (15 minutes for an empty account, shrinking to `policy.usagePollTtlMs` as it nears a bar, and 15 minutes again at or over an aggregate bar), with the direct no-spend usage read; a move reads the cached figures, which are as fresh as each account's last successful attempt or its sessions' latest push. A sample that finds the stored access token expired has Claude Code refresh it first (`claude -p /usage` under that store) and reads with the new token.
|
|
94
94
|
- When no account is usable, the session queues first come first served onto a reset target (at most four waiters per account) and pauses until that reset if it lands within `policy.maxWaitMs`, and otherwise stays put.
|
|
95
95
|
|
|
96
96
|
See [How switching decides](docs/content/docs/switching.mdx) for the policy and the [cache-cost profile](docs/content/docs/switching-profile.mdx) for measurements and their limits.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.92.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
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
|
|
3
|
-
import { withLock } from "../lib/lock.ts";
|
|
4
|
-
import { log } from "../lib/log.ts";
|
|
5
|
-
import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
6
|
-
import { livingPresences, seatCounts, writePresence } from "../lib/presence.ts";
|
|
7
|
-
import { isExhausted, pickBest } from "../lib/picker.ts";
|
|
8
|
-
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
1
|
+
import { borrowCodexSeat } from "../lib/codex.ts";
|
|
9
2
|
import { emitError } from "./render.ts";
|
|
10
3
|
|
|
11
4
|
export async function cmdSeat(pidRaw: string | undefined, extra: string[]): Promise<number> {
|
|
@@ -14,38 +7,7 @@ export async function cmdSeat(pidRaw: string | undefined, extra: string[]): Prom
|
|
|
14
7
|
emitError({ message: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account, reserved until <pid> exits" });
|
|
15
8
|
return 2;
|
|
16
9
|
}
|
|
17
|
-
const
|
|
18
|
-
const seatId = `seat-${pid}`;
|
|
19
|
-
const cfg = loadConfig();
|
|
20
|
-
await Promise.all(
|
|
21
|
-
loadAccounts(codexPool)
|
|
22
|
-
.accounts.filter((a) => a.needsReauth !== true && codexStoreUsable(a.id))
|
|
23
|
-
.map((a) => observeCodex(a, cfg, now, { probe: true, refresh: false })),
|
|
24
|
-
);
|
|
25
|
-
const granted = await withLock(codexPool.lockFile, (): { store: string; id: string; reused: boolean } | { denied: string } | null => {
|
|
26
|
-
const ctx = codexPickCtx(now, null);
|
|
27
|
-
const idx = loadAccounts(codexPool);
|
|
28
|
-
const held = livingPresences(codexPaths.presenceDir).find((p) => p.id === seatId);
|
|
29
|
-
const heldAccount = held ? (idx.accounts.find((x) => x.id === held.accountId) ?? null) : null;
|
|
30
|
-
if (heldAccount) {
|
|
31
|
-
if (heldAccount.needsReauth === true) {
|
|
32
|
-
return { denied: "the account this pid holds needs reauthentication - run `tokenmaxxing auth --codex` and borrow again" };
|
|
33
|
-
}
|
|
34
|
-
if (!codexStoreUsable(heldAccount.id)) {
|
|
35
|
-
return { denied: "the account this pid holds has no usable credential in its store - refusing to hand back a credential-less seat" };
|
|
36
|
-
}
|
|
37
|
-
return { store: ensureCodexStoreHome(heldAccount.id), id: heldAccount.id, reused: true };
|
|
38
|
-
}
|
|
39
|
-
const present = seatCounts(codexPaths.presenceDir);
|
|
40
|
-
const usable = idx.accounts.filter(
|
|
41
|
-
(a) => a.needsReauth !== true && !isExhausted(a, ctx) && !present.has(a.id) && codexStoreUsable(a.id)
|
|
42
|
-
);
|
|
43
|
-
const picked = pickBest(usable, ctx);
|
|
44
|
-
if (!picked) return null;
|
|
45
|
-
const store = ensureCodexStoreHome(picked.id);
|
|
46
|
-
writePresence({ dir: codexPaths.presenceDir, id: seatId, accountId: picked.id, pid });
|
|
47
|
-
return { store, id: picked.id, reused: false };
|
|
48
|
-
});
|
|
10
|
+
const granted = await borrowCodexSeat(pid);
|
|
49
11
|
if (!granted) {
|
|
50
12
|
emitError({ message: "no usable codex account (pool empty, every account live in another session, or every account at a limit) - use the ambient codex login" });
|
|
51
13
|
return 1;
|
|
@@ -54,7 +16,6 @@ export async function cmdSeat(pidRaw: string | undefined, extra: string[]): Prom
|
|
|
54
16
|
emitError({ message: granted.denied });
|
|
55
17
|
return 1;
|
|
56
18
|
}
|
|
57
|
-
log("seat.grant", { account: granted.id.slice(0, 8), pid, reused: granted.reused });
|
|
58
19
|
console.log(granted.store);
|
|
59
20
|
return 0;
|
|
60
21
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Subprocess } from "bun";
|
|
1
2
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -5,7 +6,7 @@ import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
|
5
6
|
import { withLock } from "../lib/lock.ts";
|
|
6
7
|
import { CODEX_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
7
8
|
import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
|
|
8
|
-
import { codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
|
|
9
|
+
import { borrowCodexSeat, codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
|
|
9
10
|
import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
10
11
|
import { isExhausted } from "../lib/picker.ts";
|
|
11
12
|
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
@@ -17,11 +18,13 @@ import { errorMessage, log } from "../lib/log.ts";
|
|
|
17
18
|
export const CODEX_SUPERVISOR_ID_ENV = "TOKENMAXXING_CODEX_SUPERVISOR_ID";
|
|
18
19
|
|
|
19
20
|
const NONINTERACTIVE_SUBCMDS = new Set([
|
|
20
|
-
"exec", "review", "login", "logout", "mcp", "plugin", "mcp-server", "app-server",
|
|
21
|
+
"exec", "e", "review", "login", "logout", "mcp", "plugin", "mcp-server", "app-server",
|
|
21
22
|
"remote-control", "app", "completion", "update", "doctor", "sandbox", "debug",
|
|
22
23
|
"apply", "archive", "delete", "unarchive", "cloud", "exec-server", "features", "help",
|
|
23
24
|
]);
|
|
24
25
|
|
|
26
|
+
const BORROWING_SUBCMDS = new Set(["exec", "e", "review", "app-server"]);
|
|
27
|
+
|
|
25
28
|
const PASSTHROUGH_FLAGS = new Set(["--version", "-V", "--help", "-h"]);
|
|
26
29
|
|
|
27
30
|
function readCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchema> {
|
|
@@ -40,19 +43,32 @@ const VALUE_TAKING_ROOT_FLAGS = new Set([
|
|
|
40
43
|
"-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--add-dir", "--enable",
|
|
41
44
|
]);
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (PASSTHROUGH_FLAGS.has(arg)) return
|
|
46
|
+
function scanCodexArgs(argv: string[]): { help: boolean; positionals: string[] } {
|
|
47
|
+
const positionals: string[] = [];
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const arg = argv[i]!;
|
|
50
|
+
if (arg === "--") break;
|
|
51
|
+
if (PASSTHROUGH_FLAGS.has(arg)) return { help: true, positionals };
|
|
49
52
|
if (VALUE_TAKING_ROOT_FLAGS.has(arg)) {
|
|
50
53
|
i++;
|
|
51
54
|
continue;
|
|
52
55
|
}
|
|
53
|
-
if (!arg.startsWith("-")
|
|
56
|
+
if (!arg.startsWith("-")) positionals.push(arg);
|
|
54
57
|
}
|
|
55
|
-
return
|
|
58
|
+
return { help: false, positionals };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function shouldManageCodex(input: { argv: string[] }): boolean {
|
|
62
|
+
if (process.env.TOKENMAXXING_PROBE) return false;
|
|
63
|
+
const { help, positionals: [sub] } = scanCodexArgs(input.argv);
|
|
64
|
+
return !help && (sub === undefined || !NONINTERACTIVE_SUBCMDS.has(sub));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function borrowsCodexSeat(input: { argv: string[] }): boolean {
|
|
68
|
+
if (process.env.TOKENMAXXING_PROBE || process.env[UNMANAGED_ENV] || process.env.CODEX_HOME) return false;
|
|
69
|
+
const { help, positionals: [sub, ...rest] } = scanCodexArgs(input.argv);
|
|
70
|
+
if (help || sub === undefined || !BORROWING_SUBCMDS.has(sub)) return false;
|
|
71
|
+
return sub !== "app-server" || rest.length === 0;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
function validWanted(input: { wanted: string | null; now: number; supervisorId: string }): Account | null {
|
|
@@ -79,7 +95,34 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
79
95
|
if (!shouldManageCodex({ argv }) || process.env[UNMANAGED_ENV]) {
|
|
80
96
|
const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
|
|
81
97
|
delete passthroughEnv[CODEX_SUPERVISOR_ID_ENV];
|
|
82
|
-
|
|
98
|
+
let child: Subprocess | null = null;
|
|
99
|
+
process.on("SIGTERM", () => {
|
|
100
|
+
if (child) child.kill("SIGTERM");
|
|
101
|
+
else process.exit(143);
|
|
102
|
+
});
|
|
103
|
+
const granted = borrowsCodexSeat({ argv }) ? await borrowCodexSeat(process.pid) : undefined;
|
|
104
|
+
const seat = granted && !("denied" in granted) ? granted : null;
|
|
105
|
+
if (seat) passthroughEnv.CODEX_HOME = seat.store;
|
|
106
|
+
else if (granted !== undefined) log("codexsupervisor.borrow_none", { reason: granted && "denied" in granted ? granted.denied : "no usable account" });
|
|
107
|
+
return runPassthrough({
|
|
108
|
+
real,
|
|
109
|
+
argv,
|
|
110
|
+
env: passthroughEnv,
|
|
111
|
+
onSpawn: (p) => {
|
|
112
|
+
child = p;
|
|
113
|
+
return seat
|
|
114
|
+
? recordPresenceOrStop({
|
|
115
|
+
child: p,
|
|
116
|
+
dir: codexPaths.presenceDir,
|
|
117
|
+
id: `seat-${process.pid}`,
|
|
118
|
+
accountId: seat.id,
|
|
119
|
+
event: "codexsupervisor.borrow_presence_failed",
|
|
120
|
+
message: "could not key the borrowed codex seat to its child - refusing to run on an unreserved store",
|
|
121
|
+
savedTermios: null,
|
|
122
|
+
})
|
|
123
|
+
: undefined;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
83
126
|
}
|
|
84
127
|
|
|
85
128
|
const supervisorId = crypto.randomUUID();
|
package/src/lib/codex.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { writeFileAtomic } from "./atomic.ts";
|
|
4
4
|
import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
|
|
5
|
-
import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
|
|
5
|
+
import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, ensureCodexStoreHome, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
|
|
6
6
|
import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
|
|
7
7
|
import { deletePiStore } from "./piauth.ts";
|
|
8
|
-
import { livingPresences, seatCounts } from "./presence.ts";
|
|
8
|
+
import { livingPresences, seatCounts, writePresence } from "./presence.ts";
|
|
9
9
|
import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
|
|
10
10
|
import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
|
|
11
11
|
import { withLock } from "./lock.ts";
|
|
@@ -236,6 +236,45 @@ export function pickCodexSeat(now: number, eligible: (a: Account) => boolean = (
|
|
|
236
236
|
return pickBest(open.filter((a) => !isExhausted(a, ctx)), ctx) ?? pickEarliestReset(open, ctx)?.account ?? null;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
export type CodexBorrow = { store: string; id: string; reused: boolean } | { denied: string } | null;
|
|
240
|
+
|
|
241
|
+
export async function borrowCodexSeat(pid: number): Promise<CodexBorrow> {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const seatId = `seat-${pid}`;
|
|
244
|
+
const cfg = loadConfig();
|
|
245
|
+
await Promise.all(
|
|
246
|
+
loadAccounts(codexPool)
|
|
247
|
+
.accounts.filter((a) => a.needsReauth !== true && codexStoreUsable(a.id))
|
|
248
|
+
.map((a) => observeCodex(a, cfg, now, { probe: true, refresh: false })),
|
|
249
|
+
);
|
|
250
|
+
const granted = await withLock(codexPool.lockFile, (): CodexBorrow => {
|
|
251
|
+
const ctx = codexPickCtx(now, null);
|
|
252
|
+
const idx = loadAccounts(codexPool);
|
|
253
|
+
const held = livingPresences(codexPaths.presenceDir).find((p) => p.id === seatId);
|
|
254
|
+
const heldAccount = held ? (idx.accounts.find((x) => x.id === held.accountId) ?? null) : null;
|
|
255
|
+
if (heldAccount) {
|
|
256
|
+
if (heldAccount.needsReauth === true) {
|
|
257
|
+
return { denied: "the account this pid holds needs reauthentication - run `tokenmaxxing auth --codex` and borrow again" };
|
|
258
|
+
}
|
|
259
|
+
if (!codexStoreUsable(heldAccount.id)) {
|
|
260
|
+
return { denied: "the account this pid holds has no usable credential in its store - refusing to hand back a credential-less seat" };
|
|
261
|
+
}
|
|
262
|
+
return { store: ensureCodexStoreHome(heldAccount.id), id: heldAccount.id, reused: true };
|
|
263
|
+
}
|
|
264
|
+
const present = seatCounts(codexPaths.presenceDir);
|
|
265
|
+
const usable = idx.accounts.filter(
|
|
266
|
+
(a) => a.needsReauth !== true && !isExhausted(a, ctx) && !present.has(a.id) && codexStoreUsable(a.id)
|
|
267
|
+
);
|
|
268
|
+
const picked = pickBest(usable, ctx);
|
|
269
|
+
if (!picked) return null;
|
|
270
|
+
const store = ensureCodexStoreHome(picked.id);
|
|
271
|
+
writePresence({ dir: codexPaths.presenceDir, id: seatId, accountId: picked.id, pid });
|
|
272
|
+
return { store, id: picked.id, reused: false };
|
|
273
|
+
});
|
|
274
|
+
if (granted && !("denied" in granted)) log("seat.grant", { account: granted.id.slice(0, 8), pid, reused: granted.reused });
|
|
275
|
+
return granted;
|
|
276
|
+
}
|
|
277
|
+
|
|
239
278
|
export const codex: Provider = {
|
|
240
279
|
name: "codex",
|
|
241
280
|
flag: " --codex",
|
package/src/lib/grokusage.ts
CHANGED
|
@@ -20,7 +20,7 @@ const CreditsPeriodSchema = z.looseObject({
|
|
|
20
20
|
|
|
21
21
|
const CreditsConfigSchema = z.looseObject({
|
|
22
22
|
currentPeriod: CreditsPeriodSchema,
|
|
23
|
-
creditUsagePercent: z.number(),
|
|
23
|
+
creditUsagePercent: z.number().default(0),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
const CreditsBillingSchema = z.looseObject({
|
package/src/lib/sample.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CLAUDE_BIN, MAX_WRAP_DEPTH, resolveRealBin, WRAP_DEPTH_ENV } from "./claudebin.ts";
|
|
5
|
+
import { deleteItem, readStore, storeTarget } from "./credstore.ts";
|
|
2
6
|
import { withLock } from "./lock.ts";
|
|
3
7
|
import { errorMessage, log } from "./log.ts";
|
|
4
8
|
import { claudeTierLabel, isDeadCredential } from "./oauth.ts";
|
|
5
|
-
import { claudePool, paths } from "./paths.ts";
|
|
9
|
+
import { claudePool, paths, storeDirFor } from "./paths.ts";
|
|
6
10
|
import { barFor, gatedWindows, landWindows, liveUsed, thresholdBars } from "./picker.ts";
|
|
7
11
|
import { seatCounts } from "./presence.ts";
|
|
8
12
|
import type { Observation } from "./provider.ts";
|
|
9
13
|
import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
|
|
10
|
-
import { fetchUsageDirect, mergeWindows, windowsOf } from "./usage.ts";
|
|
14
|
+
import { fetchUsageDirect, mergeWindows, scrubCredentialEnv, windowsOf } from "./usage.ts";
|
|
11
15
|
import type { Account, Config, Thresholds, UsageWindows, Window } from "./types.ts";
|
|
12
16
|
|
|
13
17
|
export type SampleOutcome = { ok: true; usage: UsageWindows; via: "get" } | { ok: false; reason: string; retryAt?: number };
|
|
@@ -76,11 +80,46 @@ function rateLimitedReason(retryAt: number, now: number): string {
|
|
|
76
80
|
return `the usage endpoint rate limited this account, next read in ${Math.ceil((retryAt - now) / 60_000)}m`;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
const
|
|
83
|
+
const REFRESH_KILL_MS = 60_000;
|
|
84
|
+
|
|
85
|
+
async function refreshStore(account: Account): Promise<number | null> {
|
|
86
|
+
const home = mkdtempSync(join(tmpdir(), "tokenmaxxing-refresh-"));
|
|
87
|
+
const env = scrubCredentialEnv({ ...process.env, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) });
|
|
88
|
+
env.CLAUDE_CONFIG_DIR = home;
|
|
89
|
+
env.CLAUDE_SECURESTORAGE_CONFIG_DIR = storeDirFor(account.id);
|
|
90
|
+
try {
|
|
91
|
+
const p = Bun.spawn([resolveRealBin(CLAUDE_BIN), "-p", "/usage", "--no-session-persistence", "--safe-mode"], {
|
|
92
|
+
env,
|
|
93
|
+
cwd: home,
|
|
94
|
+
stdin: "ignore",
|
|
95
|
+
stdout: "ignore",
|
|
96
|
+
stderr: "ignore",
|
|
97
|
+
timeout: REFRESH_KILL_MS,
|
|
98
|
+
killSignal: "SIGKILL",
|
|
99
|
+
});
|
|
100
|
+
await p.exited;
|
|
101
|
+
return p.exitCode;
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(home, { recursive: true, force: true });
|
|
104
|
+
await withLock(claudePool.lockFile, async () => {
|
|
105
|
+
if (loadAccounts(claudePool).accounts.some((a) => a.id === account.id)) return;
|
|
106
|
+
await deleteItem(storeTarget(account.id));
|
|
107
|
+
rmSync(storeDirFor(account.id), { recursive: true, force: true });
|
|
108
|
+
rmSync(`${storeDirFor(account.id)}.lock`, { recursive: true, force: true });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
80
112
|
|
|
81
113
|
export async function runSample(account: Account, prepared: { token: string; expiresAt: number }): Promise<SampleOutcome> {
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
let token = prepared.token;
|
|
115
|
+
if (prepared.expiresAt <= Date.now()) {
|
|
116
|
+
const exit = await refreshStore(account);
|
|
117
|
+
const refreshed = await prepareSample(account);
|
|
118
|
+
if (!refreshed.ok) return refreshed;
|
|
119
|
+
if (refreshed.expiresAt <= Date.now()) return { ok: false, reason: `the stored access token expired and claude did not refresh it (exit ${exit ?? "killed"})` };
|
|
120
|
+
token = refreshed.token;
|
|
121
|
+
}
|
|
122
|
+
const read = await fetchUsageDirect(token);
|
|
84
123
|
if (read.ok) return { ok: true, usage: read.usage, via: "get" };
|
|
85
124
|
if (read.retryAt == null) return { ok: false, reason: "usage read failed (see log)" };
|
|
86
125
|
return { ok: false, reason: rateLimitedReason(read.retryAt, Date.now()), retryAt: read.retryAt };
|
|
@@ -89,9 +128,7 @@ export async function runSample(account: Account, prepared: { token: string; exp
|
|
|
89
128
|
export async function readyToSample(account: Account, now: number): Promise<PreparedSample> {
|
|
90
129
|
const blockedUntil = usageBlockedUntil(account, now);
|
|
91
130
|
if (blockedUntil != null) return { ok: false, reason: rateLimitedReason(blockedUntil, now) };
|
|
92
|
-
|
|
93
|
-
if (prepared.ok && prepared.expiresAt <= now) return { ok: false, reason: EXPIRED_REASON };
|
|
94
|
-
return prepared;
|
|
131
|
+
return prepareSample(account);
|
|
95
132
|
}
|
|
96
133
|
|
|
97
134
|
export async function sampleAccountUsage(account: Account): Promise<SampleOutcome> {
|
package/src/lib/supervise.ts
CHANGED
|
@@ -75,9 +75,9 @@ export function loopGuardTripped(product: WrappedProduct): boolean {
|
|
|
75
75
|
return false;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export async function runPassthrough(input: { real: string; argv: string[]; env: Record<string, string | undefined>; onSpawn?: (child: Subprocess) => void }): Promise<number> {
|
|
78
|
+
export async function runPassthrough(input: { real: string; argv: string[]; env: Record<string, string | undefined>; onSpawn?: (child: Subprocess) => void | Promise<void> }): Promise<number> {
|
|
79
79
|
const p = Bun.spawn([input.real, ...input.argv], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: input.env });
|
|
80
|
-
input.onSpawn?.(p);
|
|
80
|
+
await input.onSpawn?.(p);
|
|
81
81
|
await p.exited;
|
|
82
82
|
return exitStatus(p);
|
|
83
83
|
}
|