@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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
clearModelCache,
|
|
12
12
|
clearProviderDiscoveryStatus,
|
|
13
13
|
captureModelCacheGeneration,
|
|
14
|
+
observeModelCacheRevision,
|
|
14
15
|
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
15
16
|
getFreshCached,
|
|
16
17
|
getStaleCached,
|
|
@@ -51,7 +52,7 @@ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../
|
|
|
51
52
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
52
53
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
53
54
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
54
|
-
import { recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
|
|
55
|
+
import { cursorLiveRosterScope, recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
|
|
55
56
|
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
56
57
|
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
57
58
|
import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
|
|
@@ -108,6 +109,14 @@ import { captureProviderGather, materializeCapturedHeaders } from "./gather-capt
|
|
|
108
109
|
export interface ProviderModelsResult {
|
|
109
110
|
readonly models: CatalogModel[];
|
|
110
111
|
readonly outcome: CatalogGatherProviderModelOutcome;
|
|
112
|
+
/**
|
|
113
|
+
* The provider's cache content revision as it stood when these rows were chosen.
|
|
114
|
+
*
|
|
115
|
+
* Sampled here rather than by the caller because only this point is synchronous with the
|
|
116
|
+
* choice. A caller reading it after its own await can observe a revision another flight
|
|
117
|
+
* published in between, and would then record these rows under that flight's identity.
|
|
118
|
+
*/
|
|
119
|
+
readonly contentRevision: string;
|
|
111
120
|
}
|
|
112
121
|
export const refreshingModelsAuthResolver: ModelsAuthResolver = { kind: "refreshing" };
|
|
113
122
|
|
|
@@ -145,7 +154,11 @@ export async function fetchProviderModelsWithAuth(
|
|
|
145
154
|
const observed = (
|
|
146
155
|
models: CatalogModel[],
|
|
147
156
|
state: CatalogGatherProviderModelOutcome["state"],
|
|
148
|
-
): ProviderModelsResult => ({
|
|
157
|
+
): ProviderModelsResult => ({
|
|
158
|
+
models,
|
|
159
|
+
outcome: { provider: name, state },
|
|
160
|
+
contentRevision: observeModelCacheRevision(name),
|
|
161
|
+
});
|
|
149
162
|
// Capture before any credential refresh or outbound await. OAuth account changes clear this
|
|
150
163
|
// generation, so a request started with the former account cannot later publish its result.
|
|
151
164
|
const cacheGeneration = captureModelCacheGeneration(name);
|
|
@@ -276,15 +289,19 @@ export async function fetchProviderModelsWithAuth(
|
|
|
276
289
|
}
|
|
277
290
|
if (prov.adapter === "devin") {
|
|
278
291
|
if (!apiKey) return observed(configured, "degraded");
|
|
279
|
-
|
|
292
|
+
// Devin's usable-model list is entitlement-specific. Bind cache reads/writes to an
|
|
293
|
+
// irreversible credential fingerprint so a credential switch cannot observe another
|
|
294
|
+
// account's roster or stale fallback (the Qoder precedent above).
|
|
295
|
+
const authorityIdentity = createHash("sha256").update(apiKey).digest("hex");
|
|
296
|
+
const cachedDevin = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
|
|
280
297
|
if (cachedDevin) {
|
|
281
298
|
return observed(
|
|
282
299
|
withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedDevin)),
|
|
283
300
|
"authoritative",
|
|
284
301
|
);
|
|
285
302
|
}
|
|
286
|
-
if (isModelsFetchCoolingDown(name)) {
|
|
287
|
-
const cooling = getStaleCached(name);
|
|
303
|
+
if (isModelsFetchCoolingDown(name, undefined, undefined, authorityIdentity)) {
|
|
304
|
+
const cooling = getStaleCached(name, authorityIdentity);
|
|
288
305
|
return observed(
|
|
289
306
|
withConfiguredRetention(
|
|
290
307
|
cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured,
|
|
@@ -326,17 +343,17 @@ export async function fetchProviderModelsWithAuth(
|
|
|
326
343
|
} as CatalogModel;
|
|
327
344
|
});
|
|
328
345
|
const forCache = withConfiguredRetention(result, { retainComboTargets: false });
|
|
329
|
-
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
346
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
|
|
330
347
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
331
348
|
}
|
|
332
349
|
markProviderDiscoveryOk(name, liveResult.models.length);
|
|
333
350
|
return observed(withConfiguredRetention(forCache), "authoritative");
|
|
334
351
|
}
|
|
335
352
|
if (isCurrentCacheGeneration()) {
|
|
336
|
-
markModelsFetchFailure(name);
|
|
353
|
+
markModelsFetchFailure(name, undefined, authorityIdentity);
|
|
337
354
|
markProviderDiscoveryFailed(name, { reason: liveResult.error === "auth" ? "provider" : "invalid_response" });
|
|
338
355
|
}
|
|
339
|
-
const stale = getStaleCached(name);
|
|
356
|
+
const stale = getStaleCached(name, authorityIdentity);
|
|
340
357
|
return observed(
|
|
341
358
|
withConfiguredRetention(stale ? applyConfigHintsToCachedModels(name, prov, stale) : configured),
|
|
342
359
|
"degraded",
|
|
@@ -348,15 +365,19 @@ export async function fetchProviderModelsWithAuth(
|
|
|
348
365
|
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
|
|
349
366
|
// suffix) but filter the static seed to the bases the account actually has — so models not on the
|
|
350
367
|
// plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
|
|
351
|
-
|
|
368
|
+
// The roster is entitlement-specific, so the cache entry is bound to an irreversible
|
|
369
|
+
// credential fingerprint: a credential switch must not observe the previous account's
|
|
370
|
+
// plan roster, its stale fallback, or its failure cooldown suppression.
|
|
371
|
+
const authorityIdentity = createHash("sha256").update(apiKey).digest("hex");
|
|
372
|
+
const cachedCursor = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
|
|
352
373
|
if (cachedCursor) {
|
|
353
374
|
return observed(
|
|
354
375
|
withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias)),
|
|
355
376
|
"authoritative",
|
|
356
377
|
);
|
|
357
378
|
}
|
|
358
|
-
if (isModelsFetchCoolingDown(name)) {
|
|
359
|
-
const cooling = getStaleCached(name);
|
|
379
|
+
if (isModelsFetchCoolingDown(name, undefined, undefined, authorityIdentity)) {
|
|
380
|
+
const cooling = getStaleCached(name, authorityIdentity);
|
|
360
381
|
return observed(
|
|
361
382
|
withConfiguredRetention(
|
|
362
383
|
cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
|
|
@@ -372,32 +393,37 @@ export async function fetchProviderModelsWithAuth(
|
|
|
372
393
|
...(cursorFetch ? { fetch: cursorFetch } : {}),
|
|
373
394
|
});
|
|
374
395
|
if (liveResult.ok) {
|
|
375
|
-
const available = filterCursorConfiguredModelsByLiveDiscovery(
|
|
396
|
+
const available = filterCursorConfiguredModelsByLiveDiscovery(
|
|
397
|
+
configured,
|
|
398
|
+
liveResult.models,
|
|
399
|
+
liveResult.maxModeModels ?? [],
|
|
400
|
+
);
|
|
376
401
|
const result = available.length > 0 ? available : configured;
|
|
377
402
|
// Cache the discovery-filtered roster without combo retention so a later
|
|
378
403
|
// gather can re-apply the current capture's retain set on read.
|
|
379
404
|
const forCache = withConfiguredRetention(result, { retainComboTargets: false });
|
|
380
|
-
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
405
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
|
|
381
406
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
382
407
|
}
|
|
383
408
|
// Publish roster-derived state only for a discovery the cache accepted: a stale
|
|
384
409
|
// in-flight capture (generation revoked by a credential/config change) must not
|
|
385
410
|
// overwrite the spelling or Max-Mode evidence of the newer one.
|
|
386
|
-
|
|
411
|
+
const liveRosterScope = { provider: name, key: cursorLiveRosterScope(prov.baseUrl, apiKey) };
|
|
412
|
+
recordLiveCursorClaudeModels(liveResult.models, liveRosterScope);
|
|
387
413
|
// Live Max-Mode evidence feeds the umbrella resolver's ultra gate
|
|
388
414
|
// (devlog 260828_cursor_umbrella_catalog; union with static evidence).
|
|
389
|
-
recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? []);
|
|
415
|
+
recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? [], liveRosterScope);
|
|
390
416
|
markProviderDiscoveryOk(name, liveResult.models.length);
|
|
391
417
|
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
392
418
|
}
|
|
393
419
|
if (isCurrentCacheGeneration()) {
|
|
394
|
-
markModelsFetchFailure(name);
|
|
420
|
+
markModelsFetchFailure(name, undefined, authorityIdentity);
|
|
395
421
|
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
396
422
|
console.warn(
|
|
397
423
|
`[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
|
|
398
424
|
);
|
|
399
425
|
}
|
|
400
|
-
const staleCursor = getStaleCached(name);
|
|
426
|
+
const staleCursor = getStaleCached(name, authorityIdentity);
|
|
401
427
|
return observed(
|
|
402
428
|
withConfiguredRetention(
|
|
403
429
|
staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
availableAccountGatedNativeModels,
|
|
16
16
|
codexModelEntitlementStateForAccount,
|
|
17
17
|
isCodexModelEntitlementSnapshotCurrent,
|
|
18
|
-
resolveCodexModelEntitlements,
|
|
19
18
|
type CodexModelEntitlementSnapshot,
|
|
20
19
|
} from "../model-entitlements";
|
|
20
|
+
import { resolveAdmittedCodexModelEntitlements } from "../model-entitlement-admission";
|
|
21
21
|
import { isAccountNeedsReauth } from "../account-runtime-state";
|
|
22
22
|
import { codexRuntimeStatePath } from "../runtime";
|
|
23
23
|
import {
|
|
@@ -59,10 +59,12 @@ import {
|
|
|
59
59
|
type CatalogWritePermit,
|
|
60
60
|
} from "../catalog-write-serialization";
|
|
61
61
|
import {
|
|
62
|
+
preparedBytesDifferFromDisk,
|
|
62
63
|
publishHashedCodexCatalogBackup,
|
|
63
64
|
publishLegacyCodexCatalogBackup,
|
|
64
65
|
replaceActiveCodexCatalog,
|
|
65
66
|
replaceCodexModelsCache,
|
|
67
|
+
type PreparedCatalogFileWrite,
|
|
66
68
|
} from "../internal/catalog-writer";
|
|
67
69
|
import { visibleCodexAccountSelectors } from "./account-models";
|
|
68
70
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
|
|
@@ -234,24 +236,6 @@ function revalidateRetainedCatalogSync(
|
|
|
234
236
|
};
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
/**
|
|
238
|
-
* Exact bytes currently on disk at `path`, or null when unreadable/absent.
|
|
239
|
-
*
|
|
240
|
-
* Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
|
|
241
|
-
* substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
|
|
242
|
-
* equal to prepared content holding a legitimately encoded U+FFFD. Comparing the
|
|
243
|
-
* decoded strings would then classify a malformed catalog as identical, skip the
|
|
244
|
-
* atomic repair write, and leave the corruption on disk while reporting
|
|
245
|
-
* `catalogWritten: false`.
|
|
246
|
-
*/
|
|
247
|
-
function currentCatalogFileContent(path: string): Buffer | null {
|
|
248
|
-
try {
|
|
249
|
-
return readFileSync(path);
|
|
250
|
-
} catch {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
239
|
function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
|
|
256
240
|
if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
|
|
257
241
|
try {
|
|
@@ -558,15 +542,12 @@ function writeRetainedCatalogSync({
|
|
|
558
542
|
// nothing about the catalog changed. Skipping the no-op write keeps both the mtime
|
|
559
543
|
// and `catalogWritten` honest; `added` still reports the routed rows the catalog
|
|
560
544
|
// carries, because they are on disk either way.
|
|
561
|
-
const
|
|
562
|
-
if (
|
|
545
|
+
const preparedCatalog: PreparedCatalogFileWrite = { path: catalogPath, content };
|
|
546
|
+
if (!preparedBytesDifferFromDisk(preparedCatalog)) {
|
|
563
547
|
return { added, path: catalogPath, catalogWritten: false, comboOmissions };
|
|
564
548
|
}
|
|
565
549
|
|
|
566
|
-
replaceActiveCodexCatalog(permit, owningCodexHome,
|
|
567
|
-
path: catalogPath,
|
|
568
|
-
content,
|
|
569
|
-
});
|
|
550
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
|
|
570
551
|
return {
|
|
571
552
|
added,
|
|
572
553
|
path: catalogPath,
|
|
@@ -620,7 +601,7 @@ export async function syncCatalogModels(
|
|
|
620
601
|
comboOmissions,
|
|
621
602
|
providerModelOutcomes,
|
|
622
603
|
}),
|
|
623
|
-
|
|
604
|
+
resolveAdmittedCodexModelEntitlements(config),
|
|
624
605
|
]);
|
|
625
606
|
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
626
607
|
// Desired state can flip OFF during the provider await above. The catalog
|
|
@@ -713,10 +694,25 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
713
694
|
client_version: "0.0.0",
|
|
714
695
|
models: [...models, ...observedAccountModels],
|
|
715
696
|
};
|
|
716
|
-
|
|
697
|
+
const preparedCache: PreparedCatalogFileWrite = {
|
|
717
698
|
path: cachePath,
|
|
718
699
|
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
719
|
-
}
|
|
700
|
+
};
|
|
701
|
+
// The same no-op rule the active catalog already applies (#1459), for the same
|
|
702
|
+
// reason and at the second writer that has to obey it.
|
|
703
|
+
//
|
|
704
|
+
// This function is what `refreshCodexModelCatalog` reports as `cacheSynced`, and
|
|
705
|
+
// `handleStart` ORs that into the stale-app-server warning. Rewriting identical
|
|
706
|
+
// bytes bumped this file's mtime and returned `true`, so on a start where the
|
|
707
|
+
// catalog reproduced byte-identically — the settled case — the warning still
|
|
708
|
+
// claimed "Disk catalog/cache were updated" and told the operator their Codex
|
|
709
|
+
// model list might be stale, when nothing on disk had changed and Codex held the
|
|
710
|
+
// same model set the file already described. Returning `false` here makes
|
|
711
|
+
// `cacheSynced` mean what its name and its consumers already assume, and what
|
|
712
|
+
// `pullRemoteCatalog` and the early returns in `refreshCodexModelCatalog`
|
|
713
|
+
// already assert: a write happened.
|
|
714
|
+
if (!preparedBytesDifferFromDisk(preparedCache)) return false;
|
|
715
|
+
replaceCodexModelsCache(permit, owningCodexHome, preparedCache);
|
|
720
716
|
return true;
|
|
721
717
|
} catch {
|
|
722
718
|
return false;
|
|
@@ -113,6 +113,15 @@ export interface GatherRoutedModelsOptions {
|
|
|
113
113
|
providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
|
|
114
114
|
/** Internal convergence sink for the immutable policy that produced the returned rows. */
|
|
115
115
|
discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
|
|
116
|
+
/**
|
|
117
|
+
* Each provider's cache content revision as of the moment its rows were chosen.
|
|
118
|
+
*
|
|
119
|
+
* A caller that derives something from these rows and wants to know later whether the rows are
|
|
120
|
+
* still current must use this, not a revision sampled after the gather returns: another flight
|
|
121
|
+
* can publish between the choice and the sample, and the derived value would then carry that
|
|
122
|
+
* flight's identity while holding these rows.
|
|
123
|
+
*/
|
|
124
|
+
providerContentRevisions?: Map<string, string>;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
interface GatherFlightResult {
|
|
@@ -121,6 +130,7 @@ interface GatherFlightResult {
|
|
|
121
130
|
providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
122
131
|
providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
123
132
|
discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
133
|
+
providerContentRevisions: ReadonlyMap<string, string>;
|
|
124
134
|
}
|
|
125
135
|
interface GatherInflightEntry {
|
|
126
136
|
readonly discoveryPolicyIdentity: string;
|
|
@@ -256,6 +266,7 @@ async function gatherRoutedModelsWithAuth(
|
|
|
256
266
|
providerAuthOutcomes,
|
|
257
267
|
providerModelOutcomes,
|
|
258
268
|
discoveryPolicySnapshots,
|
|
269
|
+
providerContentRevisions,
|
|
259
270
|
} = await entry.promise;
|
|
260
271
|
if (options?.comboOmissions) {
|
|
261
272
|
options.comboOmissions.length = 0;
|
|
@@ -269,6 +280,12 @@ async function gatherRoutedModelsWithAuth(
|
|
|
269
280
|
options.providerModelOutcomes.length = 0;
|
|
270
281
|
options.providerModelOutcomes.push(...providerModelOutcomes);
|
|
271
282
|
}
|
|
283
|
+
if (options?.providerContentRevisions) {
|
|
284
|
+
options.providerContentRevisions.clear();
|
|
285
|
+
for (const [provider, revision] of providerContentRevisions) {
|
|
286
|
+
options.providerContentRevisions.set(provider, revision);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
272
289
|
if (options?.discoveryPolicySnapshots) {
|
|
273
290
|
options.discoveryPolicySnapshots.length = 0;
|
|
274
291
|
options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
|
|
@@ -763,6 +780,8 @@ async function gatherRoutedModelsUncached(
|
|
|
763
780
|
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
764
781
|
providerModelOutcomes,
|
|
765
782
|
discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
|
|
783
|
+
// Stamped by each provider at the moment it chose its rows, not sampled here.
|
|
784
|
+
providerContentRevisions: new Map(providerResults.map(result => [result.outcome.provider, result.contentRevision])),
|
|
766
785
|
};
|
|
767
786
|
}
|
|
768
787
|
|
|
@@ -7,14 +7,17 @@ export const CONTEXT_BACKEND_PREFIX = "/backend-api/codex";
|
|
|
7
7
|
|
|
8
8
|
/** The single definition of the opt-in, shared by injection and the runtime gate. */
|
|
9
9
|
export function contextExperimentalEnabled(configContent: string): boolean {
|
|
10
|
-
let parsed: { features?: { context_management?: { experimental_mode?: boolean } } };
|
|
10
|
+
let parsed: { features?: { context_management?: boolean | { experimental_mode?: boolean } } };
|
|
11
11
|
try {
|
|
12
12
|
parsed = Bun.TOML.parse(configContent) as typeof parsed;
|
|
13
13
|
} catch {
|
|
14
14
|
// Injection tolerates incomplete user config; malformed TOML is not an opt-in.
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// Codex FeatureToml accepts either a boolean or the experimental_mode table.
|
|
18
|
+
// Keep strict true checks: malformed values must not activate a credentialed relay.
|
|
19
|
+
const setting = parsed.features?.context_management;
|
|
20
|
+
return setting === true || (typeof setting === "object" && setting?.experimental_mode === true);
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
let activation: { key: string; active: boolean } | undefined;
|
package/src/codex/convergence.ts
CHANGED
|
@@ -78,9 +78,9 @@ import {
|
|
|
78
78
|
availableAccountGatedNativeModels,
|
|
79
79
|
codexModelEntitlementStateForAccount,
|
|
80
80
|
isCodexModelEntitlementSnapshotCurrent,
|
|
81
|
-
resolveCodexModelEntitlements,
|
|
82
81
|
type CodexModelEntitlementSnapshot,
|
|
83
82
|
} from "./model-entitlements";
|
|
83
|
+
import { resolveAdmittedCodexModelEntitlements } from "./model-entitlement-admission";
|
|
84
84
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
85
85
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
86
86
|
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
@@ -434,7 +434,7 @@ export async function gatherCodexCatalogCandidate(
|
|
|
434
434
|
providerModelOutcomes,
|
|
435
435
|
discoveryPolicySnapshots: discoveryPolicies,
|
|
436
436
|
}),
|
|
437
|
-
|
|
437
|
+
resolveAdmittedCodexModelEntitlements(snapshot.config),
|
|
438
438
|
]);
|
|
439
439
|
const processLocal = processEvidence(source);
|
|
440
440
|
const sourceEvidence = sealCatalogGatherEvidenceSession(session);
|
|
@@ -260,7 +260,10 @@ export async function syncCodexOnStartIfEnabled(
|
|
|
260
260
|
// The `.catch` is deliberate and stays: a failure to APPLY must not stop the
|
|
261
261
|
// proxy from coming up. A failed sync simply reports no writes. The readiness
|
|
262
262
|
// gate observes the real outcome so /readyz reflects the sync state exactly as
|
|
263
|
-
// the
|
|
263
|
+
// the contract defines: ready on ok=true once the sync has settled. A nonempty
|
|
264
|
+
// `warning` is a local-Codex-artifact degradation the sync chose to continue
|
|
265
|
+
// past, and #5181 is what it cost to treat it as terminal — a healthy
|
|
266
|
+
// multi-provider proxy reported itself permanently unready.
|
|
264
267
|
const outcome = readinessGate
|
|
265
268
|
? await runStartupReadinessSync(readinessGate, async () => (await sync(port)) ?? null)
|
|
266
269
|
: await sync(port).catch(() => undefined);
|
package/src/codex/history-job.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
CodexHistoryWorkerOperation,
|
|
24
24
|
HistoryWorkerResult,
|
|
25
25
|
} from "./history-worker";
|
|
26
|
-
import { currentHistoryDbBusyTimeoutMs,
|
|
26
|
+
import { currentHistoryDbBusyTimeoutMs, resolveExistingHistoryBackupPath } from "./history-provider";
|
|
27
27
|
import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
|
|
28
28
|
import { getCodexHome, resolveCodexStateDbPath } from "./paths";
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ import { getCodexHome, resolveCodexStateDbPath } from "./paths";
|
|
|
32
32
|
*
|
|
33
33
|
* The SQLite root can differ from CODEX_HOME and both environment/config inputs
|
|
34
34
|
* can change between invocations. The parent resolves one exact target and hands
|
|
35
|
-
* those
|
|
35
|
+
* those resolved paths to the Worker rather than asking the Worker to infer a
|
|
36
36
|
* possibly different environment.
|
|
37
37
|
*/
|
|
38
38
|
export function resolveCodexHistoryJobTarget(): {
|
|
@@ -45,10 +45,10 @@ export function resolveCodexHistoryJobTarget(): {
|
|
|
45
45
|
return {
|
|
46
46
|
canonicalCodexHome: home,
|
|
47
47
|
canonicalStateDbPath: stateDb,
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
canonicalBackupPath:
|
|
48
|
+
// Resolve through the provider's canonical-first compatibility rule. Passing
|
|
49
|
+
// only the newly normalized name would hide a pre-#4442 Windows manifest from
|
|
50
|
+
// the Worker and make an upgrade look like an empty backup.
|
|
51
|
+
canonicalBackupPath: resolveExistingHistoryBackupPath(stateDb),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { parseSessionMetaLine, type ParsedSessionMeta } from "./history-rollout-read";
|
|
5
|
+
export { MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES, readThreadFieldsFromRollout, type RolloutThreadFields } from "./history-rollout-read";
|
|
5
6
|
import { Database } from "bun:sqlite";
|
|
6
7
|
import { resolveCodexStateDbPath } from "./paths";
|
|
7
8
|
import { openCodexStateForPreflight } from "./history-state-open";
|
|
@@ -16,13 +17,6 @@ import {
|
|
|
16
17
|
type CodexHistoryBackupManifest,
|
|
17
18
|
} from "./history-manifest";
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
21
|
-
* Bounds peak memory while reconstructing thread rows; never write the decoded
|
|
22
|
-
* JSONL to disk.
|
|
23
|
-
*/
|
|
24
|
-
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
25
|
-
|
|
26
20
|
/**
|
|
27
21
|
* The manifest that shadows one state database.
|
|
28
22
|
*
|
|
@@ -341,6 +335,17 @@ class CodexHistoryIntegrityError extends Error {
|
|
|
341
335
|
*/
|
|
342
336
|
export const HISTORY_RELABEL_STANDS_DOWN = "history_paginated_requires_native_writer";
|
|
343
337
|
|
|
338
|
+
/**
|
|
339
|
+
* The narrower reason: a provider-table transition found an `openai`-tagged row Codex has
|
|
340
|
+
* already paginated. It is not a plain stand-down, because the transition also takes the root
|
|
341
|
+
* `openai_base_url` out, and that combination would send the conversation to Codex's built-in
|
|
342
|
+
* OpenAI endpoint rather than this proxy.
|
|
343
|
+
*
|
|
344
|
+
* A constant for the same reason as the one above: `src/codex/inject/paginated-openai-compat.ts`
|
|
345
|
+
* decides what to do about it, and a literal repeated in two files is how the pair drifts apart.
|
|
346
|
+
*/
|
|
347
|
+
export const HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE = "history_paginated_openai_requires_native_writer";
|
|
348
|
+
|
|
344
349
|
function assertLegacyHistoryRecord(line: string): void {
|
|
345
350
|
let value: unknown;
|
|
346
351
|
try { value = JSON.parse(line); } catch { throw new CodexHistoryIntegrityError("history_rollout_record_invalid"); }
|
|
@@ -441,18 +446,32 @@ export function preflightCodexHistoryInjection(
|
|
|
441
446
|
const paginatedColumn = columns.some(column => column.name === "history_mode");
|
|
442
447
|
if (paginatedColumn && restoreEntries.length > 0) return HISTORY_RELABEL_STANDS_DOWN;
|
|
443
448
|
for (const entry of restoreEntries) assertLegacyHistoryWritable(entry.rolloutPath);
|
|
444
|
-
const rows = db.query<{ rollout_path: string; history_mode: string | null }, []>(`
|
|
445
|
-
SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}
|
|
449
|
+
const rows = db.query<{ rollout_path: string; history_mode: string | null; model_provider: string }, []>(`
|
|
450
|
+
SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}, model_provider
|
|
446
451
|
FROM threads
|
|
447
452
|
WHERE ${providerTableMode
|
|
448
453
|
? resumeHistory ? "model_provider IN ('openai', 'opencodex')" : "0"
|
|
449
454
|
: "model_provider = 'opencodex'"}
|
|
450
455
|
`).all();
|
|
456
|
+
// No ORDER BY: a paginated opencodex row can precede a paginated openai one, so the
|
|
457
|
+
// verdict waits for the full scan instead of standing down on the first paginated row.
|
|
458
|
+
let foundPaginatedRow = false;
|
|
459
|
+
let foundPaginatedOpenaiRow = false;
|
|
451
460
|
for (const row of rows) {
|
|
452
|
-
if (paginatedColumn || row.history_mode === "paginated")
|
|
461
|
+
if (paginatedColumn || row.history_mode === "paginated") {
|
|
462
|
+
foundPaginatedRow = true;
|
|
463
|
+
// A provider-table transition removes the root openai_base_url, and a row already
|
|
464
|
+
// paginated cannot be relabeled: standing it down would route the openai-tagged
|
|
465
|
+
// thread to Codex's built-in OpenAI endpoint. A still-legacy row only stands down.
|
|
466
|
+
if (providerTableMode && row.history_mode === "paginated" && row.model_provider === "openai") {
|
|
467
|
+
foundPaginatedOpenaiRow = true;
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
453
471
|
assertLegacyHistoryWritable(row.rollout_path);
|
|
454
472
|
}
|
|
455
|
-
return
|
|
473
|
+
if (foundPaginatedOpenaiRow) return HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE;
|
|
474
|
+
return foundPaginatedRow ? HISTORY_RELABEL_STANDS_DOWN : null;
|
|
456
475
|
} catch (error) {
|
|
457
476
|
return error instanceof CodexHistoryIntegrityError
|
|
458
477
|
? error.message
|
|
@@ -1021,24 +1040,6 @@ function assertRestoreReadback(
|
|
|
1021
1040
|
}
|
|
1022
1041
|
}
|
|
1023
1042
|
|
|
1024
|
-
interface ParsedSessionMeta {
|
|
1025
|
-
record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
|
|
1029
|
-
function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
1030
|
-
let parsed: unknown;
|
|
1031
|
-
try {
|
|
1032
|
-
parsed = JSON.parse(line);
|
|
1033
|
-
} catch {
|
|
1034
|
-
return null;
|
|
1035
|
-
}
|
|
1036
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
1037
|
-
const record = parsed as ParsedSessionMeta["record"];
|
|
1038
|
-
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
|
|
1039
|
-
return { record };
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1043
|
/**
|
|
1043
1044
|
* Find the LAST `session_meta` line in a rollout, mirroring the app's last-writer-wins fold
|
|
1044
1045
|
* (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
|
|
@@ -1107,142 +1108,6 @@ function compensateConcurrentSessionMetaAppend(
|
|
|
1107
1108
|
}
|
|
1108
1109
|
}
|
|
1109
1110
|
|
|
1110
|
-
/**
|
|
1111
|
-
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
1112
|
-
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
1113
|
-
*
|
|
1114
|
-
* Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
|
|
1115
|
-
* first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
|
|
1116
|
-
*/
|
|
1117
|
-
export interface RolloutThreadFields {
|
|
1118
|
-
id: string;
|
|
1119
|
-
modelProvider: string;
|
|
1120
|
-
source: string;
|
|
1121
|
-
firstUserMessage: string;
|
|
1122
|
-
hasUserEvent: number;
|
|
1123
|
-
cwd?: string;
|
|
1124
|
-
historyMode?: string;
|
|
1125
|
-
cliVersion?: string;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
function textFromContentParts(content: unknown): string | null {
|
|
1129
|
-
if (typeof content === "string" && content.trim()) return content.trim();
|
|
1130
|
-
if (!Array.isArray(content)) return null;
|
|
1131
|
-
const parts: string[] = [];
|
|
1132
|
-
for (const part of content) {
|
|
1133
|
-
if (!part || typeof part !== "object") continue;
|
|
1134
|
-
const p = part as Record<string, unknown>;
|
|
1135
|
-
if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
|
|
1136
|
-
else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
|
|
1137
|
-
}
|
|
1138
|
-
const joined = parts.join("\n").trim();
|
|
1139
|
-
return joined || null;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/** Extract the first user-message preview from a rollout line, or null. */
|
|
1143
|
-
function extractUserMessagePreview(line: string): string | null {
|
|
1144
|
-
let parsed: unknown;
|
|
1145
|
-
try {
|
|
1146
|
-
parsed = JSON.parse(line);
|
|
1147
|
-
} catch {
|
|
1148
|
-
return null;
|
|
1149
|
-
}
|
|
1150
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
1151
|
-
const record = parsed as { type?: unknown; payload?: unknown };
|
|
1152
|
-
const payload = record.payload;
|
|
1153
|
-
if (!payload || typeof payload !== "object") return null;
|
|
1154
|
-
const p = payload as Record<string, unknown>;
|
|
1155
|
-
|
|
1156
|
-
if (record.type === "event_msg") {
|
|
1157
|
-
// codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
|
|
1158
|
-
if (p.type === "user_message" || typeof p.message === "string") {
|
|
1159
|
-
if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
|
|
1160
|
-
const fromContent = textFromContentParts(p.content);
|
|
1161
|
-
if (fromContent) return fromContent;
|
|
1162
|
-
}
|
|
1163
|
-
return null;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
if (record.type === "response_item") {
|
|
1167
|
-
if (p.type === "message" && p.role === "user") {
|
|
1168
|
-
return textFromContentParts(p.content);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/**
|
|
1175
|
-
* Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
|
|
1176
|
-
* Returns null when the file is missing or has no parseable `session_meta`.
|
|
1177
|
-
*
|
|
1178
|
-
* Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
|
|
1179
|
-
* rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
|
|
1180
|
-
* no decompressed copy is written to disk.
|
|
1181
|
-
*/
|
|
1182
|
-
export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
|
|
1183
|
-
if (!path || !existsSync(path)) return null;
|
|
1184
|
-
let raw: string;
|
|
1185
|
-
try {
|
|
1186
|
-
raw = path.endsWith(".zst")
|
|
1187
|
-
? decompressRolloutZstUtf8(path)
|
|
1188
|
-
: readFileSync(path, "utf8");
|
|
1189
|
-
} catch {
|
|
1190
|
-
return null;
|
|
1191
|
-
}
|
|
1192
|
-
return parseThreadFieldsFromRolloutText(raw);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function decompressRolloutZstUtf8(
|
|
1196
|
-
path: string,
|
|
1197
|
-
maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
|
|
1198
|
-
): string {
|
|
1199
|
-
const compressed = readFileSync(path);
|
|
1200
|
-
const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
|
|
1201
|
-
maxOutputLength: maxBytes,
|
|
1202
|
-
});
|
|
1203
|
-
if (decoded.byteLength > maxBytes) {
|
|
1204
|
-
throw new Error("rollout_zst_too_large");
|
|
1205
|
-
}
|
|
1206
|
-
return new TextDecoder().decode(decoded);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
|
|
1210
|
-
const lines = raw.split("\n");
|
|
1211
|
-
let latest: ParsedSessionMeta | null = null;
|
|
1212
|
-
let firstUserMessage = "";
|
|
1213
|
-
for (const line of lines) {
|
|
1214
|
-
if (!line) continue;
|
|
1215
|
-
if (line.includes("\"session_meta\"")) {
|
|
1216
|
-
const meta = parseSessionMetaLine(line);
|
|
1217
|
-
if (meta) latest = meta;
|
|
1218
|
-
}
|
|
1219
|
-
if (!firstUserMessage) {
|
|
1220
|
-
const preview = extractUserMessagePreview(line);
|
|
1221
|
-
if (preview) firstUserMessage = preview;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (!latest) return null;
|
|
1225
|
-
const payload = latest.record.payload;
|
|
1226
|
-
const id = typeof payload.id === "string" ? payload.id : "";
|
|
1227
|
-
if (!id) return null;
|
|
1228
|
-
const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
|
|
1229
|
-
? payload.model_provider
|
|
1230
|
-
: "openai";
|
|
1231
|
-
const source = typeof payload.source === "string" && payload.source
|
|
1232
|
-
? payload.source
|
|
1233
|
-
: "cli";
|
|
1234
|
-
return {
|
|
1235
|
-
id,
|
|
1236
|
-
modelProvider,
|
|
1237
|
-
source,
|
|
1238
|
-
firstUserMessage,
|
|
1239
|
-
hasUserEvent: firstUserMessage.trim() ? 1 : 0,
|
|
1240
|
-
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
1241
|
-
...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
|
|
1242
|
-
...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
1111
|
/**
|
|
1247
1112
|
* Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
|
|
1248
1113
|
* rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
|