codex-multi-auth 1.2.1 → 1.2.2

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