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.
Files changed (54) hide show
  1. package/DESIGN.md +6 -6
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/src/cli/auth.ts +6 -17
  5. package/src/cli/check.ts +2 -2
  6. package/src/cli/cloudrun.ts +12 -19
  7. package/src/cli/config.ts +2 -1
  8. package/src/cli/doctor.ts +5 -4
  9. package/src/cli/init.ts +0 -1
  10. package/src/cli/render.ts +24 -9
  11. package/src/cli/rm.ts +0 -4
  12. package/src/cli/status.ts +37 -55
  13. package/src/entries/codexstophook.ts +11 -24
  14. package/src/entries/codexsupervisor.ts +30 -72
  15. package/src/entries/statusline.ts +17 -26
  16. package/src/entries/stopfailurehook.ts +4 -5
  17. package/src/entries/stophook.ts +12 -10
  18. package/src/entries/subagentstatusline.ts +4 -10
  19. package/src/entries/supervisor.ts +71 -113
  20. package/src/lib/claude.ts +18 -19
  21. package/src/lib/claudebin.ts +3 -2
  22. package/src/lib/claudelock.ts +2 -2
  23. package/src/lib/cloud.ts +4 -8
  24. package/src/lib/codex.ts +3 -3
  25. package/src/lib/codexauth.ts +2 -11
  26. package/src/lib/codexbin.ts +2 -1
  27. package/src/lib/codexoauth.ts +3 -8
  28. package/src/lib/codexusage.ts +5 -5
  29. package/src/lib/compact.ts +5 -18
  30. package/src/lib/credstore.ts +1 -6
  31. package/src/lib/decide.ts +12 -29
  32. package/src/lib/grok.ts +38 -152
  33. package/src/lib/http.ts +1 -2
  34. package/src/lib/install.ts +20 -38
  35. package/src/lib/keychain.ts +1 -4
  36. package/src/lib/log.ts +5 -5
  37. package/src/lib/oauth.ts +6 -5
  38. package/src/lib/opencodego.ts +31 -140
  39. package/src/lib/paths.ts +6 -22
  40. package/src/lib/picker.ts +9 -20
  41. package/src/lib/presence.ts +4 -5
  42. package/src/lib/proc.ts +22 -0
  43. package/src/lib/provider.ts +1 -1
  44. package/src/lib/sample.ts +25 -24
  45. package/src/lib/sessions.ts +5 -6
  46. package/src/lib/settings.ts +1 -8
  47. package/src/lib/state.ts +6 -29
  48. package/src/lib/statusonly.ts +143 -0
  49. package/src/lib/supervise.ts +111 -0
  50. package/src/lib/types.ts +27 -36
  51. package/src/lib/update.ts +6 -7
  52. package/src/lib/usage.ts +45 -188
  53. package/src/main.ts +5 -5
  54. 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 { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, resolveRealClaude, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
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, writePresence } from "../lib/presence.ts";
13
+ import { clearPresence } from "../lib/presence.ts";
14
+ import { readLines } from "../lib/proc.ts";
14
15
  import { teeObservation } from "../lib/sample.ts";
15
- import { saveTermios, restoreTermios } from "../lib/tty.ts";
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 { RespawnMarkerSchema, type Account } from "../lib/types.ts";
19
- import { log } from "../lib/log.ts";
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
- const AnalysisSchema = z.object({
48
- manage: z.boolean(),
49
- sessionId: z.string().nullable(),
50
- resumeId: z.string().nullable(),
51
- continueLatest: z.boolean(),
52
- streamInput: z.boolean(),
53
- streamOutput: z.boolean(),
54
- });
55
- type Analysis = z.infer<typeof AnalysisSchema>;
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
- const MarkerGateSchema = z.object({
173
- launchedAt: z.number(),
174
- overriddenUntil: z.number(),
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: e instanceof Error ? e.message : String(e) });
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: cfg.policy.switchModels, seats: null });
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 until = seatBlockedUntil(seat.id, now);
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: e instanceof Error ? e.message : String(e) });
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: e instanceof Error ? e.message : String(e) }))
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 decoder = new TextDecoder();
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: e instanceof Error ? e.message : String(e) });
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: e instanceof Error ? e.message : String(e) });
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
- const depth = wrapDepth();
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(depth + 1) };
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
- const p = Bun.spawn([real, ...argv], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: passthroughEnv });
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) await recordPresence(spawned, sid, picked, savedTermios);
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
- const markerWatch = (async () => {
469
- while (!done) {
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
- await Bun.sleep(150);
476
- }
477
- return false;
478
- })();
479
- const exited = proc.exited.then(() => { done = true; return "exit" as const; });
480
- try {
481
- const winner = await Promise.race([exited, markerWatch.then((m) => (m ? "marker" : "exit"))]);
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
- proc.kill();
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 proc.exitCode ?? (proc.signalCode ? 1 : 0);
489
+ return exitStatus(proc);
532
490
  }
533
491
  }
package/src/lib/claude.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
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 { CRED_ENV_OVERRIDES, fetchUsageDirect, gatedFamilies, mergeWindows, windowsOf } from "./usage.ts";
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
- const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
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, { retries: 0 });
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 (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
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
- const oauthAccount = JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount;
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: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
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 (${e instanceof Error ? e.message : String(e)}) - nothing changed.`));
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
- return gatedFamilies(seat == null ? null : (loadUsageSnapshot(seat)?.state.model ?? null), cfg.policy.switchModels);
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,
@@ -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 claude";
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 e instanceof Error ? e.message : String(e);
100
+ return errorMessage(e);
100
101
  }
101
102
  const outText = (p.stdout?.toString() ?? "").trim();
102
103
  const err = (p.stderr?.toString() ?? "").trim();
@@ -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: e instanceof Error ? e.message : String(e) });
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 { CloudTokenSchema, CloudTokensSchema, type CloudToken } from "./setuptokens.ts";
10
- import { normalizeResetsAt, parseErrorBody, readTranscriptTail, scrubCredentialEnv } from "./usage.ts";
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
- const PickSchema = z.discriminatedUnion("ok", [
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 != null ? normalizeResetsAt(row.quotaLimits.resetsAt) : null;
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 instanceof Error ? e.message : String(e)).slice(0, 80)})`, deadGrant: false };
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 (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
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 });
@@ -84,12 +84,7 @@ function decodeJwtPayload(input: { jwt: string }): unknown {
84
84
  return JSON.parse(payload);
85
85
  }
86
86
 
87
- const CodexIdentitySchema = z.object({
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 CodexIdentitySchema.parse({
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 {
@@ -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 e instanceof Error ? e.message : String(e);
45
+ return errorMessage(e);
45
46
  }
46
47
  const outText = (p.stdout?.toString() ?? "").trim();
47
48
  const err = (p.stderr?.toString() ?? "").trim();
@@ -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
- headers: { "Content-Type": "application/json" },
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
- const message = e instanceof Error ? e.message : String(e);
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();