@bitkyc08/opencodex 2.59.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 +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- 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-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- 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 +13 -151
- 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/account-extended.ts +4 -4
- 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 +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -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-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- 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/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- 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/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- 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/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- 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 +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- 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/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- 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/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -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/legacy-dotted-tool-name-repair.ts +134 -0
- 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/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- 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 +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- 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 +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- 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/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- 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 +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- 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 +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- 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 +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -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 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -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/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
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;
|
|
@@ -71,6 +72,8 @@ function deleteCachedProvider(provider: string): number {
|
|
|
71
72
|
cache.delete(provider);
|
|
72
73
|
cacheBytes = Math.max(0, cacheBytes - entry.sizeBytes);
|
|
73
74
|
if (oldestCachedProvider === provider) recomputeOldestCachedProvider();
|
|
75
|
+
// A removal changes this provider's content as surely as a publication does.
|
|
76
|
+
bumpProviderCacheRevision(provider);
|
|
74
77
|
return entry.sizeBytes;
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -78,7 +81,17 @@ function deleteCachedProvider(provider: string): number {
|
|
|
78
81
|
* the full fetch timeout on every catalog poll (issue #54: UI stalls behind corporate proxies). */
|
|
79
82
|
export const MODELS_FETCH_FAILURE_COOLDOWN_MS = 30_000;
|
|
80
83
|
|
|
81
|
-
|
|
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>();
|
|
82
95
|
const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
|
|
83
96
|
/**
|
|
84
97
|
* How many models the last successful discovery actually returned, before any configured-alias
|
|
@@ -89,8 +102,12 @@ const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
|
|
|
89
102
|
const liveModelCounts = new Map<string, number>();
|
|
90
103
|
let lastReconciledGeneration = 0;
|
|
91
104
|
|
|
92
|
-
export function markModelsFetchFailure(
|
|
93
|
-
|
|
105
|
+
export function markModelsFetchFailure(
|
|
106
|
+
provider: string,
|
|
107
|
+
now = Date.now(),
|
|
108
|
+
authorityIdentity?: string,
|
|
109
|
+
): void {
|
|
110
|
+
failureAt.set(provider, { at: now, ...(authorityIdentity ? { authorityIdentity } : {}) });
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
/** `liveModelCount` is required so a caller that forgets to pass it fails typecheck instead of
|
|
@@ -145,9 +162,25 @@ export function getProviderLiveModelCount(provider: string): number | undefined
|
|
|
145
162
|
return liveModelCounts.get(provider);
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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;
|
|
151
184
|
}
|
|
152
185
|
|
|
153
186
|
/** Fresh cached models for a provider, or null when absent/stale (caller should re-fetch). */
|
|
@@ -166,6 +199,20 @@ export function getStaleCached(provider: string, authorityIdentity?: string): Ca
|
|
|
166
199
|
return entry.models;
|
|
167
200
|
}
|
|
168
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
|
+
|
|
169
216
|
/** Capture the cache generation before an asynchronous provider discovery starts. */
|
|
170
217
|
export function captureModelCacheGeneration(provider: string): string {
|
|
171
218
|
if (!providerCacheGenerations.has(provider)) providerCacheGenerations.set(provider, 0);
|
|
@@ -177,6 +224,34 @@ export function isModelCacheGenerationCurrent(provider: string, generation: stri
|
|
|
177
224
|
return generation === captureModelCacheGeneration(provider);
|
|
178
225
|
}
|
|
179
226
|
|
|
227
|
+
/**
|
|
228
|
+
* How many times this provider's cached content has actually changed.
|
|
229
|
+
*
|
|
230
|
+
* Deliberately separate from the generation. A generation revokes an in-flight discovery's right
|
|
231
|
+
* to publish, so it advances on an authority clear and must not be repurposed: moving it on a
|
|
232
|
+
* successful publication would cancel writes that are still legitimate. This counts accepted
|
|
233
|
+
* publications and removals instead, which is what a reader holding a derived roster needs to
|
|
234
|
+
* know, and it advances on exactly the event a generation does not: a discovery that succeeded
|
|
235
|
+
* and changed the rows.
|
|
236
|
+
*
|
|
237
|
+
* Reading is passive. An unseen provider reads as `0` rather than seeding an entry, so observing
|
|
238
|
+
* one cannot alter what a later capture or publication sees.
|
|
239
|
+
*/
|
|
240
|
+
const providerCacheRevisions = new Map<string, number>();
|
|
241
|
+
let globalContentRevision = 0;
|
|
242
|
+
|
|
243
|
+
function bumpProviderCacheRevision(provider: string): void {
|
|
244
|
+
providerCacheRevisions.set(provider, (providerCacheRevisions.get(provider) ?? 0) + 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function observeModelCacheRevision(provider: string): string {
|
|
248
|
+
// The global term covers a clear that empties the map wholesale, which per-provider counters
|
|
249
|
+
// cannot express: without it, wiping every entry and republishing identical-looking rows would
|
|
250
|
+
// read as unchanged. It also survives pruning, so a retired provider cannot come back with a
|
|
251
|
+
// counter that matches a roster built before it left.
|
|
252
|
+
return `${globalContentRevision}:${providerCacheRevisions.get(provider) ?? 0}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
180
255
|
/**
|
|
181
256
|
* Store a live result unless the cache was cleared while that asynchronous discovery was running.
|
|
182
257
|
* The optional generation keeps existing direct cache writers unchanged while discovery callers can
|
|
@@ -200,6 +275,8 @@ export function setCached(
|
|
|
200
275
|
oldestCachedAt = now;
|
|
201
276
|
}
|
|
202
277
|
enforceAppOwnedMemoryBudget();
|
|
278
|
+
// Published and accepted, so anything derived from this provider's rows is now out of date.
|
|
279
|
+
bumpProviderCacheRevision(provider);
|
|
203
280
|
return true;
|
|
204
281
|
}
|
|
205
282
|
|
|
@@ -208,6 +285,7 @@ export function clearModelCache(
|
|
|
208
285
|
provider?: string,
|
|
209
286
|
reason: ModelCacheClearReason = "authority",
|
|
210
287
|
): void {
|
|
288
|
+
clearLiveCursorRosterState(provider);
|
|
211
289
|
const revokesInFlightDiscovery = reason === "authority";
|
|
212
290
|
if (provider) {
|
|
213
291
|
if (revokesInFlightDiscovery) {
|
|
@@ -220,6 +298,11 @@ export function clearModelCache(
|
|
|
220
298
|
} else {
|
|
221
299
|
if (revokesInFlightDiscovery) globalCacheGeneration += 1;
|
|
222
300
|
cache.clear();
|
|
301
|
+
// A wholesale clear changes every provider's content at once, and clearing the map means no
|
|
302
|
+
// per-provider counter can record it. Advancing the global term retires every derived roster
|
|
303
|
+
// and lets the per-provider entries be dropped without an ABA on the way back.
|
|
304
|
+
globalContentRevision += 1;
|
|
305
|
+
providerCacheRevisions.clear();
|
|
223
306
|
cacheBytes = 0;
|
|
224
307
|
oldestCachedProvider = undefined;
|
|
225
308
|
oldestCachedAt = null;
|
|
@@ -237,6 +320,7 @@ export function reconcileModelCacheProviders(
|
|
|
237
320
|
const removedProviders = new Set<string>();
|
|
238
321
|
const trackedProviders = new Set([
|
|
239
322
|
...providerCacheGenerations.keys(),
|
|
323
|
+
...providerCacheRevisions.keys(),
|
|
240
324
|
...failureAt.keys(),
|
|
241
325
|
...discoveryStatus.keys(),
|
|
242
326
|
...liveModelCounts.keys(),
|
|
@@ -247,6 +331,10 @@ export function reconcileModelCacheProviders(
|
|
|
247
331
|
if (validProviders.has(provider)) continue;
|
|
248
332
|
if (!revokedRemovedProviderAuthority) {
|
|
249
333
|
globalCacheGeneration += 1;
|
|
334
|
+
// Advanced BEFORE any revision entry is dropped, so a provider that comes back cannot
|
|
335
|
+
// present the counter a roster was built against. Without this the delete below is an ABA:
|
|
336
|
+
// the entry returns at zero and an old stamp matches again.
|
|
337
|
+
globalContentRevision += 1;
|
|
250
338
|
revokedRemovedProviderAuthority = true;
|
|
251
339
|
}
|
|
252
340
|
providerCacheGenerations.set(provider, (providerCacheGenerations.get(provider) ?? 0) + 1);
|
|
@@ -255,6 +343,11 @@ export function reconcileModelCacheProviders(
|
|
|
255
343
|
failureAt.delete(provider);
|
|
256
344
|
discoveryStatus.delete(provider);
|
|
257
345
|
liveModelCounts.delete(provider);
|
|
346
|
+
// AFTER the cache deletion, which bumps this provider's revision and would otherwise recreate
|
|
347
|
+
// the entry we just removed. Dropped rather than left behind: a provider the configuration no
|
|
348
|
+
// longer has must not keep an entry alive for the life of the process merely because nothing
|
|
349
|
+
// cleared the whole cache. The global epoch advanced above, so the removal is not an ABA.
|
|
350
|
+
providerCacheRevisions.delete(provider);
|
|
258
351
|
removedProviders.add(provider);
|
|
259
352
|
}
|
|
260
353
|
lastReconciledGeneration = generation;
|
|
@@ -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
|
+
}
|
|
@@ -2,13 +2,18 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
3
3
|
import type { CodexAccountCredentialRecord, OcxConfig } from "../types";
|
|
4
4
|
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
beginCodexAccountGenerationLiveCheck,
|
|
7
|
+
getValidCodexToken,
|
|
8
|
+
loadCodexAccountRecordSnapshot,
|
|
9
|
+
} from "./account-store";
|
|
6
10
|
import {
|
|
7
11
|
getMainAccountToken,
|
|
8
12
|
getValidMainAccountToken,
|
|
9
13
|
MAIN_CODEX_ACCOUNT_ID,
|
|
10
14
|
type NativeMainRefreshDependencies,
|
|
11
15
|
} from "./main-account";
|
|
16
|
+
import { withNativeMainCredentialAdmission } from "./native-main-admission";
|
|
12
17
|
import {
|
|
13
18
|
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
14
19
|
NATIVE_GPT6_ASTRA_MODEL,
|
|
@@ -22,6 +27,7 @@ import {
|
|
|
22
27
|
forgetObservedCodexModelDenialsForAccount,
|
|
23
28
|
observedDeniedCodexAccountIdsForModel,
|
|
24
29
|
recordObservedCodexModelDenial,
|
|
30
|
+
setObservedDenialGenerationCheck,
|
|
25
31
|
resetObservedCodexModelDenialsForTests,
|
|
26
32
|
} from "./observed-model-denials";
|
|
27
33
|
|
|
@@ -434,6 +440,12 @@ export interface CodexModelEntitlementResolveOptions {
|
|
|
434
440
|
readonly excludeAccountIds?: ReadonlySet<string>;
|
|
435
441
|
/** Ensure-only fence; ordinary request resolvers retain their established flight identity. */
|
|
436
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;
|
|
437
449
|
}
|
|
438
450
|
|
|
439
451
|
export interface CodexEntitlementFreshnessOptions extends Pick<
|
|
@@ -447,6 +459,8 @@ export interface CodexEntitlementFreshnessOptions extends Pick<
|
|
|
447
459
|
| "signal"
|
|
448
460
|
> {
|
|
449
461
|
readonly waitMs?: number;
|
|
462
|
+
/** Test seam for the native-main admission fence around the refresh workset. */
|
|
463
|
+
readonly nativeMainCredentialAdmission?: typeof withNativeMainCredentialAdmission;
|
|
450
464
|
}
|
|
451
465
|
|
|
452
466
|
const accountModelsCache = new Map<string, CachedAccountModels>();
|
|
@@ -576,21 +590,31 @@ function currentCredentialIdentity(accountId: string): string | undefined {
|
|
|
576
590
|
|
|
577
591
|
async function accountCredentialSnapshot(
|
|
578
592
|
accountId: string,
|
|
579
|
-
options: Pick<
|
|
593
|
+
options: Pick<
|
|
594
|
+
CodexModelEntitlementResolveOptions,
|
|
595
|
+
"nativeMainRefreshDependencies" | "releaseNativeMainCredentialLease" | "signal"
|
|
596
|
+
> = {},
|
|
580
597
|
): Promise<CodexModelEntitlementCredentialSnapshot | null> {
|
|
581
598
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
+
}
|
|
594
618
|
}
|
|
595
619
|
try {
|
|
596
620
|
const token = await getValidCodexToken(accountId);
|
|
@@ -906,38 +930,58 @@ async function refreshCodexEntitlementWorkset(
|
|
|
906
930
|
mutationEpoch: number,
|
|
907
931
|
options: CodexEntitlementFreshnessOptions,
|
|
908
932
|
): Promise<void> {
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
+
}
|
|
929
969
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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);
|
|
941
985
|
}
|
|
942
986
|
|
|
943
987
|
function waitForEntitlementEnsureFlight(
|
|
@@ -1105,6 +1149,10 @@ export async function resolveCodexModelEntitlements(
|
|
|
1105
1149
|
? [...options.credentials].filter(credential => !options.excludeAccountIds?.has(credential.accountId))
|
|
1106
1150
|
: (await Promise.all(allowedAccountIds.map(accountId => credentialSnapshot(accountId, options))))
|
|
1107
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?.();
|
|
1108
1156
|
const results = await Promise.all(credentials.map(async credential => ({
|
|
1109
1157
|
credential,
|
|
1110
1158
|
result: await modelsForCredential(
|
|
@@ -1297,13 +1345,14 @@ export function cachedDeniedCodexAccountIdsForModel(
|
|
|
1297
1345
|
// absent an ongoing catalog sync the loop above contributes nothing at all. An upstream
|
|
1298
1346
|
// refusal does not expire on that schedule and is not a snapshot of a pending answer: it is
|
|
1299
1347
|
// the account's own Codex surface naming this model and declining it (#4906).
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1348
|
+
// The caller's read fence is passed IN rather than applied to the result, so an excluded
|
|
1349
|
+
// account is skipped before the credential-generation validation reads account storage
|
|
1350
|
+
// (#4952). An excluded account must stay UNKNOWN rather than denied, so a profile switch or
|
|
1351
|
+
// a request-owned credential produces the same selection it does today.
|
|
1352
|
+
const observedDenied = observedDeniedCodexAccountIdsForModel(modelId, now, {
|
|
1353
|
+
...(options.excludeAccountIds ? { excludeAccountIds: options.excludeAccountIds } : {}),
|
|
1354
|
+
});
|
|
1355
|
+
for (const accountId of observedDenied ?? []) denied.add(accountId);
|
|
1307
1356
|
// One account holds one entry per client version, and upstream filters the roster by that
|
|
1308
1357
|
// version. So the same account can legitimately carry a granted entry under a current client
|
|
1309
1358
|
// and a denied one under an older client that predates the model. Positive evidence is
|
|
@@ -1330,23 +1379,36 @@ export function cachedDeniedCodexAccountIdsForModel(
|
|
|
1330
1379
|
* admissible here: 400 covers every malformed request too, and remembering one of those as an
|
|
1331
1380
|
* entitlement fact would steer routing away from a perfectly capable account.
|
|
1332
1381
|
*/
|
|
1382
|
+
// Denial evidence is credential-scoped (#4952). The store stays a leaf module, so the
|
|
1383
|
+
// liveness predicate is injected here, where the account store is already a dependency.
|
|
1384
|
+
setObservedDenialGenerationCheck(beginCodexAccountGenerationLiveCheck);
|
|
1385
|
+
|
|
1333
1386
|
export function recordCodexModelDenialEvidence(
|
|
1334
1387
|
accountId: string | null | undefined,
|
|
1335
1388
|
modelId: string | undefined,
|
|
1389
|
+
generation: number | null | undefined,
|
|
1336
1390
|
now = Date.now(),
|
|
1337
1391
|
): void {
|
|
1338
1392
|
if (!accountId || !modelId) return;
|
|
1339
1393
|
if (!ENTITLEMENT_PREFERRED_NATIVE_OPENAI_MODELS.has(modelId)) return;
|
|
1340
|
-
|
|
1394
|
+
// A caller that cannot name a credential generation records ACCOUNT-scoped evidence rather
|
|
1395
|
+
// than none. The one production context in that position is `main-pool`, whose credential
|
|
1396
|
+
// lives in `auth.json` and has no pool generation; discarding its refusals would revert
|
|
1397
|
+
// #4906 for the stored main login (#4952). Request-owned `main` never reaches here — its
|
|
1398
|
+
// `accountId` is null and the guard above returns.
|
|
1399
|
+
recordObservedCodexModelDenial(accountId, modelId, typeof generation === "number" ? generation : undefined, now);
|
|
1341
1400
|
}
|
|
1342
1401
|
|
|
1343
1402
|
/** Drop the refusal evidence for a pair the account has just served successfully. */
|
|
1344
1403
|
export function clearCodexModelDenialEvidence(
|
|
1345
1404
|
accountId: string | null | undefined,
|
|
1346
1405
|
modelId: string | undefined,
|
|
1406
|
+
generation: number | null | undefined,
|
|
1347
1407
|
): void {
|
|
1348
1408
|
if (!accountId || !modelId) return;
|
|
1349
|
-
|
|
1409
|
+
// Mirror of the write: an account-scoped success clears account-scoped evidence. It cannot
|
|
1410
|
+
// clear a credential-scoped entry that names a newer generation, and vice versa (#4952).
|
|
1411
|
+
clearObservedCodexModelDenial(accountId, modelId, typeof generation === "number" ? generation : undefined);
|
|
1350
1412
|
}
|
|
1351
1413
|
|
|
1352
1414
|
/** Synchronous projection for management/catalog readers after a discovery pass. */
|
|
@@ -3,7 +3,13 @@ import {
|
|
|
3
3
|
tryAcquireNativeMainProfileClaim as tryAcquireLifecycleNativeMainProfileClaim,
|
|
4
4
|
tryClaimNativeMainProfileForTurn as tryClaimLifecycleNativeMainProfileForTurn,
|
|
5
5
|
} from "../server/lifecycle";
|
|
6
|
+
import {
|
|
7
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
8
|
+
MainAccountTokenRefreshError,
|
|
9
|
+
MainAuthJsonChangedDuringRefreshError,
|
|
10
|
+
} from "./main-account";
|
|
6
11
|
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
12
|
+
import { NativeProfileError } from "./native-profile-types";
|
|
7
13
|
|
|
8
14
|
export interface NativeMainTurnClaimDeps {
|
|
9
15
|
/** Test seams for the synchronous precheck/claim/postcheck transition. */
|
|
@@ -45,3 +51,80 @@ export function tryAcquireNativeMainProfileClaim(): AdmissionLease | null {
|
|
|
45
51
|
claim.release();
|
|
46
52
|
return null;
|
|
47
53
|
}
|
|
54
|
+
|
|
55
|
+
export interface NativeMainCredentialAdmissionDeps {
|
|
56
|
+
/** Test seam for the synchronous admission precheck. */
|
|
57
|
+
readonly acquireNativeMain?: () => AdmissionLease | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const NO_EXCLUDED_ACCOUNT_IDS: ReadonlySet<string> = new Set();
|
|
61
|
+
const NATIVE_MAIN_EXCLUDED_ACCOUNT_IDS: ReadonlySet<string> = new Set([MAIN_CODEX_ACCOUNT_ID]);
|
|
62
|
+
const RELEASE_NOTHING = () => {};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run credential-backed work inside the native-main lifecycle fence.
|
|
66
|
+
*
|
|
67
|
+
* The lease keeps startup recovery and profile drains from owning the physical
|
|
68
|
+
* credential while the operation reads it. Cross-process ownership of the file
|
|
69
|
+
* itself is already coordinated inside the refresh path's exclusive claim, so
|
|
70
|
+
* this fence deliberately does not take the shared claim: holding it across an
|
|
71
|
+
* operation that may refresh would ask for exclusive ownership against our own
|
|
72
|
+
* shared lock, and holding it across the upstream work that follows would
|
|
73
|
+
* stall an unrelated credential commit behind a network fetch.
|
|
74
|
+
*
|
|
75
|
+
* The fence covers only the credential read. The operation must invoke
|
|
76
|
+
* `releaseMainLease` as soon as the native-main credential settles — on both
|
|
77
|
+
* the success and the credential-error path — and before any upstream model
|
|
78
|
+
* listing, so a profile drain never waits on a network fetch while this turn
|
|
79
|
+
* is still counted. The wrapper releases on settle regardless, so an
|
|
80
|
+
* operation without a fenced credential phase may ignore the callback.
|
|
81
|
+
*
|
|
82
|
+
* When the gate refuses, or the credential cannot be read because another
|
|
83
|
+
* lifecycle owns it, the operation reruns with main excluded so independent
|
|
84
|
+
* Pool work is never suppressed by main's unavailability.
|
|
85
|
+
*/
|
|
86
|
+
export async function withNativeMainCredentialAdmission<T>(
|
|
87
|
+
operation: (
|
|
88
|
+
excludeAccountIds: ReadonlySet<string>,
|
|
89
|
+
releaseMainLease?: () => void,
|
|
90
|
+
) => Promise<T>,
|
|
91
|
+
deps: NativeMainCredentialAdmissionDeps = {},
|
|
92
|
+
): Promise<T> {
|
|
93
|
+
const lease = (deps.acquireNativeMain ?? tryAcquireNativeMainProfileClaim)();
|
|
94
|
+
if (!lease) return operation(NATIVE_MAIN_EXCLUDED_ACCOUNT_IDS, RELEASE_NOTHING);
|
|
95
|
+
let released = false;
|
|
96
|
+
const releaseMainLease = () => {
|
|
97
|
+
if (released) return;
|
|
98
|
+
released = true;
|
|
99
|
+
lease.release();
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
return await operation(NO_EXCLUDED_ACCOUNT_IDS, releaseMainLease);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// The Pool-only retry never reads the native-main credential; release first
|
|
105
|
+
// so a profile drain is not kept waiting behind Pool network work.
|
|
106
|
+
releaseMainLease();
|
|
107
|
+
if (!isNativeMainCredentialUnavailableError(error)) throw error;
|
|
108
|
+
return await operation(NATIVE_MAIN_EXCLUDED_ACCOUNT_IDS, RELEASE_NOTHING);
|
|
109
|
+
} finally {
|
|
110
|
+
releaseMainLease();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The credential-ownership failures that make main unavailable for one
|
|
116
|
+
* operation: a foreign exclusive holder or an unsupported claim filesystem
|
|
117
|
+
* (NATIVE_MAIN_CLAIM_BUSY / NATIVE_MAIN_CLAIM_UNAVAILABLE), a writer that moved
|
|
118
|
+
* auth.json mid-refresh, or a grant that no longer refreshes. Like a Pool
|
|
119
|
+
* credential failure, none of them may suppress independent Pool discovery.
|
|
120
|
+
* Any other NativeProfileError — MAIN_REQUESTS_ACTIVE, VAULT_INVALID,
|
|
121
|
+
* INTERNAL_ERROR — is not a credential-ownership failure and propagates.
|
|
122
|
+
*/
|
|
123
|
+
function isNativeMainCredentialUnavailableError(error: unknown): boolean {
|
|
124
|
+
if (error instanceof NativeProfileError) {
|
|
125
|
+
return error.code === "NATIVE_MAIN_CLAIM_BUSY"
|
|
126
|
+
|| error.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE";
|
|
127
|
+
}
|
|
128
|
+
return error instanceof MainAuthJsonChangedDuringRefreshError
|
|
129
|
+
|| error instanceof MainAccountTokenRefreshError;
|
|
130
|
+
}
|