tokenmaxxing 1.171.0 → 1.172.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/lib/codex.ts +38 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.171.0",
3
+ "version": "1.172.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/lib/codex.ts CHANGED
@@ -41,13 +41,22 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
41
41
  if (usage.planType != null) account.tier = usage.planType;
42
42
  }
43
43
 
44
- type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean; rejectedToken?: string };
44
+ type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; deadAccessToken?: string; expiring?: boolean; rejected?: boolean; rejectedToken?: string };
45
45
  type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | CodexReadFailure;
46
46
 
47
47
  const REJECTED_REFRESH_BACKOFF_MS = 60 * 60 * 1000;
48
48
 
49
49
  function afterRejection(rejected: CodexReadFailure, refreshFailure: CodexReadFailure): CodexReadFailure {
50
- return { ...refreshFailure, reason: `${rejected.reason}; refresh skipped: ${refreshFailure.reason}` };
50
+ return { ...refreshFailure, rejected: true, reason: `${rejected.reason}; refresh skipped: ${refreshFailure.reason}` };
51
+ }
52
+
53
+ function storeHolds(accountId: string, accessToken: string | undefined): boolean {
54
+ if (accessToken === undefined) return true;
55
+ try {
56
+ return readCodexStoreAuth(accountId)?.tokens.access_token === accessToken;
57
+ } catch {
58
+ return false;
59
+ }
51
60
  }
52
61
 
53
62
  async function freshCodexAuth(account: Account, now: number, refresh: boolean, holder?: string, rejectedToken?: string): Promise<{ ok: true; auth: CodexAuthJson } | CodexReadFailure> {
@@ -70,7 +79,7 @@ async function freshCodexAuth(account: Account, now: number, refresh: boolean, h
70
79
  try {
71
80
  auth = await refreshCodexAuth({ auth, now });
72
81
  } catch (e) {
73
- if (e instanceof CodexInvalidGrantError) return { ok: false, reason: e.message, deadGrant: true };
82
+ if (e instanceof CodexInvalidGrantError) return { ok: false, reason: e.message, deadGrant: true, deadAccessToken: auth.tokens.access_token };
74
83
  if (e instanceof CodexRefreshFailedError) return { ok: false, reason: e.message, deadGrant: false };
75
84
  throw e;
76
85
  }
@@ -85,22 +94,25 @@ async function readCodexUsageOnce(account: Account, now: number, refresh: boolea
85
94
  const at = Date.now();
86
95
  return { ok: true, usage: await fetchCodexUsage({ auth: fresh.auth, at }), at };
87
96
  } catch (e) {
88
- if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false, rejectedToken: e.status === 401 ? fresh.auth.tokens.access_token : undefined };
97
+ if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false, rejected: e.status === 401, rejectedToken: e.status === 401 ? fresh.auth.tokens.access_token : undefined };
89
98
  throw e;
90
99
  }
91
100
  }
92
101
 
93
102
  async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
94
103
  const outcome = await readCodexUsageOnce(account, now, refresh, holder);
95
- if (outcome.ok || outcome.rejectedToken === undefined || !refresh) return outcome;
104
+ if (outcome.ok || outcome.rejectedToken === undefined) return outcome;
96
105
  const { rejectedToken } = outcome;
97
106
  const fresh = await withLock(codexPool.lockFile, () => freshCodexAuth(account, now, true, holder, rejectedToken));
98
- return fresh.ok ? readCodexUsageOnce(account, now, false) : afterRejection(outcome, fresh);
107
+ if (!fresh.ok) return afterRejection(outcome, fresh);
108
+ const retried = await readCodexUsageOnce(account, now, false);
109
+ return retried.ok ? retried : { ...retried, rejectedToken: undefined };
99
110
  }
100
111
 
101
- export async function observeCodex(account: Account, cfg: Config, now: number, opts: { probe: boolean; refresh: boolean; holder?: string }): Promise<Observation | null> {
112
+ export async function observeCodex(account: Account, cfg: Config, now: number, opts: { probe: boolean; refresh: boolean; holder?: string; rejected?: Set<string> }): Promise<Observation | null> {
102
113
  if (opts.probe && (account.lastUsageAt == null || now - account.lastUsageAt > cfg.policy.usagePollTtlMs)) {
103
114
  const outcome = await readCodexUsage(account, now, opts.refresh, opts.holder);
115
+ if (!outcome.ok && outcome.rejected === true) opts.rejected?.add(account.id);
104
116
  await withLock(codexPool.lockFile, () => {
105
117
  const idx = loadAccounts(codexPool);
106
118
  const a = idx.accounts.find((x) => x.id === account.id);
@@ -109,7 +121,7 @@ export async function observeCodex(account: Account, cfg: Config, now: number, o
109
121
  if (a.lastUsageAt == null || outcome.at > a.lastUsageAt) {
110
122
  applyUsage(a, outcome.usage, outcome.at);
111
123
  }
112
- } else if (outcome.deadGrant) {
124
+ } else if (outcome.deadGrant && storeHolds(a.id, outcome.deadAccessToken)) {
113
125
  a.needsReauth = true;
114
126
  }
115
127
  saveAccounts(codexPool, idx);
@@ -126,23 +138,31 @@ async function samplePool(accounts: Account[], now: number): Promise<Map<string,
126
138
  applyUsage(account, outcome.usage, outcome.at);
127
139
  reports.set(account.id, { ok: true, source: "probe" });
128
140
  } else {
129
- if (outcome.deadGrant) account.needsReauth = true;
130
141
  reports.set(account.id, { ok: false, reason: outcome.reason });
131
142
  }
132
143
  };
133
144
  const expiring: { account: Account; rejected?: CodexReadFailure }[] = [];
134
145
  await Promise.all(
135
146
  accounts.map(async (account) => {
136
- const outcome = await readCodexUsage(account, now, false);
147
+ const outcome = await readCodexUsageOnce(account, now, false);
137
148
  if (!outcome.ok && (outcome.expiring === true || outcome.rejectedToken !== undefined)) expiring.push({ account, rejected: outcome.rejectedToken === undefined ? undefined : outcome });
138
149
  else record(account, outcome);
139
150
  }),
140
151
  );
141
152
  if (expiring.length > 0) {
142
- const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async ({ account, rejected }) => ({ account, rejected, fresh: await freshCodexAuth(account, now, true, undefined, rejected?.rejectedToken) }))));
153
+ const rotated = await withLock(codexPool.lockFile, async () => {
154
+ const results = await Promise.all(expiring.map(async ({ account, rejected }) => ({ account, rejected, fresh: await freshCodexAuth(account, now, true, undefined, rejected?.rejectedToken) })));
155
+ const dead = new Set(results.filter(({ fresh }) => !fresh.ok && fresh.deadGrant).map(({ account }) => account.id));
156
+ if (dead.size > 0) {
157
+ const idx = loadAccounts(codexPool);
158
+ for (const a of idx.accounts) if (dead.has(a.id)) a.needsReauth = true;
159
+ saveAccounts(codexPool, idx);
160
+ }
161
+ return results;
162
+ });
143
163
  await Promise.all(
144
164
  rotated.map(async ({ account, rejected, fresh }) =>
145
- record(account, fresh.ok ? await readCodexUsage(account, now, false) : rejected ? afterRejection(rejected, fresh) : fresh),
165
+ record(account, fresh.ok ? await readCodexUsageOnce(account, now, false) : rejected ? afterRejection(rejected, fresh) : fresh),
146
166
  ),
147
167
  );
148
168
  }
@@ -271,10 +291,11 @@ export async function borrowCodexSeat(pid: number): Promise<SeatBorrow> {
271
291
  const now = Date.now();
272
292
  const seatId = `seat-${pid}`;
273
293
  const cfg = loadConfig();
294
+ const rejected = new Set<string>();
274
295
  await Promise.all(
275
296
  loadAccounts(codexPool)
276
297
  .accounts.filter((a) => a.needsReauth !== true && codexStoreUsable(a.id))
277
- .map((a) => observeCodex(a, cfg, now, { probe: true, refresh: false })),
298
+ .map((a) => observeCodex(a, cfg, now, { probe: true, refresh: false, rejected })),
278
299
  );
279
300
  const granted = await withLock(codexPool.lockFile, (): SeatBorrow => {
280
301
  const ctx = codexPickCtx(now, null);
@@ -283,6 +304,9 @@ export async function borrowCodexSeat(pid: number): Promise<SeatBorrow> {
283
304
  const held = living.find((p) => p.id === seatId);
284
305
  const heldAccount = held ? (idx.accounts.find((x) => x.id === held.accountId) ?? null) : null;
285
306
  if (heldAccount) {
307
+ if (rejected.has(heldAccount.id)) {
308
+ return { denied: "the usage endpoint rejects the account this pid holds and its store could not be refreshed now - borrow again later" };
309
+ }
286
310
  if (heldAccount.needsReauth === true) {
287
311
  return { denied: "the account this pid holds needs reauthentication - run `tokenmaxxing auth --codex` and borrow again" };
288
312
  }
@@ -295,7 +319,7 @@ export async function borrowCodexSeat(pid: number): Promise<SeatBorrow> {
295
319
  const closed = new Set(living.filter((p) => p.id === parent || p.id.startsWith(PI_PRESENCE_PREFIX)).map((p) => p.accountId));
296
320
  const lent = countBy(living, (p) => p.accountId);
297
321
  const usable = idx.accounts.filter(
298
- (a) => !closed.has(a.id) && a.needsReauth !== true && !isExhausted(a, ctx) && codexStoreUsable(a.id)
322
+ (a) => !closed.has(a.id) && !rejected.has(a.id) && a.needsReauth !== true && !isExhausted(a, ctx) && codexStoreUsable(a.id)
299
323
  );
300
324
  const plan = usable.filter((a) => !onCredits(a, ctx));
301
325
  const open = plan.length > 0 ? plan : usable;