@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +5 -1
- package/src/adapters/anthropic.ts +16 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +10 -2
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +13 -3
- package/src/adapters/cursor.ts +11 -2
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin.ts +75 -23
- package/src/adapters/google-antigravity-wire.ts +5 -2
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google.ts +29 -5
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat.ts +18 -57
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +10 -0
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +43 -4
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/index.ts +200 -61
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-listener.ts +2 -5
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/provider-models.ts +24 -15
- package/src/codex/catalog/retained-sync.ts +2 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/history-provider.ts +12 -1
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +52 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +87 -44
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +5 -41
- package/src/codex/shim-templates.ts +29 -3
- package/src/companion/settings.ts +132 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/load-degrade.ts +34 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/schema/config-schema.ts +27 -1
- package/src/config/schema/leaf-validators.ts +47 -0
- package/src/config.ts +1 -1
- package/src/generated/compatibility-version.json +418 -174
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +124 -18
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +49 -11
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug.ts +40 -0
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +59 -14
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/index.ts +24 -1
- package/src/oauth/login-cli.ts +80 -29
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/key-failover.ts +31 -1
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +124 -37
- package/src/providers/quota/vendor-probes-key.ts +37 -22
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +9 -4
- package/src/providers/registry/entries-extended.ts +29 -4
- package/src/providers/registry/model-seeds.ts +47 -10
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/schema.ts +3 -0
- package/src/router.ts +17 -2
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +55 -2
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +55 -17
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +56 -10
- package/src/server/index/spend-ledger-lifecycle.ts +34 -8
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index.ts +21 -28
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +6 -0
- package/src/server/management/shared.ts +18 -1
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +8 -9
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/relay.ts +19 -2
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +114 -0
- package/src/server/request-metrics.ts +92 -30
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +90 -0
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +14 -1
- package/src/server/responses/passthrough-dispatch.ts +179 -35
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/request-prepare.ts +42 -1
- package/src/server/responses/request-send-budget.ts +12 -0
- package/src/server/responses/request-transport.ts +24 -4
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +4 -1
- package/src/server/search.ts +25 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +1 -1
- package/src/types/config.ts +37 -0
- package/src/types/provider.ts +73 -0
- package/src/types/request.ts +28 -2
- package/src/types/tools.ts +19 -0
- package/src/types.ts +3 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/web-search/alpha-search.ts +21 -1
- package/gui/dist/assets/index-BTuCbqQd.css +0 -1
- package/gui/dist/assets/index-DoBVdPHP.js +0 -134
|
@@ -4,6 +4,7 @@ import { contextCompatibleBaseLine } from "../context-compat";
|
|
|
4
4
|
import { resolveEffectiveProjectModelProvider } from "../project-config-warnings";
|
|
5
5
|
import {
|
|
6
6
|
OCX_SECTION_MARKER,
|
|
7
|
+
OCX_ROUTING_MARKER_LINE,
|
|
7
8
|
REALTIME_WS_BASE_URL_KEY,
|
|
8
9
|
isRootOpenaiBaseUrlLine,
|
|
9
10
|
isRootRealtimeWsBaseUrlLine,
|
|
@@ -128,7 +129,7 @@ export function buildProviderTableBlockForTarget(
|
|
|
128
129
|
): string {
|
|
129
130
|
const lines = [
|
|
130
131
|
"",
|
|
131
|
-
|
|
132
|
+
OCX_ROUTING_MARKER_LINE,
|
|
132
133
|
"[model_providers.opencodex]",
|
|
133
134
|
`name = ${tomlString(resolveCodexProviderDisplayName(displayName))}`,
|
|
134
135
|
`base_url = ${tomlString(target.baseUrl)}`,
|
|
@@ -213,6 +214,9 @@ export function setRootOpenaiBaseUrl(
|
|
|
213
214
|
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
214
215
|
const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
|
|
215
216
|
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
217
|
+
// Refresh the marker too, so a config injected by a build that predates the recovery
|
|
218
|
+
// hint gains it on the next `ocx start` instead of keeping a bare marker forever.
|
|
219
|
+
lines[i - 1] = OCX_ROUTING_MARKER_LINE;
|
|
216
220
|
lines[i] = key;
|
|
217
221
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
218
222
|
}
|
|
@@ -222,7 +226,7 @@ export function setRootOpenaiBaseUrl(
|
|
|
222
226
|
content:
|
|
223
227
|
content.replace(/\n+$/, "") +
|
|
224
228
|
"\n" +
|
|
225
|
-
|
|
229
|
+
OCX_ROUTING_MARKER_LINE +
|
|
226
230
|
"\n" +
|
|
227
231
|
key +
|
|
228
232
|
"\n",
|
|
@@ -231,7 +235,7 @@ export function setRootOpenaiBaseUrl(
|
|
|
231
235
|
}
|
|
232
236
|
let insertAt = firstTable;
|
|
233
237
|
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
234
|
-
lines.splice(insertAt, 0,
|
|
238
|
+
lines.splice(insertAt, 0, OCX_ROUTING_MARKER_LINE, key);
|
|
235
239
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
236
240
|
}
|
|
237
241
|
|
|
@@ -247,18 +251,19 @@ export function setRootOpenaiBaseUrlForTarget(
|
|
|
247
251
|
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
248
252
|
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
249
253
|
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
254
|
+
lines[index - 1] = OCX_ROUTING_MARKER_LINE;
|
|
250
255
|
lines[index] = key;
|
|
251
256
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
252
257
|
}
|
|
253
258
|
if (firstTable === -1) {
|
|
254
259
|
return {
|
|
255
|
-
content: `${content.replace(/\n+$/, "")}\n${
|
|
260
|
+
content: `${content.replace(/\n+$/, "")}\n${OCX_ROUTING_MARKER_LINE}\n${key}\n`,
|
|
256
261
|
keptUserBaseUrl: false,
|
|
257
262
|
};
|
|
258
263
|
}
|
|
259
264
|
let insertAt = firstTable;
|
|
260
265
|
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
|
|
261
|
-
lines.splice(insertAt, 0,
|
|
266
|
+
lines.splice(insertAt, 0, OCX_ROUTING_MARKER_LINE, key);
|
|
262
267
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
263
268
|
}
|
|
264
269
|
|
|
@@ -284,13 +289,14 @@ export function setRootRealtimeWsBaseUrl(
|
|
|
284
289
|
if (!isRootRealtimeWsBaseUrlLine(lines[index])) continue;
|
|
285
290
|
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
286
291
|
if (!markerOwned) return { content, keptUserRealtimeWsBaseUrl: true };
|
|
292
|
+
lines[index - 1] = OCX_ROUTING_MARKER_LINE;
|
|
287
293
|
lines[index] = key;
|
|
288
294
|
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
289
295
|
}
|
|
290
296
|
for (let index = 0; index < rootEnd; index += 1) {
|
|
291
297
|
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
292
298
|
if (!(index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER))) continue;
|
|
293
|
-
lines.splice(index + 1, 0,
|
|
299
|
+
lines.splice(index + 1, 0, OCX_ROUTING_MARKER_LINE, key);
|
|
294
300
|
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
295
301
|
}
|
|
296
302
|
// No marker-owned routing override to attach to: the override has no owner, so inject nothing.
|
|
@@ -603,3 +609,32 @@ export function chooseCatalogPathForInjection(
|
|
|
603
609
|
|
|
604
610
|
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
605
611
|
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The effective `model_catalog_json` is one of ours and the file is gone.
|
|
615
|
+
*
|
|
616
|
+
* Codex does not degrade on this: a catalog path it cannot read stops it loading its
|
|
617
|
+
* configuration at all, which looks exactly like the routing lockout in #5261 and is what the
|
|
618
|
+
* reporter's machine was left in after the catalog file was deleted by hand.
|
|
619
|
+
*
|
|
620
|
+
* Injection already repairs it — the chooser refuses a missing owned path and the caller strips
|
|
621
|
+
* the stale line. That only helps someone who runs opencodex again, and the whole difficulty of
|
|
622
|
+
* this state is that Codex is the thing that stopped working, so nothing prompts them to. This
|
|
623
|
+
* predicate exists so the CLI can say it out loud.
|
|
624
|
+
*
|
|
625
|
+
* A user-owned catalog assignment wins here exactly as it does during injection: if they named
|
|
626
|
+
* the file, its absence is theirs to explain, and we do not claim it.
|
|
627
|
+
*
|
|
628
|
+
* Ownership is decided by basename, which is a weak test — a file the user happens to name
|
|
629
|
+
* `opencodex-catalog.json` is read as ours wherever it sits. That is deliberate rather than
|
|
630
|
+
* overlooked: it is the same test injection already applies, and a detector that drew the line
|
|
631
|
+
* somewhere else would report a state injection would then treat differently. Tightening it is a
|
|
632
|
+
* change to injection, not to this.
|
|
633
|
+
*/
|
|
634
|
+
export function missingOwnedCatalogPath(content: string): string | null {
|
|
635
|
+
const existing = readRootModelCatalogPath(content);
|
|
636
|
+
if (!existing) return null;
|
|
637
|
+
const resolved = resolveCodexConfigPath(existing);
|
|
638
|
+
if (!isOpencodexCatalogPath(resolved)) return null;
|
|
639
|
+
return existsSync(resolved) ? null : existing;
|
|
640
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The way forward for a provider-table transition that finds an `openai`-tagged row
|
|
3
|
+
* Codex has already migrated to paginated history.
|
|
4
|
+
*
|
|
5
|
+
* The refusal this resolves is correct about the danger and wrong about the remedy.
|
|
6
|
+
* A provider-table transition takes the root `openai_base_url` out, and a paginated row
|
|
7
|
+
* cannot be relabeled, so the transition as planned would send that conversation to
|
|
8
|
+
* Codex's built-in OpenAI endpoint. Refusing to relabel is right. Refusing the whole
|
|
9
|
+
* transition left #5321's reporter with 173 conversations and two unsupported exits:
|
|
10
|
+
* delete them, or downgrade.
|
|
11
|
+
*
|
|
12
|
+
* There is a third state, and the injector already builds it for the client-compaction
|
|
13
|
+
* form: keep the marker-owned root override beside the provider table. Codex merges the
|
|
14
|
+
* override onto its built-in `openai` entry when it builds the provider map, so the
|
|
15
|
+
* paginated row keeps reaching this proxy while never being rewritten. The transition
|
|
16
|
+
* completes, the relabel stands down, and no rollout byte or thread row is touched.
|
|
17
|
+
*
|
|
18
|
+
* Two cases cannot reach that state. An admission-token form cannot use the root key at
|
|
19
|
+
* all, because Codex's built-in `openai` entry carries no `x-opencodex-api-key` header;
|
|
20
|
+
* that one keeps the refusal and names what the operator can actually do. A root line the
|
|
21
|
+
* user owns is left alone, and the conversation follows their configuration rather than
|
|
22
|
+
* this proxy — the same guarantee the injector already makes everywhere else about a line
|
|
23
|
+
* it does not own.
|
|
24
|
+
*/
|
|
25
|
+
import { HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE, HISTORY_RELABEL_STANDS_DOWN } from "../history-provider";
|
|
26
|
+
import { applyEol, setRootOpenaiBaseUrlForTarget, stripInjectedOpenaiBaseUrl } from "./config-toml";
|
|
27
|
+
import type { CodexRoutingTarget } from "./routing-target";
|
|
28
|
+
|
|
29
|
+
export interface PaginatedOpenaiCompatDecision {
|
|
30
|
+
/** The refusal that survives. `null` only when the preflight raised none. */
|
|
31
|
+
readonly refusal: string | null;
|
|
32
|
+
/** Candidate config bytes, carrying the retained root override when one was written. */
|
|
33
|
+
readonly content: string;
|
|
34
|
+
/** True only when OpenCodex wrote the retained line and must journal it as its own. */
|
|
35
|
+
readonly retainedRootOverride: boolean;
|
|
36
|
+
/** What the caller reports when `refusal` is not the stand-down reason. */
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function genericRefusal(refusal: string): string {
|
|
41
|
+
return `Codex config injection refused: ${refusal}. `
|
|
42
|
+
+ "Existing provider definitions and conversation files were preserved. "
|
|
43
|
+
+ "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Named next actions, because "do not retry" is what trapped the reporter. Both are
|
|
48
|
+
* configuration the operator already owns: the loopback listener is what makes the root
|
|
49
|
+
* override usable, and `syncResumeHistory` is the existing opt-out from history remapping.
|
|
50
|
+
*/
|
|
51
|
+
function admissionTokenRefusal(): string {
|
|
52
|
+
return `Codex config injection refused: ${HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE}. `
|
|
53
|
+
+ "Existing provider definitions and conversation files were preserved. "
|
|
54
|
+
+ "This home has conversations tagged openai whose history Codex has already migrated to its paginated "
|
|
55
|
+
+ "format, and they cannot be relabeled. Keeping them on this proxy needs a root openai_base_url override, "
|
|
56
|
+
+ "which this routing form cannot use: Codex's built-in openai provider carries no x-opencodex-api-key header. "
|
|
57
|
+
+ "To complete the transition, either route Codex through the loopback listener "
|
|
58
|
+
+ "(unauthenticatedLoopbackListener.enabled, or a loopback hostname) so the override can be retained, "
|
|
59
|
+
+ "or set syncResumeHistory to false to accept that those conversations resume against Codex's own OpenAI endpoint.";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the transition, retaining the root override when this routing form can own one.
|
|
64
|
+
*
|
|
65
|
+
* `content` is the fully assembled provider-table candidate: OpenCodex's own root override
|
|
66
|
+
* has already been stripped and not re-added, so a root `openai_base_url` still present in it
|
|
67
|
+
* belongs to the user.
|
|
68
|
+
*/
|
|
69
|
+
export function applyPaginatedOpenaiCompat(
|
|
70
|
+
refusal: string | null,
|
|
71
|
+
target: CodexRoutingTarget,
|
|
72
|
+
content: string,
|
|
73
|
+
eol: "\r\n" | "\n",
|
|
74
|
+
): PaginatedOpenaiCompatDecision {
|
|
75
|
+
if (refusal !== HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE) {
|
|
76
|
+
return { refusal, content, retainedRootOverride: false, message: refusal ? genericRefusal(refusal) : "" };
|
|
77
|
+
}
|
|
78
|
+
if (target.requiresAdmissionToken === true) {
|
|
79
|
+
return { refusal, content, retainedRootOverride: false, message: admissionTokenRefusal() };
|
|
80
|
+
}
|
|
81
|
+
const written = setRootOpenaiBaseUrlForTarget(stripInjectedOpenaiBaseUrl(applyEol(content, "\n")), target);
|
|
82
|
+
// The relabel still stands down either way: the row stays tagged openai and paginated, and
|
|
83
|
+
// the caller uses this reason to skip the history unit rather than let it start and refuse.
|
|
84
|
+
return {
|
|
85
|
+
refusal: HISTORY_RELABEL_STANDS_DOWN,
|
|
86
|
+
content: written.keptUserBaseUrl ? content : applyEol(written.content, eol),
|
|
87
|
+
retainedRootOverride: !written.keptUserBaseUrl,
|
|
88
|
+
message: "",
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/codex/inject.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
writeJournal,
|
|
42
42
|
} from "./journal";
|
|
43
43
|
import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "./history-provider";
|
|
44
|
+
import { applyPaginatedOpenaiCompat } from "./inject/paginated-openai-compat";
|
|
44
45
|
import {
|
|
45
46
|
describeHistoryJobFailure,
|
|
46
47
|
deriveCodexHistoryOperation,
|
|
@@ -347,12 +348,13 @@ async function injectCodexConfigImpl(
|
|
|
347
348
|
// first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
|
|
348
349
|
// codex re-appends that stale first line whenever it writes git or memory-mode metadata.
|
|
349
350
|
//
|
|
350
|
-
// Authless is excluded on purpose: its whole point is a provider that carries
|
|
351
|
-
// requires_openai_auth = false,
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
351
|
+
// Authless is excluded here on purpose: its whole point is a provider that carries
|
|
352
|
+
// requires_openai_auth = false, so it forward-tags resume history with originals backed up
|
|
353
|
+
// instead, and that includes the case where a user enables authless and client compaction
|
|
354
|
+
// together. Only the compaction-only form skips the history unit up front. When forward
|
|
355
|
+
// tagging turns out to be impossible because Codex already paginated those rows, the same
|
|
356
|
+
// retention is selected below from the preflight verdict rather than from the routing form.
|
|
357
|
+
let keepRootOverrideAlongsideTable = providerTableMode
|
|
356
358
|
&& routingTarget.clientCompaction === true
|
|
357
359
|
&& routingTarget.desktopAuthless !== true
|
|
358
360
|
&& routingTarget.requiresAdmissionToken !== true;
|
|
@@ -472,14 +474,15 @@ async function injectCodexConfigImpl(
|
|
|
472
474
|
if (observed && observed !== HISTORY_RELABEL_STANDS_DOWN) throw new CodexHistoryPreflightRefusal(observed);
|
|
473
475
|
return observed;
|
|
474
476
|
};
|
|
475
|
-
const
|
|
477
|
+
const compat = applyPaginatedOpenaiCompat(historyPreflight(), routingTarget, content, eol);
|
|
478
|
+
content = compat.content;
|
|
479
|
+
keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
|
|
480
|
+
const observedHistoryRefusal = compat.refusal;
|
|
476
481
|
if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
|
|
477
482
|
return {
|
|
478
483
|
success: false,
|
|
479
484
|
historyPreflightFailureReason: observedHistoryRefusal,
|
|
480
|
-
message:
|
|
481
|
-
+ "Existing provider definitions and conversation files were preserved. "
|
|
482
|
-
+ "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.",
|
|
485
|
+
message: compat.message,
|
|
483
486
|
};
|
|
484
487
|
}
|
|
485
488
|
let historyRelabelRefusal = observedHistoryRefusal;
|
|
@@ -607,11 +610,11 @@ async function injectCodexConfigImpl(
|
|
|
607
610
|
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
608
611
|
markJournalInjectedState(content, profileContent, {
|
|
609
612
|
// A root override is ours whenever we wrote one and no user-owned value won. That is
|
|
610
|
-
// loopback Design B,
|
|
611
|
-
//
|
|
612
|
-
// marker comment is not durable: the Codex
|
|
613
|
-
// comments, and restore then has only the
|
|
614
|
-
//
|
|
613
|
+
// loopback Design B, the client-compaction form, and any table form that retained the
|
|
614
|
+
// root line for a paginated openai row, all of which keep the marker-owned line beside
|
|
615
|
+
// the table. Journaling it matters because the marker comment is not durable: the Codex
|
|
616
|
+
// app can reserialize config.toml and drop comments, and restore then has only the
|
|
617
|
+
// journaled value to tell our line from a user's (#1798). Other table forms record null.
|
|
615
618
|
injectedOpenaiBaseUrl: (providerTableMode && !keepRootOverrideAlongsideTable) || keptUserBaseUrl
|
|
616
619
|
? null
|
|
617
620
|
: rootTomlString(content, "openai_base_url"),
|
|
@@ -11,6 +11,24 @@ import { parseTomlString } from "./paths";
|
|
|
11
11
|
|
|
12
12
|
export const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The marker line actually written above ROUTING keys, carrying the command that undoes them.
|
|
16
|
+
*
|
|
17
|
+
* #5261: a Windows user whose proxy had stopped was locked out of Codex sign-in, because the
|
|
18
|
+
* root `openai_base_url` we write keeps pointing Codex's built-in openai provider at a port
|
|
19
|
+
* nothing is listening on. The only surface such a user can still read is `config.toml` itself,
|
|
20
|
+
* and it said nothing but "Auto-injected by opencodex" — so the recovery they found was to
|
|
21
|
+
* hand-delete lines and the catalog file, which is strictly worse than `ocx restore`.
|
|
22
|
+
*
|
|
23
|
+
* Every ownership predicate below matches on {@link OCX_SECTION_MARKER} as a SUBSTRING, never by
|
|
24
|
+
* equality, so this longer line is recognized by them and by any opencodex old enough to predate
|
|
25
|
+
* it. That is the whole reason the hint is appended to the marker instead of occupying a line of
|
|
26
|
+
* its own: a separate comment line would survive removal as orphaned text.
|
|
27
|
+
*
|
|
28
|
+
* Scope is routing only. Prompt layers keep the bare marker: `ocx restore` is not their undo.
|
|
29
|
+
*/
|
|
30
|
+
export const OCX_ROUTING_MARKER_LINE = `${OCX_SECTION_MARKER} (undo: ocx restore)`;
|
|
31
|
+
|
|
14
32
|
export function isRootOpenaiBaseUrlLine(line: string): boolean {
|
|
15
33
|
return /^\s*openai_base_url\s*=/.test(line);
|
|
16
34
|
}
|
|
@@ -404,6 +404,12 @@ async function resolveMainAccountToken(
|
|
|
404
404
|
: "transient" as const;
|
|
405
405
|
throw new MainAccountTokenRefreshError(reason, { cause });
|
|
406
406
|
}
|
|
407
|
+
// The refresh may resolve after the caller went away (an implementation that does
|
|
408
|
+
// not observe the signal, or an abort landing in the window between resolution and
|
|
409
|
+
// commit). A cancelled request's late refresh must not rewrite auth.json on behalf
|
|
410
|
+
// of a request that no longer exists -- the same fence the reauth twin applies
|
|
411
|
+
// before its own commit above.
|
|
412
|
+
if (dependencies.signal?.aborted) throw dependencies.signal.reason;
|
|
407
413
|
const result = persistRefreshedMainAuthJson(locked, refreshed);
|
|
408
414
|
if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
409
415
|
return result;
|
package/src/codex/model-cache.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { CatalogModel } from "./catalog";
|
|
11
11
|
import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
12
12
|
import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
|
|
13
|
+
import { clearLiveCursorRosterState } from "../adapters/cursor/catalog";
|
|
13
14
|
|
|
14
15
|
/** Default freshness window. Matches Codex's own 5-min models cache so the two stay in step. */
|
|
15
16
|
export const DEFAULT_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -80,7 +81,17 @@ function deleteCachedProvider(provider: string): number {
|
|
|
80
81
|
* the full fetch timeout on every catalog poll (issue #54: UI stalls behind corporate proxies). */
|
|
81
82
|
export const MODELS_FETCH_FAILURE_COOLDOWN_MS = 30_000;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
interface DiscoveryFailure {
|
|
85
|
+
at: number;
|
|
86
|
+
/**
|
|
87
|
+
* Credential the failure was observed under, for entitlement-specific rosters. Absent means
|
|
88
|
+
* the failure is credential-agnostic (a plain `/models` endpoint) and suppresses every
|
|
89
|
+
* caller, which is the original #54 behaviour.
|
|
90
|
+
*/
|
|
91
|
+
authorityIdentity?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const failureAt = new Map<string, DiscoveryFailure>();
|
|
84
95
|
const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
|
|
85
96
|
/**
|
|
86
97
|
* How many models the last successful discovery actually returned, before any configured-alias
|
|
@@ -91,8 +102,12 @@ const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
|
|
|
91
102
|
const liveModelCounts = new Map<string, number>();
|
|
92
103
|
let lastReconciledGeneration = 0;
|
|
93
104
|
|
|
94
|
-
export function markModelsFetchFailure(
|
|
95
|
-
|
|
105
|
+
export function markModelsFetchFailure(
|
|
106
|
+
provider: string,
|
|
107
|
+
now = Date.now(),
|
|
108
|
+
authorityIdentity?: string,
|
|
109
|
+
): void {
|
|
110
|
+
failureAt.set(provider, { at: now, ...(authorityIdentity ? { authorityIdentity } : {}) });
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
/** `liveModelCount` is required so a caller that forgets to pass it fails typecheck instead of
|
|
@@ -147,9 +162,25 @@ export function getProviderLiveModelCount(provider: string): number | undefined
|
|
|
147
162
|
return liveModelCounts.get(provider);
|
|
148
163
|
}
|
|
149
164
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Whether a failed discovery still suppresses the next one.
|
|
167
|
+
*
|
|
168
|
+
* `authorityIdentity` scopes the suppression to the credential that actually observed the
|
|
169
|
+
* failure. A roster that upstream filters per account is evidence about that account, and
|
|
170
|
+
* one account's 401 or 404 must not decide that a different account has no catalog. A
|
|
171
|
+
* failure recorded without an identity stays credential-agnostic and suppresses everyone,
|
|
172
|
+
* so the plain-endpoint providers keep the timeout protection #54 added.
|
|
173
|
+
*/
|
|
174
|
+
export function isModelsFetchCoolingDown(
|
|
175
|
+
provider: string,
|
|
176
|
+
cooldownMs = MODELS_FETCH_FAILURE_COOLDOWN_MS,
|
|
177
|
+
now = Date.now(),
|
|
178
|
+
authorityIdentity?: string,
|
|
179
|
+
): boolean {
|
|
180
|
+
const failure = failureAt.get(provider);
|
|
181
|
+
if (failure === undefined || now - failure.at >= cooldownMs) return false;
|
|
182
|
+
if (failure.authorityIdentity === undefined || authorityIdentity === undefined) return true;
|
|
183
|
+
return failure.authorityIdentity === authorityIdentity;
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
/** Fresh cached models for a provider, or null when absent/stale (caller should re-fetch). */
|
|
@@ -168,6 +199,20 @@ export function getStaleCached(provider: string, authorityIdentity?: string): Ca
|
|
|
168
199
|
return entry.models;
|
|
169
200
|
}
|
|
170
201
|
|
|
202
|
+
/** Selector decoding may use unscoped rows, but scoped rows need current authority. */
|
|
203
|
+
export function getRoutingCached(
|
|
204
|
+
provider: string, resolveAuthority: () => string | undefined,
|
|
205
|
+
): CatalogModel[] | null {
|
|
206
|
+
const entry = cache.get(provider);
|
|
207
|
+
if (!entry) return null;
|
|
208
|
+
if (entry.authorityIdentity !== undefined) {
|
|
209
|
+
try {
|
|
210
|
+
if (resolveAuthority() !== entry.authorityIdentity) return null;
|
|
211
|
+
} catch { return null; }
|
|
212
|
+
}
|
|
213
|
+
return entry.models;
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
/** Capture the cache generation before an asynchronous provider discovery starts. */
|
|
172
217
|
export function captureModelCacheGeneration(provider: string): string {
|
|
173
218
|
if (!providerCacheGenerations.has(provider)) providerCacheGenerations.set(provider, 0);
|
|
@@ -240,6 +285,7 @@ export function clearModelCache(
|
|
|
240
285
|
provider?: string,
|
|
241
286
|
reason: ModelCacheClearReason = "authority",
|
|
242
287
|
): void {
|
|
288
|
+
clearLiveCursorRosterState(provider);
|
|
243
289
|
const revokesInFlightDiscovery = reason === "authority";
|
|
244
290
|
if (provider) {
|
|
245
291
|
if (revokesInFlightDiscovery) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
3
|
+
import {
|
|
4
|
+
resolveCodexModelEntitlements,
|
|
5
|
+
type CodexModelEntitlementResolveOptions,
|
|
6
|
+
type CodexModelEntitlementSnapshot,
|
|
7
|
+
} from "./model-entitlements";
|
|
8
|
+
import {
|
|
9
|
+
type NativeMainCredentialAdmissionDeps,
|
|
10
|
+
withNativeMainCredentialAdmission,
|
|
11
|
+
} from "./native-main-admission";
|
|
12
|
+
|
|
13
|
+
interface ModelEntitlementAdmissionDeps extends NativeMainCredentialAdmissionDeps {
|
|
14
|
+
readonly resolve?: typeof resolveCodexModelEntitlements;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function excludeNativeMain(
|
|
18
|
+
options: CodexModelEntitlementResolveOptions,
|
|
19
|
+
): CodexModelEntitlementResolveOptions {
|
|
20
|
+
return {
|
|
21
|
+
...options,
|
|
22
|
+
excludeAccountIds: new Set([
|
|
23
|
+
...(options.excludeAccountIds ?? []),
|
|
24
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
25
|
+
]),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve background/data-plane entitlements inside the native-main fences.
|
|
31
|
+
*
|
|
32
|
+
* Pool discovery remains available when startup recovery or a profile drain
|
|
33
|
+
* owns the physical credential. When main is admitted, the process-local lease
|
|
34
|
+
* covers only the credential snapshot: getValidMainAccountToken holds the
|
|
35
|
+
* cross-process exclusive claim just for the auth.json refresh write, and the
|
|
36
|
+
* lease is released before the upstream entitlement request so a profile drain
|
|
37
|
+
* never waits on a network fetch.
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveAdmittedCodexModelEntitlements(
|
|
40
|
+
config: Pick<OcxConfig, "codexAccounts">,
|
|
41
|
+
options: CodexModelEntitlementResolveOptions = {},
|
|
42
|
+
deps: ModelEntitlementAdmissionDeps = {},
|
|
43
|
+
): Promise<CodexModelEntitlementSnapshot> {
|
|
44
|
+
const resolve = deps.resolve ?? resolveCodexModelEntitlements;
|
|
45
|
+
// A caller-supplied roster or an already-excluded main never reads auth.json,
|
|
46
|
+
// so there is no native credential to fence.
|
|
47
|
+
if (options.credentials || options.excludeAccountIds?.has(MAIN_CODEX_ACCOUNT_ID)) {
|
|
48
|
+
return resolve(config, options);
|
|
49
|
+
}
|
|
50
|
+
return withNativeMainCredentialAdmission(
|
|
51
|
+
(excludedAccountIds, releaseMainLease) => resolve(
|
|
52
|
+
config,
|
|
53
|
+
excludedAccountIds.size === 0
|
|
54
|
+
? { ...options, releaseNativeMainCredentialLease: releaseMainLease }
|
|
55
|
+
: excludeNativeMain(options),
|
|
56
|
+
),
|
|
57
|
+
{ acquireNativeMain: deps.acquireNativeMain },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
MAIN_CODEX_ACCOUNT_ID,
|
|
14
14
|
type NativeMainRefreshDependencies,
|
|
15
15
|
} from "./main-account";
|
|
16
|
+
import { withNativeMainCredentialAdmission } from "./native-main-admission";
|
|
16
17
|
import {
|
|
17
18
|
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
18
19
|
NATIVE_GPT6_ASTRA_MODEL,
|
|
@@ -439,6 +440,12 @@ export interface CodexModelEntitlementResolveOptions {
|
|
|
439
440
|
readonly excludeAccountIds?: ReadonlySet<string>;
|
|
440
441
|
/** Ensure-only fence; ordinary request resolvers retain their established flight identity. */
|
|
441
442
|
readonly credentialMutationEpoch?: number;
|
|
443
|
+
/**
|
|
444
|
+
* Internal plumbing from `withNativeMainCredentialAdmission`: releases the
|
|
445
|
+
* native-main lifecycle lease once the credential phase settles, before any
|
|
446
|
+
* upstream roster fetch, so a profile drain never waits on network work.
|
|
447
|
+
*/
|
|
448
|
+
readonly releaseNativeMainCredentialLease?: () => void;
|
|
442
449
|
}
|
|
443
450
|
|
|
444
451
|
export interface CodexEntitlementFreshnessOptions extends Pick<
|
|
@@ -452,6 +459,8 @@ export interface CodexEntitlementFreshnessOptions extends Pick<
|
|
|
452
459
|
| "signal"
|
|
453
460
|
> {
|
|
454
461
|
readonly waitMs?: number;
|
|
462
|
+
/** Test seam for the native-main admission fence around the refresh workset. */
|
|
463
|
+
readonly nativeMainCredentialAdmission?: typeof withNativeMainCredentialAdmission;
|
|
455
464
|
}
|
|
456
465
|
|
|
457
466
|
const accountModelsCache = new Map<string, CachedAccountModels>();
|
|
@@ -581,21 +590,31 @@ function currentCredentialIdentity(accountId: string): string | undefined {
|
|
|
581
590
|
|
|
582
591
|
async function accountCredentialSnapshot(
|
|
583
592
|
accountId: string,
|
|
584
|
-
options: Pick<
|
|
593
|
+
options: Pick<
|
|
594
|
+
CodexModelEntitlementResolveOptions,
|
|
595
|
+
"nativeMainRefreshDependencies" | "releaseNativeMainCredentialLease" | "signal"
|
|
596
|
+
> = {},
|
|
585
597
|
): Promise<CodexModelEntitlementCredentialSnapshot | null> {
|
|
586
598
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
+
try {
|
|
600
|
+
const token = await getValidMainAccountToken({
|
|
601
|
+
signal: options.signal,
|
|
602
|
+
...(options.nativeMainRefreshDependencies ?? {}),
|
|
603
|
+
});
|
|
604
|
+
return token
|
|
605
|
+
? {
|
|
606
|
+
accountId,
|
|
607
|
+
accessToken: token.accessToken,
|
|
608
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
609
|
+
credentialIdentity: `main:${token.chatgptAccountId}`,
|
|
610
|
+
}
|
|
611
|
+
: null;
|
|
612
|
+
} finally {
|
|
613
|
+
// The lifecycle lease fences only this credential read; releasing here —
|
|
614
|
+
// on success and on a credential-ownership failure alike — keeps a
|
|
615
|
+
// profile drain from waiting on the roster fetches that follow.
|
|
616
|
+
options.releaseNativeMainCredentialLease?.();
|
|
617
|
+
}
|
|
599
618
|
}
|
|
600
619
|
try {
|
|
601
620
|
const token = await getValidCodexToken(accountId);
|
|
@@ -911,38 +930,58 @@ async function refreshCodexEntitlementWorkset(
|
|
|
911
930
|
mutationEpoch: number,
|
|
912
931
|
options: CodexEntitlementFreshnessOptions,
|
|
913
932
|
): Promise<void> {
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
933
|
+
const run = async (
|
|
934
|
+
excludedAccountIds: ReadonlySet<string>,
|
|
935
|
+
releaseMainLease?: () => void,
|
|
936
|
+
): Promise<void> => {
|
|
937
|
+
// An excluded main is filtered before the snapshot phase, not just before the
|
|
938
|
+
// roster fetch: it never produces an absence observation, so a denied
|
|
939
|
+
// admission cannot memoize a credential read that never happened.
|
|
940
|
+
const admittedWorkset = excludedAccountIds.size === 0
|
|
941
|
+
? workset
|
|
942
|
+
: workset.filter(accountId => !excludedAccountIds.has(accountId));
|
|
943
|
+
const credentialSnapshot = options.credentialSnapshot ?? accountCredentialSnapshot;
|
|
944
|
+
const observations = await Promise.all(admittedWorkset.map(async accountId => {
|
|
945
|
+
const credential = await credentialSnapshot(accountId, {
|
|
946
|
+
...options,
|
|
947
|
+
releaseNativeMainCredentialLease: releaseMainLease,
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
accountId,
|
|
951
|
+
credential,
|
|
952
|
+
absenceObservedAt: options.now ?? Date.now(),
|
|
953
|
+
};
|
|
954
|
+
}));
|
|
955
|
+
// The lease fences only the credential phase; release before roster fetches
|
|
956
|
+
// so a profile drain never waits on upstream network work.
|
|
957
|
+
releaseMainLease?.();
|
|
958
|
+
const credentials = observations.flatMap(observation => observation.credential
|
|
959
|
+
? [observation.credential]
|
|
960
|
+
: []);
|
|
961
|
+
if (credentials.length > 0) {
|
|
962
|
+
await resolveCodexModelEntitlements(config, {
|
|
963
|
+
...options,
|
|
964
|
+
clientVersion,
|
|
965
|
+
credentialMutationEpoch: mutationEpoch,
|
|
966
|
+
credentials,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
934
969
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
970
|
+
for (const observation of observations) {
|
|
971
|
+
if (observation.credential) continue;
|
|
972
|
+
const capturedIdentity = identityVector.get(observation.accountId) ?? null;
|
|
973
|
+
if (codexCredentialMutationEpoch() !== mutationEpoch) continue;
|
|
974
|
+
if ((currentCredentialIdentity(observation.accountId) ?? null) !== capturedIdentity) continue;
|
|
975
|
+
boundedNegativeCredentialMemoSet(observation.accountId, {
|
|
976
|
+
credentialIdentity: capturedIdentity,
|
|
977
|
+
mutationEpoch,
|
|
978
|
+
expiresAt: observation.absenceObservedAt + MODEL_ROSTER_NEGATIVE_CREDENTIAL_TTL_MS,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
if (!workset.includes(MAIN_CODEX_ACCOUNT_ID)) return run(new Set<string>());
|
|
983
|
+
const admission = options.nativeMainCredentialAdmission ?? withNativeMainCredentialAdmission;
|
|
984
|
+
return admission(run);
|
|
946
985
|
}
|
|
947
986
|
|
|
948
987
|
function waitForEntitlementEnsureFlight(
|
|
@@ -1110,6 +1149,10 @@ export async function resolveCodexModelEntitlements(
|
|
|
1110
1149
|
? [...options.credentials].filter(credential => !options.excludeAccountIds?.has(credential.accountId))
|
|
1111
1150
|
: (await Promise.all(allowedAccountIds.map(accountId => credentialSnapshot(accountId, options))))
|
|
1112
1151
|
.filter((value): value is CodexModelEntitlementCredentialSnapshot => value !== null);
|
|
1152
|
+
// The credential phase is the only part the native-main lease fences. The
|
|
1153
|
+
// real snapshot releases it as soon as the main token settles; this boundary
|
|
1154
|
+
// release keeps the guarantee when a seam snapshot never invokes it.
|
|
1155
|
+
options.releaseNativeMainCredentialLease?.();
|
|
1113
1156
|
const results = await Promise.all(credentials.map(async credential => ({
|
|
1114
1157
|
credential,
|
|
1115
1158
|
result: await modelsForCredential(
|