tokenmaxxing 1.39.0 → 1.40.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/src/cli/doctor.ts CHANGED
@@ -2,20 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { verifyRealClaude } from "../lib/claudebin.ts";
3
3
  import { checkSettings, installedBin } from "../lib/settings.ts";
4
4
  import { checkTimerHealthy, findClaudeShadowers, isBinDirAhead, shellRcPath, timerActivationHint } from "../lib/install.ts";
5
- import { claudePool, credItemFor, paths } from "../lib/paths.ts";
5
+ import { claudePool, paths } from "../lib/paths.ts";
6
6
  import { loadAccounts, loadConfig } from "../lib/state.ts";
7
- import { readItem, liveTarget, parkedTarget } from "../lib/credstore.ts";
8
- import { isAccessTokenExpiring, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
9
- import { CredentialBlobSchema, type TokenIdentity } from "../lib/types.ts";
7
+ import { readStore } from "../lib/credstore.ts";
8
+ import { isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
10
9
  import { SETUP_TOKEN_STALE_MS, loadSetupTokens } from "../lib/setuptokens.ts";
11
10
  import { c, fmtAgo } from "./render.ts";
12
11
 
13
- async function blobIdentity(raw: string): Promise<TokenIdentity | null> {
14
- const creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
15
- if (isAccessTokenExpiring(creds)) return null;
16
- return fetchTokenIdentity(creds.accessToken);
17
- }
18
-
19
12
  export async function cmdDoctor(): Promise<number> {
20
13
  let failed = 0;
21
14
  const check = (cond: boolean, label: string, hint?: string) => {
@@ -39,32 +32,26 @@ export async function cmdDoctor(): Promise<number> {
39
32
 
40
33
  const idx = loadAccounts(claudePool);
41
34
  check(idx.accounts.length > 0, "at least one account in the pool", "run `tokenmaxxing init`");
42
- check(!!idx.activeId, "an active account is set");
43
35
 
44
- const live = await readItem(liveTarget());
45
- check(!!live, "live credential readable");
46
-
47
- const active = idx.accounts.find((a) => a.id === idx.activeId);
48
- if (live && active) {
36
+ for (const a of idx.accounts) {
37
+ let creds;
49
38
  try {
50
- const identity = await blobIdentity(live);
51
- if (identity) check(identity.accountUuid === active.id, `live credential identity matches active (${active.label})`, `token belongs to ${describeIdentity(identity)} - run \`tokenmaxxing switch\``);
52
- else note("live credential identity unverifiable (access token expired)");
39
+ creds = await readStore(a.id);
53
40
  } catch (e) {
54
- check(false, `live credential identity matches active (${active.label})`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
41
+ check(false, `store credential readable for ${a.label}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
42
+ continue;
55
43
  }
56
- }
57
-
58
- for (const a of idx.accounts) {
59
- const parked = await readItem(parkedTarget(credItemFor(a.id)));
60
- check(!!parked, `parked credential present for ${a.label}`, `run \`tokenmaxxing auth ${a.label}\``);
61
- if (parked) {
62
- try {
63
- const identity = await blobIdentity(parked);
64
- if (identity) check(identity.accountUuid === a.id, `parked credential identity matches ${a.label}`, `token belongs to ${describeIdentity(identity)} - run \`tokenmaxxing auth ${a.label}\``);
65
- else note(`${a.label} identity unverifiable (access token expired)`);
66
- } catch (e) {
67
- check(false, `parked credential identity matches ${a.label}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
44
+ check(creds != null, `store credential present for ${a.label}`, `run \`tokenmaxxing auth ${a.label}\``);
45
+ if (creds) {
46
+ if (isDeadCredential(creds)) check(false, `${a.label}'s store credential is usable`, `cleared after a failed refresh - run \`tokenmaxxing auth ${a.label}\``);
47
+ else if (isAccessTokenExpiring(creds)) note(`${a.label} identity unverifiable (access token expired)`);
48
+ else {
49
+ try {
50
+ const identity = await fetchTokenIdentity(creds.accessToken);
51
+ check(identity.accountUuid === a.id, `store credential identity matches ${a.label}`, `token belongs to ${describeIdentity(identity)} - run \`tokenmaxxing auth ${a.label}\``);
52
+ } catch (e) {
53
+ check(false, `store credential identity matches ${a.label}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
54
+ }
68
55
  }
69
56
  }
70
57
  if (a.needsReauth) check(false, `${a.label} needs re-auth`, `run \`tokenmaxxing auth ${a.label}\` to re-login`);
package/src/cli/init.ts CHANGED
@@ -25,7 +25,7 @@ export async function cmdInit(p: Provider): Promise<number> {
25
25
  p.install();
26
26
  const active = existing.accounts.find((a) => a.id === existing.activeId);
27
27
  console.log(`${c.green("✓")} re-installed (pool already has ${count({ n: existing.accounts.length, noun: "account" })} - not re-importing)`);
28
- console.log(` active: ${c.bold(active?.label ?? "unknown")}`);
28
+ if (active && p.seats === "live") console.log(` active: ${c.bold(active.label)}`);
29
29
  printUsage(p);
30
30
  return 0;
31
31
  }
@@ -37,7 +37,7 @@ export async function cmdInit(p: Provider): Promise<number> {
37
37
  await harvested.park();
38
38
  const idx = loadAccounts(p.pool);
39
39
  const imported = upsertAccount(idx, harvested, p.mergeWindows);
40
- idx.activeId = harvested.id;
40
+ idx.activeId = p.seats === "live" ? harvested.id : null;
41
41
  saveAccounts(p.pool, idx);
42
42
  return imported;
43
43
  });
package/src/cli/rm.ts CHANGED
@@ -20,20 +20,11 @@ export async function cmdRm(p: Provider, selector?: string): Promise<number> {
20
20
  emitError({ message: `${a.label} is the ACTIVE account - switch away before removing it.` });
21
21
  return 1;
22
22
  }
23
- let liveOwner: string | null;
24
- try {
25
- liveOwner = await p.liveOwner();
26
- } catch (e) {
27
- emitError({
28
- message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the profile endpoint is reachable.`,
29
- });
23
+ if (p.liveId() === a.id) {
24
+ emitError({ message: `${a.label} is the ${p.name} login this command runs under - remove it from a session on another account.` });
30
25
  return 1;
31
26
  }
32
- if (liveOwner === a.id) {
33
- emitError({ message: `${a.label}'s credential is currently LIVE (the active label is stale - a manual login drifted it); run \`tokenmaxxing switch${p.flag}\` to move off it first.` });
34
- return 1;
35
- }
36
- if (p.presentIds().has(a.id)) {
27
+ if (p.presence().has(a.id)) {
37
28
  emitError({ message: `${a.label} is running in a live ${p.name} session - close that session before removing it.` });
38
29
  return 1;
39
30
  }
@@ -2,7 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { resolveRealClaude } from "../lib/claudebin.ts";
4
4
  import { withLock } from "../lib/lock.ts";
5
- import { claudePool, credItemFor, paths } from "../lib/paths.ts";
5
+ import { claudePool, paths, shortId } from "../lib/paths.ts";
6
6
  import { loadAccounts } from "../lib/state.ts";
7
7
  import { scrubCredentialEnv } from "../lib/usage.ts";
8
8
  import { CURSOR_SECRET_VALUE_CAP_BYTES, cursorSecretValue, loadSetupTokens, saveSetupTokens, type SetupToken } from "../lib/setuptokens.ts";
@@ -102,7 +102,7 @@ export async function cmdSetupToken(args: string[]): Promise<number> {
102
102
  for (const a of missing) {
103
103
  console.log();
104
104
  console.log(`${c.bold(a.label)} - sign into this account in the browser (${a.email ?? a.label})`);
105
- const token = await mint({ real, item: credItemFor(a.id) });
105
+ const token = await mint({ real, item: shortId(a.id) });
106
106
  if (!token) return 1;
107
107
  const stored = await withLock(claudePool.lockFile, async () => {
108
108
  if (!loadAccounts(claudePool).accounts.some((x) => x.id === a.id)) return false;
package/src/cli/status.ts CHANGED
@@ -28,6 +28,7 @@ const StatusAccountSchema = z.object({
28
28
  id: z.string(),
29
29
  tier: z.string().nullable(),
30
30
  active: z.boolean(),
31
+ sessions: z.number(),
31
32
  needsReauth: z.boolean(),
32
33
  exhausted: z.boolean(),
33
34
  usage: z
@@ -77,7 +78,7 @@ function usageReport(a: Account, now: number): StatusAccount["usage"] {
77
78
  async function collect(p: Provider, cfg: Config, now: number, cached: boolean): Promise<PoolReport> {
78
79
  let idx = loadAccounts(p.pool);
79
80
  let reports = new Map<string, SampleReport>();
80
- let liveId: string | null = idx.activeId;
81
+ let liveId: string | null = p.seats === "live" ? idx.activeId : null;
81
82
  if (!cached) {
82
83
  liveId = null;
83
84
  if (idx.accounts.length > 0) {
@@ -92,7 +93,8 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
92
93
  }
93
94
 
94
95
  const bars = thresholdBars(cfg);
95
- const ctx = { now, thresholds: bars, currentId: idx.activeId, families: p.gatedFamilies(cfg) };
96
+ const present = p.presence();
97
+ const ctx = { now, thresholds: bars, currentId: idx.activeId, families: p.gatedFamilies(cfg), seats: null };
96
98
  const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
97
99
  const accounts = ordered.map((a): StatusAccount => {
98
100
  const limits = limitWindows(a);
@@ -101,7 +103,8 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
101
103
  email: a.email,
102
104
  id: a.id,
103
105
  tier: a.tier,
104
- active: liveId != null && a.id === liveId,
106
+ active: (liveId != null && a.id === liveId) || present.has(a.id),
107
+ sessions: present.get(a.id) ?? 0,
105
108
  needsReauth: a.needsReauth === true,
106
109
  exhausted: isExhausted(a, ctx),
107
110
  usage: usageReport(a, now),
@@ -190,10 +193,11 @@ function cachedNote(usageAt: number | null, now: number): Note {
190
193
  return { paint: c.dim, text: usageAt != null ? `cached ${fmtAgo(usageAt, now)}` : "never sampled" };
191
194
  }
192
195
 
193
- function headerLine(input: { active: boolean; needsReauth: boolean; exhausted: boolean; name: string; tier: string | null }): string {
196
+ function headerLine(input: { active: boolean; sessions: number; needsReauth: boolean; exhausted: boolean; name: string; tier: string | null }): string {
194
197
  const marker = input.active ? c.green("●") : c.dim("○");
195
198
  const badges: string[] = [];
196
199
  if (input.active) badges.push(c.green("active"));
200
+ if (input.sessions > 0) badges.push(c.green(count({ n: input.sessions, noun: "session" })));
197
201
  if (input.needsReauth) badges.push(c.red("needs-reauth"));
198
202
  if (input.exhausted) badges.push(c.yellow("exhausted"));
199
203
  return `${marker} ${c.bold(input.name)}${input.tier ? ` ${c.dim(input.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`;
package/src/cli/switch.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import { withLock } from "../lib/lock.ts";
2
- import type { Provider } from "../lib/provider.ts";
2
+ import { codex } from "../lib/codex.ts";
3
3
  import { loadAccounts, loadConfig } from "../lib/state.ts";
4
- import { currentWins, pickBest, pickEarliestReset, thresholdBars, weeklyExpiry, type PickCtx } from "../lib/picker.ts";
4
+ import { currentWins, pickBest, thresholdBars, weeklyExpiry, type PickCtx } from "../lib/picker.ts";
5
5
  import { findAccount } from "./rename.ts";
6
6
  import { log } from "../lib/log.ts";
7
7
  import { c, emitError, emitJson, fmtReset } from "./render.ts";
8
8
  import type { Account } from "../lib/types.ts";
9
9
 
10
- export async function cmdSwitch(p: Provider, selector?: string, json = false): Promise<number> {
10
+ const SWITCH_MARGIN = 1.2;
11
+
12
+ export async function cmdSwitch(selector?: string, json = false): Promise<number> {
13
+ const p = codex;
11
14
  const deadGrants: string[] = [];
12
15
  const withDeadGrants = (report: Record<string, unknown>) => (deadGrants.length > 0 ? { ...report, deadGrants } : report);
13
16
  const emit = (text: string, report: Record<string, unknown>): void => {
@@ -20,7 +23,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
20
23
  };
21
24
  const reauthHint = (a: Account) => `run \`tokenmaxxing auth${p.flag} ${a.label}\``;
22
25
  const deadGrantMessage = (a: Account) => `${a.label}'s refresh token is dead - ${reauthHint(a)}`;
23
- const switched = (target: Account) => `${c.green("↻")} switched to ${c.bold(target.label)}${p.switchNote}`;
26
+ const switched = (target: Account) => `${c.green("↻")} switched to ${c.bold(target.label)} (takes effect on the next codex start)`;
24
27
 
25
28
  const idx0 = loadAccounts(p.pool);
26
29
  if (idx0.accounts.length < 2) return fail(`need at least 2 accounts to switch - add one with \`tokenmaxxing add${p.flag}\``, { paint: c.yellow });
@@ -31,7 +34,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
31
34
  const idx = loadAccounts(p.pool);
32
35
  const claimed = p.liveId();
33
36
  const drifted = claimed != null && claimed !== idx.activeId;
34
- const present = p.presentIds();
37
+ const present = p.presence();
35
38
 
36
39
  const swapTo = async (target: Account, reason: string, extra: Record<string, unknown> = {}): Promise<number> => {
37
40
  try {
@@ -62,7 +65,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
62
65
  return swapTo(target, "selected");
63
66
  }
64
67
 
65
- const everyone: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: p.gatedFamilies(cfg) };
68
+ const everyone: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: p.gatedFamilies(cfg), seats: null };
66
69
  const rejected = new Set<string>();
67
70
  const candidatesOf = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (a.id === claimed || !present.has(a.id)));
68
71
  while (true) {
@@ -72,7 +75,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
72
75
  (claimed != null ? cur.accounts.find((a) => a.id === claimed) : null) ??
73
76
  cur.accounts.find((a) => a.id === cur.activeId) ??
74
77
  null;
75
- if (active != null && currentWins(active, pool, everyone, p.switchMargin)) {
78
+ if (active != null && currentWins(active, pool, everyone, SWITCH_MARGIN)) {
76
79
  if (drifted) return swapTo(active, "drift-reconciled");
77
80
  const expiry = weeklyExpiry(active, now);
78
81
  const why = Number.isFinite(expiry) ? ` (weekly ${fmtReset(expiry, now)})` : "";
@@ -107,69 +110,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
107
110
  return 0;
108
111
  }
109
112
 
110
- if (!p.waitsWhenDepleted) {
111
- const reauth = loadAccounts(p.pool).accounts.filter((a) => a.needsReauth).map((a) => a.label);
112
- return fail(`no usable ${p.name} switch target (all at their bars, unmeasured, or needing reauth)`, { paint: c.yellow, extra: { reauthNeeded: reauth } });
113
- }
114
-
115
- while (true) {
116
- const fresh = loadAccounts(p.pool);
117
- const pool = candidatesOf(fresh.accounts);
118
- const earliest = pickEarliestReset(pool, everyone);
119
- const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
120
- if (!earliest) {
121
- if (reauth.length > 0) {
122
- return fail(`no switchable account - reauth needed (run \`tokenmaxxing auth${p.flag} --all\`): ${reauth.join(", ")}`, {
123
- paint: c.yellow,
124
- extra: { reauthNeeded: reauth },
125
- });
126
- }
127
- const freshActive = fresh.accounts.find((a) => a.id === fresh.activeId) ?? null;
128
- if (drifted && freshActive) return swapTo(freshActive, "drift-reconciled");
129
- emit(c.yellow("all accounts at their limit with unknown reset times (unparsed reset clocks? see tokenmaxxing.log) - staying put"), {
130
- switched: false,
131
- account: freshActive?.label ?? null,
132
- reason: "unknown-resets",
133
- });
134
- return 0;
135
- }
136
- const reauthNote = reauth.length ? ` - re-auth needed: ${reauth.join(", ")}` : "";
137
- const availableAt = earliest.availableAt > now ? earliest.availableAt : null;
138
- if (earliest.account.id === fresh.activeId && !drifted) {
139
- const msg = availableAt == null
140
- ? `staying on ${c.bold(earliest.account.label)} - no usable switch target${reauthNote}`
141
- : `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(availableAt, now)})${reauthNote}`;
142
- emit(c.yellow(msg), {
143
- switched: false,
144
- account: earliest.account.label,
145
- reason: availableAt == null ? "no-target" : "all-at-limit",
146
- availableAt,
147
- reauthNeeded: reauth,
148
- });
149
- return 0;
150
- }
151
- try {
152
- await p.swap(earliest.account);
153
- } catch (e) {
154
- rejected.add(earliest.account.id);
155
- const kind = p.classifySwapError(e);
156
- if (kind === "dead-grant") {
157
- deadGrants.push(earliest.account.label);
158
- if (!json) console.error(c.red(deadGrantMessage(earliest.account)));
159
- continue;
160
- }
161
- if (kind === "skip") {
162
- if (!json) console.error(c.yellow(`${earliest.account.label}: ${e instanceof Error ? e.message : String(e)} - skipped for this run`));
163
- continue;
164
- }
165
- throw e;
166
- }
167
- log("switch.manual", { account: earliest.account.id.slice(0, 8), reason: "earliest-reset" });
168
- emit(switched(earliest.account), { switched: true, account: earliest.account.label, reason: "earliest-reset", availableAt, reauthNeeded: reauth });
169
- if (availableAt != null && !json) {
170
- console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(availableAt, now)})${reauthNote}`));
171
- }
172
- return 0;
173
- }
113
+ const reauth = loadAccounts(p.pool).accounts.filter((a) => a.needsReauth).map((a) => a.label);
114
+ return fail(`no usable ${p.name} switch target (all at their bars, unmeasured, or needing reauth)`, { paint: c.yellow, extra: { reauthNeeded: reauth } });
174
115
  });
175
116
  }
@@ -8,7 +8,7 @@ import { codex, codexPickCtx, reconcileSiblings } from "../lib/codex.ts";
8
8
  import { liveCodexAccountId } from "../lib/codexauth.ts";
9
9
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
10
10
  import { isExhausted } from "../lib/picker.ts";
11
- import { livingCodexPresences } from "../lib/codexpresence.ts";
11
+ import { livingPresences } from "../lib/presence.ts";
12
12
  import { loadAccounts } from "../lib/state.ts";
13
13
  import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
14
14
  import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, JsonTextSchema } from "../lib/types.ts";
@@ -37,7 +37,7 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
37
37
  log("codexstop.reconcile_unparsable", {});
38
38
  return false;
39
39
  }
40
- const presence = livingCodexPresences().find((p) => p.supervisorId === input.supervisorId) ?? null;
40
+ const presence = livingPresences(codexPaths.presenceDir).find((p) => p.id === input.supervisorId) ?? null;
41
41
  if (presence == null || presence.accountId !== parsed.data.accountId) {
42
42
  rmSync(markerPath, { force: true });
43
43
  log("codexstop.reconcile_stale", {});
@@ -5,7 +5,7 @@ import { codexPaths, codexPool, paths } from "../lib/paths.ts";
5
5
  import { withLock } from "../lib/lock.ts";
6
6
  import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
7
7
  import { resolveRealCodex } from "../lib/codexbin.ts";
8
- import { clearCodexPresence, writeCodexPresence } from "../lib/codexpresence.ts";
8
+ import { clearPresence, writePresence } from "../lib/presence.ts";
9
9
  import { liveCodexAccountId } from "../lib/codexauth.ts";
10
10
  import { saveTermios, restoreTermios } from "../lib/tty.ts";
11
11
  import { CodexRespawnMarkerSchema } from "../lib/types.ts";
@@ -106,7 +106,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
106
106
  if (spawnAccountId) {
107
107
  for (let attempt = 0; attempt < 10; attempt++) {
108
108
  try {
109
- writeCodexPresence({ supervisorId, accountId: spawnAccountId, pid: spawned.pid });
109
+ writePresence({ dir: codexPaths.presenceDir, id: supervisorId, accountId: spawnAccountId, pid: spawned.pid });
110
110
  break;
111
111
  } catch (e) {
112
112
  if (spawned.exitCode !== null || spawned.signalCode !== null) break;
@@ -160,7 +160,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
160
160
  launchArgs = payload.sessionId ? ["resume", payload.sessionId] : ["resume", "--last"];
161
161
  continue;
162
162
  }
163
- clearCodexPresence({ supervisorId });
163
+ clearPresence({ dir: codexPaths.presenceDir, id: supervisorId });
164
164
  rmSync(join(codexPaths.reconcileDir, supervisorId), { force: true });
165
165
  log("codexsupervisor.exit", { supervisorId: supervisorId.slice(0, 8), respawns, code: child.exitCode, signal: child.signalCode });
166
166
  return child.exitCode ?? (child.signalCode ? 1 : 0);
@@ -1,31 +1,30 @@
1
1
  import { z } from "zod";
2
2
  import { claude } from "../lib/claude.ts";
3
3
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
4
+ import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
4
5
  import { log } from "../lib/log.ts";
6
+ import { readStdin } from "./statusline.ts";
5
7
  import { JsonTextSchema } from "../lib/types.ts";
6
8
 
7
9
  const SessionStartStdin = z.looseObject({
8
10
  source: z.string().optional(),
9
- session_id: z.string().optional(),
11
+ session_id: z.uuid().optional().catch(undefined),
10
12
  });
11
13
 
12
- async function readStdin(): Promise<string> {
13
- const chunks: Uint8Array[] = [];
14
- for await (const c of Bun.stdin.stream()) chunks.push(c);
15
- return Buffer.concat(chunks).toString("utf8");
16
- }
17
-
18
14
  export async function runSessionStart(): Promise<number> {
19
15
  if (process.env.TOKENMAXXING_PROBE) return 0;
20
16
 
21
17
  const raw = await readStdin();
22
18
  const parsed = SessionStartStdin.safeParse(JsonTextSchema.safeParse(raw).data);
23
19
  const source = parsed.success ? parsed.data.source : undefined;
20
+ const stdinSid = parsed.success ? parsed.data.session_id : undefined;
21
+ const session = supervisedSession();
24
22
 
25
23
  try {
26
- const decision = await evaluateAndMaybeSwap(claude);
27
- if (decision.swapped && decision.account) {
28
- log("sessionstart.swapped", { source, account: decision.account.id.slice(0, 8) });
24
+ const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null);
25
+ if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
26
+ writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now() });
27
+ log("sessionstart.marker", { source, account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil });
29
28
  }
30
29
  } catch (e) {
31
30
  log("sessionstart.error", { err: e instanceof Error ? e.message : String(e) });
@@ -1,8 +1,7 @@
1
1
  import { sortBy } from "es-toolkit";
2
2
  import { z } from "zod";
3
- import { readOAuthAccount } from "../lib/claudejson.ts";
4
- import { claudePool } from "../lib/paths.ts";
5
- import { loadAccounts, loadConfig, loadLastSwapAt, writeUsage } from "../lib/state.ts";
3
+ import { claudePool, seatFromEnv } from "../lib/paths.ts";
4
+ import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
6
5
  import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
7
6
  import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
8
7
  import { worktreeName } from "../lib/worktree.ts";
@@ -18,8 +17,6 @@ import {
18
17
  type UsageWindow,
19
18
  } from "../lib/types.ts";
20
19
 
21
- const ADOPTION_GRACE_MS = 45_000;
22
-
23
20
  const RenderCtxSchema = z.object({
24
21
  accounts: AccountsIndexSchema,
25
22
  perModel: z.record(z.string(), WindowSchema),
@@ -79,9 +76,7 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
79
76
  windows.push(seg("", wins.fiveHour, wins.fiveHour.resetsAt));
80
77
  windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
81
78
  }
82
- const seatUuid =
83
- (ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.id === ctx.liveAccount) ? ctx.liveAccount : null) ??
84
- ctx.accounts.activeId;
79
+ const seatUuid = ctx.liveAccount;
85
80
  const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
86
81
  const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
87
82
  const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
@@ -124,10 +119,9 @@ export async function runStatusline(): Promise<number> {
124
119
 
125
120
  let account: string | null = null;
126
121
  try {
127
- account = readOAuthAccount()?.accountUuid ?? null;
122
+ account = seatFromEnv(loadAccounts(claudePool).accounts.map((a) => a.id));
128
123
  const windows = obj == null ? null : parseStatusLineStdin(obj);
129
- const lastSwapAt = loadLastSwapAt(claudePool);
130
- if (windows && (lastSwapAt == null || now - lastSwapAt >= ADOPTION_GRACE_MS)) {
124
+ if (windows && account != null) {
131
125
  const state: UsageState = { fiveHour: windows.fiveHour, sevenDay: windows.sevenDay, account, ts: now, model: parseStatusLineModel(obj) };
132
126
  writeUsage(state);
133
127
  }
@@ -1,16 +1,12 @@
1
- import { join } from "node:path";
2
1
  import { z } from "zod";
3
- import { paths } from "../lib/paths.ts";
4
- import { writeFileAtomic } from "../lib/atomic.ts";
5
2
  import { claude } from "../lib/claude.ts";
6
- import { readOAuthAccount } from "../lib/claudejson.ts";
7
3
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
8
- import { withLock } from "../lib/lock.ts";
9
- import { claudePool } from "../lib/paths.ts";
10
- import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
4
+ import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
5
+ import { loadConfig } from "../lib/state.ts";
11
6
  import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
12
- import { JsonTextSchema, RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
7
+ import { JsonTextSchema, type EnforcedLimit } from "../lib/types.ts";
13
8
  import { log } from "../lib/log.ts";
9
+ import { readStdin } from "./statusline.ts";
14
10
 
15
11
  const StopFailureStdin = z.looseObject({
16
12
  session_id: z.uuid().optional().catch(undefined),
@@ -20,19 +16,10 @@ const StopFailureStdin = z.looseObject({
20
16
  last_assistant_message: z.string().optional().catch(undefined),
21
17
  });
22
18
 
23
- const LaunchedAtSchema = z.coerce.number().finite().optional().catch(undefined);
24
-
25
- async function readStdin(): Promise<string> {
26
- const chunks: Uint8Array[] = [];
27
- for await (const c of Bun.stdin.stream()) chunks.push(c);
28
- return Buffer.concat(chunks).toString("utf8");
29
- }
30
-
31
19
  export async function runStopFailureHook(): Promise<number> {
32
20
  if (process.env.TOKENMAXXING_PROBE) return 0;
33
21
 
34
- const entry = await withLock(claudePool.lockFile, () => ({ swapClock: loadLastSwapAt(claudePool), account: readOAuthAccount()?.accountUuid ?? null }));
35
- const account = entry.account;
22
+ const account = claude.liveId();
36
23
  const now = Date.now();
37
24
  const raw = await readStdin();
38
25
  const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(raw).data);
@@ -40,17 +27,11 @@ export async function runStopFailureHook(): Promise<number> {
40
27
  if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
41
28
 
42
29
  const stdinSid = stdin.session_id;
43
- const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
44
- const launchedAt = LaunchedAtSchema.parse(process.env.TOKENMAXXING_LAUNCHED_AT) ?? null;
30
+ const session = supervisedSession();
45
31
  const mainLoop = stdin.agent_id === undefined;
46
- const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
32
+ const canRespawn = session != null && mainLoop;
47
33
 
48
34
  try {
49
- const lastSwapAt = loadLastSwapAt(claudePool);
50
- if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
51
- log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
52
- return 0;
53
- }
54
35
  const cfg = loadConfig();
55
36
  const row = stdin.transcript_path
56
37
  ? findEnforcedRow({ rows: readTranscriptTail(stdin.transcript_path), lastAssistantMessage: stdin.last_assistant_message, now })
@@ -63,6 +44,7 @@ export async function runStopFailureHook(): Promise<number> {
63
44
  log("stopfailure.enforced", { kind: limit.kind, family: enforced.family ?? undefined, resetsAt: limit.resetsAt, subagent: !mainLoop });
64
45
  } else {
65
46
  log("stopfailure.unclassified", {
47
+ seat: account != null,
66
48
  row: row != null,
67
49
  type: row?.quotaLimits?.rateLimitType,
68
50
  transient: row?.apiErrorIsTransient,
@@ -70,18 +52,10 @@ export async function runStopFailureHook(): Promise<number> {
70
52
  });
71
53
  }
72
54
 
73
- const decision = await evaluateAndMaybeSwap(claude, now, canPause && enforced != null, enforced);
74
- if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
75
- const marker = join(paths.respawnDir, pinnedSid);
76
- const payload = RespawnMarkerSchema.parse({
77
- account: decision.account.label,
78
- ts: Date.now(),
79
- waitUntil: decision.waitUntil ?? now,
80
- sessionId: stdinSid ?? pinnedSid,
81
- ...(launchedAt != null ? { launchedAt } : {}),
82
- });
83
- writeFileAtomic(marker, JSON.stringify(payload));
84
- log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: payload.waitUntil });
55
+ const decision = await evaluateAndMaybeSwap(claude, now, canRespawn && enforced != null, enforced);
56
+ if (enforced && session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
57
+ writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now });
58
+ log("stopfailure.marker", { session: session.sid.slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil ?? now });
85
59
  } else {
86
60
  log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
87
61
  }
@@ -1,46 +1,26 @@
1
- import { join } from "node:path";
2
1
  import { z } from "zod";
3
- import { paths } from "../lib/paths.ts";
4
- import { writeFileAtomic } from "../lib/atomic.ts";
5
2
  import { claude } from "../lib/claude.ts";
6
3
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
7
- import { JsonTextSchema, RespawnMarkerSchema } from "../lib/types.ts";
4
+ import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
5
+ import { JsonTextSchema } from "../lib/types.ts";
8
6
  import { log } from "../lib/log.ts";
7
+ import { readStdin } from "./statusline.ts";
9
8
 
10
9
  const StopStdin = z.looseObject({ session_id: z.uuid().optional().catch(undefined) });
11
10
 
12
- async function readStdin(): Promise<string> {
13
- const chunks: Uint8Array[] = [];
14
- for await (const c of Bun.stdin.stream()) chunks.push(c);
15
- return Buffer.concat(chunks).toString("utf8");
16
- }
17
-
18
11
  export async function runStopHook(): Promise<number> {
19
12
  if (process.env.TOKENMAXXING_PROBE) return 0;
20
13
 
21
14
  const raw = await readStdin();
22
15
  const parsed = StopStdin.safeParse(JsonTextSchema.safeParse(raw).data);
23
16
  const stdinSid = parsed.success ? parsed.data.session_id : undefined;
24
- const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
17
+ const session = supervisedSession();
25
18
 
26
19
  try {
27
- const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null;
28
- const decision = await evaluateAndMaybeSwap(claude, Date.now(), canPause);
29
- if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
30
- log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil });
31
- if (decision.waitUntil !== undefined && canPause && pinnedSid) {
32
- const marker = join(paths.respawnDir, pinnedSid);
33
- const launchedAt = z.coerce.number().finite().optional().catch(undefined).parse(process.env.TOKENMAXXING_LAUNCHED_AT);
34
- const payload = RespawnMarkerSchema.parse({
35
- account: decision.account.label,
36
- ts: Date.now(),
37
- waitUntil: decision.waitUntil,
38
- sessionId: stdinSid ?? pinnedSid,
39
- ...(launchedAt != null ? { launchedAt } : {}),
40
- });
41
- writeFileAtomic(marker, JSON.stringify(payload));
42
- log("stop.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8) });
43
- }
20
+ const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null);
21
+ if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
22
+ writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now() });
23
+ log(decision.waitUntil !== undefined ? "stop.wait" : "stop.move", { account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil, session: session.sid.slice(0, 8) });
44
24
  }
45
25
  } catch (e) {
46
26
  log("stop.error", { err: e instanceof Error ? e.message : String(e) });