tokenmaxxing 1.56.0 → 1.58.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 +7 -7
- 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 +34 -57
- package/src/entries/codexstophook.ts +10 -23
- 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 +91 -116
- package/src/lib/claude.ts +11 -12
- 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 +6 -24
- 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/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 +40 -191
- package/src/main.ts +5 -5
- package/src/entries/sessionstart.ts +0 -33
|
@@ -4,19 +4,21 @@ 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
20
|
import { RespawnMarkerSchema, type Account } from "../lib/types.ts";
|
|
19
|
-
import { log } from "../lib/log.ts";
|
|
21
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
20
22
|
|
|
21
23
|
const NONINTERACTIVE_SUBCMDS = new Set([
|
|
22
24
|
"mcp", "config", "doctor", "update", "install", "migrate-installer",
|
|
@@ -44,20 +46,21 @@ const OPTIONAL_VALUE_ROOT_FLAGS = new Set([
|
|
|
44
46
|
|
|
45
47
|
const isUuid = (s: string) => z.uuid().safeParse(s).success;
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
manage:
|
|
49
|
-
sessionId:
|
|
50
|
-
resumeId:
|
|
51
|
-
continueLatest:
|
|
52
|
-
streamInput:
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
type Analysis = {
|
|
50
|
+
manage: boolean;
|
|
51
|
+
sessionId: string | null;
|
|
52
|
+
resumeId: string | null;
|
|
53
|
+
continueLatest: boolean;
|
|
54
|
+
streamInput: boolean;
|
|
55
|
+
streamOutput: boolean;
|
|
56
|
+
};
|
|
55
57
|
|
|
56
58
|
export function analyzeArgs(argv: string[]): Analysis {
|
|
57
59
|
let sessionId: string | null = null;
|
|
58
60
|
let resumeId: string | null = null;
|
|
59
61
|
let continueLatest = false;
|
|
60
62
|
let streamInput = false;
|
|
63
|
+
let streamOutput = false;
|
|
61
64
|
let printMode = false;
|
|
62
65
|
let invalidSessionArg = false;
|
|
63
66
|
let pickerResume = false;
|
|
@@ -66,7 +69,8 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
66
69
|
|
|
67
70
|
for (let i = 0; i < argv.length; i++) {
|
|
68
71
|
const a = argv[i]!;
|
|
69
|
-
if (a === "
|
|
72
|
+
if (a === "--") break;
|
|
73
|
+
else if (a === "-p" || a === "--print") printMode = true;
|
|
70
74
|
else if (a === "--version" || a === "-v" || a === "--help" || a === "-h") printMode = true;
|
|
71
75
|
else if (a === "--session-id") {
|
|
72
76
|
const next = argv[++i] ?? null;
|
|
@@ -92,6 +96,8 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
92
96
|
}
|
|
93
97
|
else if (a === "--input-format") streamInput = argv[++i] === "stream-json";
|
|
94
98
|
else if (a.startsWith("--input-format=")) streamInput = a.slice("--input-format=".length) === "stream-json";
|
|
99
|
+
else if (a === "--output-format") streamOutput = argv[++i] === "stream-json";
|
|
100
|
+
else if (a.startsWith("--output-format=")) streamOutput = a.slice("--output-format=".length) === "stream-json";
|
|
95
101
|
else if (VALUE_TAKING_ROOT_FLAGS.has(a)) i++;
|
|
96
102
|
else if (VARIADIC_ROOT_FLAGS.has(a)) {
|
|
97
103
|
while (i + 1 < argv.length && !argv[i + 1]!.startsWith("-")) i++;
|
|
@@ -107,7 +113,7 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
107
113
|
const isSubcmd = firstPositional !== null && NONINTERACTIVE_SUBCMDS.has(firstPositional);
|
|
108
114
|
const forkResume = forkSession && (resumeId !== null || continueLatest);
|
|
109
115
|
const manage = !printMode && !isSubcmd && !invalidSessionArg && !pickerResume && !forkResume && !process.env.TOKENMAXXING_PROBE;
|
|
110
|
-
return { manage, sessionId, resumeId, continueLatest, streamInput };
|
|
116
|
+
return { manage, sessionId, resumeId, continueLatest, streamInput, streamOutput };
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
export function stripSessionFlags(argv: string[]): string[] {
|
|
@@ -164,18 +170,17 @@ function latestSessionForCwd(): string | null {
|
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
launchedAt:
|
|
169
|
-
overriddenUntil:
|
|
170
|
-
}
|
|
171
|
-
type MarkerGate = z.infer<typeof MarkerGateSchema>;
|
|
173
|
+
type MarkerGate = {
|
|
174
|
+
launchedAt: number;
|
|
175
|
+
overriddenUntil: number;
|
|
176
|
+
};
|
|
172
177
|
|
|
173
178
|
function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
|
|
174
179
|
let m: z.infer<typeof RespawnMarkerSchema>;
|
|
175
180
|
try {
|
|
176
181
|
m = RespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
|
|
177
182
|
} catch (e) {
|
|
178
|
-
log("supervisor.marker_invalid", { err:
|
|
183
|
+
log("supervisor.marker_invalid", { err: errorMessage(e) });
|
|
179
184
|
throw new Error(
|
|
180
185
|
`${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)`,
|
|
181
186
|
);
|
|
@@ -217,25 +222,39 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate):
|
|
|
217
222
|
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt }, sessionId: sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: true });
|
|
218
223
|
}
|
|
219
224
|
} catch (e) {
|
|
220
|
-
log("supervisor.seat_watch_error", { err:
|
|
225
|
+
log("supervisor.seat_watch_error", { err: errorMessage(e) });
|
|
221
226
|
}
|
|
222
227
|
return true;
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
|
|
230
|
+
type Say = (terminal: string, text: string) => void;
|
|
231
|
+
|
|
232
|
+
function systemLine(sid: string, text: string): string {
|
|
233
|
+
return `${JSON.stringify({ type: "system", subtype: "informational", content: text, level: "warning", uuid: crypto.randomUUID(), session_id: sid })}\n`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const clockFmt = new Intl.DateTimeFormat("en-US", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", timeZoneName: "short" });
|
|
237
|
+
|
|
238
|
+
async function countdownWait(acct: string, until: number, out: { stream: boolean; say: Say }): Promise<boolean> {
|
|
226
239
|
let aborted = false;
|
|
227
240
|
const onInt = () => { aborted = true; };
|
|
228
241
|
process.on("SIGINT", onInt);
|
|
229
|
-
|
|
242
|
+
const minutes = Math.max(1, Math.ceil((until - Date.now()) / 60000));
|
|
243
|
+
out.say(
|
|
244
|
+
`\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit. Resuming on ${acct} when it resets (Ctrl-C to resume now).\x1b[0m\n`,
|
|
245
|
+
`tokenmaxxing: all accounts at their limit. Resuming at ${clockFmt.format(until)} (in ${minutes} min) on ${acct}.`,
|
|
246
|
+
);
|
|
230
247
|
while (!aborted && Date.now() < until) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
248
|
+
if (!out.stream) {
|
|
249
|
+
const left = until - Date.now();
|
|
250
|
+
const m = Math.floor(left / 60000);
|
|
251
|
+
const s = Math.floor((left % 60000) / 1000);
|
|
252
|
+
process.stderr.write(`\r\x1b[36m resuming in ${m}m ${String(s).padStart(2, "0")}s \x1b[0m`);
|
|
253
|
+
}
|
|
235
254
|
await Bun.sleep(1000);
|
|
236
255
|
}
|
|
237
256
|
process.removeListener("SIGINT", onInt);
|
|
238
|
-
|
|
257
|
+
out.say(`\n\x1b[36m↻ resuming on ${acct}...\x1b[0m\n`, `tokenmaxxing: resuming on ${acct}.`);
|
|
239
258
|
return aborted;
|
|
240
259
|
}
|
|
241
260
|
|
|
@@ -257,7 +276,7 @@ class StdinRelay {
|
|
|
257
276
|
|
|
258
277
|
constructor() {
|
|
259
278
|
this.pump()
|
|
260
|
-
.catch((e: unknown) => log("supervisor.relay_read_failed", { err:
|
|
279
|
+
.catch((e: unknown) => log("supervisor.relay_read_failed", { err: errorMessage(e) }))
|
|
261
280
|
.finally(() => {
|
|
262
281
|
this.ended = true;
|
|
263
282
|
this.close();
|
|
@@ -277,19 +296,7 @@ class StdinRelay {
|
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
private async pump(): Promise<void> {
|
|
280
|
-
const
|
|
281
|
-
let pending = "";
|
|
282
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
283
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
284
|
-
let nl = pending.indexOf("\n");
|
|
285
|
-
while (nl !== -1) {
|
|
286
|
-
this.forward(pending.slice(0, nl + 1));
|
|
287
|
-
pending = pending.slice(nl + 1);
|
|
288
|
-
nl = pending.indexOf("\n");
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
pending += decoder.decode();
|
|
292
|
-
if (pending.length > 0) this.forward(`${pending}\n`);
|
|
299
|
+
for await (const line of readLines(Bun.stdin.stream())) this.forward(`${line}\n`);
|
|
293
300
|
}
|
|
294
301
|
|
|
295
302
|
private forward(line: string): void {
|
|
@@ -299,7 +306,7 @@ class StdinRelay {
|
|
|
299
306
|
this.sink.flush();
|
|
300
307
|
return;
|
|
301
308
|
} catch (e) {
|
|
302
|
-
log("supervisor.relay_write_failed", { err:
|
|
309
|
+
log("supervisor.relay_write_failed", { err: errorMessage(e) });
|
|
303
310
|
this.sink = null;
|
|
304
311
|
}
|
|
305
312
|
}
|
|
@@ -311,50 +318,17 @@ class StdinRelay {
|
|
|
311
318
|
try {
|
|
312
319
|
this.sink.end();
|
|
313
320
|
} catch (e) {
|
|
314
|
-
log("supervisor.relay_end_failed", { err:
|
|
321
|
+
log("supervisor.relay_end_failed", { err: errorMessage(e) });
|
|
315
322
|
}
|
|
316
323
|
this.sink = null;
|
|
317
324
|
}
|
|
318
325
|
}
|
|
319
326
|
|
|
320
|
-
async function recordPresence(child: Subprocess, sid: string, seat: Account, savedTermios: string | null): Promise<void> {
|
|
321
|
-
for (let attempt = 0; ; attempt++) {
|
|
322
|
-
try {
|
|
323
|
-
writePresence({ dir: paths.presenceDir, id: sid, accountId: seat.id, pid: child.pid });
|
|
324
|
-
return;
|
|
325
|
-
} catch (e) {
|
|
326
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
327
|
-
if (attempt >= 9) {
|
|
328
|
-
log("supervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
329
|
-
child.kill();
|
|
330
|
-
await child.exited;
|
|
331
|
-
restoreTermios(savedTermios);
|
|
332
|
-
throw new Error("could not write the session presence file - refusing to run a session whose seat placement cannot see");
|
|
333
|
-
}
|
|
334
|
-
await Bun.sleep(100);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
327
|
export async function runSupervisor(argv: string[]): Promise<number> {
|
|
340
|
-
|
|
341
|
-
if (depth >= MAX_WRAP_DEPTH) {
|
|
342
|
-
console.error(
|
|
343
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (depth ${depth}) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
|
|
344
|
-
);
|
|
345
|
-
log("supervisor.loop_abort", { depth });
|
|
346
|
-
return 1;
|
|
347
|
-
}
|
|
348
|
-
if (wrapperEntryRateTripped(Date.now())) {
|
|
349
|
-
console.error(
|
|
350
|
-
`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\`.`,
|
|
351
|
-
);
|
|
352
|
-
log("supervisor.rate_abort", { max: WRAP_RATE_MAX });
|
|
353
|
-
return 1;
|
|
354
|
-
}
|
|
328
|
+
if (loopGuardTripped("claude")) return 1;
|
|
355
329
|
const real = resolveRealClaude();
|
|
356
330
|
const info = analyzeArgs(argv);
|
|
357
|
-
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(
|
|
331
|
+
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
|
|
358
332
|
|
|
359
333
|
let child: Subprocess | null = null;
|
|
360
334
|
let terminating = false;
|
|
@@ -368,10 +342,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
368
342
|
const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
|
|
369
343
|
delete passthroughEnv.TOKENMAXXING_SUPERVISED;
|
|
370
344
|
delete passthroughEnv.TOKENMAXXING_SESSION_ID;
|
|
371
|
-
|
|
372
|
-
child = p;
|
|
373
|
-
await p.exited;
|
|
374
|
-
return p.exitCode ?? (p.signalCode ? 1 : 0);
|
|
345
|
+
return runPassthrough({ real, argv, env: passthroughEnv, onSpawn: (p) => { child = p; } });
|
|
375
346
|
}
|
|
376
347
|
|
|
377
348
|
let base = stripSessionFlags(argv);
|
|
@@ -406,7 +377,14 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
406
377
|
process.on("SIGINT", () => {});
|
|
407
378
|
process.on("SIGHUP", () => {});
|
|
408
379
|
|
|
409
|
-
const
|
|
380
|
+
const effective = analyzeArgs(base);
|
|
381
|
+
const relay = effective.streamInput ? new StdinRelay() : null;
|
|
382
|
+
const stream = effective.streamOutput;
|
|
383
|
+
let noticeSid = sid;
|
|
384
|
+
const say: Say = (terminal, text) => {
|
|
385
|
+
if (stream) process.stdout.write(systemLine(noticeSid, text));
|
|
386
|
+
else process.stderr.write(terminal);
|
|
387
|
+
};
|
|
410
388
|
let firstLine: string | null = null;
|
|
411
389
|
let respawns = 0;
|
|
412
390
|
let overriddenUntil = 0;
|
|
@@ -432,66 +410,63 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
432
410
|
});
|
|
433
411
|
if (relay !== null) relay.attach(spawned.stdin!, firstLine);
|
|
434
412
|
firstLine = null;
|
|
435
|
-
if (picked)
|
|
413
|
+
if (picked) {
|
|
414
|
+
await recordPresenceOrStop({
|
|
415
|
+
child: spawned,
|
|
416
|
+
dir: paths.presenceDir,
|
|
417
|
+
id: sid,
|
|
418
|
+
accountId: picked.id,
|
|
419
|
+
event: "supervisor.presence_failed",
|
|
420
|
+
message: "could not write the session presence file - refusing to run a session whose seat placement cannot see",
|
|
421
|
+
savedTermios,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
436
424
|
return { child: spawned, seat: picked };
|
|
437
425
|
});
|
|
438
426
|
|
|
439
427
|
child = proc;
|
|
440
|
-
let done = false;
|
|
441
428
|
let seatCheckAt = 0;
|
|
442
|
-
|
|
443
|
-
|
|
429
|
+
await raceMarkerOrExit({
|
|
430
|
+
child: proc,
|
|
431
|
+
tick: async () => {
|
|
444
432
|
if (existsSync(marker) && consumableMarker(marker, gate) != null) return true;
|
|
445
433
|
if (seat && !terminating && Date.now() >= seatCheckAt) {
|
|
446
434
|
const decided = await moveExhaustedSeat(seat, sid, gate);
|
|
447
435
|
seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
448
436
|
}
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (winner === "marker") {
|
|
437
|
+
return false;
|
|
438
|
+
},
|
|
439
|
+
savedTermios,
|
|
440
|
+
onKill: () => relay?.detach(),
|
|
441
|
+
onError: () => clearPresence({ dir: paths.presenceDir, id: sid }),
|
|
442
|
+
onExited: () => {
|
|
443
|
+
child = null;
|
|
457
444
|
relay?.detach();
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
} catch (e) {
|
|
461
|
-
relay?.detach();
|
|
462
|
-
proc.kill();
|
|
463
|
-
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
464
|
-
throw e;
|
|
465
|
-
} finally {
|
|
466
|
-
await proc.exited;
|
|
467
|
-
child = null;
|
|
468
|
-
relay?.detach();
|
|
469
|
-
done = true;
|
|
470
|
-
await markerWatch.catch(() => {});
|
|
471
|
-
restoreTermios(savedTermios);
|
|
472
|
-
}
|
|
445
|
+
},
|
|
446
|
+
});
|
|
473
447
|
|
|
474
448
|
const m = !terminating && existsSync(marker) ? consumableMarker(marker, gate) : null;
|
|
475
449
|
if (m) {
|
|
476
450
|
rmSync(marker, { force: true });
|
|
477
451
|
respawns++;
|
|
452
|
+
noticeSid = m.sessionId;
|
|
478
453
|
const label = loadAccounts(claudePool).accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8);
|
|
479
454
|
const resumable = existsSync(transcriptPath(m.sessionId));
|
|
480
455
|
let compacted = false;
|
|
481
456
|
if (m.compact && seat && resumable) {
|
|
482
|
-
|
|
457
|
+
say(`\n\x1b[36m↻ tokenmaxxing: compacting the conversation on ${seat.label} before the move...\x1b[0m\n`, `tokenmaxxing: compacting the conversation on ${seat.label} before the move.`);
|
|
483
458
|
const compactEnv: Record<string, string | undefined> = { ...childEnv, TOKENMAXXING_PROBE: "1", CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) };
|
|
484
459
|
delete compactEnv.TOKENMAXXING_SUPERVISED;
|
|
485
460
|
delete compactEnv.TOKENMAXXING_SESSION_ID;
|
|
486
461
|
delete compactEnv.TOKENMAXXING_LAUNCHED_AT;
|
|
487
462
|
const outcome = await compactClaudeSession({ real, sid: m.sessionId, env: compactEnv });
|
|
488
463
|
log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat.id.slice(0, 8), ok: outcome.ok, reason: outcome.ok ? undefined : outcome.reason });
|
|
489
|
-
if (!outcome.ok)
|
|
464
|
+
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})`);
|
|
490
465
|
compacted = outcome.ok;
|
|
491
466
|
}
|
|
492
467
|
if (m.waitUntil > Date.now()) {
|
|
493
|
-
if (await countdownWait(label, m.waitUntil)) overriddenUntil = m.waitUntil;
|
|
494
|
-
} else
|
|
468
|
+
if (await countdownWait(label, m.waitUntil, { stream, say })) overriddenUntil = m.waitUntil;
|
|
469
|
+
} else say(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`, `tokenmaxxing: moving to ${label} and resuming.`);
|
|
495
470
|
wanted = m.accountId;
|
|
496
471
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
497
472
|
const prompt = resumable ? resumePrompt(compacted) : null;
|
|
@@ -501,6 +476,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
501
476
|
}
|
|
502
477
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
503
478
|
log("supervisor.exit", { sid, respawns, code: proc.exitCode, signal: proc.signalCode, terminated: terminating || undefined });
|
|
504
|
-
return
|
|
479
|
+
return exitStatus(proc);
|
|
505
480
|
}
|
|
506
481
|
}
|
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,7 +6,7 @@ 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
11
|
import { claudePool, paths, sampleDirFor, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
12
12
|
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.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, scrubCredentialEnv, windowsOf } from "./usage.ts";
|
|
20
|
+
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
|
|
21
21
|
import { c } from "../cli/render.ts";
|
|
22
22
|
|
|
23
23
|
class StoreUnusableError extends Error {
|
|
@@ -46,7 +46,7 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
|
|
|
46
46
|
const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
|
|
47
47
|
if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
|
|
48
48
|
const startedAt = Date.now();
|
|
49
|
-
const outcome = await sampleAccountUsage(account
|
|
49
|
+
const outcome = await sampleAccountUsage(account);
|
|
50
50
|
await withLock(claudePool.lockFile, () => {
|
|
51
51
|
const idx = loadAccounts(claudePool);
|
|
52
52
|
const a = idx.accounts.find((x) => x.id === account.id);
|
|
@@ -100,7 +100,7 @@ async function prepareMove(target: Account): Promise<void> {
|
|
|
100
100
|
try {
|
|
101
101
|
creds = await readStore(target.id);
|
|
102
102
|
} catch (e) {
|
|
103
|
-
throw new StoreUnusableError(`${target.label}'s store is unreadable (${
|
|
103
|
+
throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
104
104
|
}
|
|
105
105
|
if (!creds) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
106
106
|
if (isDeadCredential(creds)) {
|
|
@@ -147,11 +147,11 @@ async function removeCredentials(a: Account): Promise<void> {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
const IdentityReadySchema = z.looseObject({ oauthAccount: z.looseObject({ accountUuid: z.string().min(1) }) });
|
|
151
|
+
|
|
150
152
|
function identityReady(cjPath: string): boolean {
|
|
151
|
-
if (!existsSync(cjPath)) return false;
|
|
152
153
|
try {
|
|
153
|
-
|
|
154
|
-
return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
|
|
154
|
+
return IdentityReadySchema.safeParse(JsonTextSchema.safeParse(readFileSync(cjPath, "utf8")).data).success;
|
|
155
155
|
} catch {
|
|
156
156
|
return false;
|
|
157
157
|
}
|
|
@@ -167,8 +167,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
167
167
|
const real = resolveRealClaude();
|
|
168
168
|
|
|
169
169
|
const savedTermios = saveTermios();
|
|
170
|
-
const env
|
|
171
|
-
for (const key of CRED_ENV_OVERRIDES) delete env[key];
|
|
170
|
+
const env = scrubCredentialEnv({ ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" });
|
|
172
171
|
const p = Bun.spawn([real], {
|
|
173
172
|
stdin: "inherit",
|
|
174
173
|
stdout: "inherit",
|
|
@@ -208,7 +207,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
208
207
|
try {
|
|
209
208
|
identity = await fetchTokenIdentity(blob.claudeAiOauth.accessToken);
|
|
210
209
|
} catch (e) {
|
|
211
|
-
console.error(c.red(`could not verify which account the login belongs to (${
|
|
210
|
+
console.error(c.red(`could not verify which account the login belongs to (${errorMessage(e)}) - nothing changed.`));
|
|
212
211
|
return null;
|
|
213
212
|
}
|
|
214
213
|
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
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();
|