@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { closeSync, existsSync, fstatSync, readFileSync, mkdirSync, openSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import {
5
5
  ConfigMutationLockError,
@@ -17,6 +17,14 @@ import { isValidCodexAccountId } from "./account-id";
17
17
  import type { PoolQuotaWriter } from "./quota-types";
18
18
  import { CODEX_REFRESH_FLIGHT_CEILING_MS } from "./quota-recovery-timing";
19
19
 
20
+ import {
21
+ CodexPoolRefreshCooldownError,
22
+ clearCodexPoolRefreshFailure,
23
+ codexPoolRefreshFence,
24
+ isCodexPoolRefreshCooling,
25
+ noteCodexPoolRefreshFailure,
26
+ } from "./pool-refresh-backoff";
27
+
20
28
  type LegacyCodexAccountStore = Record<string, CodexAccountCredentials>;
21
29
  type CodexAccountStore = Record<string, CodexAccountCredentialRecord>;
22
30
  type RawCodexAccountStore = Record<string, CodexAccountCredentials | CodexAccountCredentialRecord>;
@@ -260,6 +268,23 @@ export function readCodexAccountRecord(id: string): CodexAccountCredentialRecord
260
268
  return loadCodexAccountRecordStore()[id] ?? null;
261
269
  }
262
270
 
271
+ /**
272
+ * One store load, every record, for a caller that resolves MANY ids in a single synchronous pass.
273
+ *
274
+ * `readCodexAccountRecord` reloads, reparses and renormalizes the whole file per id. That is the
275
+ * right shape for one lookup and the wrong shape for a loop: the entitlement denial reader holds
276
+ * up to 64 accounts with four client versions each, so scoring one warm flagship request could
277
+ * perform up to 256 full-store reads on the request path.
278
+ *
279
+ * These are the same normalized records `readCodexAccountRecord` hands out, tombstones included,
280
+ * so the caller keeps its own `deletedAt` and `generation` checks instead of trusting a filtered
281
+ * view. That is the difference from `loadCodexAccountStore`, which drops both and cannot answer a
282
+ * question about credential generation.
283
+ */
284
+ export function loadCodexAccountRecordSnapshot(): Readonly<Record<string, CodexAccountCredentialRecord>> {
285
+ return loadCodexAccountRecordStore();
286
+ }
287
+
263
288
  const QUOTA_HISTORY_IDENTITY_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
264
289
 
265
290
  function validQuotaHistoryIdentity(value: unknown): value is string {
@@ -480,6 +505,17 @@ export class TokenRefreshError extends Error {
480
505
  }
481
506
  }
482
507
 
508
+ /**
509
+ * The stored record or its refresh-grant fingerprint is gone. Retrying cannot
510
+ * conjure a missing credential, so callers must treat this as terminal.
511
+ */
512
+ export class CodexCredentialUnavailableError extends Error {
513
+ constructor(message = "Codex account credential is unavailable; reauthenticate the account.") {
514
+ super(message);
515
+ this.name = "CodexCredentialUnavailableError";
516
+ }
517
+ }
518
+
483
519
  export class CodexCredentialGenerationConflictError extends Error {
484
520
  constructor(message = "Codex account changed during refresh") {
485
521
  super(message);
@@ -514,6 +550,30 @@ export class CodexCredentialRefreshStaleError extends Error {
514
550
  }
515
551
  }
516
552
 
553
+ /**
554
+ * Terminal means the grant itself is dead, or there is no grant to refresh.
555
+ * Token-endpoint 5xx (`unknown`) and a generation CAS loss stay transient
556
+ * because those genuinely may clear (#2887).
557
+ */
558
+ export function isTerminalCodexPoolRefreshFailure(error: unknown): boolean {
559
+ return (error instanceof TokenRefreshError && (error.reason === "revoked" || error.reason === "expired"))
560
+ || error instanceof CodexCredentialUnavailableError;
561
+ }
562
+
563
+ function isOperationalCodexPoolRefreshFailure(error: unknown): boolean {
564
+ if (error instanceof CodexPoolRefreshCooldownError) return true;
565
+ if (error instanceof CodexCredentialRefreshBusyError) return true;
566
+ if (error instanceof CodexCredentialRefreshStaleError) return true;
567
+ if (error instanceof CodexCredentialRefreshLockTimeoutError) return true;
568
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
569
+ }
570
+
571
+ function classifyCodexPoolRefreshFailureReason(error: unknown): string {
572
+ if (error instanceof TokenRefreshError) return error.reason;
573
+ if (error instanceof CodexCredentialGenerationConflictError) return "generation_conflict";
574
+ return "network";
575
+ }
576
+
517
577
  /** Credential writers share the config mutation coordinator; contention is transient, not reauth. */
518
578
  function withCredentialMutationLockSync<T>(fn: () => T): T {
519
579
  try {
@@ -612,10 +672,43 @@ function isRefreshLockStale(path: string): boolean {
612
672
  const parsed = JSON.parse(readFileSync(path, "utf-8")) as { acquiredAt?: unknown };
613
673
  return typeof parsed.acquiredAt !== "number" || Date.now() - parsed.acquiredAt > REFRESH_LOCK_STALE_MS;
614
674
  } catch {
615
- return true;
675
+ // The owner creates the file and writes its metadata in two steps, so a live lock is
676
+ // briefly unreadable. Age the file itself instead of calling that window stale, which
677
+ // let a waiter delete a lock whose owner was still inside its critical section.
678
+ try {
679
+ return Date.now() - statSync(path).mtimeMs > REFRESH_LOCK_STALE_MS;
680
+ } catch {
681
+ return false;
682
+ }
616
683
  }
617
684
  }
618
685
 
686
+ function releaseCodexRefreshFileLock(path: string, fd: number): void {
687
+ let owned: { dev: bigint; ino: bigint } | null = null;
688
+ try {
689
+ const info = fstatSync(fd, { bigint: true });
690
+ if (info.dev >= 0n && info.ino > 0n) owned = { dev: info.dev, ino: info.ino };
691
+ } catch { /* Unknown descriptor identity never authorizes unlink. */ }
692
+ try {
693
+ withConfigMutationLockSync(() => {
694
+ let current: { dev: bigint; ino: bigint } | null = null;
695
+ try {
696
+ const info = statSync(path, { bigint: true });
697
+ if (info.dev >= 0n && info.ino > 0n) current = { dev: info.dev, ino: info.ino };
698
+ } catch { /* Keep the lock and the callback outcome when the path probe fails. */ }
699
+ if (owned && current && current.dev === owned.dev && current.ino === owned.ino) {
700
+ try { unlinkSync(path); } catch (err) {
701
+ if (errCode(err) !== "ENOENT") throw err;
702
+ }
703
+ }
704
+ });
705
+ } catch (err) {
706
+ // Keep the descriptor alive through comparison/unlink so its inode cannot be recycled.
707
+ // Unavailable coordination leaves the path without masking the completed refresh.
708
+ if (!(err instanceof ConfigMutationLockError)) throw err;
709
+ } finally { closeSync(fd); }
710
+ }
711
+
619
712
  export async function withCodexRefreshFileLock<T>(lockKey: string, signal: AbortSignal, fn: () => Promise<T>): Promise<T> {
620
713
  hardenConfigDir();
621
714
  const dir = getConfigDir();
@@ -627,33 +720,45 @@ export async function withCodexRefreshFileLock<T>(lockKey: string, signal: Abort
627
720
  while (fd == null) {
628
721
  if (signal.aborted) throw signal.reason;
629
722
  try {
630
- fd = openSync(path, "wx", 0o600);
631
- writeFileSync(fd, JSON.stringify({ acquiredAt: Date.now(), pid: process.pid }) + "\n");
632
- break;
633
- } catch (err) {
634
- if (errCode(err) !== "EEXIST") throw err;
635
- if (isRefreshLockStale(path)) {
723
+ // Serialize only metadata operations, never the async refresh callback. Cooperating
724
+ // contenders cannot reclaim a successor between stale observation and path mutation.
725
+ withConfigMutationLockSync(() => {
636
726
  try {
637
- unlinkSync(path);
638
- } catch (unlinkErr) {
639
- if (errCode(unlinkErr) !== "ENOENT") throw unlinkErr;
727
+ fd = openSync(path, "wx", 0o600);
728
+ writeFileSync(fd, JSON.stringify({ acquiredAt: Date.now(), pid: process.pid }) + "\n");
729
+ } catch (err) {
730
+ if (fd != null) {
731
+ const failedFd = fd;
732
+ fd = null;
733
+ try { releaseCodexRefreshFileLock(path, failedFd); } catch { /* Preserve write failure. */ }
734
+ throw err;
735
+ }
736
+ if (errCode(err) !== "EEXIST") throw err;
737
+ if (isRefreshLockStale(path)) {
738
+ try { unlinkSync(path); } catch (unlinkErr) {
739
+ if (errCode(unlinkErr) !== "ENOENT") throw unlinkErr;
740
+ }
741
+ }
640
742
  }
641
- continue;
743
+ });
744
+ } catch (err) {
745
+ // A failed SQLite commit can follow successful file creation; it still owns an fd.
746
+ if (fd != null) {
747
+ const failedFd = fd;
748
+ fd = null;
749
+ try { releaseCodexRefreshFileLock(path, failedFd); } catch { /* Preserve admission failure. */ }
642
750
  }
643
- if (Date.now() >= deadline) throw new CodexCredentialRefreshLockTimeoutError();
644
- await sleep(REFRESH_LOCK_POLL_MS, signal);
751
+ if (!(err instanceof ConfigMutationLockError)) throw err;
645
752
  }
753
+ if (fd != null) break;
754
+ if (Date.now() >= deadline) throw new CodexCredentialRefreshLockTimeoutError();
755
+ await sleep(REFRESH_LOCK_POLL_MS, signal);
646
756
  }
647
757
 
648
758
  try {
649
759
  return await fn();
650
760
  } finally {
651
- if (fd != null) closeSync(fd);
652
- try {
653
- unlinkSync(path);
654
- } catch (err) {
655
- if (errCode(err) !== "ENOENT") throw err;
656
- }
761
+ releaseCodexRefreshFileLock(path, fd);
657
762
  }
658
763
  }
659
764
 
@@ -797,6 +902,11 @@ export async function forceRefreshCodexPoolToken(
797
902
  settle({ kind: "failed", error: options.signal.reason });
798
903
  throw options.signal.reason;
799
904
  }
905
+ if (isCodexPoolRefreshCooling(id)) {
906
+ const error = new CodexPoolRefreshCooldownError();
907
+ settle({ kind: "failed", error });
908
+ throw error;
909
+ }
800
910
  const completion = resolveCodexToken(
801
911
  id,
802
912
  { rejectedGeneration: options.rejectedGeneration, rejectedAccessToken: options.rejectedAccessToken },
@@ -804,14 +914,28 @@ export async function forceRefreshCodexPoolToken(
804
914
  // the credential, not for whoever happened to be waiting.
805
915
  undefined,
806
916
  );
917
+ // Captured before the flight settles, spent only if it fails. A reauthentication that lands
918
+ // while this is in the air replaces the grant and clears its failures; this fence is how the
919
+ // late failure knows it is talking about a credential that no longer exists.
920
+ const refreshFence = codexPoolRefreshFence(id);
807
921
  completion.then(
808
- resolved => settle({
809
- kind: "resolved",
810
- provenance: classify(resolved),
811
- generation: resolved.generation,
812
- rotated: resolved.accessToken !== options.rejectedAccessToken,
813
- }),
814
- error => settle({ kind: "failed", error }),
922
+ resolved => {
923
+ clearCodexPoolRefreshFailure(id);
924
+ settle({
925
+ kind: "resolved",
926
+ provenance: classify(resolved),
927
+ generation: resolved.generation,
928
+ rotated: resolved.accessToken !== options.rejectedAccessToken,
929
+ });
930
+ },
931
+ error => {
932
+ if (isTerminalCodexPoolRefreshFailure(error) || isOperationalCodexPoolRefreshFailure(error)) {
933
+ if (isTerminalCodexPoolRefreshFailure(error)) clearCodexPoolRefreshFailure(id);
934
+ } else {
935
+ noteCodexPoolRefreshFailure(id, classifyCodexPoolRefreshFailureReason(error), undefined, refreshFence);
936
+ }
937
+ settle({ kind: "failed", error });
938
+ },
815
939
  );
816
940
  const result = await awaitOwnCancellation(completion, options.signal);
817
941
  const provenance = classify(result);
@@ -851,9 +975,9 @@ async function resolveCodexToken(
851
975
  if (callerSignal?.aborted) throw callerSignal.reason;
852
976
  const record = readCodexAccountRecord(id);
853
977
  const cred = record?.deletedAt == null ? record?.credential : undefined;
854
- if (!record || !cred) throw new Error("Codex account credential is unavailable; reauthenticate the account.");
978
+ if (!record || !cred) throw new CodexCredentialUnavailableError();
855
979
  const refreshGrantFingerprint = recordGrantFingerprint(record);
856
- if (!refreshGrantFingerprint) throw new Error("Codex account credential is unavailable; reauthenticate the account.");
980
+ if (!refreshGrantFingerprint) throw new CodexCredentialUnavailableError();
857
981
 
858
982
  // The freshness shortcut is exactly what makes a 401 on a time-valid token
859
983
  // unrecoverable, so a forced caller skips it — but only while the stored credential
@@ -1075,9 +1199,20 @@ async function resolveCodexToken(
1075
1199
  let errDesc: string;
1076
1200
  let errCodeExact: string | undefined;
1077
1201
  try {
1078
- const parsed = JSON.parse(errText) as { error?: string; error_description?: string };
1079
- errCodeExact = typeof parsed.error === "string" ? parsed.error.trim() : undefined;
1080
- errDesc = [parsed.error, parsed.error_description].filter(Boolean).join(": ") || `HTTP ${res.status}`;
1202
+ const parsed = JSON.parse(errText) as {
1203
+ error?: string | { code?: string; message?: string };
1204
+ error_description?: string;
1205
+ };
1206
+ if (typeof parsed.error === "string") {
1207
+ errCodeExact = parsed.error.trim();
1208
+ errDesc = [parsed.error, parsed.error_description].filter(Boolean).join(": ");
1209
+ } else if (parsed.error && typeof parsed.error === "object") {
1210
+ errCodeExact = typeof parsed.error.code === "string" ? parsed.error.code.trim() : undefined;
1211
+ errDesc = [parsed.error.code, parsed.error.message, parsed.error_description].filter(Boolean).join(": ");
1212
+ } else {
1213
+ errDesc = parsed.error_description || `HTTP ${res.status}`;
1214
+ }
1215
+ if (!errDesc) errDesc = `HTTP ${res.status}`;
1081
1216
  } catch { errDesc = `HTTP ${res.status}`; }
1082
1217
  // `invalid_grant` is the standard OAuth code for a refresh token that is no longer
1083
1218
  // usable, and upstream sends it bare with no description. Without it here the dead
@@ -1087,9 +1222,23 @@ async function resolveCodexToken(
1087
1222
  // Matched on the exact `error` CODE, not anywhere in the combined text: a transient
1088
1223
  // `server_error` whose description happens to mention invalid_grant would otherwise
1089
1224
  // retire a healthy account, which is the failure this whole change exists to remove.
1090
- const reason = errCodeExact === "invalid_grant"
1091
- || errDesc.includes("invalidated") || errDesc.includes("revoked") ? "revoked" as const
1092
- : errDesc.includes("expired") ? "expired" as const
1225
+ //
1226
+ // That rule binds the DESCRIPTION words too. "invalidated", "revoked" and "expired" read
1227
+ // as terminal prose, but upstream puts arbitrary text there: a `server_error` whose
1228
+ // description says "token was revoked" or "session expired" is still a 5xx blip, and
1229
+ // retiring the account on it is exactly the false quarantine #2887 exists to prevent.
1230
+ // So a body that carries a structured code is classified by that code ALONE. The
1231
+ // substring fallback survives only where there is no structured code to read at all --
1232
+ // a description-only body, or one this parser could not decode -- because there the
1233
+ // prose is the only signal upstream gave us.
1234
+ const structuredCode = errCodeExact ? errCodeExact : undefined;
1235
+ const proseIsOnlySignal = structuredCode === undefined;
1236
+ const reason = structuredCode === "invalid_grant"
1237
+ || structuredCode === "refresh_token_invalidated"
1238
+ || (proseIsOnlySignal
1239
+ && (errDesc.includes("invalidated") || errDesc.includes("revoked"))) ? "revoked" as const
1240
+ : structuredCode === "refresh_token_expired"
1241
+ || (proseIsOnlySignal && errDesc.includes("expired")) ? "expired" as const
1093
1242
  : "unknown" as const;
1094
1243
  throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`);
1095
1244
  }
@@ -18,6 +18,27 @@ export interface CodexAccountUsabilityOptions {
18
18
  isMainAccountTokenLive?: typeof isMainAccountTokenLive;
19
19
  /** Confirmed account ids for an account-gated model; omitted for ordinary native models. */
20
20
  modelEligibleAccountIds?: ReadonlySet<string>;
21
+ /**
22
+ * Accounts whose own confirmed roster definitively omits the requested model (#4768).
23
+ *
24
+ * Deliberately NOT read by this module. `modelEligibleAccountIds` is an eligibility boundary and
25
+ * produces `model_not_entitled`; this is an ORDERING preference applied once, in
26
+ * `getEligiblePoolAccounts`, and dropped whenever honouring it would leave no candidate. Reading
27
+ * it here would turn a preference into a refusal and re-create the fail-closed behaviour the
28
+ * flagships were deliberately taken out of.
29
+ */
30
+ deniedModelAccountIds?: ReadonlySet<string>;
31
+ /**
32
+ * This request's conversation carries live uploaded-file references (#4778).
33
+ *
34
+ * Also not read by this module, and for the same reason: it is a retention preference, never an
35
+ * eligibility boundary. Uploaded files are scoped to the account that issued them, so moving
36
+ * such a conversation orphans the reference and every later turn is refused with
37
+ * `409 account_change_file_scope` -- the reference stays in history, so the conversation is
38
+ * effectively dead. Retention makes that refusal rarer; it can never replace it, because an
39
+ * account can always become unable to serve.
40
+ */
41
+ retainAccountForUploadedFiles?: boolean;
21
42
  }
22
43
 
23
44
  /**