@bitkyc08/opencodex 2.8.0 → 2.9.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 (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -1,9 +1,27 @@
1
+ import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
2
+
1
3
  const reauthAccounts = new Set<string>();
4
+ let lastReconciledGeneration = 0;
5
+ let liveAccountIds = new Set<string>();
2
6
 
3
- export function markAccountNeedsReauth(id: string): void {
7
+ export function markAccountNeedsReauth(id: string, writerGeneration = captureConfigGeneration()): void {
8
+ if (writerGeneration < lastReconciledGeneration && !liveAccountIds.has(id)) return;
4
9
  reauthAccounts.add(id);
5
10
  }
6
11
 
12
+ export function reconcileCodexReauthState(context: GenerationContext): number {
13
+ if (context.generation <= lastReconciledGeneration) return 0;
14
+ let removed = 0;
15
+ for (const id of reauthAccounts) {
16
+ if (context.codexAccountIds.has(id)) continue;
17
+ reauthAccounts.delete(id);
18
+ removed += 1;
19
+ }
20
+ liveAccountIds = new Set(context.codexAccountIds);
21
+ lastReconciledGeneration = context.generation;
22
+ return removed;
23
+ }
24
+
7
25
  export function isAccountNeedsReauth(id: string): boolean {
8
26
  return reauthAccounts.has(id);
9
27
  }
@@ -1,7 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
4
+ import {
5
+ ConfigMutationLockError,
6
+ getConfigDir,
7
+ atomicWriteFile,
8
+ backupInvalidConfig,
9
+ hardenConfigDir,
10
+ hardenExistingSecret,
11
+ withConfigMutationLockSync,
12
+ } from "../config";
5
13
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
6
14
  import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
7
15
 
@@ -121,44 +129,50 @@ export function getCodexAccountCredential(id: string): CodexAccountCredentials |
121
129
  }
122
130
 
123
131
  export function saveCodexAccountCredential(id: string, cred: CodexAccountCredentials): void {
124
- const store = loadCodexAccountRecordStore();
125
- const current = store[id];
126
- const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
127
- ? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
128
- : refreshGrantFingerprintForToken(cred.refreshToken);
129
- store[id] = {
130
- credential: cred,
131
- generation: (current?.generation ?? 0) + 1,
132
- refreshGrantFingerprint,
133
- replacedAt: current ? Date.now() : undefined,
134
- ...preservedValidationMetadata(current),
135
- };
136
- persist(store);
132
+ withCredentialMutationLockSync(() => {
133
+ const store = loadCodexAccountRecordStore();
134
+ const current = store[id];
135
+ const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
136
+ ? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
137
+ : refreshGrantFingerprintForToken(cred.refreshToken);
138
+ store[id] = {
139
+ credential: cred,
140
+ generation: (current?.generation ?? 0) + 1,
141
+ refreshGrantFingerprint,
142
+ replacedAt: current ? Date.now() : undefined,
143
+ ...preservedValidationMetadata(current),
144
+ };
145
+ persist(store);
146
+ });
137
147
  }
138
148
 
139
149
  export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
140
- const store = loadCodexAccountRecordStore();
141
- const current = store[id];
142
- if (!current || current.deletedAt != null || !current.credential) return;
143
- store[id] = {
144
- ...current,
145
- lastCodexValidatedAt: atMs,
146
- lastCodexValidationStatus: "ok",
147
- lastCodexValidationError: undefined,
148
- };
149
- persist(store);
150
+ withCredentialMutationLockSync(() => {
151
+ const store = loadCodexAccountRecordStore();
152
+ const current = store[id];
153
+ if (!current || current.deletedAt != null || !current.credential) return;
154
+ store[id] = {
155
+ ...current,
156
+ lastCodexValidatedAt: atMs,
157
+ lastCodexValidationStatus: "ok",
158
+ lastCodexValidationError: undefined,
159
+ };
160
+ persist(store);
161
+ });
150
162
  }
151
163
 
152
164
  export function markCodexAccountValidationFailed(id: string, reason: string): void {
153
- const store = loadCodexAccountRecordStore();
154
- const current = store[id];
155
- if (!current || current.deletedAt != null || !current.credential) return;
156
- store[id] = {
157
- ...current,
158
- lastCodexValidationStatus: "failed",
159
- lastCodexValidationError: reason,
160
- };
161
- persist(store);
165
+ withCredentialMutationLockSync(() => {
166
+ const store = loadCodexAccountRecordStore();
167
+ const current = store[id];
168
+ if (!current || current.deletedAt != null || !current.credential) return;
169
+ store[id] = {
170
+ ...current,
171
+ lastCodexValidationStatus: "failed",
172
+ lastCodexValidationError: reason,
173
+ };
174
+ persist(store);
175
+ });
162
176
  }
163
177
 
164
178
  export function removeCodexAccountCredential(id: string): void {
@@ -183,32 +197,36 @@ export function saveCodexAccountCredentialIfGeneration(
183
197
  generation: number,
184
198
  cred: CodexAccountCredentials,
185
199
  ): boolean {
186
- const store = loadCodexAccountRecordStore();
187
- const current = store[id];
188
- if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
189
- return false;
190
- }
191
- const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
192
- ? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
193
- : refreshGrantFingerprintForToken(cred.refreshToken);
194
- store[id] = {
195
- credential: cred,
196
- generation: generation + 1,
197
- refreshGrantFingerprint,
198
- replacedAt: current.replacedAt,
199
- ...preservedValidationMetadata(current),
200
- };
201
- persist(store);
202
- return true;
200
+ return withCredentialMutationLockSync(() => {
201
+ const store = loadCodexAccountRecordStore();
202
+ const current = store[id];
203
+ if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
204
+ return false;
205
+ }
206
+ const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
207
+ ? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
208
+ : refreshGrantFingerprintForToken(cred.refreshToken);
209
+ store[id] = {
210
+ credential: cred,
211
+ generation: generation + 1,
212
+ refreshGrantFingerprint,
213
+ replacedAt: current.replacedAt,
214
+ ...preservedValidationMetadata(current),
215
+ };
216
+ persist(store);
217
+ return true;
218
+ });
203
219
  }
204
220
 
205
221
  export function tombstoneCodexAccount(id: string): number {
206
- const store = loadCodexAccountRecordStore();
207
- const current = store[id];
208
- const generation = (current?.generation ?? 0) + 1;
209
- store[id] = { generation, deletedAt: Date.now() };
210
- persist(store);
211
- return generation;
222
+ return withCredentialMutationLockSync(() => {
223
+ const store = loadCodexAccountRecordStore();
224
+ const current = store[id];
225
+ const generation = (current?.generation ?? 0) + 1;
226
+ store[id] = { generation, deletedAt: Date.now() };
227
+ persist(store);
228
+ return generation;
229
+ });
212
230
  }
213
231
 
214
232
  const CHATGPT_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -237,17 +255,66 @@ export class CodexCredentialRefreshLockTimeoutError extends Error {
237
255
  }
238
256
  }
239
257
 
258
+ export class CodexCredentialRefreshBusyError extends Error {
259
+ readonly code = "CODEX_REFRESH_BUSY";
260
+ readonly retryable = true;
261
+
262
+ constructor() {
263
+ super("Codex credential refresh capacity reached");
264
+ this.name = "CodexCredentialRefreshBusyError";
265
+ }
266
+ }
267
+
268
+ export class CodexCredentialRefreshStaleError extends Error {
269
+ readonly code = "CODEX_REFRESH_STALE";
270
+ readonly retryable = true;
271
+
272
+ constructor() {
273
+ super("Codex credential refresh owner became stale");
274
+ this.name = "CodexCredentialRefreshStaleError";
275
+ }
276
+ }
277
+
278
+ /** Credential writers share the config mutation coordinator; contention is transient, not reauth. */
279
+ function withCredentialMutationLockSync<T>(fn: () => T): T {
280
+ try {
281
+ return withConfigMutationLockSync(fn);
282
+ } catch (error) {
283
+ if (error instanceof ConfigMutationLockError) throw new CodexCredentialRefreshLockTimeoutError();
284
+ throw error;
285
+ }
286
+ }
287
+
240
288
  type CodexTokenResult = { accessToken: string; chatgptAccountId: string; generation: number };
241
289
  type CodexRefreshResult = CodexTokenResult & { credential?: CodexAccountCredentials };
242
- const refreshLocks = new Map<string, Promise<CodexRefreshResult>>();
290
+ const MAX_CODEX_REFRESH_FLIGHTS = 32;
291
+ const CODEX_REFRESH_FLIGHT_STALE_MS = 120_000;
292
+ interface RefreshFlight {
293
+ promise: Promise<CodexRefreshResult>;
294
+ startedAt: number;
295
+ abort: AbortController;
296
+ }
297
+ const refreshLocks = new Map<string, RefreshFlight>();
243
298
 
244
299
  function codexRefreshLockPath(lockKey: string): string {
245
300
  const digest = createHash("sha256").update(lockKey).digest("hex").slice(0, 32);
246
301
  return join(getConfigDir(), `codex-refresh-${digest}.lock`);
247
302
  }
248
303
 
249
- function sleep(ms: number): Promise<void> {
250
- return new Promise(resolve => setTimeout(resolve, ms));
304
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
305
+ if (!signal) return new Promise(resolve => setTimeout(resolve, ms));
306
+ if (signal.aborted) return Promise.reject(signal.reason);
307
+ return new Promise((resolve, reject) => {
308
+ const timer = setTimeout(() => {
309
+ signal.removeEventListener("abort", onAbort);
310
+ resolve();
311
+ }, ms);
312
+ const onAbort = () => {
313
+ clearTimeout(timer);
314
+ reject(signal.reason);
315
+ };
316
+ signal.addEventListener("abort", onAbort, { once: true });
317
+ });
251
318
  }
252
319
 
253
320
  function errCode(err: unknown): string | undefined {
@@ -264,7 +331,7 @@ function isRefreshLockStale(path: string): boolean {
264
331
  }
265
332
  }
266
333
 
267
- async function withCodexRefreshFileLock<T>(lockKey: string, fn: () => Promise<T>): Promise<T> {
334
+ async function withCodexRefreshFileLock<T>(lockKey: string, signal: AbortSignal, fn: () => Promise<T>): Promise<T> {
268
335
  hardenConfigDir();
269
336
  const dir = getConfigDir();
270
337
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -273,6 +340,7 @@ async function withCodexRefreshFileLock<T>(lockKey: string, fn: () => Promise<T>
273
340
  const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
274
341
  let fd: number | null = null;
275
342
  while (fd == null) {
343
+ if (signal.aborted) throw signal.reason;
276
344
  try {
277
345
  fd = openSync(path, "wx", 0o600);
278
346
  writeFileSync(fd, JSON.stringify({ acquiredAt: Date.now(), pid: process.pid }) + "\n");
@@ -288,7 +356,7 @@ async function withCodexRefreshFileLock<T>(lockKey: string, fn: () => Promise<T>
288
356
  continue;
289
357
  }
290
358
  if (Date.now() >= deadline) throw new CodexCredentialRefreshLockTimeoutError();
291
- await sleep(REFRESH_LOCK_POLL_MS);
359
+ await sleep(REFRESH_LOCK_POLL_MS, signal);
292
360
  }
293
361
  }
294
362
 
@@ -331,28 +399,39 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
331
399
 
332
400
  const existing = refreshLocks.get(refreshGrantFingerprint);
333
401
  if (existing) {
334
- const refreshed = await existing;
335
- const current = readCodexAccountRecord(id);
336
- const currentCred = current?.deletedAt == null ? current?.credential : undefined;
337
- if (
338
- current &&
339
- currentCred &&
340
- refreshed.credential &&
341
- recordGrantFingerprint(current) === refreshGrantFingerprint
342
- ) {
343
- if (!saveCodexAccountCredentialIfGeneration(id, current.generation, refreshed.credential)) {
344
- throw new CodexCredentialGenerationConflictError();
402
+ if (Date.now() - existing.startedAt > CODEX_REFRESH_FLIGHT_STALE_MS) {
403
+ existing.abort.abort(new CodexCredentialRefreshStaleError());
404
+ if (refreshLocks.get(refreshGrantFingerprint) === existing) refreshLocks.delete(refreshGrantFingerprint);
405
+ } else {
406
+ const refreshed = await existing.promise;
407
+ const current = readCodexAccountRecord(id);
408
+ const currentCred = current?.deletedAt == null ? current?.credential : undefined;
409
+ if (
410
+ current &&
411
+ currentCred &&
412
+ refreshed.credential &&
413
+ recordGrantFingerprint(current) === refreshGrantFingerprint
414
+ ) {
415
+ if (!saveCodexAccountCredentialIfGeneration(id, current.generation, refreshed.credential)) {
416
+ throw new CodexCredentialGenerationConflictError();
417
+ }
418
+ return {
419
+ accessToken: refreshed.credential.accessToken,
420
+ chatgptAccountId: refreshed.credential.chatgptAccountId,
421
+ generation: current.generation + 1,
422
+ };
345
423
  }
346
- return {
347
- accessToken: refreshed.credential.accessToken,
348
- chatgptAccountId: refreshed.credential.chatgptAccountId,
349
- generation: current.generation + 1,
350
- };
424
+ return getValidCodexToken(id);
351
425
  }
352
- return getValidCodexToken(id);
353
426
  }
354
427
 
355
- const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, async (): Promise<CodexRefreshResult> => {
428
+ if (refreshLocks.size >= MAX_CODEX_REFRESH_FLIGHTS) throw new CodexCredentialRefreshBusyError();
429
+
430
+ const abort = new AbortController();
431
+ const signal = AbortSignal.any([abort.signal, AbortSignal.timeout(30_000)]);
432
+ let flight!: RefreshFlight;
433
+ const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, signal, async (): Promise<CodexRefreshResult> => {
434
+ const current = readCodexAccountRecord(id);
356
435
  const lockedRecord = readCodexAccountRecord(id);
357
436
  const lockedCred = lockedRecord?.deletedAt == null ? lockedRecord?.credential : undefined;
358
437
  if (!lockedRecord || !lockedCred) throw new CodexCredentialGenerationConflictError();
@@ -397,7 +476,7 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
397
476
  client_id: CHATGPT_CLIENT_ID,
398
477
  refresh_token: lockedCred.refreshToken,
399
478
  }).toString(),
400
- signal: AbortSignal.timeout(30_000),
479
+ signal,
401
480
  });
402
481
  if (!res.ok) {
403
482
  const errText = await res.text().catch(() => "");
@@ -424,10 +503,11 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
424
503
  }
425
504
  return { accessToken: updated.accessToken, chatgptAccountId: updated.chatgptAccountId, generation: startGeneration + 1, credential: updated };
426
505
  }).finally(() => {
427
- refreshLocks.delete(refreshGrantFingerprint);
506
+ if (refreshLocks.get(refreshGrantFingerprint) === flight) refreshLocks.delete(refreshGrantFingerprint);
428
507
  });
429
508
 
430
- refreshLocks.set(refreshGrantFingerprint, refreshPromise);
509
+ flight = { promise: refreshPromise, startedAt: Date.now(), abort };
510
+ refreshLocks.set(refreshGrantFingerprint, flight);
431
511
  const result = await refreshPromise;
432
512
  return {
433
513
  accessToken: result.accessToken,