codex-multi-auth 1.2.1 → 1.2.4

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 (151) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +55 -21
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts.d.ts +11 -0
  6. package/dist/lib/accounts.d.ts.map +1 -1
  7. package/dist/lib/accounts.js +320 -70
  8. package/dist/lib/accounts.js.map +1 -1
  9. package/dist/lib/circuit-breaker.d.ts +4 -0
  10. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  11. package/dist/lib/circuit-breaker.js +33 -1
  12. package/dist/lib/circuit-breaker.js.map +1 -1
  13. package/dist/lib/codex-manager/commands/forecast.d.ts +2 -1
  14. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
  15. package/dist/lib/codex-manager/commands/forecast.js +92 -2
  16. package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
  17. package/dist/lib/codex-manager/commands/report.d.ts +3 -1
  18. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
  19. package/dist/lib/codex-manager/commands/report.js +102 -12
  20. package/dist/lib/codex-manager/commands/report.js.map +1 -1
  21. package/dist/lib/codex-manager.d.ts.map +1 -1
  22. package/dist/lib/codex-manager.js +50 -40
  23. package/dist/lib/codex-manager.js.map +1 -1
  24. package/dist/lib/config.d.ts.map +1 -1
  25. package/dist/lib/config.js +172 -17
  26. package/dist/lib/config.js.map +1 -1
  27. package/dist/lib/forecast.d.ts.map +1 -1
  28. package/dist/lib/forecast.js +1 -19
  29. package/dist/lib/forecast.js.map +1 -1
  30. package/dist/lib/health.d.ts +1 -0
  31. package/dist/lib/health.d.ts.map +1 -1
  32. package/dist/lib/health.js +6 -2
  33. package/dist/lib/health.js.map +1 -1
  34. package/dist/lib/parallel-probe.d.ts +1 -1
  35. package/dist/lib/parallel-probe.d.ts.map +1 -1
  36. package/dist/lib/parallel-probe.js +5 -2
  37. package/dist/lib/parallel-probe.js.map +1 -1
  38. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  39. package/dist/lib/preemptive-quota-scheduler.js +6 -3
  40. package/dist/lib/preemptive-quota-scheduler.js.map +1 -1
  41. package/dist/lib/proactive-refresh.d.ts +1 -1
  42. package/dist/lib/proactive-refresh.d.ts.map +1 -1
  43. package/dist/lib/proactive-refresh.js +14 -5
  44. package/dist/lib/proactive-refresh.js.map +1 -1
  45. package/dist/lib/refresh-guardian.d.ts +3 -0
  46. package/dist/lib/refresh-guardian.d.ts.map +1 -1
  47. package/dist/lib/refresh-guardian.js +142 -52
  48. package/dist/lib/refresh-guardian.js.map +1 -1
  49. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  50. package/dist/lib/request/fetch-helpers.js +180 -43
  51. package/dist/lib/request/fetch-helpers.js.map +1 -1
  52. package/dist/lib/rotation.d.ts +14 -11
  53. package/dist/lib/rotation.d.ts.map +1 -1
  54. package/dist/lib/rotation.js +36 -30
  55. package/dist/lib/rotation.js.map +1 -1
  56. package/dist/lib/runtime/account-check.d.ts +4 -0
  57. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  58. package/dist/lib/runtime/account-check.js +15 -10
  59. package/dist/lib/runtime/account-check.js.map +1 -1
  60. package/dist/lib/runtime/account-state.d.ts +1 -12
  61. package/dist/lib/runtime/account-state.d.ts.map +1 -1
  62. package/dist/lib/runtime/account-state.js +1 -37
  63. package/dist/lib/runtime/account-state.js.map +1 -1
  64. package/dist/lib/runtime/verify-flagged.d.ts +4 -0
  65. package/dist/lib/runtime/verify-flagged.d.ts.map +1 -1
  66. package/dist/lib/runtime/verify-flagged.js +32 -23
  67. package/dist/lib/runtime/verify-flagged.js.map +1 -1
  68. package/dist/lib/storage/flagged-load-entry.d.ts +2 -0
  69. package/dist/lib/storage/flagged-load-entry.d.ts.map +1 -1
  70. package/dist/lib/storage/flagged-load-entry.js +1 -0
  71. package/dist/lib/storage/flagged-load-entry.js.map +1 -1
  72. package/dist/lib/storage/flagged-storage-file.d.ts +4 -0
  73. package/dist/lib/storage/flagged-storage-file.d.ts.map +1 -1
  74. package/dist/lib/storage/flagged-storage-file.js +1 -1
  75. package/dist/lib/storage/flagged-storage-file.js.map +1 -1
  76. package/dist/lib/storage/flagged-storage-io.d.ts +1 -0
  77. package/dist/lib/storage/flagged-storage-io.d.ts.map +1 -1
  78. package/dist/lib/storage/flagged-storage-io.js +130 -5
  79. package/dist/lib/storage/flagged-storage-io.js.map +1 -1
  80. package/dist/lib/storage/identity.d.ts +6 -0
  81. package/dist/lib/storage/identity.d.ts.map +1 -1
  82. package/dist/lib/storage/identity.js +14 -3
  83. package/dist/lib/storage/identity.js.map +1 -1
  84. package/dist/lib/storage/path-state.d.ts +1 -0
  85. package/dist/lib/storage/path-state.d.ts.map +1 -1
  86. package/dist/lib/storage/path-state.js +3 -0
  87. package/dist/lib/storage/path-state.js.map +1 -1
  88. package/dist/lib/storage/paths.d.ts.map +1 -1
  89. package/dist/lib/storage/paths.js +3 -2
  90. package/dist/lib/storage/paths.js.map +1 -1
  91. package/dist/lib/storage.d.ts.map +1 -1
  92. package/dist/lib/storage.js +7 -0
  93. package/dist/lib/storage.js.map +1 -1
  94. package/dist/lib/unified-settings.d.ts.map +1 -1
  95. package/dist/lib/unified-settings.js +226 -28
  96. package/dist/lib/unified-settings.js.map +1 -1
  97. package/package.json +8 -3
  98. package/scripts/codex-bin-resolver.js +99 -0
  99. package/scripts/codex.js +709 -81
  100. package/dist/lib/codex-manager/best.d.ts +0 -26
  101. package/dist/lib/codex-manager/best.d.ts.map +0 -1
  102. package/dist/lib/codex-manager/best.js +0 -249
  103. package/dist/lib/codex-manager/best.js.map +0 -1
  104. package/dist/lib/codex-manager/config-explain.d.ts +0 -2
  105. package/dist/lib/codex-manager/config-explain.d.ts.map +0 -1
  106. package/dist/lib/codex-manager/config-explain.js +0 -62
  107. package/dist/lib/codex-manager/config-explain.js.map +0 -1
  108. package/dist/lib/codex-manager/config-template.d.ts +0 -2
  109. package/dist/lib/codex-manager/config-template.d.ts.map +0 -1
  110. package/dist/lib/codex-manager/config-template.js +0 -112
  111. package/dist/lib/codex-manager/config-template.js.map +0 -1
  112. package/dist/lib/codex-manager/debug-bundle.d.ts +0 -2
  113. package/dist/lib/codex-manager/debug-bundle.d.ts.map +0 -1
  114. package/dist/lib/codex-manager/debug-bundle.js +0 -128
  115. package/dist/lib/codex-manager/debug-bundle.js.map +0 -1
  116. package/dist/lib/codex-manager/doctor.d.ts +0 -20
  117. package/dist/lib/codex-manager/doctor.d.ts.map +0 -1
  118. package/dist/lib/codex-manager/doctor.js +0 -546
  119. package/dist/lib/codex-manager/doctor.js.map +0 -1
  120. package/dist/lib/codex-manager/fix.d.ts +0 -26
  121. package/dist/lib/codex-manager/fix.d.ts.map +0 -1
  122. package/dist/lib/codex-manager/fix.js +0 -366
  123. package/dist/lib/codex-manager/fix.js.map +0 -1
  124. package/dist/lib/codex-manager/forecast.d.ts +0 -26
  125. package/dist/lib/codex-manager/forecast.d.ts.map +0 -1
  126. package/dist/lib/codex-manager/forecast.js +0 -218
  127. package/dist/lib/codex-manager/forecast.js.map +0 -1
  128. package/dist/lib/codex-manager/info.d.ts +0 -11
  129. package/dist/lib/codex-manager/info.d.ts.map +0 -1
  130. package/dist/lib/codex-manager/info.js +0 -91
  131. package/dist/lib/codex-manager/info.js.map +0 -1
  132. package/dist/lib/codex-manager/login.d.ts +0 -40
  133. package/dist/lib/codex-manager/login.d.ts.map +0 -1
  134. package/dist/lib/codex-manager/login.js +0 -1041
  135. package/dist/lib/codex-manager/login.js.map +0 -1
  136. package/dist/lib/codex-manager/report.d.ts +0 -39
  137. package/dist/lib/codex-manager/report.d.ts.map +0 -1
  138. package/dist/lib/codex-manager/report.js +0 -243
  139. package/dist/lib/codex-manager/report.js.map +0 -1
  140. package/dist/lib/codex-manager/switch.d.ts +0 -31
  141. package/dist/lib/codex-manager/switch.d.ts.map +0 -1
  142. package/dist/lib/codex-manager/switch.js +0 -103
  143. package/dist/lib/codex-manager/switch.js.map +0 -1
  144. package/dist/lib/codex-manager/verify-flagged.d.ts +0 -20
  145. package/dist/lib/codex-manager/verify-flagged.d.ts.map +0 -1
  146. package/dist/lib/codex-manager/verify-flagged.js +0 -316
  147. package/dist/lib/codex-manager/verify-flagged.js.map +0 -1
  148. package/dist/lib/request/trace-mode.d.ts +0 -9
  149. package/dist/lib/request/trace-mode.d.ts.map +0 -1
  150. package/dist/lib/request/trace-mode.js +0 -14
  151. package/dist/lib/request/trace-mode.js.map +0 -1
@@ -1,20 +1,113 @@
1
1
  import { createLogger } from "./logger.js";
2
- import { loadAccounts, saveAccounts, findMatchingAccountIndex, } from "./storage.js";
2
+ import { loadAccounts, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, } from "./storage.js";
3
3
  import { MODEL_FAMILIES } from "./prompts/codex.js";
4
4
  import { getHealthTracker, getTokenTracker, selectHybridAccount, } from "./rotation.js";
5
5
  import { nowMs } from "./utils.js";
6
+ import { ERROR_MESSAGES, HTTP_STATUS } from "./constants.js";
7
+ import { CodexAuthError } from "./errors.js";
6
8
  import { loadCodexCliState, } from "./codex-cli/state.js";
7
9
  import { syncAccountStorageFromCodexCli } from "./codex-cli/sync.js";
8
10
  import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
11
+ import { getAccountIdentityKey, getRuntimeAccountIdentityKey, } from "./storage/identity.js";
12
+ import { getCircuitBreaker } from "./circuit-breaker.js";
13
+ import { getStoragePathState, runWithStoragePathState, } from "./storage/path-state.js";
9
14
  export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, resolveRuntimeRequestIdentity, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
10
15
  export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
11
16
  export { lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./codex-cli/state.js";
12
17
  import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
13
18
  import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
14
19
  const log = createLogger("accounts");
20
+ let nextRuntimeCircuitKeyId = 0;
21
+ function getAccountCircuitKey(account) {
22
+ if (!account.circuitKeyId) {
23
+ account.circuitKeyId =
24
+ getAccountIdentityKey(account) ?? `circuit:${nextRuntimeCircuitKeyId++}`;
25
+ }
26
+ return account.circuitKeyId;
27
+ }
28
+ export function getRuntimeTrackerKey(account) {
29
+ if (account._runtimeTrackerKey !== undefined) {
30
+ return account._runtimeTrackerKey;
31
+ }
32
+ const trackerKey = getRuntimeAccountIdentityKey(account) ?? account.index;
33
+ account._runtimeTrackerKey = trackerKey;
34
+ return trackerKey;
35
+ }
15
36
  function initFamilyState(defaultValue) {
16
37
  return Object.fromEntries(MODEL_FAMILIES.map((family) => [family, defaultValue]));
17
38
  }
39
+ function getAuthIdentityCandidate(auth) {
40
+ const accountId = extractAccountId(auth?.access)?.trim() || undefined;
41
+ const email = sanitizeEmail(extractAccountEmail(auth?.access));
42
+ return {
43
+ accountId,
44
+ email,
45
+ refreshToken: auth?.refresh,
46
+ };
47
+ }
48
+ function buildAccountIdentityCandidates(source, auth) {
49
+ const derived = getAuthIdentityCandidate(auth);
50
+ const candidates = [];
51
+ const seen = new Set();
52
+ const pushCandidate = (candidate) => {
53
+ const key = `${candidate.accountId ?? ""}|${candidate.email ?? ""}|${candidate.refreshToken ?? ""}`;
54
+ if (seen.has(key))
55
+ return;
56
+ seen.add(key);
57
+ candidates.push(candidate);
58
+ };
59
+ pushCandidate(source);
60
+ pushCandidate({
61
+ accountId: source.accountId ?? derived.accountId,
62
+ email: source.email ?? derived.email,
63
+ refreshToken: source.refreshToken,
64
+ index: source.index,
65
+ });
66
+ pushCandidate({
67
+ accountId: derived.accountId ?? source.accountId,
68
+ email: derived.email ?? source.email,
69
+ refreshToken: source.refreshToken,
70
+ index: source.index,
71
+ });
72
+ pushCandidate({
73
+ accountId: derived.accountId ?? source.accountId,
74
+ email: derived.email ?? source.email,
75
+ refreshToken: derived.refreshToken ?? source.refreshToken,
76
+ index: source.index,
77
+ });
78
+ return candidates;
79
+ }
80
+ function findAccountIndexByIdentity(accounts, source, auth) {
81
+ for (const candidate of buildAccountIdentityCandidates(source, auth)) {
82
+ const matchIndex = findMatchingAccountIndex(accounts, candidate, {
83
+ allowUniqueAccountIdFallbackWithoutEmail: true,
84
+ });
85
+ if (matchIndex !== undefined) {
86
+ return matchIndex;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ const RETRYABLE_AUTH_PERSISTENCE_CODES = new Set(["EAGAIN", "EBUSY", "EPERM"]);
92
+ function isRetryableAuthPersistenceError(error) {
93
+ if (!error || typeof error !== "object") {
94
+ return false;
95
+ }
96
+ const candidate = error;
97
+ const code = typeof candidate.code === "string"
98
+ ? candidate.code.toUpperCase()
99
+ : undefined;
100
+ if (code && RETRYABLE_AUTH_PERSISTENCE_CODES.has(code)) {
101
+ return true;
102
+ }
103
+ if (candidate.status === HTTP_STATUS.TOO_MANY_REQUESTS) {
104
+ return true;
105
+ }
106
+ if (candidate.cause && candidate.cause !== error) {
107
+ return isRetryableAuthPersistenceError(candidate.cause);
108
+ }
109
+ return false;
110
+ }
18
111
  export class AccountManager {
19
112
  accounts = [];
20
113
  cursorByFamily = initFamilyState(0);
@@ -23,6 +116,7 @@ export class AccountManager {
23
116
  lastToastTime = 0;
24
117
  saveDebounceTimer = null;
25
118
  pendingSave = null;
119
+ storagePathState;
26
120
  static async loadFromDisk(authFallback) {
27
121
  const stored = await loadAccounts();
28
122
  const synced = await syncAccountStorageFromCodexCli(stored);
@@ -100,6 +194,7 @@ export class AccountManager {
100
194
  }
101
195
  }
102
196
  constructor(authFallback, stored) {
197
+ this.storagePathState = { ...getStoragePathState() };
103
198
  const fallbackAccountId = extractAccountId(authFallback?.access)?.trim() || undefined;
104
199
  const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
105
200
  if (stored && stored.accounts.length > 0) {
@@ -227,10 +322,16 @@ export class AccountManager {
227
322
  return index;
228
323
  }
229
324
  getAccountsSnapshot() {
230
- return this.accounts.map((account) => ({
231
- ...account,
232
- rateLimitResetTimes: { ...account.rateLimitResetTimes },
233
- }));
325
+ return this.accounts.map((account) => {
326
+ const trackerKey = getRuntimeTrackerKey(account);
327
+ const circuitKeyId = getAccountCircuitKey(account);
328
+ return {
329
+ ...account,
330
+ _runtimeTrackerKey: trackerKey,
331
+ circuitKeyId,
332
+ rateLimitResetTimes: { ...account.rateLimitResetTimes },
333
+ };
334
+ });
234
335
  }
235
336
  getAccountByIndex(index) {
236
337
  if (!Number.isFinite(index))
@@ -247,7 +348,9 @@ export class AccountManager {
247
348
  if (account.enabled === false)
248
349
  return false;
249
350
  clearExpiredRateLimits(account);
250
- return !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
351
+ return (!isRateLimitedForFamily(account, family, model) &&
352
+ !this.isAccountCoolingDown(account) &&
353
+ this.isCircuitAvailable(account));
251
354
  }
252
355
  setActiveIndex(index) {
253
356
  if (!Number.isFinite(index))
@@ -314,7 +417,9 @@ export class AccountManager {
314
417
  if (account.enabled === false)
315
418
  continue;
316
419
  clearExpiredRateLimits(account);
317
- if (isRateLimitedForFamily(account, family, model) || this.isAccountCoolingDown(account)) {
420
+ if (isRateLimitedForFamily(account, family, model) ||
421
+ this.isAccountCoolingDown(account) ||
422
+ !this.isCircuitAvailable(account)) {
318
423
  continue;
319
424
  }
320
425
  this.cursorByFamily[family] = (idx + 1) % count;
@@ -337,7 +442,9 @@ export class AccountManager {
337
442
  if (account.enabled === false)
338
443
  continue;
339
444
  clearExpiredRateLimits(account);
340
- if (isRateLimitedForFamily(account, family, model) || this.isAccountCoolingDown(account)) {
445
+ if (isRateLimitedForFamily(account, family, model) ||
446
+ this.isAccountCoolingDown(account) ||
447
+ !this.isCircuitAvailable(account)) {
341
448
  continue;
342
449
  }
343
450
  this.cursorByFamily[family] = (idx + 1) % count;
@@ -350,23 +457,6 @@ export class AccountManager {
350
457
  const count = this.accounts.length;
351
458
  if (count === 0)
352
459
  return null;
353
- const currentIndex = this.currentAccountIndexByFamily[family];
354
- if (currentIndex >= 0 && currentIndex < count) {
355
- const currentAccount = this.accounts[currentIndex];
356
- if (currentAccount) {
357
- if (currentAccount.enabled === false) {
358
- // Fall through to hybrid selection.
359
- }
360
- else {
361
- clearExpiredRateLimits(currentAccount);
362
- if (!isRateLimitedForFamily(currentAccount, family, model) &&
363
- !this.isAccountCoolingDown(currentAccount)) {
364
- currentAccount.lastUsed = nowMs();
365
- return currentAccount;
366
- }
367
- }
368
- }
369
- }
370
460
  const quotaKey = model ? `${family}:${model}` : family;
371
461
  const healthTracker = getHealthTracker();
372
462
  const tokenTracker = getTokenTracker();
@@ -377,9 +467,12 @@ export class AccountManager {
377
467
  if (account.enabled === false)
378
468
  return null;
379
469
  clearExpiredRateLimits(account);
380
- const isAvailable = !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
470
+ const isAvailable = !isRateLimitedForFamily(account, family, model) &&
471
+ !this.isAccountCoolingDown(account) &&
472
+ this.isCircuitAvailable(account);
381
473
  return {
382
474
  index: account.index,
475
+ trackerKey: getRuntimeTrackerKey(account),
383
476
  isAvailable,
384
477
  lastUsed: account.lastUsed,
385
478
  };
@@ -399,24 +492,53 @@ export class AccountManager {
399
492
  recordSuccess(account, family, model) {
400
493
  const quotaKey = model ? `${family}:${model}` : family;
401
494
  const healthTracker = getHealthTracker();
402
- healthTracker.recordSuccess(account.index, quotaKey);
495
+ healthTracker.recordSuccess(getRuntimeTrackerKey(account), quotaKey);
496
+ const hadCooldownMetadata = account.coolingDownUntil !== undefined || account.cooldownReason !== undefined;
497
+ const hadAuthFailures = (account.consecutiveAuthFailures ?? 0) > 0;
498
+ const isCoolingDown = this.isAccountCoolingDown(account);
499
+ let healed = false;
500
+ if (!isCoolingDown && hadCooldownMetadata) {
501
+ this.clearAccountCooldown(account);
502
+ healed = true;
503
+ }
504
+ if (!isCoolingDown && hadAuthFailures) {
505
+ this.clearAuthFailures(account);
506
+ healed = true;
507
+ }
508
+ if (healed) {
509
+ this.saveToDiskDebounced();
510
+ }
511
+ getCircuitBreaker(getAccountCircuitKey(account)).recordSuccess();
403
512
  }
404
513
  recordRateLimit(account, family, model) {
405
514
  const quotaKey = model ? `${family}:${model}` : family;
406
515
  const healthTracker = getHealthTracker();
407
516
  const tokenTracker = getTokenTracker();
408
- healthTracker.recordRateLimit(account.index, quotaKey);
409
- tokenTracker.drain(account.index, quotaKey);
517
+ const trackerKey = getRuntimeTrackerKey(account);
518
+ healthTracker.recordRateLimit(trackerKey, quotaKey);
519
+ tokenTracker.drain(trackerKey, quotaKey);
410
520
  }
411
521
  recordFailure(account, family, model) {
412
522
  const quotaKey = model ? `${family}:${model}` : family;
413
523
  const healthTracker = getHealthTracker();
414
- healthTracker.recordFailure(account.index, quotaKey);
524
+ healthTracker.recordFailure(getRuntimeTrackerKey(account), quotaKey);
525
+ getCircuitBreaker(getAccountCircuitKey(account)).recordFailure();
415
526
  }
416
527
  consumeToken(account, family, model) {
417
528
  const quotaKey = model ? `${family}:${model}` : family;
418
529
  const tokenTracker = getTokenTracker();
419
- return tokenTracker.tryConsume(account.index, quotaKey);
530
+ const trackerKey = getRuntimeTrackerKey(account);
531
+ if (!tokenTracker.tryConsume(trackerKey, quotaKey)) {
532
+ return false;
533
+ }
534
+ try {
535
+ getCircuitBreaker(getAccountCircuitKey(account)).canExecute();
536
+ return true;
537
+ }
538
+ catch {
539
+ tokenTracker.refundToken(trackerKey, quotaKey);
540
+ return false;
541
+ }
420
542
  }
421
543
  /**
422
544
  * Refund a token consumed within the refund window (30 seconds).
@@ -426,7 +548,7 @@ export class AccountManager {
426
548
  refundToken(account, family, model) {
427
549
  const quotaKey = model ? `${family}:${model}` : family;
428
550
  const tokenTracker = getTokenTracker();
429
- return tokenTracker.refundToken(account.index, quotaKey);
551
+ return tokenTracker.refundToken(getRuntimeTrackerKey(account), quotaKey);
430
552
  }
431
553
  markSwitched(account, reason, family) {
432
554
  account.lastSwitchReason = reason;
@@ -439,10 +561,15 @@ export class AccountManager {
439
561
  const retryMs = Math.max(0, Math.floor(retryAfterMs));
440
562
  const resetAt = nowMs() + retryMs;
441
563
  const baseKey = getQuotaKey(family);
442
- account.rateLimitResetTimes[baseKey] = resetAt;
443
- if (model) {
564
+ if (!model || reason === "quota" || reason === "unknown") {
565
+ const currentResetAt = account.rateLimitResetTimes[baseKey] ?? 0;
566
+ account.rateLimitResetTimes[baseKey] = Math.max(currentResetAt, resetAt);
567
+ }
568
+ if (model &&
569
+ (reason === "tokens" || reason === "concurrent" || reason === "unknown")) {
444
570
  const modelKey = getQuotaKey(family, model);
445
- account.rateLimitResetTimes[modelKey] = resetAt;
571
+ const currentResetAt = account.rateLimitResetTimes[modelKey] ?? 0;
572
+ account.rateLimitResetTimes[modelKey] = Math.max(currentResetAt, resetAt);
446
573
  }
447
574
  account.lastRateLimitReason = reason;
448
575
  }
@@ -464,6 +591,9 @@ export class AccountManager {
464
591
  delete account.coolingDownUntil;
465
592
  delete account.cooldownReason;
466
593
  }
594
+ isCircuitAvailable(account) {
595
+ return getCircuitBreaker(getAccountCircuitKey(account)).isAvailable();
596
+ }
467
597
  incrementAuthFailures(account) {
468
598
  account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
469
599
  return account.consecutiveAuthFailures;
@@ -471,6 +601,13 @@ export class AccountManager {
471
601
  clearAuthFailures(account) {
472
602
  account.consecutiveAuthFailures = 0;
473
603
  }
604
+ getAccountByIdentity(candidate, auth) {
605
+ const index = findAccountIndexByIdentity(this.accounts, candidate, auth);
606
+ if (index === undefined) {
607
+ return null;
608
+ }
609
+ return this.accounts[index] ?? null;
610
+ }
474
611
  shouldShowAccountToast(accountIndex, debounceMs = 30000) {
475
612
  const now = nowMs();
476
613
  if (accountIndex === this.lastToastAccountIndex && now - this.lastToastTime < debounceMs) {
@@ -486,7 +623,7 @@ export class AccountManager {
486
623
  account.refreshToken = auth.refresh;
487
624
  account.access = auth.access;
488
625
  account.expires = auth.expires;
489
- const tokenAccountId = extractAccountId(auth.access);
626
+ const tokenAccountId = extractAccountId(auth.access)?.trim() || undefined;
490
627
  if (tokenAccountId &&
491
628
  (shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId))) {
492
629
  account.accountId = tokenAccountId;
@@ -494,6 +631,134 @@ export class AccountManager {
494
631
  }
495
632
  account.email = sanitizeEmail(extractAccountEmail(auth.access)) ?? account.email;
496
633
  }
634
+ buildStorageSnapshot() {
635
+ const activeIndexByFamily = {};
636
+ for (const family of MODEL_FAMILIES) {
637
+ const raw = this.currentAccountIndexByFamily[family];
638
+ activeIndexByFamily[family] = clampNonNegativeInt(raw, 0);
639
+ }
640
+ const activeIndex = clampNonNegativeInt(activeIndexByFamily.codex, 0);
641
+ return {
642
+ version: 3,
643
+ accounts: this.accounts.map((account) => ({
644
+ accountId: account.accountId,
645
+ accountIdSource: account.accountIdSource,
646
+ accountLabel: account.accountLabel,
647
+ email: account.email,
648
+ refreshToken: account.refreshToken,
649
+ accessToken: account.access,
650
+ expiresAt: account.expires,
651
+ enabled: account.enabled === false ? false : undefined,
652
+ addedAt: account.addedAt,
653
+ lastUsed: account.lastUsed,
654
+ lastSwitchReason: account.lastSwitchReason,
655
+ rateLimitResetTimes: Object.keys(account.rateLimitResetTimes).length > 0 ? account.rateLimitResetTimes : undefined,
656
+ coolingDownUntil: account.coolingDownUntil,
657
+ cooldownReason: account.cooldownReason,
658
+ workspaces: account.workspaces,
659
+ currentWorkspaceIndex: account.currentWorkspaceIndex,
660
+ })),
661
+ activeIndex,
662
+ activeIndexByFamily,
663
+ };
664
+ }
665
+ async commitRefreshedAuth(source, auth) {
666
+ const nextAccountId = extractAccountId(auth.access)?.trim() || undefined;
667
+ const nextEmail = sanitizeEmail(extractAccountEmail(auth.access));
668
+ try {
669
+ return await withAccountStorageTransaction(async (_current, persist) => {
670
+ // Snapshot the live in-memory pool under the storage lock so refresh
671
+ // persistence merges against the latest account state.
672
+ const nextStorage = structuredClone(this.buildStorageSnapshot());
673
+ const storageIndex = findAccountIndexByIdentity(nextStorage.accounts, source, auth);
674
+ if (storageIndex === undefined) {
675
+ log.warn("Unable to resolve refreshed account for persistence", {
676
+ sourceIndex: source.index,
677
+ });
678
+ return null;
679
+ }
680
+ const storedAccount = nextStorage.accounts[storageIndex];
681
+ if (!storedAccount) {
682
+ return null;
683
+ }
684
+ storedAccount.refreshToken = auth.refresh;
685
+ storedAccount.accessToken = auth.access;
686
+ storedAccount.expiresAt = auth.expires;
687
+ if (nextAccountId &&
688
+ shouldUpdateAccountIdFromToken(storedAccount.accountIdSource, storedAccount.accountId)) {
689
+ storedAccount.accountId = nextAccountId;
690
+ storedAccount.accountIdSource = "token";
691
+ }
692
+ if (nextEmail) {
693
+ storedAccount.email = nextEmail;
694
+ }
695
+ storedAccount.enabled = undefined;
696
+ delete storedAccount.coolingDownUntil;
697
+ delete storedAccount.cooldownReason;
698
+ const liveAccount = this.getAccountByIdentity(source, auth);
699
+ if (liveAccount) {
700
+ const previousLiveAccountState = {
701
+ access: liveAccount.access,
702
+ refreshToken: liveAccount.refreshToken,
703
+ expires: liveAccount.expires,
704
+ accountId: liveAccount.accountId,
705
+ accountIdSource: liveAccount.accountIdSource,
706
+ email: liveAccount.email,
707
+ enabled: liveAccount.enabled,
708
+ coolingDownUntil: liveAccount.coolingDownUntil,
709
+ cooldownReason: liveAccount.cooldownReason,
710
+ consecutiveAuthFailures: liveAccount.consecutiveAuthFailures,
711
+ };
712
+ this.updateFromAuth(liveAccount, auth);
713
+ liveAccount.enabled = true;
714
+ this.clearAccountCooldown(liveAccount);
715
+ this.clearAuthFailures(liveAccount);
716
+ try {
717
+ await persist(nextStorage);
718
+ }
719
+ catch (error) {
720
+ liveAccount.access = previousLiveAccountState.access;
721
+ liveAccount.refreshToken = previousLiveAccountState.refreshToken;
722
+ liveAccount.expires = previousLiveAccountState.expires;
723
+ liveAccount.accountId = previousLiveAccountState.accountId;
724
+ liveAccount.accountIdSource =
725
+ previousLiveAccountState.accountIdSource;
726
+ liveAccount.email = previousLiveAccountState.email;
727
+ liveAccount.enabled = previousLiveAccountState.enabled;
728
+ liveAccount.consecutiveAuthFailures =
729
+ previousLiveAccountState.consecutiveAuthFailures;
730
+ if (previousLiveAccountState.coolingDownUntil === undefined) {
731
+ delete liveAccount.coolingDownUntil;
732
+ }
733
+ else {
734
+ liveAccount.coolingDownUntil =
735
+ previousLiveAccountState.coolingDownUntil;
736
+ }
737
+ if (previousLiveAccountState.cooldownReason === undefined) {
738
+ delete liveAccount.cooldownReason;
739
+ }
740
+ else {
741
+ liveAccount.cooldownReason =
742
+ previousLiveAccountState.cooldownReason;
743
+ }
744
+ throw error;
745
+ }
746
+ return liveAccount;
747
+ }
748
+ await persist(nextStorage);
749
+ log.warn("Unable to resolve refreshed live account after persistence", {
750
+ sourceIndex: source.index,
751
+ });
752
+ return null;
753
+ });
754
+ }
755
+ catch (error) {
756
+ throw new CodexAuthError(ERROR_MESSAGES.TOKEN_REFRESH_FAILED, {
757
+ retryable: isRetryableAuthPersistenceError(error),
758
+ cause: error,
759
+ });
760
+ }
761
+ }
497
762
  toAuthDetails(account) {
498
763
  return {
499
764
  type: "oauth",
@@ -510,7 +775,9 @@ export class AccountManager {
510
775
  const enabledAccounts = this.accounts.filter((account) => account.enabled !== false);
511
776
  const available = enabledAccounts.filter((account) => {
512
777
  clearExpiredRateLimits(account);
513
- return !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
778
+ return (!isRateLimitedForFamily(account, family, model) &&
779
+ !this.isAccountCoolingDown(account) &&
780
+ this.isCircuitAvailable(account));
514
781
  });
515
782
  if (available.length > 0)
516
783
  return 0;
@@ -520,18 +787,26 @@ export class AccountManager {
520
787
  const baseKey = getQuotaKey(family);
521
788
  const modelKey = model ? getQuotaKey(family, model) : null;
522
789
  for (const account of enabledAccounts) {
790
+ const perAccountWaitTimes = [];
523
791
  const baseResetAt = account.rateLimitResetTimes[baseKey];
524
792
  if (typeof baseResetAt === "number") {
525
- waitTimes.push(Math.max(0, baseResetAt - now));
793
+ perAccountWaitTimes.push(Math.max(0, baseResetAt - now));
526
794
  }
527
795
  if (modelKey) {
528
796
  const modelResetAt = account.rateLimitResetTimes[modelKey];
529
797
  if (typeof modelResetAt === "number") {
530
- waitTimes.push(Math.max(0, modelResetAt - now));
798
+ perAccountWaitTimes.push(Math.max(0, modelResetAt - now));
531
799
  }
532
800
  }
533
801
  if (typeof account.coolingDownUntil === "number") {
534
- waitTimes.push(Math.max(0, account.coolingDownUntil - now));
802
+ perAccountWaitTimes.push(Math.max(0, account.coolingDownUntil - now));
803
+ }
804
+ const breakerWait = getCircuitBreaker(getAccountCircuitKey(account)).getTimeUntilAvailable();
805
+ if (breakerWait > 0) {
806
+ perAccountWaitTimes.push(breakerWait);
807
+ }
808
+ if (perAccountWaitTimes.length > 0) {
809
+ waitTimes.push(Math.max(...perAccountWaitTimes));
535
810
  }
536
811
  }
537
812
  return waitTimes.length > 0 ? Math.min(...waitTimes) : 0;
@@ -596,36 +871,11 @@ export class AccountManager {
596
871
  return account;
597
872
  }
598
873
  async saveToDisk() {
599
- const activeIndexByFamily = {};
600
- for (const family of MODEL_FAMILIES) {
601
- const raw = this.currentAccountIndexByFamily[family];
602
- activeIndexByFamily[family] = clampNonNegativeInt(raw, 0);
603
- }
604
- const activeIndex = clampNonNegativeInt(activeIndexByFamily.codex, 0);
605
- const storage = {
606
- version: 3,
607
- accounts: this.accounts.map((account) => ({
608
- accountId: account.accountId,
609
- accountIdSource: account.accountIdSource,
610
- accountLabel: account.accountLabel,
611
- email: account.email,
612
- refreshToken: account.refreshToken,
613
- accessToken: account.access,
614
- expiresAt: account.expires,
615
- enabled: account.enabled === false ? false : undefined,
616
- addedAt: account.addedAt,
617
- lastUsed: account.lastUsed,
618
- lastSwitchReason: account.lastSwitchReason,
619
- rateLimitResetTimes: Object.keys(account.rateLimitResetTimes).length > 0 ? account.rateLimitResetTimes : undefined,
620
- coolingDownUntil: account.coolingDownUntil,
621
- cooldownReason: account.cooldownReason,
622
- workspaces: account.workspaces,
623
- currentWorkspaceIndex: account.currentWorkspaceIndex,
624
- })),
625
- activeIndex,
626
- activeIndexByFamily,
627
- };
628
- await saveAccounts(storage);
874
+ await runWithStoragePathState(this.storagePathState, async () => {
875
+ await withAccountStorageTransaction(async (_current, persist) => {
876
+ await persist(this.buildStorageSnapshot());
877
+ });
878
+ });
629
879
  }
630
880
  saveToDiskDebounced(delayMs = 500) {
631
881
  if (this.saveDebounceTimer) {