@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.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- 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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =>
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
|
978
|
+
if (!record || !cred) throw new CodexCredentialUnavailableError();
|
|
855
979
|
const refreshGrantFingerprint = recordGrantFingerprint(record);
|
|
856
|
-
if (!refreshGrantFingerprint) throw new
|
|
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 {
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
/**
|