@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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reading side of Codex rollout JSONL files: the `session_meta` fold and the thread fields a
|
|
6
|
+
* quarantine restore reconstructs from a rollout.
|
|
7
|
+
*
|
|
8
|
+
* Split out of `history-provider.ts` when that file crossed the repository file-size ratchet at
|
|
9
|
+
* 2009 lines. The cut follows a real seam rather than a line count: everything here reads a
|
|
10
|
+
* rollout file and returns plain data, while the module it left keeps the database and manifest
|
|
11
|
+
* writes. Nothing here opens a database or touches a backup manifest.
|
|
12
|
+
*
|
|
13
|
+
* `history-provider.ts` re-exports the public names, so existing importers are unaffected.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
17
|
+
* Bounds peak memory while reconstructing thread rows; never write the decoded
|
|
18
|
+
* JSONL to disk.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
export interface ParsedSessionMeta {
|
|
23
|
+
record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
|
|
27
|
+
export function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
28
|
+
let parsed: unknown;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(line);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
35
|
+
const record = parsed as ParsedSessionMeta["record"];
|
|
36
|
+
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
|
|
37
|
+
return { record };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
42
|
+
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
43
|
+
*
|
|
44
|
+
* Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
|
|
45
|
+
* first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
|
|
46
|
+
*/
|
|
47
|
+
export interface RolloutThreadFields {
|
|
48
|
+
id: string;
|
|
49
|
+
modelProvider: string;
|
|
50
|
+
source: string;
|
|
51
|
+
firstUserMessage: string;
|
|
52
|
+
hasUserEvent: number;
|
|
53
|
+
cwd?: string;
|
|
54
|
+
historyMode?: string;
|
|
55
|
+
cliVersion?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function textFromContentParts(content: unknown): string | null {
|
|
59
|
+
if (typeof content === "string" && content.trim()) return content.trim();
|
|
60
|
+
if (!Array.isArray(content)) return null;
|
|
61
|
+
const parts: string[] = [];
|
|
62
|
+
for (const part of content) {
|
|
63
|
+
if (!part || typeof part !== "object") continue;
|
|
64
|
+
const p = part as Record<string, unknown>;
|
|
65
|
+
if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
|
|
66
|
+
else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
|
|
67
|
+
}
|
|
68
|
+
const joined = parts.join("\n").trim();
|
|
69
|
+
return joined || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Extract the first user-message preview from a rollout line, or null. */
|
|
73
|
+
function extractUserMessagePreview(line: string): string | null {
|
|
74
|
+
let parsed: unknown;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(line);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
81
|
+
const record = parsed as { type?: unknown; payload?: unknown };
|
|
82
|
+
const payload = record.payload;
|
|
83
|
+
if (!payload || typeof payload !== "object") return null;
|
|
84
|
+
const p = payload as Record<string, unknown>;
|
|
85
|
+
|
|
86
|
+
if (record.type === "event_msg") {
|
|
87
|
+
// codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
|
|
88
|
+
if (p.type === "user_message" || typeof p.message === "string") {
|
|
89
|
+
if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
|
|
90
|
+
const fromContent = textFromContentParts(p.content);
|
|
91
|
+
if (fromContent) return fromContent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (record.type === "response_item") {
|
|
97
|
+
if (p.type === "message" && p.role === "user") {
|
|
98
|
+
return textFromContentParts(p.content);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
|
|
106
|
+
* Returns null when the file is missing or has no parseable `session_meta`.
|
|
107
|
+
*
|
|
108
|
+
* Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
|
|
109
|
+
* rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
|
|
110
|
+
* no decompressed copy is written to disk.
|
|
111
|
+
*/
|
|
112
|
+
export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
|
|
113
|
+
if (!path || !existsSync(path)) return null;
|
|
114
|
+
let raw: string;
|
|
115
|
+
try {
|
|
116
|
+
raw = path.endsWith(".zst")
|
|
117
|
+
? decompressRolloutZstUtf8(path)
|
|
118
|
+
: readFileSync(path, "utf8");
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return parseThreadFieldsFromRolloutText(raw);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decompressRolloutZstUtf8(
|
|
126
|
+
path: string,
|
|
127
|
+
maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
|
|
128
|
+
): string {
|
|
129
|
+
const compressed = readFileSync(path);
|
|
130
|
+
const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
|
|
131
|
+
maxOutputLength: maxBytes,
|
|
132
|
+
});
|
|
133
|
+
if (decoded.byteLength > maxBytes) {
|
|
134
|
+
throw new Error("rollout_zst_too_large");
|
|
135
|
+
}
|
|
136
|
+
return new TextDecoder().decode(decoded);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
|
|
140
|
+
const lines = raw.split("\n");
|
|
141
|
+
let latest: ParsedSessionMeta | null = null;
|
|
142
|
+
let firstUserMessage = "";
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (!line) continue;
|
|
145
|
+
if (line.includes("\"session_meta\"")) {
|
|
146
|
+
const meta = parseSessionMetaLine(line);
|
|
147
|
+
if (meta) latest = meta;
|
|
148
|
+
}
|
|
149
|
+
if (!firstUserMessage) {
|
|
150
|
+
const preview = extractUserMessagePreview(line);
|
|
151
|
+
if (preview) firstUserMessage = preview;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!latest) return null;
|
|
155
|
+
const payload = latest.record.payload;
|
|
156
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
157
|
+
if (!id) return null;
|
|
158
|
+
const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
|
|
159
|
+
? payload.model_provider
|
|
160
|
+
: "openai";
|
|
161
|
+
const source = typeof payload.source === "string" && payload.source
|
|
162
|
+
? payload.source
|
|
163
|
+
: "cli";
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
modelProvider,
|
|
167
|
+
source,
|
|
168
|
+
firstUserMessage,
|
|
169
|
+
hasUserEvent: firstUserMessage.trim() ? 1 : 0,
|
|
170
|
+
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
171
|
+
...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
|
|
172
|
+
...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -4,6 +4,7 @@ import { contextCompatibleBaseLine } from "../context-compat";
|
|
|
4
4
|
import { resolveEffectiveProjectModelProvider } from "../project-config-warnings";
|
|
5
5
|
import {
|
|
6
6
|
OCX_SECTION_MARKER,
|
|
7
|
+
OCX_ROUTING_MARKER_LINE,
|
|
7
8
|
REALTIME_WS_BASE_URL_KEY,
|
|
8
9
|
isRootOpenaiBaseUrlLine,
|
|
9
10
|
isRootRealtimeWsBaseUrlLine,
|
|
@@ -128,7 +129,7 @@ export function buildProviderTableBlockForTarget(
|
|
|
128
129
|
): string {
|
|
129
130
|
const lines = [
|
|
130
131
|
"",
|
|
131
|
-
|
|
132
|
+
OCX_ROUTING_MARKER_LINE,
|
|
132
133
|
"[model_providers.opencodex]",
|
|
133
134
|
`name = ${tomlString(resolveCodexProviderDisplayName(displayName))}`,
|
|
134
135
|
`base_url = ${tomlString(target.baseUrl)}`,
|
|
@@ -213,6 +214,9 @@ export function setRootOpenaiBaseUrl(
|
|
|
213
214
|
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
214
215
|
const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
|
|
215
216
|
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
217
|
+
// Refresh the marker too, so a config injected by a build that predates the recovery
|
|
218
|
+
// hint gains it on the next `ocx start` instead of keeping a bare marker forever.
|
|
219
|
+
lines[i - 1] = OCX_ROUTING_MARKER_LINE;
|
|
216
220
|
lines[i] = key;
|
|
217
221
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
218
222
|
}
|
|
@@ -222,7 +226,7 @@ export function setRootOpenaiBaseUrl(
|
|
|
222
226
|
content:
|
|
223
227
|
content.replace(/\n+$/, "") +
|
|
224
228
|
"\n" +
|
|
225
|
-
|
|
229
|
+
OCX_ROUTING_MARKER_LINE +
|
|
226
230
|
"\n" +
|
|
227
231
|
key +
|
|
228
232
|
"\n",
|
|
@@ -231,7 +235,7 @@ export function setRootOpenaiBaseUrl(
|
|
|
231
235
|
}
|
|
232
236
|
let insertAt = firstTable;
|
|
233
237
|
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
234
|
-
lines.splice(insertAt, 0,
|
|
238
|
+
lines.splice(insertAt, 0, OCX_ROUTING_MARKER_LINE, key);
|
|
235
239
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
236
240
|
}
|
|
237
241
|
|
|
@@ -247,18 +251,19 @@ export function setRootOpenaiBaseUrlForTarget(
|
|
|
247
251
|
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
248
252
|
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
249
253
|
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
254
|
+
lines[index - 1] = OCX_ROUTING_MARKER_LINE;
|
|
250
255
|
lines[index] = key;
|
|
251
256
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
252
257
|
}
|
|
253
258
|
if (firstTable === -1) {
|
|
254
259
|
return {
|
|
255
|
-
content: `${content.replace(/\n+$/, "")}\n${
|
|
260
|
+
content: `${content.replace(/\n+$/, "")}\n${OCX_ROUTING_MARKER_LINE}\n${key}\n`,
|
|
256
261
|
keptUserBaseUrl: false,
|
|
257
262
|
};
|
|
258
263
|
}
|
|
259
264
|
let insertAt = firstTable;
|
|
260
265
|
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
|
|
261
|
-
lines.splice(insertAt, 0,
|
|
266
|
+
lines.splice(insertAt, 0, OCX_ROUTING_MARKER_LINE, key);
|
|
262
267
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
263
268
|
}
|
|
264
269
|
|
|
@@ -284,13 +289,14 @@ export function setRootRealtimeWsBaseUrl(
|
|
|
284
289
|
if (!isRootRealtimeWsBaseUrlLine(lines[index])) continue;
|
|
285
290
|
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
286
291
|
if (!markerOwned) return { content, keptUserRealtimeWsBaseUrl: true };
|
|
292
|
+
lines[index - 1] = OCX_ROUTING_MARKER_LINE;
|
|
287
293
|
lines[index] = key;
|
|
288
294
|
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
289
295
|
}
|
|
290
296
|
for (let index = 0; index < rootEnd; index += 1) {
|
|
291
297
|
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
292
298
|
if (!(index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER))) continue;
|
|
293
|
-
lines.splice(index + 1, 0,
|
|
299
|
+
lines.splice(index + 1, 0, OCX_ROUTING_MARKER_LINE, key);
|
|
294
300
|
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
295
301
|
}
|
|
296
302
|
// No marker-owned routing override to attach to: the override has no owner, so inject nothing.
|
|
@@ -603,3 +609,32 @@ export function chooseCatalogPathForInjection(
|
|
|
603
609
|
|
|
604
610
|
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
605
611
|
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The effective `model_catalog_json` is one of ours and the file is gone.
|
|
615
|
+
*
|
|
616
|
+
* Codex does not degrade on this: a catalog path it cannot read stops it loading its
|
|
617
|
+
* configuration at all, which looks exactly like the routing lockout in #5261 and is what the
|
|
618
|
+
* reporter's machine was left in after the catalog file was deleted by hand.
|
|
619
|
+
*
|
|
620
|
+
* Injection already repairs it — the chooser refuses a missing owned path and the caller strips
|
|
621
|
+
* the stale line. That only helps someone who runs opencodex again, and the whole difficulty of
|
|
622
|
+
* this state is that Codex is the thing that stopped working, so nothing prompts them to. This
|
|
623
|
+
* predicate exists so the CLI can say it out loud.
|
|
624
|
+
*
|
|
625
|
+
* A user-owned catalog assignment wins here exactly as it does during injection: if they named
|
|
626
|
+
* the file, its absence is theirs to explain, and we do not claim it.
|
|
627
|
+
*
|
|
628
|
+
* Ownership is decided by basename, which is a weak test — a file the user happens to name
|
|
629
|
+
* `opencodex-catalog.json` is read as ours wherever it sits. That is deliberate rather than
|
|
630
|
+
* overlooked: it is the same test injection already applies, and a detector that drew the line
|
|
631
|
+
* somewhere else would report a state injection would then treat differently. Tightening it is a
|
|
632
|
+
* change to injection, not to this.
|
|
633
|
+
*/
|
|
634
|
+
export function missingOwnedCatalogPath(content: string): string | null {
|
|
635
|
+
const existing = readRootModelCatalogPath(content);
|
|
636
|
+
if (!existing) return null;
|
|
637
|
+
const resolved = resolveCodexConfigPath(existing);
|
|
638
|
+
if (!isOpencodexCatalogPath(resolved)) return null;
|
|
639
|
+
return existsSync(resolved) ? null : existing;
|
|
640
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The way forward for a provider-table transition that finds an `openai`-tagged row
|
|
3
|
+
* Codex has already migrated to paginated history.
|
|
4
|
+
*
|
|
5
|
+
* The refusal this resolves is correct about the danger and wrong about the remedy.
|
|
6
|
+
* A provider-table transition takes the root `openai_base_url` out, and a paginated row
|
|
7
|
+
* cannot be relabeled, so the transition as planned would send that conversation to
|
|
8
|
+
* Codex's built-in OpenAI endpoint. Refusing to relabel is right. Refusing the whole
|
|
9
|
+
* transition left #5321's reporter with 173 conversations and two unsupported exits:
|
|
10
|
+
* delete them, or downgrade.
|
|
11
|
+
*
|
|
12
|
+
* There is a third state, and the injector already builds it for the client-compaction
|
|
13
|
+
* form: keep the marker-owned root override beside the provider table. Codex merges the
|
|
14
|
+
* override onto its built-in `openai` entry when it builds the provider map, so the
|
|
15
|
+
* paginated row keeps reaching this proxy while never being rewritten. The transition
|
|
16
|
+
* completes, the relabel stands down, and no rollout byte or thread row is touched.
|
|
17
|
+
*
|
|
18
|
+
* Two cases cannot reach that state. An admission-token form cannot use the root key at
|
|
19
|
+
* all, because Codex's built-in `openai` entry carries no `x-opencodex-api-key` header;
|
|
20
|
+
* that one keeps the refusal and names what the operator can actually do. A root line the
|
|
21
|
+
* user owns is left alone, and the conversation follows their configuration rather than
|
|
22
|
+
* this proxy — the same guarantee the injector already makes everywhere else about a line
|
|
23
|
+
* it does not own.
|
|
24
|
+
*/
|
|
25
|
+
import { HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE, HISTORY_RELABEL_STANDS_DOWN } from "../history-provider";
|
|
26
|
+
import { applyEol, setRootOpenaiBaseUrlForTarget, stripInjectedOpenaiBaseUrl } from "./config-toml";
|
|
27
|
+
import type { CodexRoutingTarget } from "./routing-target";
|
|
28
|
+
|
|
29
|
+
export interface PaginatedOpenaiCompatDecision {
|
|
30
|
+
/** The refusal that survives. `null` only when the preflight raised none. */
|
|
31
|
+
readonly refusal: string | null;
|
|
32
|
+
/** Candidate config bytes, carrying the retained root override when one was written. */
|
|
33
|
+
readonly content: string;
|
|
34
|
+
/** True only when OpenCodex wrote the retained line and must journal it as its own. */
|
|
35
|
+
readonly retainedRootOverride: boolean;
|
|
36
|
+
/** What the caller reports when `refusal` is not the stand-down reason. */
|
|
37
|
+
readonly message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function genericRefusal(refusal: string): string {
|
|
41
|
+
return `Codex config injection refused: ${refusal}. `
|
|
42
|
+
+ "Existing provider definitions and conversation files were preserved. "
|
|
43
|
+
+ "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Named next actions, because "do not retry" is what trapped the reporter. Both are
|
|
48
|
+
* configuration the operator already owns: the loopback listener is what makes the root
|
|
49
|
+
* override usable, and `syncResumeHistory` is the existing opt-out from history remapping.
|
|
50
|
+
*/
|
|
51
|
+
function admissionTokenRefusal(): string {
|
|
52
|
+
return `Codex config injection refused: ${HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE}. `
|
|
53
|
+
+ "Existing provider definitions and conversation files were preserved. "
|
|
54
|
+
+ "This home has conversations tagged openai whose history Codex has already migrated to its paginated "
|
|
55
|
+
+ "format, and they cannot be relabeled. Keeping them on this proxy needs a root openai_base_url override, "
|
|
56
|
+
+ "which this routing form cannot use: Codex's built-in openai provider carries no x-opencodex-api-key header. "
|
|
57
|
+
+ "To complete the transition, either route Codex through the loopback listener "
|
|
58
|
+
+ "(unauthenticatedLoopbackListener.enabled, or a loopback hostname) so the override can be retained, "
|
|
59
|
+
+ "or set syncResumeHistory to false to accept that those conversations resume against Codex's own OpenAI endpoint.";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the transition, retaining the root override when this routing form can own one.
|
|
64
|
+
*
|
|
65
|
+
* `content` is the fully assembled provider-table candidate: OpenCodex's own root override
|
|
66
|
+
* has already been stripped and not re-added, so a root `openai_base_url` still present in it
|
|
67
|
+
* belongs to the user.
|
|
68
|
+
*/
|
|
69
|
+
export function applyPaginatedOpenaiCompat(
|
|
70
|
+
refusal: string | null,
|
|
71
|
+
target: CodexRoutingTarget,
|
|
72
|
+
content: string,
|
|
73
|
+
eol: "\r\n" | "\n",
|
|
74
|
+
): PaginatedOpenaiCompatDecision {
|
|
75
|
+
if (refusal !== HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE) {
|
|
76
|
+
return { refusal, content, retainedRootOverride: false, message: refusal ? genericRefusal(refusal) : "" };
|
|
77
|
+
}
|
|
78
|
+
if (target.requiresAdmissionToken === true) {
|
|
79
|
+
return { refusal, content, retainedRootOverride: false, message: admissionTokenRefusal() };
|
|
80
|
+
}
|
|
81
|
+
const written = setRootOpenaiBaseUrlForTarget(stripInjectedOpenaiBaseUrl(applyEol(content, "\n")), target);
|
|
82
|
+
// The relabel still stands down either way: the row stays tagged openai and paginated, and
|
|
83
|
+
// the caller uses this reason to skip the history unit rather than let it start and refuse.
|
|
84
|
+
return {
|
|
85
|
+
refusal: HISTORY_RELABEL_STANDS_DOWN,
|
|
86
|
+
content: written.keptUserBaseUrl ? content : applyEol(written.content, eol),
|
|
87
|
+
retainedRootOverride: !written.keptUserBaseUrl,
|
|
88
|
+
message: "",
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/codex/inject.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
writeJournal,
|
|
42
42
|
} from "./journal";
|
|
43
43
|
import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "./history-provider";
|
|
44
|
+
import { applyPaginatedOpenaiCompat } from "./inject/paginated-openai-compat";
|
|
44
45
|
import {
|
|
45
46
|
describeHistoryJobFailure,
|
|
46
47
|
deriveCodexHistoryOperation,
|
|
@@ -347,12 +348,13 @@ async function injectCodexConfigImpl(
|
|
|
347
348
|
// first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
|
|
348
349
|
// codex re-appends that stale first line whenever it writes git or memory-mode metadata.
|
|
349
350
|
//
|
|
350
|
-
// Authless is excluded on purpose: its whole point is a provider that carries
|
|
351
|
-
// requires_openai_auth = false,
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
351
|
+
// Authless is excluded here on purpose: its whole point is a provider that carries
|
|
352
|
+
// requires_openai_auth = false, so it forward-tags resume history with originals backed up
|
|
353
|
+
// instead, and that includes the case where a user enables authless and client compaction
|
|
354
|
+
// together. Only the compaction-only form skips the history unit up front. When forward
|
|
355
|
+
// tagging turns out to be impossible because Codex already paginated those rows, the same
|
|
356
|
+
// retention is selected below from the preflight verdict rather than from the routing form.
|
|
357
|
+
let keepRootOverrideAlongsideTable = providerTableMode
|
|
356
358
|
&& routingTarget.clientCompaction === true
|
|
357
359
|
&& routingTarget.desktopAuthless !== true
|
|
358
360
|
&& routingTarget.requiresAdmissionToken !== true;
|
|
@@ -472,14 +474,15 @@ async function injectCodexConfigImpl(
|
|
|
472
474
|
if (observed && observed !== HISTORY_RELABEL_STANDS_DOWN) throw new CodexHistoryPreflightRefusal(observed);
|
|
473
475
|
return observed;
|
|
474
476
|
};
|
|
475
|
-
const
|
|
477
|
+
const compat = applyPaginatedOpenaiCompat(historyPreflight(), routingTarget, content, eol);
|
|
478
|
+
content = compat.content;
|
|
479
|
+
keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
|
|
480
|
+
const observedHistoryRefusal = compat.refusal;
|
|
476
481
|
if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
|
|
477
482
|
return {
|
|
478
483
|
success: false,
|
|
479
484
|
historyPreflightFailureReason: observedHistoryRefusal,
|
|
480
|
-
message:
|
|
481
|
-
+ "Existing provider definitions and conversation files were preserved. "
|
|
482
|
-
+ "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.",
|
|
485
|
+
message: compat.message,
|
|
483
486
|
};
|
|
484
487
|
}
|
|
485
488
|
let historyRelabelRefusal = observedHistoryRefusal;
|
|
@@ -607,11 +610,11 @@ async function injectCodexConfigImpl(
|
|
|
607
610
|
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
608
611
|
markJournalInjectedState(content, profileContent, {
|
|
609
612
|
// A root override is ours whenever we wrote one and no user-owned value won. That is
|
|
610
|
-
// loopback Design B,
|
|
611
|
-
//
|
|
612
|
-
// marker comment is not durable: the Codex
|
|
613
|
-
// comments, and restore then has only the
|
|
614
|
-
//
|
|
613
|
+
// loopback Design B, the client-compaction form, and any table form that retained the
|
|
614
|
+
// root line for a paginated openai row, all of which keep the marker-owned line beside
|
|
615
|
+
// the table. Journaling it matters because the marker comment is not durable: the Codex
|
|
616
|
+
// app can reserialize config.toml and drop comments, and restore then has only the
|
|
617
|
+
// journaled value to tell our line from a user's (#1798). Other table forms record null.
|
|
615
618
|
injectedOpenaiBaseUrl: (providerTableMode && !keepRootOverrideAlongsideTable) || keptUserBaseUrl
|
|
616
619
|
? null
|
|
617
620
|
: rootTomlString(content, "openai_base_url"),
|
|
@@ -11,6 +11,24 @@ import { parseTomlString } from "./paths";
|
|
|
11
11
|
|
|
12
12
|
export const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The marker line actually written above ROUTING keys, carrying the command that undoes them.
|
|
16
|
+
*
|
|
17
|
+
* #5261: a Windows user whose proxy had stopped was locked out of Codex sign-in, because the
|
|
18
|
+
* root `openai_base_url` we write keeps pointing Codex's built-in openai provider at a port
|
|
19
|
+
* nothing is listening on. The only surface such a user can still read is `config.toml` itself,
|
|
20
|
+
* and it said nothing but "Auto-injected by opencodex" — so the recovery they found was to
|
|
21
|
+
* hand-delete lines and the catalog file, which is strictly worse than `ocx restore`.
|
|
22
|
+
*
|
|
23
|
+
* Every ownership predicate below matches on {@link OCX_SECTION_MARKER} as a SUBSTRING, never by
|
|
24
|
+
* equality, so this longer line is recognized by them and by any opencodex old enough to predate
|
|
25
|
+
* it. That is the whole reason the hint is appended to the marker instead of occupying a line of
|
|
26
|
+
* its own: a separate comment line would survive removal as orphaned text.
|
|
27
|
+
*
|
|
28
|
+
* Scope is routing only. Prompt layers keep the bare marker: `ocx restore` is not their undo.
|
|
29
|
+
*/
|
|
30
|
+
export const OCX_ROUTING_MARKER_LINE = `${OCX_SECTION_MARKER} (undo: ocx restore)`;
|
|
31
|
+
|
|
14
32
|
export function isRootOpenaiBaseUrlLine(line: string): boolean {
|
|
15
33
|
return /^\s*openai_base_url\s*=/.test(line);
|
|
16
34
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, linkSync, mkdirSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, linkSync, mkdirSync, readFileSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -37,6 +37,38 @@ export interface CatalogBackupWriteIO {
|
|
|
37
37
|
|
|
38
38
|
let backupTempSequence = 0;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Do the prepared bytes differ from what is already on disk at `prepared.path`?
|
|
42
|
+
*
|
|
43
|
+
* The single no-op rule for both files the catalog layer owns — the active catalog
|
|
44
|
+
* and Codex's models cache — so the two writers cannot drift apart. Every
|
|
45
|
+
* mtime-keyed reader has to treat a rewrite as a change: the app-server staleness
|
|
46
|
+
* classifier (#857) compares a file's mtime against each running Codex's start
|
|
47
|
+
* time, so rewriting identical bytes marks every already-running Codex as stale
|
|
48
|
+
* even though nothing changed. #1459 established this rule for the catalog; the
|
|
49
|
+
* cache is the second writer that has to apply it, because
|
|
50
|
+
* `refreshCodexModelCatalog` reports `cacheSynced` straight into the startup
|
|
51
|
+
* warning in `handleStart`.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
|
|
54
|
+
* substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
|
|
55
|
+
* equal to prepared content holding a legitimately encoded U+FFFD. Comparing
|
|
56
|
+
* decoded strings would then classify a malformed file as identical, skip the
|
|
57
|
+
* atomic repair write, and leave the corruption on disk.
|
|
58
|
+
*
|
|
59
|
+
* An unreadable or absent file reports "differs", so the caller performs the real
|
|
60
|
+
* write; that also converges a file that does not exist yet.
|
|
61
|
+
*/
|
|
62
|
+
export function preparedBytesDifferFromDisk(prepared: PreparedCatalogFileWrite): boolean {
|
|
63
|
+
let onDisk: Buffer;
|
|
64
|
+
try {
|
|
65
|
+
onDisk = readFileSync(prepared.path);
|
|
66
|
+
} catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return !onDisk.equals(Buffer.from(prepared.content, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
function isMissingPathError(error: unknown): boolean {
|
|
41
73
|
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
42
74
|
}
|
|
@@ -404,6 +404,12 @@ async function resolveMainAccountToken(
|
|
|
404
404
|
: "transient" as const;
|
|
405
405
|
throw new MainAccountTokenRefreshError(reason, { cause });
|
|
406
406
|
}
|
|
407
|
+
// The refresh may resolve after the caller went away (an implementation that does
|
|
408
|
+
// not observe the signal, or an abort landing in the window between resolution and
|
|
409
|
+
// commit). A cancelled request's late refresh must not rewrite auth.json on behalf
|
|
410
|
+
// of a request that no longer exists -- the same fence the reauth twin applies
|
|
411
|
+
// before its own commit above.
|
|
412
|
+
if (dependencies.signal?.aborted) throw dependencies.signal.reason;
|
|
407
413
|
const result = persistRefreshedMainAuthJson(locked, refreshed);
|
|
408
414
|
if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
409
415
|
return result;
|