tokenmaxxing 1.57.0 → 1.59.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 +6 -6
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli/auth.ts +6 -17
- package/src/cli/check.ts +2 -2
- package/src/cli/cloudrun.ts +12 -19
- package/src/cli/config.ts +2 -1
- package/src/cli/doctor.ts +5 -4
- package/src/cli/init.ts +0 -1
- package/src/cli/render.ts +24 -9
- package/src/cli/rm.ts +0 -4
- package/src/cli/status.ts +37 -55
- package/src/entries/codexstophook.ts +11 -24
- package/src/entries/codexsupervisor.ts +30 -72
- package/src/entries/statusline.ts +17 -26
- package/src/entries/stopfailurehook.ts +4 -5
- package/src/entries/stophook.ts +12 -10
- package/src/entries/subagentstatusline.ts +4 -10
- package/src/entries/supervisor.ts +71 -113
- package/src/lib/claude.ts +18 -19
- package/src/lib/claudebin.ts +3 -2
- package/src/lib/claudelock.ts +2 -2
- package/src/lib/cloud.ts +4 -8
- package/src/lib/codex.ts +3 -3
- package/src/lib/codexauth.ts +2 -11
- package/src/lib/codexbin.ts +2 -1
- package/src/lib/codexoauth.ts +3 -8
- package/src/lib/codexusage.ts +5 -5
- package/src/lib/compact.ts +5 -18
- package/src/lib/credstore.ts +1 -6
- package/src/lib/decide.ts +12 -29
- package/src/lib/grok.ts +38 -152
- package/src/lib/http.ts +1 -2
- package/src/lib/install.ts +20 -38
- package/src/lib/keychain.ts +1 -4
- package/src/lib/log.ts +5 -5
- package/src/lib/oauth.ts +6 -5
- package/src/lib/opencodego.ts +31 -140
- package/src/lib/paths.ts +6 -22
- package/src/lib/picker.ts +9 -20
- package/src/lib/presence.ts +4 -5
- package/src/lib/proc.ts +22 -0
- package/src/lib/provider.ts +1 -1
- package/src/lib/sample.ts +25 -24
- package/src/lib/sessions.ts +5 -6
- package/src/lib/settings.ts +1 -8
- package/src/lib/state.ts +6 -29
- package/src/lib/statusonly.ts +143 -0
- package/src/lib/supervise.ts +111 -0
- package/src/lib/types.ts +27 -36
- package/src/lib/update.ts +6 -7
- package/src/lib/usage.ts +45 -188
- package/src/main.ts +5 -5
- package/src/entries/sessionstart.ts +0 -33
|
@@ -4,19 +4,22 @@ import type { FileSink, Subprocess } from "bun";
|
|
|
4
4
|
import { maxBy } from "es-toolkit";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealClaude, wrapDepth } from "../lib/claudebin.ts";
|
|
8
8
|
import { claude, pickSeat } from "../lib/claude.ts";
|
|
9
9
|
import { compactClaudeSession } from "../lib/compact.ts";
|
|
10
10
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
11
11
|
import { withLock } from "../lib/lock.ts";
|
|
12
12
|
import { thresholdBars, usableAt } from "../lib/picker.ts";
|
|
13
|
-
import { clearPresence
|
|
13
|
+
import { clearPresence } from "../lib/presence.ts";
|
|
14
|
+
import { readLines } from "../lib/proc.ts";
|
|
14
15
|
import { teeObservation } from "../lib/sample.ts";
|
|
15
|
-
import {
|
|
16
|
+
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
17
|
+
import { saveTermios } from "../lib/tty.ts";
|
|
16
18
|
import { loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
17
19
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
+
import { gatedFamilies, modelFromFlag } from "../lib/usage.ts";
|
|
21
|
+
import { RespawnMarkerSchema, type Account, type Config, type ModelInfo } from "../lib/types.ts";
|
|
22
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
20
23
|
|
|
21
24
|
const NONINTERACTIVE_SUBCMDS = new Set([
|
|
22
25
|
"mcp", "config", "doctor", "update", "install", "migrate-installer",
|
|
@@ -44,15 +47,15 @@ const OPTIONAL_VALUE_ROOT_FLAGS = new Set([
|
|
|
44
47
|
|
|
45
48
|
const isUuid = (s: string) => z.uuid().safeParse(s).success;
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
manage:
|
|
49
|
-
sessionId:
|
|
50
|
-
resumeId:
|
|
51
|
-
continueLatest:
|
|
52
|
-
streamInput:
|
|
53
|
-
streamOutput:
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
type Analysis = {
|
|
51
|
+
manage: boolean;
|
|
52
|
+
sessionId: string | null;
|
|
53
|
+
resumeId: string | null;
|
|
54
|
+
continueLatest: boolean;
|
|
55
|
+
streamInput: boolean;
|
|
56
|
+
streamOutput: boolean;
|
|
57
|
+
model: string | null;
|
|
58
|
+
};
|
|
56
59
|
|
|
57
60
|
export function analyzeArgs(argv: string[]): Analysis {
|
|
58
61
|
let sessionId: string | null = null;
|
|
@@ -60,6 +63,7 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
60
63
|
let continueLatest = false;
|
|
61
64
|
let streamInput = false;
|
|
62
65
|
let streamOutput = false;
|
|
66
|
+
let model: string | null = null;
|
|
63
67
|
let printMode = false;
|
|
64
68
|
let invalidSessionArg = false;
|
|
65
69
|
let pickerResume = false;
|
|
@@ -97,6 +101,8 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
97
101
|
else if (a.startsWith("--input-format=")) streamInput = a.slice("--input-format=".length) === "stream-json";
|
|
98
102
|
else if (a === "--output-format") streamOutput = argv[++i] === "stream-json";
|
|
99
103
|
else if (a.startsWith("--output-format=")) streamOutput = a.slice("--output-format=".length) === "stream-json";
|
|
104
|
+
else if (a === "--model") model = argv[++i] ?? null;
|
|
105
|
+
else if (a.startsWith("--model=")) model = a.slice("--model=".length);
|
|
100
106
|
else if (VALUE_TAKING_ROOT_FLAGS.has(a)) i++;
|
|
101
107
|
else if (VARIADIC_ROOT_FLAGS.has(a)) {
|
|
102
108
|
while (i + 1 < argv.length && !argv[i + 1]!.startsWith("-")) i++;
|
|
@@ -112,7 +118,7 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
112
118
|
const isSubcmd = firstPositional !== null && NONINTERACTIVE_SUBCMDS.has(firstPositional);
|
|
113
119
|
const forkResume = forkSession && (resumeId !== null || continueLatest);
|
|
114
120
|
const manage = !printMode && !isSubcmd && !invalidSessionArg && !pickerResume && !forkResume && !process.env.TOKENMAXXING_PROBE;
|
|
115
|
-
return { manage, sessionId, resumeId, continueLatest, streamInput, streamOutput };
|
|
121
|
+
return { manage, sessionId, resumeId, continueLatest, streamInput, streamOutput, model };
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
export function stripSessionFlags(argv: string[]): string[] {
|
|
@@ -169,18 +175,17 @@ function latestSessionForCwd(): string | null {
|
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
launchedAt:
|
|
174
|
-
overriddenUntil:
|
|
175
|
-
}
|
|
176
|
-
type MarkerGate = z.infer<typeof MarkerGateSchema>;
|
|
178
|
+
type MarkerGate = {
|
|
179
|
+
launchedAt: number;
|
|
180
|
+
overriddenUntil: number;
|
|
181
|
+
};
|
|
177
182
|
|
|
178
183
|
function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
|
|
179
184
|
let m: z.infer<typeof RespawnMarkerSchema>;
|
|
180
185
|
try {
|
|
181
186
|
m = RespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
|
|
182
187
|
} catch (e) {
|
|
183
|
-
log("supervisor.marker_invalid", { err:
|
|
188
|
+
log("supervisor.marker_invalid", { err: errorMessage(e) });
|
|
184
189
|
throw new Error(
|
|
185
190
|
`${marker} is corrupt (unparsable JSON or off-schema) - the session was stopped instead of guessing whether the pool is depleted; inspect the marker, then run \`claude --resume ${basename(marker)}\` (a fresh launch clears it)`,
|
|
186
191
|
);
|
|
@@ -201,28 +206,29 @@ function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof Resp
|
|
|
201
206
|
const SEAT_POLL_MS = 10_000;
|
|
202
207
|
const SEAT_RETRY_MS = 60_000;
|
|
203
208
|
|
|
204
|
-
function seatBlockedUntil(seatId: string, now: number): number | null {
|
|
209
|
+
function seatBlockedUntil(seatId: string, now: number, families: string[], cfg: Config): number | null {
|
|
205
210
|
const account = loadAccounts(claudePool).accounts.find((a) => a.id === seatId);
|
|
206
211
|
if (!account) return null;
|
|
207
|
-
const cfg = loadConfig();
|
|
208
212
|
const observed = teeObservation(account);
|
|
209
213
|
const current = observed ? { ...account, windows: observed.windows } : account;
|
|
210
|
-
const until = usableAt(current, { now, thresholds: thresholdBars(cfg), currentId: seatId, families
|
|
214
|
+
const until = usableAt(current, { now, thresholds: thresholdBars(cfg), currentId: seatId, families, seats: null });
|
|
211
215
|
return until > now ? until : null;
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate): Promise<boolean> {
|
|
218
|
+
async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, model: ModelInfo | null): Promise<boolean> {
|
|
215
219
|
try {
|
|
216
220
|
const now = Date.now();
|
|
217
|
-
const
|
|
221
|
+
const cfg = loadConfig();
|
|
222
|
+
const families = gatedFamilies(model, cfg.policy.switchModels);
|
|
223
|
+
const until = seatBlockedUntil(seat.id, now, families, cfg);
|
|
218
224
|
if (until == null || until <= gate.overriddenUntil) return false;
|
|
219
|
-
const decision = await evaluateAndMaybeSwap(claude, now, true, null, seat.id);
|
|
225
|
+
const decision = await evaluateAndMaybeSwap(claude, now, true, null, seat.id, families);
|
|
220
226
|
log("supervisor.seat_exhausted", { seat: seat.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
221
227
|
if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
222
228
|
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt }, sessionId: sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: true });
|
|
223
229
|
}
|
|
224
230
|
} catch (e) {
|
|
225
|
-
log("supervisor.seat_watch_error", { err:
|
|
231
|
+
log("supervisor.seat_watch_error", { err: errorMessage(e) });
|
|
226
232
|
}
|
|
227
233
|
return true;
|
|
228
234
|
}
|
|
@@ -276,7 +282,7 @@ class StdinRelay {
|
|
|
276
282
|
|
|
277
283
|
constructor() {
|
|
278
284
|
this.pump()
|
|
279
|
-
.catch((e: unknown) => log("supervisor.relay_read_failed", { err:
|
|
285
|
+
.catch((e: unknown) => log("supervisor.relay_read_failed", { err: errorMessage(e) }))
|
|
280
286
|
.finally(() => {
|
|
281
287
|
this.ended = true;
|
|
282
288
|
this.close();
|
|
@@ -296,19 +302,7 @@ class StdinRelay {
|
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
private async pump(): Promise<void> {
|
|
299
|
-
const
|
|
300
|
-
let pending = "";
|
|
301
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
302
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
303
|
-
let nl = pending.indexOf("\n");
|
|
304
|
-
while (nl !== -1) {
|
|
305
|
-
this.forward(pending.slice(0, nl + 1));
|
|
306
|
-
pending = pending.slice(nl + 1);
|
|
307
|
-
nl = pending.indexOf("\n");
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
pending += decoder.decode();
|
|
311
|
-
if (pending.length > 0) this.forward(`${pending}\n`);
|
|
305
|
+
for await (const line of readLines(Bun.stdin.stream())) this.forward(`${line}\n`);
|
|
312
306
|
}
|
|
313
307
|
|
|
314
308
|
private forward(line: string): void {
|
|
@@ -318,7 +312,7 @@ class StdinRelay {
|
|
|
318
312
|
this.sink.flush();
|
|
319
313
|
return;
|
|
320
314
|
} catch (e) {
|
|
321
|
-
log("supervisor.relay_write_failed", { err:
|
|
315
|
+
log("supervisor.relay_write_failed", { err: errorMessage(e) });
|
|
322
316
|
this.sink = null;
|
|
323
317
|
}
|
|
324
318
|
}
|
|
@@ -330,50 +324,17 @@ class StdinRelay {
|
|
|
330
324
|
try {
|
|
331
325
|
this.sink.end();
|
|
332
326
|
} catch (e) {
|
|
333
|
-
log("supervisor.relay_end_failed", { err:
|
|
327
|
+
log("supervisor.relay_end_failed", { err: errorMessage(e) });
|
|
334
328
|
}
|
|
335
329
|
this.sink = null;
|
|
336
330
|
}
|
|
337
331
|
}
|
|
338
332
|
|
|
339
|
-
async function recordPresence(child: Subprocess, sid: string, seat: Account, savedTermios: string | null): Promise<void> {
|
|
340
|
-
for (let attempt = 0; ; attempt++) {
|
|
341
|
-
try {
|
|
342
|
-
writePresence({ dir: paths.presenceDir, id: sid, accountId: seat.id, pid: child.pid });
|
|
343
|
-
return;
|
|
344
|
-
} catch (e) {
|
|
345
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
346
|
-
if (attempt >= 9) {
|
|
347
|
-
log("supervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
348
|
-
child.kill();
|
|
349
|
-
await child.exited;
|
|
350
|
-
restoreTermios(savedTermios);
|
|
351
|
-
throw new Error("could not write the session presence file - refusing to run a session whose seat placement cannot see");
|
|
352
|
-
}
|
|
353
|
-
await Bun.sleep(100);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
333
|
export async function runSupervisor(argv: string[]): Promise<number> {
|
|
359
|
-
|
|
360
|
-
if (depth >= MAX_WRAP_DEPTH) {
|
|
361
|
-
console.error(
|
|
362
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (depth ${depth}) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
|
|
363
|
-
);
|
|
364
|
-
log("supervisor.loop_abort", { depth });
|
|
365
|
-
return 1;
|
|
366
|
-
}
|
|
367
|
-
if (wrapperEntryRateTripped(Date.now())) {
|
|
368
|
-
console.error(
|
|
369
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (over ${WRAP_RATE_MAX} wrapper entries in ${WRAP_RATE_WINDOW_MS / 1000}s) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
|
|
370
|
-
);
|
|
371
|
-
log("supervisor.rate_abort", { max: WRAP_RATE_MAX });
|
|
372
|
-
return 1;
|
|
373
|
-
}
|
|
334
|
+
if (loopGuardTripped("claude")) return 1;
|
|
374
335
|
const real = resolveRealClaude();
|
|
375
336
|
const info = analyzeArgs(argv);
|
|
376
|
-
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(
|
|
337
|
+
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
|
|
377
338
|
|
|
378
339
|
let child: Subprocess | null = null;
|
|
379
340
|
let terminating = false;
|
|
@@ -387,10 +348,8 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
387
348
|
const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
|
|
388
349
|
delete passthroughEnv.TOKENMAXXING_SUPERVISED;
|
|
389
350
|
delete passthroughEnv.TOKENMAXXING_SESSION_ID;
|
|
390
|
-
|
|
391
|
-
child = p;
|
|
392
|
-
await p.exited;
|
|
393
|
-
return p.exitCode ?? (p.signalCode ? 1 : 0);
|
|
351
|
+
delete passthroughEnv.TOKENMAXXING_MODEL;
|
|
352
|
+
return runPassthrough({ real, argv, env: passthroughEnv, onSpawn: (p) => { child = p; } });
|
|
394
353
|
}
|
|
395
354
|
|
|
396
355
|
let base = stripSessionFlags(argv);
|
|
@@ -428,6 +387,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
428
387
|
const effective = analyzeArgs(base);
|
|
429
388
|
const relay = effective.streamInput ? new StdinRelay() : null;
|
|
430
389
|
const stream = effective.streamOutput;
|
|
390
|
+
const model = modelFromFlag(effective.model);
|
|
431
391
|
let noticeSid = sid;
|
|
432
392
|
const say: Say = (terminal, text) => {
|
|
433
393
|
if (stream) process.stdout.write(systemLine(noticeSid, text));
|
|
@@ -442,7 +402,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
442
402
|
|
|
443
403
|
const gate: MarkerGate = { launchedAt: Date.now(), overriddenUntil };
|
|
444
404
|
const { child: proc, seat } = await withLock(claudePool.lockFile, async () => {
|
|
445
|
-
const picked = (wanted == null ? null : (loadAccounts(claudePool).accounts.find((a) => a.id === wanted) ?? null)) ?? pickSeat(gate.launchedAt);
|
|
405
|
+
const picked = (wanted == null ? null : (loadAccounts(claudePool).accounts.find((a) => a.id === wanted) ?? null)) ?? pickSeat(gate.launchedAt, model);
|
|
446
406
|
log("supervisor.launch", { sid, respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" "), injected: firstLine !== null });
|
|
447
407
|
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
448
408
|
stdin: relay === null ? "inherit" : "pipe",
|
|
@@ -453,49 +413,46 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
453
413
|
TOKENMAXXING_SUPERVISED: "1",
|
|
454
414
|
TOKENMAXXING_SESSION_ID: sid,
|
|
455
415
|
TOKENMAXXING_LAUNCHED_AT: String(gate.launchedAt),
|
|
416
|
+
TOKENMAXXING_MODEL: effective.model ?? "",
|
|
456
417
|
...(picked ? { CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(picked.id) } : {}),
|
|
457
418
|
},
|
|
458
419
|
});
|
|
459
420
|
if (relay !== null) relay.attach(spawned.stdin!, firstLine);
|
|
460
421
|
firstLine = null;
|
|
461
|
-
if (picked)
|
|
422
|
+
if (picked) {
|
|
423
|
+
await recordPresenceOrStop({
|
|
424
|
+
child: spawned,
|
|
425
|
+
dir: paths.presenceDir,
|
|
426
|
+
id: sid,
|
|
427
|
+
accountId: picked.id,
|
|
428
|
+
event: "supervisor.presence_failed",
|
|
429
|
+
message: "could not write the session presence file - refusing to run a session whose seat placement cannot see",
|
|
430
|
+
savedTermios,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
462
433
|
return { child: spawned, seat: picked };
|
|
463
434
|
});
|
|
464
435
|
|
|
465
436
|
child = proc;
|
|
466
|
-
let done = false;
|
|
467
437
|
let seatCheckAt = 0;
|
|
468
|
-
|
|
469
|
-
|
|
438
|
+
await raceMarkerOrExit({
|
|
439
|
+
child: proc,
|
|
440
|
+
tick: async () => {
|
|
470
441
|
if (existsSync(marker) && consumableMarker(marker, gate) != null) return true;
|
|
471
442
|
if (seat && !terminating && Date.now() >= seatCheckAt) {
|
|
472
|
-
const decided = await moveExhaustedSeat(seat, sid, gate);
|
|
443
|
+
const decided = await moveExhaustedSeat(seat, sid, gate, model);
|
|
473
444
|
seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
474
445
|
}
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (winner === "marker") {
|
|
446
|
+
return false;
|
|
447
|
+
},
|
|
448
|
+
savedTermios,
|
|
449
|
+
onKill: () => relay?.detach(),
|
|
450
|
+
onError: () => clearPresence({ dir: paths.presenceDir, id: sid }),
|
|
451
|
+
onExited: () => {
|
|
452
|
+
child = null;
|
|
483
453
|
relay?.detach();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
} catch (e) {
|
|
487
|
-
relay?.detach();
|
|
488
|
-
proc.kill();
|
|
489
|
-
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
490
|
-
throw e;
|
|
491
|
-
} finally {
|
|
492
|
-
await proc.exited;
|
|
493
|
-
child = null;
|
|
494
|
-
relay?.detach();
|
|
495
|
-
done = true;
|
|
496
|
-
await markerWatch.catch(() => {});
|
|
497
|
-
restoreTermios(savedTermios);
|
|
498
|
-
}
|
|
454
|
+
},
|
|
455
|
+
});
|
|
499
456
|
|
|
500
457
|
const m = !terminating && existsSync(marker) ? consumableMarker(marker, gate) : null;
|
|
501
458
|
if (m) {
|
|
@@ -511,6 +468,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
511
468
|
delete compactEnv.TOKENMAXXING_SUPERVISED;
|
|
512
469
|
delete compactEnv.TOKENMAXXING_SESSION_ID;
|
|
513
470
|
delete compactEnv.TOKENMAXXING_LAUNCHED_AT;
|
|
471
|
+
delete compactEnv.TOKENMAXXING_MODEL;
|
|
514
472
|
const outcome = await compactClaudeSession({ real, sid: m.sessionId, env: compactEnv });
|
|
515
473
|
log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat.id.slice(0, 8), ok: outcome.ok, reason: outcome.ok ? undefined : outcome.reason });
|
|
516
474
|
if (!outcome.ok) say(`\x1b[33m compaction did not land (${outcome.reason}) - resuming with the full context\x1b[0m\n`, `tokenmaxxing: compaction did not land; resuming with the full context. (${outcome.reason})`);
|
|
@@ -528,6 +486,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
528
486
|
}
|
|
529
487
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
530
488
|
log("supervisor.exit", { sid, respawns, code: proc.exitCode, signal: proc.signalCode, terminated: terminating || undefined });
|
|
531
|
-
return
|
|
489
|
+
return exitStatus(proc);
|
|
532
490
|
}
|
|
533
491
|
}
|
package/src/lib/claude.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
|
|
@@ -6,9 +6,9 @@ import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
|
|
|
6
6
|
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
7
7
|
import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
|
|
8
8
|
import { withLock } from "./lock.ts";
|
|
9
|
-
import { log } from "./log.ts";
|
|
9
|
+
import { errorMessage, log } from "./log.ts";
|
|
10
10
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
11
|
-
import { claudePool, paths, sampleDirFor, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
11
|
+
import { claudePool, env, paths, sampleDirFor, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
12
12
|
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
13
13
|
import { seatCounts } from "./presence.ts";
|
|
14
14
|
import type { Observation, Provider, SampleReport } from "./provider.ts";
|
|
@@ -16,8 +16,8 @@ import { foldTee, sampleAccountUsage, teeObservation } from "./sample.ts";
|
|
|
16
16
|
import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
|
|
17
17
|
import { loadSetupTokens, saveSetupTokens } from "./setuptokens.ts";
|
|
18
18
|
import { saveTermios, restoreTermios } from "./tty.ts";
|
|
19
|
-
import {
|
|
20
|
-
import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
|
|
19
|
+
import { fetchUsageDirect, gatedFamilies, mergeWindows, modelFromFlag, scrubCredentialEnv, windowsOf } from "./usage.ts";
|
|
20
|
+
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config, type ModelInfo } from "./types.ts";
|
|
21
21
|
import { c } from "../cli/render.ts";
|
|
22
22
|
|
|
23
23
|
class StoreUnusableError extends Error {
|
|
@@ -37,16 +37,15 @@ function presence(): Map<string, number> {
|
|
|
37
37
|
|
|
38
38
|
const PROBE_BACKOFF_CAP_MS = 30 * 60 * 1000;
|
|
39
39
|
|
|
40
|
-
async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean }): Promise<Observation | null> {
|
|
40
|
+
async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean; perModel: boolean }): Promise<Observation | null> {
|
|
41
41
|
const ttl = cfg.policy.usagePollTtlMs;
|
|
42
42
|
const interval = Math.min(ttl * 2 ** (account.probeFails ?? 0), PROBE_BACKOFF_CAP_MS);
|
|
43
43
|
const probeAttempted = account.lastProbeAt != null && now - account.lastProbeAt <= interval;
|
|
44
44
|
const snap = loadUsageSnapshot(account.id);
|
|
45
45
|
const fresh = snap != null && now - snap.at <= ttl;
|
|
46
|
-
|
|
47
|
-
if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
|
|
46
|
+
if (opts.probe && !probeAttempted && (!fresh || opts.perModel)) {
|
|
48
47
|
const startedAt = Date.now();
|
|
49
|
-
const outcome = await sampleAccountUsage(account
|
|
48
|
+
const outcome = await sampleAccountUsage(account);
|
|
50
49
|
await withLock(claudePool.lockFile, () => {
|
|
51
50
|
const idx = loadAccounts(claudePool);
|
|
52
51
|
const a = idx.accounts.find((x) => x.id === account.id);
|
|
@@ -100,7 +99,7 @@ async function prepareMove(target: Account): Promise<void> {
|
|
|
100
99
|
try {
|
|
101
100
|
creds = await readStore(target.id);
|
|
102
101
|
} catch (e) {
|
|
103
|
-
throw new StoreUnusableError(`${target.label}'s store is unreadable (${
|
|
102
|
+
throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
104
103
|
}
|
|
105
104
|
if (!creds) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
106
105
|
if (isDeadCredential(creds)) {
|
|
@@ -125,13 +124,13 @@ async function storeUsable(a: Account): Promise<boolean> {
|
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
export function pickSeat(now: number): Account | null {
|
|
127
|
+
export function pickSeat(now: number, model: ModelInfo | null): Account | null {
|
|
129
128
|
const cfg = loadConfig();
|
|
130
129
|
const idx = loadAccounts(claudePool);
|
|
131
130
|
let dirty = false;
|
|
132
131
|
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
133
132
|
if (dirty) saveAccounts(claudePool, idx);
|
|
134
|
-
const ctx: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: cfg.policy.switchModels, seats: presence() };
|
|
133
|
+
const ctx: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats: presence() };
|
|
135
134
|
return pickBest(idx.accounts, ctx) ?? pickEarliestReset(idx.accounts, ctx)?.account ?? null;
|
|
136
135
|
}
|
|
137
136
|
|
|
@@ -147,11 +146,11 @@ async function removeCredentials(a: Account): Promise<void> {
|
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
const IdentityReadySchema = z.looseObject({ oauthAccount: z.looseObject({ accountUuid: z.string().min(1) }) });
|
|
150
|
+
|
|
150
151
|
function identityReady(cjPath: string): boolean {
|
|
151
|
-
if (!existsSync(cjPath)) return false;
|
|
152
152
|
try {
|
|
153
|
-
|
|
154
|
-
return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
|
|
153
|
+
return IdentityReadySchema.safeParse(JsonTextSchema.safeParse(readFileSync(cjPath, "utf8")).data).success;
|
|
155
154
|
} catch {
|
|
156
155
|
return false;
|
|
157
156
|
}
|
|
@@ -167,8 +166,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
167
166
|
const real = resolveRealClaude();
|
|
168
167
|
|
|
169
168
|
const savedTermios = saveTermios();
|
|
170
|
-
const env
|
|
171
|
-
for (const key of CRED_ENV_OVERRIDES) delete env[key];
|
|
169
|
+
const env = scrubCredentialEnv({ ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" });
|
|
172
170
|
const p = Bun.spawn([real], {
|
|
173
171
|
stdin: "inherit",
|
|
174
172
|
stdout: "inherit",
|
|
@@ -208,7 +206,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
208
206
|
try {
|
|
209
207
|
identity = await fetchTokenIdentity(blob.claudeAiOauth.accessToken);
|
|
210
208
|
} catch (e) {
|
|
211
|
-
console.error(c.red(`could not verify which account the login belongs to (${
|
|
209
|
+
console.error(c.red(`could not verify which account the login belongs to (${errorMessage(e)}) - nothing changed.`));
|
|
212
210
|
return null;
|
|
213
211
|
}
|
|
214
212
|
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
|
@@ -287,7 +285,8 @@ export const claude: Provider = {
|
|
|
287
285
|
presence,
|
|
288
286
|
gatedFamilies: (cfg) => {
|
|
289
287
|
const seat = liveId();
|
|
290
|
-
|
|
288
|
+
const model = modelFromFlag(env("TOKENMAXXING_MODEL", "")) ?? (seat == null ? null : (loadUsageSnapshot(seat)?.state.model ?? null));
|
|
289
|
+
return gatedFamilies(model, cfg.policy.switchModels);
|
|
291
290
|
},
|
|
292
291
|
observeLive,
|
|
293
292
|
samplePool,
|
package/src/lib/claudebin.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync } from "nod
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { uniq } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { errorMessage } from "./log.ts";
|
|
5
6
|
import { paths } from "./paths.ts";
|
|
6
7
|
import { loadConfig } from "./state.ts";
|
|
7
8
|
import { writeFileAtomic } from "./atomic.ts";
|
|
@@ -9,7 +10,7 @@ import { writeFileAtomic } from "./atomic.ts";
|
|
|
9
10
|
export const WRAP_DEPTH_ENV = "TOKENMAXXING_WRAP_DEPTH";
|
|
10
11
|
export const MAX_WRAP_DEPTH = 5;
|
|
11
12
|
export const UNMANAGED_ENV = "TOKENMAXXING_UNMANAGED";
|
|
12
|
-
export const LOOP_DIAGNOSIS = "wrapper re-entered without reaching the real
|
|
13
|
+
export const LOOP_DIAGNOSIS = "wrapper re-entered without reaching the real";
|
|
13
14
|
|
|
14
15
|
export function wrapDepth(env: Record<string, string | undefined> = process.env): number {
|
|
15
16
|
const n = Number(env[WRAP_DEPTH_ENV] ?? "");
|
|
@@ -96,7 +97,7 @@ export function verifyRealClaude(bin: string): string | null {
|
|
|
96
97
|
try {
|
|
97
98
|
p = Bun.spawnSync([bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
98
99
|
} catch (e) {
|
|
99
|
-
return
|
|
100
|
+
return errorMessage(e);
|
|
100
101
|
}
|
|
101
102
|
const outText = (p.stdout?.toString() ?? "").trim();
|
|
102
103
|
const err = (p.stderr?.toString() ?? "").trim();
|
package/src/lib/claudelock.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, realpathSync, rmdirSync, statSync, utimesSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { delay } from "es-toolkit";
|
|
4
|
-
import { log } from "./log.ts";
|
|
4
|
+
import { errorMessage, log } from "./log.ts";
|
|
5
5
|
import { ErrnoSchema } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
const STALE_MS = 60_000;
|
|
@@ -48,7 +48,7 @@ export async function withClaudeRefreshLock<T>(
|
|
|
48
48
|
break;
|
|
49
49
|
}
|
|
50
50
|
} catch (e) {
|
|
51
|
-
log("claudelock.legacy_error", { err:
|
|
51
|
+
log("claudelock.legacy_error", { err: errorMessage(e) });
|
|
52
52
|
held.push(primary);
|
|
53
53
|
break;
|
|
54
54
|
}
|
package/src/lib/cloud.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { paths } from "./paths.ts";
|
|
|
6
6
|
import { writeFileAtomic } from "./atomic.ts";
|
|
7
7
|
import { UNMANAGED_ENV, resolveRealClaude } from "./claudebin.ts";
|
|
8
8
|
import { pidExists } from "./proc.ts";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { CloudTokensSchema, type CloudToken } from "./setuptokens.ts";
|
|
10
|
+
import { parseErrorBody, readTranscriptTail, scrubCredentialEnv } from "./usage.ts";
|
|
11
11
|
|
|
12
12
|
export const TOKENS_ENV = "TOKENMAXXING_TOKENS";
|
|
13
13
|
export const FALLBACK_WALL_MS = 5 * 3_600_000;
|
|
@@ -73,11 +73,7 @@ export function saveCloudWalled(walled: Record<string, number>): void {
|
|
|
73
73
|
writeFileAtomic(paths.cloudWalledJson, JSON.stringify(WalledSchema.parse(walled), null, 2) + "\n");
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
z.object({ ok: z.literal(true), token: CloudTokenSchema }),
|
|
78
|
-
z.object({ ok: z.literal(false), earliestReset: z.number() }),
|
|
79
|
-
]);
|
|
80
|
-
export type Pick = z.infer<typeof PickSchema>;
|
|
76
|
+
export type Pick = { ok: true; token: CloudToken } | { ok: false; earliestReset: number };
|
|
81
77
|
|
|
82
78
|
export function pickToken(input: {
|
|
83
79
|
tokens: CloudToken[];
|
|
@@ -171,6 +167,6 @@ export function limitWallUntil(input: { sessionId: string; sinceBytes: number; n
|
|
|
171
167
|
const row = rows.findLast((r) => r.isApiErrorMessage === true);
|
|
172
168
|
if (!row || row.error !== "rate_limit" || row.apiErrorIsTransient === true) return null;
|
|
173
169
|
if ((row.quotaLimits?.rateLimitType ?? "") === "" && parseErrorBody(row.errorDetails)?.error?.type !== "rate_limit_error") return null;
|
|
174
|
-
const resetsAt = row.quotaLimits?.resetsAt
|
|
170
|
+
const resetsAt = row.quotaLimits?.resetsAt ?? null;
|
|
175
171
|
return resetsAt != null && resetsAt > input.now ? resetsAt : input.now + FALLBACK_WALL_MS;
|
|
176
172
|
}
|
package/src/lib/codex.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { seatCounts } 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";
|
|
12
|
-
import { log } from "./log.ts";
|
|
12
|
+
import { errorMessage, log } from "./log.ts";
|
|
13
13
|
import { codexPaths, codexPool, codexSeatFromEnv } from "./paths.ts";
|
|
14
14
|
import { isExhausted, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
15
15
|
import type { Observation, Provider, SampleReport } from "./provider.ts";
|
|
@@ -47,7 +47,7 @@ async function readCodexUsage(account: Account, now: number): Promise<CodexReadO
|
|
|
47
47
|
try {
|
|
48
48
|
auth = readCodexStoreAuth(account.id);
|
|
49
49
|
} catch (e) {
|
|
50
|
-
return { ok: false, reason: `store credential unreadable (${(e
|
|
50
|
+
return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
|
|
51
51
|
}
|
|
52
52
|
if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
|
|
53
53
|
try {
|
|
@@ -110,7 +110,7 @@ async function prepareMove(target: Account): Promise<void> {
|
|
|
110
110
|
try {
|
|
111
111
|
auth = readCodexStoreAuth(target.id);
|
|
112
112
|
} catch (e) {
|
|
113
|
-
throw new StoreUnusableError(`${target.label}'s store is unreadable (${
|
|
113
|
+
throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
|
|
114
114
|
}
|
|
115
115
|
if (!auth) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
|
|
116
116
|
log("move.prepared", { account: target.id.slice(0, 8), label: target.label });
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -84,12 +84,7 @@ function decodeJwtPayload(input: { jwt: string }): unknown {
|
|
|
84
84
|
return JSON.parse(payload);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
accountId: z.string(),
|
|
89
|
-
email: z.string().nullable(),
|
|
90
|
-
planType: z.string().nullable(),
|
|
91
|
-
});
|
|
92
|
-
export type CodexIdentity = z.infer<typeof CodexIdentitySchema>;
|
|
87
|
+
export type CodexIdentity = { accountId: string; email: string | null; planType: string | null };
|
|
93
88
|
|
|
94
89
|
export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
|
|
95
90
|
const { auth } = input;
|
|
@@ -99,11 +94,7 @@ export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
|
|
|
99
94
|
if (!accountId) {
|
|
100
95
|
throw new Error("codex credential carries no account id (neither tokens.account_id nor the id_token claim)");
|
|
101
96
|
}
|
|
102
|
-
return
|
|
103
|
-
accountId,
|
|
104
|
-
email: claims.email ?? null,
|
|
105
|
-
planType: authClaims?.chatgpt_plan_type ?? null,
|
|
106
|
-
});
|
|
97
|
+
return { accountId, email: claims.email ?? null, planType: authClaims?.chatgpt_plan_type ?? null };
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
export function isCodexAccessExpiring(input: { auth: CodexAuthJson; skewMs?: number; now?: number }): boolean {
|
package/src/lib/codexbin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, pointsBackAtUs } from "./claudebin.ts";
|
|
4
|
+
import { errorMessage } from "./log.ts";
|
|
4
5
|
import { loadConfig } from "./state.ts";
|
|
5
6
|
import { paths } from "./paths.ts";
|
|
6
7
|
|
|
@@ -41,7 +42,7 @@ export function verifyRealCodex(input: { bin: string }): string | null {
|
|
|
41
42
|
try {
|
|
42
43
|
p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
43
44
|
} catch (e) {
|
|
44
|
-
return
|
|
45
|
+
return errorMessage(e);
|
|
45
46
|
}
|
|
46
47
|
const outText = (p.stdout?.toString() ?? "").trim();
|
|
47
48
|
const err = (p.stderr?.toString() ?? "").trim();
|
package/src/lib/codexoauth.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
|
|
3
|
+
import { errorMessage } from "./log.ts";
|
|
3
4
|
import { env } from "./paths.ts";
|
|
4
5
|
import { CodexAuthJsonSchema, JsonTextSchema, type CodexAuthJson } from "./types.ts";
|
|
5
6
|
|
|
@@ -38,16 +39,10 @@ export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: numbe
|
|
|
38
39
|
let res: Response;
|
|
39
40
|
try {
|
|
40
41
|
res = await http.post(TOKEN_URL, {
|
|
41
|
-
|
|
42
|
-
body: JSON.stringify({
|
|
43
|
-
client_id: CLIENT_ID,
|
|
44
|
-
grant_type: "refresh_token",
|
|
45
|
-
refresh_token: auth.tokens.refresh_token,
|
|
46
|
-
}),
|
|
42
|
+
json: { client_id: CLIENT_ID, grant_type: "refresh_token", refresh_token: auth.tokens.refresh_token },
|
|
47
43
|
});
|
|
48
44
|
} catch (e) {
|
|
49
|
-
|
|
50
|
-
throw new CodexRefreshFailedError(`endpoint unreachable: ${message}`);
|
|
45
|
+
throw new CodexRefreshFailedError(`endpoint unreachable: ${errorMessage(e)}`);
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
const text = await res.text();
|