oc-codex-multi-auth 6.21.0 → 6.22.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 (174) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +729 -571
  3. package/assets/icon.svg +7 -7
  4. package/assets/opencode-logo-ornate-dark.svg +18 -18
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +177 -177
  7. package/config/minimal-opencode.json +14 -14
  8. package/config/opencode-legacy.json +1496 -1496
  9. package/config/opencode-modern.json +555 -555
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +291 -53
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/account-display.d.ts +46 -0
  14. package/dist/lib/account-display.d.ts.map +1 -1
  15. package/dist/lib/account-display.js +260 -0
  16. package/dist/lib/account-display.js.map +1 -1
  17. package/dist/lib/accounts.d.ts +4 -0
  18. package/dist/lib/accounts.d.ts.map +1 -1
  19. package/dist/lib/accounts.js +20 -15
  20. package/dist/lib/accounts.js.map +1 -1
  21. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  22. package/dist/lib/auth/login-runner.js +79 -2
  23. package/dist/lib/auth/login-runner.js.map +1 -1
  24. package/dist/lib/auto-update-checker.d.ts +17 -1
  25. package/dist/lib/auto-update-checker.d.ts.map +1 -1
  26. package/dist/lib/auto-update-checker.js +49 -3
  27. package/dist/lib/auto-update-checker.js.map +1 -1
  28. package/dist/lib/cli.d.ts +1 -0
  29. package/dist/lib/cli.d.ts.map +1 -1
  30. package/dist/lib/cli.js +9 -3
  31. package/dist/lib/cli.js.map +1 -1
  32. package/dist/lib/codex-usage.d.ts +4 -3
  33. package/dist/lib/codex-usage.d.ts.map +1 -1
  34. package/dist/lib/codex-usage.js +13 -11
  35. package/dist/lib/codex-usage.js.map +1 -1
  36. package/dist/lib/config.d.ts +75 -0
  37. package/dist/lib/config.d.ts.map +1 -1
  38. package/dist/lib/config.js +152 -1
  39. package/dist/lib/config.js.map +1 -1
  40. package/dist/lib/context-overflow.js +10 -10
  41. package/dist/lib/desktop-notifications.js +6 -6
  42. package/dist/lib/oauth-success.js +202 -202
  43. package/dist/lib/plan-allotment.d.ts +72 -0
  44. package/dist/lib/plan-allotment.d.ts.map +1 -0
  45. package/dist/lib/plan-allotment.js +142 -0
  46. package/dist/lib/plan-allotment.js.map +1 -0
  47. package/dist/lib/plugin-origin.d.ts +77 -0
  48. package/dist/lib/plugin-origin.d.ts.map +1 -0
  49. package/dist/lib/plugin-origin.js +301 -0
  50. package/dist/lib/plugin-origin.js.map +1 -0
  51. package/dist/lib/prompts/codex-opencode-bridge.js +67 -67
  52. package/dist/lib/prompts/codex.js +75 -75
  53. package/dist/lib/quota-display.d.ts +40 -0
  54. package/dist/lib/quota-display.d.ts.map +1 -0
  55. package/dist/lib/quota-display.js +45 -0
  56. package/dist/lib/quota-display.js.map +1 -0
  57. package/dist/lib/quota-notifications.d.ts +2 -1
  58. package/dist/lib/quota-notifications.d.ts.map +1 -1
  59. package/dist/lib/quota-notifications.js +34 -14
  60. package/dist/lib/quota-notifications.js.map +1 -1
  61. package/dist/lib/quota-overview.d.ts +253 -0
  62. package/dist/lib/quota-overview.d.ts.map +1 -0
  63. package/dist/lib/quota-overview.js +707 -0
  64. package/dist/lib/quota-overview.js.map +1 -0
  65. package/dist/lib/request/retry-budget.d.ts +26 -0
  66. package/dist/lib/request/retry-budget.d.ts.map +1 -1
  67. package/dist/lib/request/retry-budget.js +43 -0
  68. package/dist/lib/request/retry-budget.js.map +1 -1
  69. package/dist/lib/schemas.d.ts +67 -0
  70. package/dist/lib/schemas.d.ts.map +1 -1
  71. package/dist/lib/schemas.js +53 -0
  72. package/dist/lib/schemas.js.map +1 -1
  73. package/dist/lib/storage/backup.d.ts +33 -0
  74. package/dist/lib/storage/backup.d.ts.map +1 -1
  75. package/dist/lib/storage/backup.js +43 -7
  76. package/dist/lib/storage/backup.js.map +1 -1
  77. package/dist/lib/storage/credential-snapshots.d.ts +78 -0
  78. package/dist/lib/storage/credential-snapshots.d.ts.map +1 -0
  79. package/dist/lib/storage/credential-snapshots.js +302 -0
  80. package/dist/lib/storage/credential-snapshots.js.map +1 -0
  81. package/dist/lib/storage/export-import.d.ts.map +1 -1
  82. package/dist/lib/storage/export-import.js +7 -0
  83. package/dist/lib/storage/export-import.js.map +1 -1
  84. package/dist/lib/storage/flagged.d.ts.map +1 -1
  85. package/dist/lib/storage/flagged.js +23 -1
  86. package/dist/lib/storage/flagged.js.map +1 -1
  87. package/dist/lib/storage/load-save.d.ts +4 -0
  88. package/dist/lib/storage/load-save.d.ts.map +1 -1
  89. package/dist/lib/storage/load-save.js +50 -0
  90. package/dist/lib/storage/load-save.js.map +1 -1
  91. package/dist/lib/storage/paths.d.ts +1 -0
  92. package/dist/lib/storage/paths.d.ts.map +1 -1
  93. package/dist/lib/storage/paths.js +1 -1
  94. package/dist/lib/storage/paths.js.map +1 -1
  95. package/dist/lib/storage/test-home-guard.d.ts +35 -0
  96. package/dist/lib/storage/test-home-guard.d.ts.map +1 -0
  97. package/dist/lib/storage/test-home-guard.js +59 -0
  98. package/dist/lib/storage/test-home-guard.js.map +1 -0
  99. package/dist/lib/tools/codex-dashboard.d.ts.map +1 -1
  100. package/dist/lib/tools/codex-dashboard.js +3 -2
  101. package/dist/lib/tools/codex-dashboard.js.map +1 -1
  102. package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
  103. package/dist/lib/tools/codex-doctor.js +16 -0
  104. package/dist/lib/tools/codex-doctor.js.map +1 -1
  105. package/dist/lib/tools/codex-health.d.ts.map +1 -1
  106. package/dist/lib/tools/codex-health.js +10 -2
  107. package/dist/lib/tools/codex-health.js.map +1 -1
  108. package/dist/lib/tools/codex-label.d.ts.map +1 -1
  109. package/dist/lib/tools/codex-label.js +1 -0
  110. package/dist/lib/tools/codex-label.js.map +1 -1
  111. package/dist/lib/tools/codex-limits.d.ts.map +1 -1
  112. package/dist/lib/tools/codex-limits.js +13 -9
  113. package/dist/lib/tools/codex-limits.js.map +1 -1
  114. package/dist/lib/tools/codex-list.d.ts.map +1 -1
  115. package/dist/lib/tools/codex-list.js +28 -3
  116. package/dist/lib/tools/codex-list.js.map +1 -1
  117. package/dist/lib/tools/codex-note.d.ts.map +1 -1
  118. package/dist/lib/tools/codex-note.js +4 -1
  119. package/dist/lib/tools/codex-note.js.map +1 -1
  120. package/dist/lib/tools/codex-pool.d.ts.map +1 -1
  121. package/dist/lib/tools/codex-pool.js +6 -2
  122. package/dist/lib/tools/codex-pool.js.map +1 -1
  123. package/dist/lib/tools/codex-refresh.d.ts.map +1 -1
  124. package/dist/lib/tools/codex-refresh.js +4 -1
  125. package/dist/lib/tools/codex-refresh.js.map +1 -1
  126. package/dist/lib/tools/codex-remove.d.ts.map +1 -1
  127. package/dist/lib/tools/codex-remove.js +1 -0
  128. package/dist/lib/tools/codex-remove.js.map +1 -1
  129. package/dist/lib/tools/codex-reset.d.ts.map +1 -1
  130. package/dist/lib/tools/codex-reset.js +13 -7
  131. package/dist/lib/tools/codex-reset.js.map +1 -1
  132. package/dist/lib/tools/codex-status.d.ts.map +1 -1
  133. package/dist/lib/tools/codex-status.js +25 -2
  134. package/dist/lib/tools/codex-status.js.map +1 -1
  135. package/dist/lib/tools/codex-switch.d.ts.map +1 -1
  136. package/dist/lib/tools/codex-switch.js +1 -0
  137. package/dist/lib/tools/codex-switch.js.map +1 -1
  138. package/dist/lib/tools/codex-tag.d.ts.map +1 -1
  139. package/dist/lib/tools/codex-tag.js +4 -1
  140. package/dist/lib/tools/codex-tag.js.map +1 -1
  141. package/dist/lib/tools/codex-warm.d.ts.map +1 -1
  142. package/dist/lib/tools/codex-warm.js +1 -0
  143. package/dist/lib/tools/codex-warm.js.map +1 -1
  144. package/dist/lib/tools/index.d.ts +9 -0
  145. package/dist/lib/tools/index.d.ts.map +1 -1
  146. package/dist/lib/tools/index.js.map +1 -1
  147. package/dist/lib/tui-quota-cache.d.ts +35 -0
  148. package/dist/lib/tui-quota-cache.d.ts.map +1 -1
  149. package/dist/lib/tui-quota-cache.js +76 -11
  150. package/dist/lib/tui-quota-cache.js.map +1 -1
  151. package/dist/lib/tui-quota-overview.d.ts +55 -0
  152. package/dist/lib/tui-quota-overview.d.ts.map +1 -0
  153. package/dist/lib/tui-quota-overview.js +209 -0
  154. package/dist/lib/tui-quota-overview.js.map +1 -0
  155. package/dist/lib/tui-status.d.ts +55 -0
  156. package/dist/lib/tui-status.d.ts.map +1 -1
  157. package/dist/lib/tui-status.js +155 -14
  158. package/dist/lib/tui-status.js.map +1 -1
  159. package/dist/lib/ui/auth-menu.d.ts +2 -0
  160. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  161. package/dist/lib/ui/auth-menu.js +10 -6
  162. package/dist/lib/ui/auth-menu.js.map +1 -1
  163. package/dist/tui.d.ts +55 -0
  164. package/dist/tui.d.ts.map +1 -1
  165. package/dist/tui.js +511 -29
  166. package/dist/tui.js.map +1 -1
  167. package/package.json +155 -155
  168. package/scripts/audit-dev-allowlist.js +114 -114
  169. package/scripts/clean-dist.js +27 -27
  170. package/scripts/copy-oauth-success.js +47 -47
  171. package/scripts/install-oc-codex-multi-auth-core.js +2003 -1499
  172. package/scripts/install-oc-codex-multi-auth.js +37 -37
  173. package/scripts/test-all-models.sh +260 -260
  174. package/scripts/validate-model-map.sh +97 -97
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA6U/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAg4I/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAqV/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAiqJ/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -39,15 +39,17 @@ import { buildDeviceCodeInstructions, completeDeviceCodeSession, createDeviceCod
39
39
  import { applyAccountSelectionFallbacks, persistResolvedAccountSelection, persistAccountPool, resolveAndPersistAccountSelection, resolveAccountSelection, } from "./lib/auth/login-runner.js";
40
40
  import { coordinateFlaggedPersistedRefresh, coordinatePersistedRefresh, } from "./lib/storage/coordinated-refresh.js";
41
41
  import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
42
- import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getModelAccountPoolMode, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, loadPluginConfig, } from "./lib/config.js";
42
+ import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getModelAccountPoolMode, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, getQuotaDisplay, loadPluginConfig, } from "./lib/config.js";
43
43
  import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
44
44
  import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
45
45
  import { createQuotaMonitor } from "./lib/quota-notifications.js";
46
46
  import { checkAndNotify } from "./lib/auto-update-checker.js";
47
+ import { describePluginOrigin, getPluginOrigin, recordPluginOrigin } from "./lib/plugin-origin.js";
47
48
  import { handleContextOverflow } from "./lib/context-overflow.js";
48
49
  import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
49
50
  import { getModelPoolAccountKey, matchesModelPoolAccountKey, } from "./lib/accounts/pool-identity.js";
50
- import { resolveDisplayEmail } from "./lib/account-display.js";
51
+ import { formatSeatSuffix, maskIdentityValue, resolveDisplayEmail, seatIsDisclosable, } from "./lib/account-display.js";
52
+ import { extractAccountUserId } from "./lib/auth/token-utils.js";
51
53
  import { CodexAuthError } from "./lib/errors.js";
52
54
  import { getStoragePath, loadAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
53
55
  import { getWorkspaceIdentityKey } from "./lib/storage/identity.js";
@@ -185,9 +187,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
185
187
  let accountManagerPromise = null;
186
188
  let loaderMutex = null;
187
189
  let startupPrewarmTriggered = false;
190
+ let startupOriginRecorded = false;
188
191
  let startupPreflightShown = false;
189
192
  let beginnerSafeModeEnabled = false;
190
193
  const MIN_BACKOFF_MS = 100;
194
+ // An all-accounts rate-limit wait can run for days, and the local accounts
195
+ // file is its only wake-up. A quota reset granted server-side leaves that file
196
+ // untouched, so such a wait would be slept straight through. Long waits
197
+ // therefore re-probe upstream: first after a minute, doubling to a quarter
198
+ // hour, so a multi-day sleep costs a handful of usage requests rather than one
199
+ // per countdown tick.
200
+ const UPSTREAM_REPROBE_MIN_WAIT_MS = 60_000;
201
+ const UPSTREAM_REPROBE_FIRST_DELAY_MS = 60_000;
202
+ const UPSTREAM_REPROBE_MAX_DELAY_MS = 15 * 60_000;
191
203
  const runtimeMetrics = {
192
204
  startedAt: Date.now(),
193
205
  totalRequests: 0,
@@ -240,17 +252,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
240
252
  selectionExplainability: serializeSelectionExplainability(options.selectionExplainability ?? snapshot?.explainability ?? []),
241
253
  };
242
254
  };
243
- const buildJsonAccountIdentity = (index, options = {}) => ({
244
- index: index + 1,
245
- zeroBasedIndex: index,
246
- ...(options.includeSensitive
247
- ? {
248
- label: options.label ?? formatCommandAccountLabel(options.account, index),
249
- email: options.account?.email ?? null,
250
- accountId: options.account?.accountId ?? null,
251
- }
252
- : {}),
253
- });
255
+ const buildJsonAccountIdentity = (index, options = {}) => {
256
+ const includeSensitive = options.includeSensitive ?? false;
257
+ const accountUserId = options.account?.accountUserId?.trim() || undefined;
258
+ return {
259
+ index: index + 1,
260
+ zeroBasedIndex: index,
261
+ ...(includeSensitive
262
+ ? {
263
+ label: options.label ??
264
+ formatCommandAccountLabel(options.account, index, {
265
+ peerAccounts: options.peerAccounts,
266
+ }),
267
+ email: options.account?.email ?? null,
268
+ accountId: options.account?.accountId ?? null,
269
+ }
270
+ : {}),
271
+ // Members of one Business workspace share `accountId`, so the seat is
272
+ // the field a JSON consumer can tell them apart by. It rides in both
273
+ // modes - the member id masked when sensitive output is off, and the
274
+ // suffix withheld when the id is too short to excerpt without
275
+ // disclosing it - under the same field names the standalone CLI emits.
276
+ accountUserId: maskIdentityValue(accountUserId, includeSensitive) ?? null,
277
+ seatSuffix: seatIsDisclosable(accountUserId, includeSensitive)
278
+ ? (formatSeatSuffix(accountUserId, options.peerAccounts?.map((peer) => peer?.accountUserId)) ?? null)
279
+ : null,
280
+ };
281
+ };
254
282
  const appendRoutingVisibilityText = (lines, routing, options = {}) => {
255
283
  lines.push("Routing visibility:");
256
284
  lines.push(` Requested model: ${formatRoutingValue(routing.requestedModel)}`);
@@ -449,14 +477,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
449
477
  logDebug(`[${PLUGIN_NAME}] Failed to clear TUI quota cache: ${error instanceof Error ? error.message : String(error)}`);
450
478
  }
451
479
  };
452
- const recordPromptQuotaHeaders = async (response, account, accountCount) => {
480
+ const recordPromptQuotaHeaders = async (response, account, accountCount, peerAccounts) => {
453
481
  try {
454
482
  const snapshot = parseTuiQuotaSnapshotFromHeaders(response.headers, {
455
483
  fingerprint: createUsageAccountFingerprint(account),
456
484
  accountIndex: account.index + 1,
457
485
  accountCount,
458
486
  accountEmail: account.email?.trim() || undefined,
459
- accountLabel: formatAccountLabel(account, account.index),
487
+ accountLabel: formatAccountLabel(account, account.index, {
488
+ peerAccounts,
489
+ }),
460
490
  });
461
491
  if (!snapshot)
462
492
  return;
@@ -812,6 +842,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
812
842
  const email = resolveDisplayEmail(account?.email, options.maskEmail ?? false);
813
843
  const workspace = account?.accountLabel?.trim();
814
844
  const accountId = formatAccountIdForDisplay(account?.accountId);
845
+ // `omitSeat` is for a caller that renders the seat itself in a place
846
+ // a long email cannot push it out of - a table column of its own.
847
+ // Leaving it in the label too would print the seat twice.
848
+ const seat = options.omitSeat
849
+ ? undefined
850
+ : formatSeatSuffix(account?.accountUserId, options.peerAccounts?.map((peer) => peer?.accountUserId));
815
851
  const tags = Array.isArray(account?.accountTags)
816
852
  ? account.accountTags
817
853
  .filter((tag) => typeof tag === "string")
@@ -825,6 +861,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
825
861
  details.push(`workspace:${workspace}`);
826
862
  if (accountId)
827
863
  details.push(`id:${accountId}`);
864
+ if (seat)
865
+ details.push(`seat:${seat}`);
828
866
  if (tags.length > 0)
829
867
  details.push(`tags:${tags.join(",")}`);
830
868
  if (details.length === 0) {
@@ -860,7 +898,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
860
898
  const { select } = await import("./lib/ui/select.js");
861
899
  const maskEmail = resolveMaskEmail();
862
900
  const selected = await select(storage.accounts.map((account, index) => ({
863
- label: formatCommandAccountLabel(account, index, { maskEmail }),
901
+ label: formatCommandAccountLabel(account, index, {
902
+ maskEmail,
903
+ peerAccounts: storage.accounts,
904
+ }),
864
905
  value: index,
865
906
  })), {
866
907
  message: title,
@@ -879,7 +920,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
879
920
  const toBeginnerAccountSnapshots = (storage, activeIndex, now) => {
880
921
  return storage.accounts.map((account, index) => ({
881
922
  index,
882
- label: formatCommandAccountLabel(account, index),
923
+ label: formatCommandAccountLabel(account, index, {
924
+ peerAccounts: storage.accounts,
925
+ }),
883
926
  accountLabel: account.accountLabel,
884
927
  enabled: account.enabled !== false,
885
928
  isActive: index === activeIndex,
@@ -1137,6 +1180,44 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1137
1180
  });
1138
1181
  }
1139
1182
  };
1183
+ /**
1184
+ * `loadAccounts()` reports a read or parse failure the same way it reports
1185
+ * an absent file - by returning null - and a null load builds a manager
1186
+ * holding zero accounts. Installing that over a working pool makes this
1187
+ * process answer "No Codex accounts configured" while the accounts file on
1188
+ * disk is intact, which cross-process lock contention makes reachable.
1189
+ *
1190
+ * Emptying the pool for real always goes through an explicit action
1191
+ * (`codex-remove`, logout, a storage-mode switch); each installs its own
1192
+ * manager rather than arriving here, so refusing the shrink costs a genuine
1193
+ * deletion nothing.
1194
+ */
1195
+ const isUntrustworthyEmptyReload = (incumbent, reloaded) => incumbent !== null &&
1196
+ incumbent !== reloaded &&
1197
+ reloaded.getAccountCount() === 0 &&
1198
+ incumbent.getAccountCount() > 0;
1199
+ const EMPTY_RELOAD_RETRY_DELAY_MS = 2000;
1200
+ const EMPTY_RELOAD_MAX_RETRIES = 3;
1201
+ let emptyReloadRetries = 0;
1202
+ let emptyReloadRetryTimer;
1203
+ const cancelEmptyReloadRetry = () => {
1204
+ clearTimeout(emptyReloadRetryTimer);
1205
+ emptyReloadRetryTimer = undefined;
1206
+ emptyReloadRetries = 0;
1207
+ };
1208
+ const scheduleEmptyReloadRetry = (retry) => {
1209
+ if (emptyReloadRetries >= EMPTY_RELOAD_MAX_RETRIES) {
1210
+ emptyReloadRetries = 0;
1211
+ return;
1212
+ }
1213
+ emptyReloadRetries += 1;
1214
+ clearTimeout(emptyReloadRetryTimer);
1215
+ emptyReloadRetryTimer = setTimeout(() => {
1216
+ emptyReloadRetryTimer = undefined;
1217
+ void retry();
1218
+ }, EMPTY_RELOAD_RETRY_DELAY_MS);
1219
+ emptyReloadRetryTimer.unref();
1220
+ };
1140
1221
  const reloadCachedAccountManager = async () => {
1141
1222
  if (!cachedAccountManager)
1142
1223
  return;
@@ -1155,6 +1236,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1155
1236
  }
1156
1237
  try {
1157
1238
  const reloadedManager = await AccountManager.loadFromDisk();
1239
+ if (isUntrustworthyEmptyReload(previous, reloadedManager)) {
1240
+ reloadedManager.disposeShutdownHandler();
1241
+ logWarn(`[${PLUGIN_NAME}] Account reload returned no accounts while ${previous.getAccountCount()} are held; keeping the loaded pool and retrying`);
1242
+ scheduleEmptyReloadRetry(reloadCachedAccountManager);
1243
+ return;
1244
+ }
1245
+ cancelEmptyReloadRetry();
1158
1246
  cachedAccountManager = reloadedManager;
1159
1247
  accountManagerPromise = Promise.resolve(reloadedManager);
1160
1248
  // Dispose only after the replacement is installed so we never leak
@@ -1166,6 +1254,35 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1166
1254
  logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
1167
1255
  }
1168
1256
  };
1257
+ /**
1258
+ * Refills the cache after the fetch path refused an empty reload.
1259
+ * `reloadCachedAccountManager` cannot serve here - it compares against
1260
+ * the cached incumbent, which is exactly what is missing - so the
1261
+ * incumbent the refusing request kept serving is passed in instead.
1262
+ */
1263
+ const repopulateAccountManagerCache = async (incumbent) => {
1264
+ try {
1265
+ const reloaded = await AccountManager.loadFromDisk();
1266
+ if (cachedAccountManager) {
1267
+ // Another actor repopulated first; the late load is stale and
1268
+ // retires for the same reason the fetch path retires it.
1269
+ if (cachedAccountManager !== reloaded)
1270
+ reloaded.disposeShutdownHandler();
1271
+ return;
1272
+ }
1273
+ if (isUntrustworthyEmptyReload(incumbent, reloaded)) {
1274
+ reloaded.disposeShutdownHandler();
1275
+ scheduleEmptyReloadRetry(() => repopulateAccountManagerCache(incumbent));
1276
+ return;
1277
+ }
1278
+ cancelEmptyReloadRetry();
1279
+ cachedAccountManager = reloaded;
1280
+ accountManagerPromise = Promise.resolve(reloaded);
1281
+ }
1282
+ catch (error) {
1283
+ logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
1284
+ }
1285
+ };
1169
1286
  let watchedAccountsPath;
1170
1287
  let observedAccountsDigest;
1171
1288
  let accountsReloadTimer;
@@ -1185,25 +1302,43 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1185
1302
  accountsWatcherDisposed = true;
1186
1303
  unsubscribeAccountsPath?.();
1187
1304
  stopAccountsWatcher();
1305
+ cancelEmptyReloadRetry();
1188
1306
  unregisterCleanup(disposeAccountsWatcher);
1189
1307
  };
1190
- const readAccountsDigest = async (path) => {
1308
+ const readAccountsFileState = async (path) => {
1191
1309
  try {
1192
1310
  const content = await readFile(path, "utf8");
1193
- if (!AnyAccountStorageSchema.safeParse(JSON.parse(content)).success)
1311
+ const data = JSON.parse(content);
1312
+ if (!AnyAccountStorageSchema.safeParse(data).success)
1194
1313
  return;
1195
- return createHash("sha256").update(content).digest("hex");
1314
+ // Counted off the raw document rather than the parsed union so the
1315
+ // count is the same for every storage version.
1316
+ const accounts = data.accounts;
1317
+ return {
1318
+ digest: createHash("sha256").update(content).digest("hex"),
1319
+ accountCount: Array.isArray(accounts) ? accounts.length : 0,
1320
+ };
1196
1321
  }
1197
1322
  catch {
1198
1323
  return;
1199
1324
  }
1200
1325
  };
1201
1326
  const reloadForExternalAccountsChange = async (path, generation, attempt = 0, retired) => {
1202
- const digest = await readAccountsDigest(path);
1203
- if (generation !== accountsWatchGeneration || !digest || path !== getStoragePath())
1327
+ const observed = await readAccountsFileState(path);
1328
+ if (generation !== accountsWatchGeneration || !observed || path !== getStoragePath())
1204
1329
  return;
1330
+ const digest = observed.digest;
1205
1331
  if (digest === consumeLastWrittenAccountsDigest(path))
1206
1332
  return;
1333
+ const retryLater = () => {
1334
+ if (attempt < 2 && generation === accountsWatchGeneration && !accountsWatcherDisposed) {
1335
+ accountsReloadTimer = setTimeout(() => {
1336
+ accountsReloadTimer = undefined;
1337
+ void reloadForExternalAccountsChange(path, generation, attempt + 1, retired);
1338
+ }, 1500);
1339
+ accountsReloadTimer.unref();
1340
+ }
1341
+ };
1207
1342
  const previous = cachedAccountManager;
1208
1343
  try {
1209
1344
  // A null cache means an invalidation retired the incumbent; the
@@ -1224,6 +1359,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1224
1359
  reloaded.disposeShutdownHandler();
1225
1360
  return;
1226
1361
  }
1362
+ // The file this reload observed carried accounts but the load
1363
+ // produced none, so `loadAccounts()` failed to read it rather than
1364
+ // the accounts having gone away - a failure it reports as an empty
1365
+ // result, never as a throw, so the catch below cannot see it.
1366
+ // Adopting it would answer "No Codex accounts configured" against an
1367
+ // intact file; the retired incumbent still serves its accounts until
1368
+ // a retry lands a real one.
1369
+ if (observed.accountCount > 0 && reloaded.getAccountCount() === 0) {
1370
+ reloaded.disposeShutdownHandler();
1371
+ logWarn(`[${PLUGIN_NAME}] Externally changed accounts file holds ${observed.accountCount} account(s) but loaded as empty; keeping the current pool and retrying`);
1372
+ retryLater();
1373
+ return;
1374
+ }
1227
1375
  const outgoing = cachedAccountManager;
1228
1376
  if (outgoing && outgoing !== retired) {
1229
1377
  // Another actor replaced the cached manager while this reload
@@ -1240,13 +1388,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1240
1388
  }
1241
1389
  catch {
1242
1390
  logWarn("Could not reload externally updated account storage");
1243
- if (attempt < 2 && generation === accountsWatchGeneration && !accountsWatcherDisposed) {
1244
- accountsReloadTimer = setTimeout(() => {
1245
- accountsReloadTimer = undefined;
1246
- void reloadForExternalAccountsChange(path, generation, attempt + 1, retired);
1247
- }, 1500);
1248
- accountsReloadTimer.unref();
1249
- }
1391
+ retryLater();
1250
1392
  return;
1251
1393
  }
1252
1394
  logDebug("Reloaded cached account manager after external accounts file change");
@@ -1262,9 +1404,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1262
1404
  if (!path)
1263
1405
  return;
1264
1406
  const generation = accountsWatchGeneration;
1265
- const digest = await readAccountsDigest(path);
1266
- if (generation !== accountsWatchGeneration || !digest || digest === observedAccountsDigest)
1407
+ const observed = await readAccountsFileState(path);
1408
+ if (generation !== accountsWatchGeneration || !observed || observed.digest === observedAccountsDigest)
1267
1409
  return;
1410
+ const digest = observed.digest;
1268
1411
  observedAccountsDigest = digest;
1269
1412
  clearTimeout(accountsReloadTimer);
1270
1413
  accountsReloadTimer = undefined;
@@ -1293,10 +1436,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1293
1436
  });
1294
1437
  watchedAccountsPath = path;
1295
1438
  const generation = accountsWatchGeneration;
1296
- const initialDigest = await readAccountsDigest(path);
1439
+ const initial = await readAccountsFileState(path);
1297
1440
  if (generation !== accountsWatchGeneration)
1298
1441
  return;
1299
- observedAccountsDigest = initialDigest;
1442
+ observedAccountsDigest = initial?.digest;
1300
1443
  // Stat polling follows the path across the storage writer's temp-file rename.
1301
1444
  watchFile(path, { interval: 1500, persistent: false }, onAccountsStatChanged);
1302
1445
  unregisterCleanup(disposeAccountsWatcher);
@@ -1591,9 +1734,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1591
1734
  retryAllAccountsMaxRetries: Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig)),
1592
1735
  });
1593
1736
  }
1594
- const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
1595
- process.env.VITEST !== "true" &&
1596
- process.env.NODE_ENV !== "test";
1737
+ const underTestRunner = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
1738
+ const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" && !underTestRunner;
1597
1739
  if (!startupPrewarmTriggered && prewarmEnabled && getRequestTransformMode(pluginConfig) === "legacy") {
1598
1740
  startupPrewarmTriggered = true;
1599
1741
  const configuredModels = Object.keys(userConfig.models ?? {});
@@ -1605,9 +1747,24 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1605
1747
  const recoveryHook = sessionRecoveryEnabled
1606
1748
  ? createSessionRecoveryHook({ client, directory: process.cwd() }, { sessionRecovery: true, autoResume: autoResumeEnabled })
1607
1749
  : null;
1750
+ const pluginOrigin = getPluginOrigin();
1751
+ if (pluginOrigin && !startupOriginRecorded) {
1752
+ startupOriginRecorded = true;
1753
+ if (pluginOrigin.isLocalCheckout) {
1754
+ logInfo(`Running from ${describePluginOrigin(pluginOrigin)}`);
1755
+ }
1756
+ if (!underTestRunner) {
1757
+ recordPluginOrigin(pluginOrigin).catch((err) => {
1758
+ logDebug(`Failed to record plugin origin: ${err instanceof Error ? err.message : String(err)}`);
1759
+ });
1760
+ }
1761
+ }
1608
1762
  checkAndNotify(async (message, variant) => {
1609
1763
  await showToast(message, variant);
1610
- }, { autoUpdate: autoUpdateEnabled }).catch((err) => {
1764
+ }, {
1765
+ autoUpdate: autoUpdateEnabled,
1766
+ localCheckout: pluginOrigin?.isLocalCheckout ?? false,
1767
+ }).catch((err) => {
1611
1768
  logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
1612
1769
  });
1613
1770
  await runStartupPreflight();
@@ -1794,9 +1951,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1794
1951
  runtimeMetrics.promptCacheMissingRequests++;
1795
1952
  }
1796
1953
  const retryBudget = new RetryBudgetTracker(retryBudgetLimits);
1797
- const consumeRetryBudget = (bucket, reason) => {
1798
- if (retryBudget.consume(bucket)) {
1799
- runtimeMetrics.retryBudgetUsage[bucket] += 1;
1954
+ const consumeRetryBudget = (bucket, reason, waitMs) => {
1955
+ // Pass the wait so the charge scales with how long the retry
1956
+ // blocks. Metrics follow the tracker's own counter rather than
1957
+ // assuming one unit, or a free sub-second wait would report
1958
+ // budget it never spent.
1959
+ const usedBefore = retryBudget.getUsage()[bucket];
1960
+ const granted = waitMs === undefined
1961
+ ? retryBudget.consume(bucket)
1962
+ : retryBudget.consumeWait(bucket, waitMs);
1963
+ if (granted) {
1964
+ runtimeMetrics.retryBudgetUsage[bucket] +=
1965
+ retryBudget.getUsage()[bucket] - usedBefore;
1800
1966
  return true;
1801
1967
  }
1802
1968
  runtimeMetrics.retryBudgetExhaustions += 1;
@@ -1837,27 +2003,69 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1837
2003
  };
1838
2004
  abortSignal?.addEventListener("abort", onAbort, { once: true });
1839
2005
  });
1840
- const sleepWithCountdown = async (totalMs, message, intervalMs = 5000) => {
2006
+ const sleepWithCountdown = async (totalMs, message, intervalMs = 5000, probeUpstream) => {
1841
2007
  const startTime = Date.now();
1842
2008
  const endTime = startTime + totalMs;
2009
+ let probeDelayMs = UPSTREAM_REPROBE_FIRST_DELAY_MS;
2010
+ let nextProbeAt = probeUpstream && totalMs >= UPSTREAM_REPROBE_MIN_WAIT_MS
2011
+ ? startTime + probeDelayMs
2012
+ : Number.POSITIVE_INFINITY;
1843
2013
  while (Date.now() < endTime) {
1844
2014
  if (cachedAccountManager !== accountManager)
1845
2015
  return;
1846
2016
  if (abortSignal?.aborted) {
1847
2017
  throw abortError();
1848
2018
  }
2019
+ if (probeUpstream && Date.now() >= nextProbeAt) {
2020
+ if (await probeUpstream())
2021
+ return;
2022
+ if (cachedAccountManager !== accountManager)
2023
+ return;
2024
+ if (abortSignal?.aborted) {
2025
+ throw abortError();
2026
+ }
2027
+ probeDelayMs = Math.min(probeDelayMs * 2, UPSTREAM_REPROBE_MAX_DELAY_MS);
2028
+ // Measured from the end of the probe, so a slow usage
2029
+ // request cannot schedule the next one in the past and
2030
+ // collapse the countdown sleep below to zero.
2031
+ nextProbeAt = Date.now() + probeDelayMs;
2032
+ }
1849
2033
  const remaining = Math.max(0, endTime - Date.now());
1850
2034
  const waitLabel = formatWaitTime(remaining);
1851
2035
  await showToast(`${message} (${waitLabel} remaining)`, "warning", { duration: Math.min(intervalMs + 1000, toastDurationMs) });
1852
- const sleepTime = Math.min(intervalMs, remaining);
2036
+ const sleepTime = Math.min(intervalMs, remaining, nextProbeAt - Date.now());
1853
2037
  if (sleepTime > 0) {
1854
2038
  await sleep(sleepTime);
1855
2039
  }
1856
2040
  else {
1857
- break;
2041
+ continue;
1858
2042
  }
1859
2043
  }
1860
2044
  };
2045
+ /**
2046
+ * True when an all-accounts wait can stop early.
2047
+ *
2048
+ * `runNow` refreshes `/wham/usage` for every account and persists
2049
+ * whatever it finds, so a reset that never touched local disk
2050
+ * becomes visible here. Persisting a recovery also drops the cached
2051
+ * manager, which is what makes the enclosing retry loop re-resolve
2052
+ * one that no longer reports a block.
2053
+ */
2054
+ const probeUpstreamBlockLifted = async () => {
2055
+ try {
2056
+ await quotaMonitor.runNow();
2057
+ }
2058
+ catch (error) {
2059
+ logDebug(`[${PLUGIN_NAME}] Upstream quota re-probe failed: ${error instanceof Error ? error.message : String(error)}`);
2060
+ return false;
2061
+ }
2062
+ if (cachedAccountManager !== accountManager)
2063
+ return true;
2064
+ const manager = accountManager;
2065
+ if (!manager)
2066
+ return false;
2067
+ return manager.getMinWaitTimeForFamily(modelFamily, model) === 0;
2068
+ };
1861
2069
  let allRateLimitedRetries = 0;
1862
2070
  let emptyResponseRetries = 0;
1863
2071
  const attemptedUnsupportedFallbackModels = new Set();
@@ -1965,7 +2173,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1965
2173
  accountManagerPromise = null;
1966
2174
  });
1967
2175
  }
1968
- const reloaded = await accountManagerPromise;
2176
+ const loading = accountManagerPromise;
2177
+ const reloaded = await loading;
1969
2178
  if (cachedAccountManager) {
1970
2179
  if (cachedAccountManager !== reloaded) {
1971
2180
  // The cache was repopulated while this load was in
@@ -1977,6 +2186,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1977
2186
  }
1978
2187
  accountManager = cachedAccountManager;
1979
2188
  }
2189
+ else if (accountManager && isUntrustworthyEmptyReload(accountManager, reloaded)) {
2190
+ // The incumbent this request holds still has accounts, so
2191
+ // the empty result is a failed `loadAccounts()` read rather
2192
+ // than a real removal (same refusal as
2193
+ // reloadCachedAccountManager). Keep serving the incumbent
2194
+ // and drop the resolved promise so the next pass re-reads
2195
+ // disk instead of adopting this same empty manager.
2196
+ reloaded.disposeShutdownHandler();
2197
+ if (accountManagerPromise === loading)
2198
+ accountManagerPromise = null;
2199
+ logWarn(`[${PLUGIN_NAME}] Account reload returned no accounts while ${accountManager.getAccountCount()} are held; keeping the loaded pool and retrying`);
2200
+ const incumbent = accountManager;
2201
+ scheduleEmptyReloadRetry(() => repopulateAccountManagerCache(incumbent));
2202
+ }
1980
2203
  else {
1981
2204
  cachedAccountManager = reloaded;
1982
2205
  accountManager = reloaded;
@@ -2098,6 +2321,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2098
2321
  const failures = await accountManager.incrementAuthFailures(account);
2099
2322
  const accountLabel = formatAccountLabel(account, account.index, {
2100
2323
  maskEmail: maskEmailEnabled,
2324
+ peerAccounts: accountManager.getAccountsSnapshot(),
2101
2325
  });
2102
2326
  if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
2103
2327
  const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
@@ -2147,6 +2371,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2147
2371
  accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
2148
2372
  const accountLabel = formatAccountLabel(account, account.index, {
2149
2373
  maskEmail: maskEmailEnabled,
2374
+ peerAccounts: accountManager.getAccountsSnapshot(),
2150
2375
  });
2151
2376
  await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
2152
2377
  accountManager.markToastShown(account.index);
@@ -2334,7 +2559,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2334
2559
  // only the block would leave the status line reporting "0% left" for
2335
2560
  // an account the router considers healthy.
2336
2561
  const recordQuotaHeaders = () => {
2337
- void recordPromptQuotaHeaders(response, account, accountCount);
2562
+ void recordPromptQuotaHeaders(response, account, accountCount, accountManager.getAccountsSnapshot());
2338
2563
  return applyQuotaExhaustion(accountManager, response.headers, account, modelFamily, model);
2339
2564
  };
2340
2565
  if (response.ok) {
@@ -2354,6 +2579,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2354
2579
  if (workspaceDeactivated) {
2355
2580
  const accountLabel = formatAccountLabel(account, account.index, {
2356
2581
  maskEmail: maskEmailEnabled,
2582
+ peerAccounts: accountManager.getAccountsSnapshot(),
2357
2583
  });
2358
2584
  accountManager.refundToken(account, modelFamily, model);
2359
2585
  accountManager.recordFailure(account, modelFamily, model);
@@ -2567,6 +2793,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2567
2793
  if (isInvalidatedAuthTokenError(errorBody, response.status)) {
2568
2794
  const accountLabel = formatAccountLabel(account, account.index, {
2569
2795
  maskEmail: maskEmailEnabled,
2796
+ peerAccounts: accountManager.getAccountsSnapshot(),
2570
2797
  });
2571
2798
  accountManager.refundToken(account, modelFamily, model);
2572
2799
  accountManager.recordFailure(account, modelFamily, model);
@@ -2838,9 +3065,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2838
3065
  (retryAllAccountsMaxWaitMs === 0 ||
2839
3066
  waitMs <= retryAllAccountsMaxWaitMs) &&
2840
3067
  allRateLimitedRetries < retryAllAccountsMaxRetries &&
2841
- consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms`)) {
3068
+ consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms`, waitMs)) {
2842
3069
  const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
2843
- await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
3070
+ await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage, undefined, probeUpstreamBlockLifted);
2844
3071
  allRateLimitedRetries++;
2845
3072
  continue;
2846
3073
  }
@@ -2943,19 +3170,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2943
3170
  message.includes("invalid refresh") ||
2944
3171
  message.includes("token has been revoked"));
2945
3172
  };
3173
+ const authQuotaDisplay = getQuotaDisplay(authPluginConfig);
2946
3174
  const formatCodexQuotaLine = (usage) => {
2947
3175
  const parts = [];
2948
3176
  for (const window of [usage.primary, usage.secondary]) {
2949
3177
  if (!hasUsageWindow(window))
2950
3178
  continue;
2951
- parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window)}`);
3179
+ parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window, authQuotaDisplay)}`);
2952
3180
  }
2953
3181
  if (hasUsageWindow(usage.codeReview)) {
2954
- parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview)}`);
3182
+ parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview, authQuotaDisplay)}`);
2955
3183
  }
2956
3184
  for (const limit of usage.additionalLimits) {
2957
3185
  if (hasUsageWindow(limit.window)) {
2958
- parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
3186
+ parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window, authQuotaDisplay)}`);
2959
3187
  }
2960
3188
  }
2961
3189
  const planLabel = formatPlanType(usage.planType);
@@ -3127,8 +3355,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3127
3355
  }
3128
3356
  if (deepProbe) {
3129
3357
  ok += 1;
3130
- const detail = tokenAccountId
3131
- ? `${authDetail} (id:${tokenAccountId.slice(-6)})`
3358
+ // Both read from the probed token, so the pair is the seat
3359
+ // the credential actually belongs to. The workspace id
3360
+ // alone repeats across every member of a Business
3361
+ // workspace and cannot confirm which seat answered.
3362
+ const tokenSeat = formatSeatSuffix(extractAccountUserId(accessToken), workingStorage.accounts.map((peer) => peer?.accountUserId));
3363
+ const identity = [
3364
+ tokenAccountId ? `id:${tokenAccountId.slice(-6)}` : undefined,
3365
+ tokenSeat ? `seat:${tokenSeat}` : undefined,
3366
+ ].filter((part) => part !== undefined);
3367
+ const detail = identity.length > 0
3368
+ ? `${authDetail} (${identity.join(", ")})`
3132
3369
  : authDetail;
3133
3370
  console.log(`[${i + 1}/${total}] ${label}: ${detail}`);
3134
3371
  continue;
@@ -3146,7 +3383,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3146
3383
  organizationId: account.organizationId,
3147
3384
  normalizeAccountErrors: true,
3148
3385
  });
3149
- const usage = parseCodexUsagePayload(payload);
3386
+ const usage = parseCodexUsagePayload(payload, authQuotaDisplay);
3150
3387
  ok += 1;
3151
3388
  console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(usage)}`);
3152
3389
  }
@@ -3422,6 +3659,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
3422
3659
  }
3423
3660
  return {
3424
3661
  accountId: account.accountId,
3662
+ accountUserId: account.accountUserId,
3425
3663
  accountLabel: account.accountLabel,
3426
3664
  email: account.email,
3427
3665
  index,