@iislee/opencodex 2.11.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +622 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/inject.ts
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteFile,
|
|
4
|
+
loadConfig,
|
|
5
|
+
observeConfigGeneration,
|
|
6
|
+
readConfigAdmissionSnapshot,
|
|
7
|
+
subagentDefaultSyncEffective,
|
|
8
|
+
websocketsEnabled,
|
|
9
|
+
} from "../config";
|
|
10
|
+
import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
|
|
11
|
+
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
12
|
+
import { resolveCodexHistoryTransition } from "./history-transition";
|
|
13
|
+
import {
|
|
14
|
+
buildInjectWitness,
|
|
15
|
+
captureCodexPreImages,
|
|
16
|
+
codexInjectLockOutcome,
|
|
17
|
+
codexWriteCoordinationEligibility,
|
|
18
|
+
CodexPartialWriteError,
|
|
19
|
+
CodexWriteConflictError,
|
|
20
|
+
DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
21
|
+
recomputeInjectWitness,
|
|
22
|
+
recordCodexNativeTransactionProvenance,
|
|
23
|
+
restoreCodexPreImages,
|
|
24
|
+
} from "./inject-coordination";
|
|
25
|
+
import { readIntegrationRecord } from "./integration-record";
|
|
26
|
+
import { classifyNativeRoutedResidue } from "./native-residue";
|
|
27
|
+
import { inspectNativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
28
|
+
import {
|
|
29
|
+
resolveCodexCoordinatorDatabasePath,
|
|
30
|
+
resolveEffectiveUserIdentity,
|
|
31
|
+
} from "./user-identity";
|
|
32
|
+
import {
|
|
33
|
+
markJournalInjectedState,
|
|
34
|
+
journaledInjectedOpenaiBaseUrl,
|
|
35
|
+
journaledInjectedCatalogPath,
|
|
36
|
+
removeJournal,
|
|
37
|
+
restoreJournalState,
|
|
38
|
+
writeJournal,
|
|
39
|
+
} from "./journal";
|
|
40
|
+
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
41
|
+
import { restoreCodexCatalogWithPermit } from "./catalog/sync";
|
|
42
|
+
import { syncCodexHistoryProvider, type CodexHistoryFailureReason } from "./history-provider";
|
|
43
|
+
import {
|
|
44
|
+
describeHistoryJobFailure,
|
|
45
|
+
deriveCodexHistoryOperation,
|
|
46
|
+
resolveCodexHistoryJobTarget,
|
|
47
|
+
runCodexHistoryJob,
|
|
48
|
+
type CodexHistoryJobOutcome,
|
|
49
|
+
} from "./history-job";
|
|
6
50
|
import {
|
|
7
51
|
OCX_SECTION_MARKER,
|
|
8
52
|
hasInjectedCodexRouting,
|
|
@@ -11,9 +55,19 @@ import {
|
|
|
11
55
|
providerTableStart,
|
|
12
56
|
providerTableString,
|
|
13
57
|
rootTomlString,
|
|
58
|
+
stripJournaledOpenaiBaseUrl,
|
|
14
59
|
tomlStringPattern,
|
|
15
60
|
} from "./injected-marker";
|
|
16
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
CODEX_CONFIG_PATH,
|
|
63
|
+
CODEX_PROFILE_PATH,
|
|
64
|
+
DEFAULT_CATALOG_PATH,
|
|
65
|
+
getCodexHome,
|
|
66
|
+
parseTomlString,
|
|
67
|
+
readRootTomlString,
|
|
68
|
+
resolveCodexConfigPath,
|
|
69
|
+
tomlString,
|
|
70
|
+
} from "./paths";
|
|
17
71
|
import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
|
|
18
72
|
import {
|
|
19
73
|
transformManagedSubagentDefaults,
|
|
@@ -27,7 +81,9 @@ export { hasInjectedCodexRouting, hasInjectedOpenaiBaseUrl };
|
|
|
27
81
|
|
|
28
82
|
export function externalCodexModelProvider(content: string): string | null {
|
|
29
83
|
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
30
|
-
return provider && provider !== "openai" && provider !== "opencodex"
|
|
84
|
+
return provider && provider !== "openai" && provider !== "opencodex"
|
|
85
|
+
? provider
|
|
86
|
+
: null;
|
|
31
87
|
}
|
|
32
88
|
|
|
33
89
|
export function currentExternalCodexModelProvider(): string | null {
|
|
@@ -70,15 +126,35 @@ export interface InjectCodexOptions {
|
|
|
70
126
|
* failing on a missing model_catalog_json file.
|
|
71
127
|
*/
|
|
72
128
|
catalogPath?: string | null;
|
|
129
|
+
/**
|
|
130
|
+
* How long to wait for the Codex write lock before reporting contention.
|
|
131
|
+
*
|
|
132
|
+
* Bounded by default so a stuck holder cannot wedge `ocx start`; an explicit
|
|
133
|
+
* caller that is willing to wait can raise it.
|
|
134
|
+
*/
|
|
135
|
+
lockTimeoutMs?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Validate the same config transformations and write-coordination eligibility without
|
|
138
|
+
* changing the journal, config, profile, catalog, cache, or history. Sync uses this before
|
|
139
|
+
* provider discovery so a deterministic config refusal cannot degrade an existing catalog.
|
|
140
|
+
*/
|
|
141
|
+
validateOnly?: boolean;
|
|
73
142
|
}
|
|
74
143
|
|
|
75
144
|
function configuredManagedSubagentDefaults(
|
|
76
|
-
config:
|
|
145
|
+
config:
|
|
146
|
+
| Pick<
|
|
147
|
+
OcxConfig,
|
|
148
|
+
"injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults"
|
|
149
|
+
>
|
|
150
|
+
| undefined,
|
|
77
151
|
): ManagedSubagentDefaults | null {
|
|
78
152
|
if (!subagentDefaultSyncEffective(config ?? {})) return null;
|
|
79
153
|
return {
|
|
80
154
|
model: config!.injectionModel!.trim(),
|
|
81
|
-
...(config!.injectionEffort?.trim()
|
|
155
|
+
...(config!.injectionEffort?.trim()
|
|
156
|
+
? { reasoningEffort: config!.injectionEffort.trim() }
|
|
157
|
+
: {}),
|
|
82
158
|
};
|
|
83
159
|
}
|
|
84
160
|
|
|
@@ -98,7 +174,13 @@ function configuredManagedSubagentDefaults(
|
|
|
98
174
|
*/
|
|
99
175
|
export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
100
176
|
const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
|
|
101
|
-
return
|
|
177
|
+
return (
|
|
178
|
+
normalized === "" ||
|
|
179
|
+
normalized === "localhost" ||
|
|
180
|
+
normalized === "127.0.0.1" ||
|
|
181
|
+
normalized === "::1" ||
|
|
182
|
+
normalized === "[::1]"
|
|
183
|
+
);
|
|
102
184
|
}
|
|
103
185
|
|
|
104
186
|
export function providerBaseHost(hostname: string | undefined): string {
|
|
@@ -107,16 +189,34 @@ export function providerBaseHost(hostname: string | undefined): string {
|
|
|
107
189
|
// Match what the server actually binds. Writing "localhost" while binding IPv4-only
|
|
108
190
|
// 127.0.0.1 breaks on Windows, where localhost commonly resolves to ::1 first.
|
|
109
191
|
if (lower === "::1" || lower === "[::1]") return "[::1]";
|
|
110
|
-
if (
|
|
192
|
+
if (
|
|
193
|
+
isLoopbackHostname(trimmed) ||
|
|
194
|
+
trimmed === "0.0.0.0" ||
|
|
195
|
+
trimmed === "::" ||
|
|
196
|
+
trimmed === "[::]"
|
|
197
|
+
)
|
|
198
|
+
return "127.0.0.1";
|
|
111
199
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
112
200
|
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
113
201
|
}
|
|
114
202
|
|
|
115
|
-
export function shouldInjectApiAuthHeader(
|
|
203
|
+
export function shouldInjectApiAuthHeader(
|
|
204
|
+
config: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener"> | undefined,
|
|
205
|
+
): boolean {
|
|
206
|
+
// The unauthenticated loopback listener is a loopback bind, so it admits without a
|
|
207
|
+
// credential (#1102). Emitting the env header anyway would be worse than useless: the
|
|
208
|
+
// directly-spawned app-server this exists for has no OPENCODEX_API_AUTH_TOKEN in its
|
|
209
|
+
// environment, and Codex would send an empty header value.
|
|
210
|
+
if (config?.unauthenticatedLoopbackListener?.enabled) return false;
|
|
116
211
|
return !isLoopbackHostname(config?.hostname);
|
|
117
212
|
}
|
|
118
213
|
|
|
119
|
-
export function buildProviderTableBlock(
|
|
214
|
+
export function buildProviderTableBlock(
|
|
215
|
+
port: number,
|
|
216
|
+
supportsWebsockets = false,
|
|
217
|
+
includeApiAuthHeader = false,
|
|
218
|
+
hostname?: string,
|
|
219
|
+
): string {
|
|
120
220
|
const host = providerBaseHost(hostname);
|
|
121
221
|
const lines = [
|
|
122
222
|
"",
|
|
@@ -128,13 +228,22 @@ export function buildProviderTableBlock(port: number, supportsWebsockets = false
|
|
|
128
228
|
"requires_openai_auth = true",
|
|
129
229
|
];
|
|
130
230
|
if (includeApiAuthHeader) {
|
|
131
|
-
|
|
231
|
+
// codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
|
|
232
|
+
// hard-errors on a missing/empty variable instead of silently omitting auth. It
|
|
233
|
+
// coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
|
|
234
|
+
// login/account UX), and the server substitutes stored main auth for our admission
|
|
235
|
+
// bearer (#1686), so the modern form is strictly better than the legacy
|
|
236
|
+
// env_http_headers table this line used to emit.
|
|
237
|
+
lines.push('env_key = "OPENCODEX_API_AUTH_TOKEN"');
|
|
132
238
|
}
|
|
133
239
|
if (supportsWebsockets) lines.push("supports_websockets = true");
|
|
134
240
|
return lines.join("\n") + "\n";
|
|
135
241
|
}
|
|
136
242
|
|
|
137
|
-
export function buildOpenaiBaseUrlLine(
|
|
243
|
+
export function buildOpenaiBaseUrlLine(
|
|
244
|
+
port: number,
|
|
245
|
+
hostname?: string,
|
|
246
|
+
): string {
|
|
138
247
|
return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
|
|
139
248
|
}
|
|
140
249
|
|
|
@@ -144,9 +253,13 @@ export function buildOpenaiBaseUrlLine(port: number, hostname?: string): string
|
|
|
144
253
|
* in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
|
|
145
254
|
* and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
|
|
146
255
|
*/
|
|
147
|
-
export function setRootOpenaiBaseUrl(
|
|
256
|
+
export function setRootOpenaiBaseUrl(
|
|
257
|
+
content: string,
|
|
258
|
+
port: number,
|
|
259
|
+
hostname?: string,
|
|
260
|
+
): { content: string; keptUserBaseUrl: boolean } {
|
|
148
261
|
const lines = content.split("\n");
|
|
149
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
262
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
150
263
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
151
264
|
const key = buildOpenaiBaseUrlLine(port, hostname);
|
|
152
265
|
|
|
@@ -159,7 +272,16 @@ export function setRootOpenaiBaseUrl(content: string, port: number, hostname?: s
|
|
|
159
272
|
}
|
|
160
273
|
|
|
161
274
|
if (firstTable === -1) {
|
|
162
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
content:
|
|
277
|
+
content.replace(/\n+$/, "") +
|
|
278
|
+
"\n" +
|
|
279
|
+
OCX_SECTION_MARKER +
|
|
280
|
+
"\n" +
|
|
281
|
+
key +
|
|
282
|
+
"\n",
|
|
283
|
+
keptUserBaseUrl: false,
|
|
284
|
+
};
|
|
163
285
|
}
|
|
164
286
|
let insertAt = firstTable;
|
|
165
287
|
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
@@ -174,7 +296,7 @@ export function setRootOpenaiBaseUrl(content: string, port: number, hostname?: s
|
|
|
174
296
|
*/
|
|
175
297
|
export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
176
298
|
const lines = content.split("\n");
|
|
177
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
299
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
178
300
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
179
301
|
const drop = new Set<number>();
|
|
180
302
|
for (let i = 0; i < rootEnd; i++) {
|
|
@@ -190,7 +312,8 @@ export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
|
190
312
|
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
191
313
|
}
|
|
192
314
|
|
|
193
|
-
export type CodexRoutingKind =
|
|
315
|
+
export type CodexRoutingKind =
|
|
316
|
+
"native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
194
317
|
|
|
195
318
|
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
196
319
|
|
|
@@ -198,7 +321,7 @@ function ipv4Octets(hostname: string): number[] | null {
|
|
|
198
321
|
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
199
322
|
if (dotted) {
|
|
200
323
|
const octets = dotted.slice(1).map(Number);
|
|
201
|
-
return octets.some(octet => octet > 255) ? null : octets;
|
|
324
|
+
return octets.some((octet) => octet > 255) ? null : octets;
|
|
202
325
|
}
|
|
203
326
|
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
|
|
204
327
|
if (!mapped) return null;
|
|
@@ -211,13 +334,18 @@ function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
|
211
334
|
try {
|
|
212
335
|
const url = new URL(value);
|
|
213
336
|
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
214
|
-
const hostname = url.hostname
|
|
337
|
+
const hostname = url.hostname
|
|
338
|
+
.toLowerCase()
|
|
339
|
+
.replace(/^\[|\]$/g, "")
|
|
340
|
+
.replace(/\.$/, "");
|
|
215
341
|
if (!hostname) return "unknown";
|
|
216
|
-
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
217
|
-
|
|
342
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
343
|
+
return "local";
|
|
344
|
+
if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0")
|
|
345
|
+
return "local";
|
|
218
346
|
const octets = ipv4Octets(hostname);
|
|
219
347
|
if (octets) {
|
|
220
|
-
if (octets.every(octet => octet === 0)) return "local";
|
|
348
|
+
if (octets.every((octet) => octet === 0)) return "local";
|
|
221
349
|
if (octets[0] === 127) return "local";
|
|
222
350
|
return "remote";
|
|
223
351
|
}
|
|
@@ -239,15 +367,25 @@ export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
|
239
367
|
}
|
|
240
368
|
const rootProvider = rootTomlString(content, "model_provider");
|
|
241
369
|
if (rootProvider) {
|
|
242
|
-
const providerTableExists =
|
|
243
|
-
|
|
370
|
+
const providerTableExists =
|
|
371
|
+
providerTableStart(content.split("\n"), rootProvider) !== -1;
|
|
372
|
+
const providerBaseUrl = providerTableString(
|
|
373
|
+
content,
|
|
374
|
+
rootProvider,
|
|
375
|
+
"base_url",
|
|
376
|
+
);
|
|
244
377
|
if (providerBaseUrl) {
|
|
245
378
|
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
246
379
|
if (endpoint === "unknown") return "unknown";
|
|
247
380
|
if (rootProvider === "opencodex") return "opencodex-local";
|
|
248
381
|
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
249
382
|
}
|
|
250
|
-
if (
|
|
383
|
+
if (
|
|
384
|
+
rootProvider === "opencodex" ||
|
|
385
|
+
providerTableExists ||
|
|
386
|
+
rootProvider !== "openai"
|
|
387
|
+
)
|
|
388
|
+
return "unknown";
|
|
251
389
|
}
|
|
252
390
|
return "native";
|
|
253
391
|
}
|
|
@@ -282,7 +420,7 @@ export function getCodexRoutingKind(): CodexRoutingKind {
|
|
|
282
420
|
*/
|
|
283
421
|
function stripExistingModelProvider(content: string): string {
|
|
284
422
|
const lines = content.split("\n");
|
|
285
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
423
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
286
424
|
const out: string[] = [];
|
|
287
425
|
lines.forEach((line, i) => {
|
|
288
426
|
if (/^\s*model_provider\s*=/.test(line)) {
|
|
@@ -303,7 +441,7 @@ function stripExistingModelProvider(content: string): string {
|
|
|
303
441
|
*/
|
|
304
442
|
export function stripRootContextWindowOverrides(content: string): string {
|
|
305
443
|
const lines = content.split("\n");
|
|
306
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
444
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
307
445
|
return lines
|
|
308
446
|
.filter((line, i) => {
|
|
309
447
|
const isRoot = firstTable === -1 || i < firstTable;
|
|
@@ -314,12 +452,12 @@ export function stripRootContextWindowOverrides(content: string): string {
|
|
|
314
452
|
|
|
315
453
|
function stripRootRoutedModel(content: string): string {
|
|
316
454
|
const lines = content.split("\n");
|
|
317
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
455
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
318
456
|
return lines
|
|
319
457
|
.filter((line, i) => {
|
|
320
458
|
const isRoot = firstTable === -1 || i < firstTable;
|
|
321
459
|
if (!isRoot) return true;
|
|
322
|
-
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
|
|
460
|
+
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
|
|
323
461
|
if (!m) return true;
|
|
324
462
|
const model = parseTomlString(m[1]);
|
|
325
463
|
return !model?.includes("/");
|
|
@@ -333,7 +471,7 @@ function stripRootRoutedModel(content: string): string {
|
|
|
333
471
|
*/
|
|
334
472
|
function setRootModelProvider(content: string): string {
|
|
335
473
|
const lines = content.split("\n");
|
|
336
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
474
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
337
475
|
const key = 'model_provider = "opencodex"';
|
|
338
476
|
if (firstTable === -1) {
|
|
339
477
|
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
@@ -345,23 +483,47 @@ function setRootModelProvider(content: string): string {
|
|
|
345
483
|
}
|
|
346
484
|
|
|
347
485
|
function readRootModelCatalogPath(content: string): string | null {
|
|
348
|
-
|
|
486
|
+
const lines = content.split("\n");
|
|
487
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
488
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
489
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
490
|
+
let ownedCatalogPath: string | null = null;
|
|
491
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
492
|
+
const match = modelCatalogAssignment.exec(lines[index]);
|
|
493
|
+
if (!match) continue;
|
|
494
|
+
const catalogPath = parseTomlString(match[1]);
|
|
495
|
+
if (!isOpencodexCatalogPath(catalogPath)) return catalogPath;
|
|
496
|
+
ownedCatalogPath ??= catalogPath;
|
|
497
|
+
}
|
|
498
|
+
return ownedCatalogPath;
|
|
349
499
|
}
|
|
350
500
|
|
|
351
501
|
function setRootModelCatalogPath(content: string, catalogPath: string): string {
|
|
352
502
|
const lines = content.split("\n");
|
|
353
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
503
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
354
504
|
const key = `model_catalog_json = ${tomlString(catalogPath)}`;
|
|
505
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
355
506
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
507
|
+
const ownedAssignments: number[] = [];
|
|
508
|
+
let hasUserAssignment = false;
|
|
356
509
|
for (let i = 0; i < rootEnd; i++) {
|
|
357
|
-
const m = lines[i]
|
|
510
|
+
const m = modelCatalogAssignment.exec(lines[i]);
|
|
358
511
|
if (!m) continue;
|
|
359
512
|
const existing = parseTomlString(m[1]);
|
|
360
513
|
if (isOpencodexCatalogPath(existing)) {
|
|
361
|
-
|
|
362
|
-
|
|
514
|
+
ownedAssignments.push(i);
|
|
515
|
+
} else {
|
|
516
|
+
hasUserAssignment = true;
|
|
363
517
|
}
|
|
364
|
-
|
|
518
|
+
}
|
|
519
|
+
if (hasUserAssignment) {
|
|
520
|
+
const owned = new Set(ownedAssignments);
|
|
521
|
+
return lines.filter((_, index) => !owned.has(index)).join("\n");
|
|
522
|
+
}
|
|
523
|
+
if (ownedAssignments.length > 0) {
|
|
524
|
+
lines[ownedAssignments[0]] = key;
|
|
525
|
+
const duplicates = new Set(ownedAssignments.slice(1));
|
|
526
|
+
return lines.filter((_, index) => !duplicates.has(index)).join("\n");
|
|
365
527
|
}
|
|
366
528
|
if (firstTable === -1) {
|
|
367
529
|
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
@@ -390,32 +552,52 @@ function removeProfileSection(content: string): string {
|
|
|
390
552
|
}
|
|
391
553
|
filtered.push(line);
|
|
392
554
|
}
|
|
393
|
-
return
|
|
555
|
+
return (
|
|
556
|
+
filtered
|
|
557
|
+
.join("\n")
|
|
558
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
559
|
+
.trimEnd() + "\n"
|
|
560
|
+
);
|
|
394
561
|
}
|
|
395
562
|
|
|
396
563
|
function normalizeServiceTier(content: string): string {
|
|
397
|
-
return content.replace(
|
|
564
|
+
return content.replace(
|
|
565
|
+
/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm,
|
|
566
|
+
'$1"fast"',
|
|
567
|
+
);
|
|
398
568
|
}
|
|
399
569
|
|
|
400
|
-
function ensureFastModeFeature(content: string): string {
|
|
570
|
+
function ensureFastModeFeature(content: string, fastMode?: boolean): string {
|
|
571
|
+
// Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
|
|
572
|
+
// false forces `fast_mode = false`, and undefined leaves the user's config
|
|
573
|
+
// untouched (no [features] table is added and an existing fast_mode line is
|
|
574
|
+
// preserved as-is). Table and key matching accept the valid TOML spellings
|
|
575
|
+
// `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
|
|
401
576
|
const lines = content.split("\n");
|
|
402
|
-
const
|
|
577
|
+
const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
|
|
578
|
+
const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
|
|
579
|
+
const featuresStart = lines.findIndex(line => featuresHeader.test(line));
|
|
403
580
|
if (featuresStart === -1) {
|
|
404
|
-
|
|
581
|
+
if (fastMode === undefined) return content;
|
|
582
|
+
return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
|
|
405
583
|
}
|
|
406
584
|
|
|
407
|
-
const nextTable = lines.findIndex(
|
|
585
|
+
const nextTable = lines.findIndex(
|
|
586
|
+
(line, index) => index > featuresStart && /^\s*\[/.test(line),
|
|
587
|
+
);
|
|
408
588
|
const featuresEnd = nextTable === -1 ? lines.length : nextTable;
|
|
409
589
|
for (let i = featuresStart + 1; i < featuresEnd; i++) {
|
|
410
|
-
if (
|
|
411
|
-
|
|
590
|
+
if (fastModeKey.test(lines[i])) {
|
|
591
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
592
|
+
lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
|
|
412
593
|
return lines.join("\n");
|
|
413
594
|
}
|
|
414
595
|
}
|
|
415
596
|
|
|
597
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
416
598
|
let insertAt = featuresEnd;
|
|
417
599
|
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
418
|
-
lines.splice(insertAt, 0,
|
|
600
|
+
lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
419
601
|
return lines.join("\n");
|
|
420
602
|
}
|
|
421
603
|
|
|
@@ -424,16 +606,20 @@ function isOpencodexCatalogPath(path: string): boolean {
|
|
|
424
606
|
}
|
|
425
607
|
|
|
426
608
|
function stripOpencodexCatalogPath(content: string): string {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
609
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
610
|
+
const lines = content.split("\n");
|
|
611
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
612
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
613
|
+
return lines
|
|
614
|
+
.filter((line, index) => {
|
|
615
|
+
if (index >= rootEnd) return true;
|
|
616
|
+
const m = modelCatalogAssignment.exec(line);
|
|
431
617
|
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
|
|
432
618
|
})
|
|
433
619
|
.join("\n");
|
|
434
620
|
}
|
|
435
621
|
|
|
436
|
-
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string): string {
|
|
622
|
+
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
|
|
437
623
|
const host = providerBaseHost(hostname);
|
|
438
624
|
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
439
625
|
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
@@ -446,7 +632,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
446
632
|
buildOpenaiBaseUrlLine(port, hostname),
|
|
447
633
|
];
|
|
448
634
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
449
|
-
lines.push("", "[features]",
|
|
635
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
450
636
|
return lines.join("\n");
|
|
451
637
|
}
|
|
452
638
|
const lines = [
|
|
@@ -455,18 +641,22 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
455
641
|
'model_provider = "opencodex"',
|
|
456
642
|
];
|
|
457
643
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
458
|
-
lines.push("", "[features]",
|
|
644
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
459
645
|
lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
|
|
460
646
|
return lines.join("\n");
|
|
461
647
|
}
|
|
462
648
|
|
|
463
|
-
export function chooseCatalogPathForInjection(
|
|
649
|
+
export function chooseCatalogPathForInjection(
|
|
650
|
+
content: string,
|
|
651
|
+
requested?: string | null,
|
|
652
|
+
): string | null {
|
|
464
653
|
if (requested !== undefined) return requested;
|
|
465
654
|
|
|
466
655
|
const existing = readRootModelCatalogPath(content);
|
|
467
656
|
if (existing) {
|
|
468
657
|
const resolved = resolveCodexConfigPath(existing);
|
|
469
|
-
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
658
|
+
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
659
|
+
return existing;
|
|
470
660
|
}
|
|
471
661
|
|
|
472
662
|
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
@@ -475,12 +665,32 @@ export function chooseCatalogPathForInjection(content: string, requested?: strin
|
|
|
475
665
|
export interface CodexInjectResult {
|
|
476
666
|
success: boolean;
|
|
477
667
|
message: string;
|
|
668
|
+
status?: "skipped";
|
|
669
|
+
skippedReason?: "desired_disabled" | "desired_enabled";
|
|
478
670
|
nativeSubagentDefaultsWarning?: string;
|
|
479
671
|
}
|
|
480
672
|
|
|
481
|
-
export async function injectCodexConfig(
|
|
673
|
+
export async function injectCodexConfig(
|
|
674
|
+
port: number,
|
|
675
|
+
config?: OcxConfig,
|
|
676
|
+
options: InjectCodexOptions = {},
|
|
677
|
+
): Promise<CodexInjectResult> {
|
|
678
|
+
// Point Codex at the unauthenticated loopback listener when it is enabled (#1102).
|
|
679
|
+
//
|
|
680
|
+
// Resolved here rather than at the call sites because every caller already passes the proxy
|
|
681
|
+
// port and the config together: startup sync, `ocx sync`, and the ensure path would each
|
|
682
|
+
// need the same two-line change, and a caller that missed it would silently emit a base_url
|
|
683
|
+
// requiring a credential the directly-spawned app-server does not have.
|
|
684
|
+
//
|
|
685
|
+
// The listener port is fixed in config, never OS-assigned, so this value survives restarts
|
|
686
|
+
// and matches what an already-running app-server read at startup.
|
|
687
|
+
const loopback = config?.unauthenticatedLoopbackListener;
|
|
688
|
+
if (loopback?.enabled) port = loopback.port;
|
|
482
689
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
483
|
-
return {
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?`,
|
|
693
|
+
};
|
|
484
694
|
}
|
|
485
695
|
|
|
486
696
|
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
@@ -488,14 +698,19 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
488
698
|
if (activeProvider) {
|
|
489
699
|
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
490
700
|
// replay that stale snapshot over externally managed config.
|
|
491
|
-
removeJournal();
|
|
492
|
-
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
|
|
701
|
+
if (!options.validateOnly) removeJournal();
|
|
702
|
+
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
|
|
703
|
+
config,
|
|
704
|
+
)
|
|
493
705
|
? `Native Codex sub-agent defaults were not injected: external model_provider ${tomlString(activeProvider)} owns config.toml.`
|
|
494
706
|
: undefined;
|
|
495
707
|
return {
|
|
496
708
|
success: true,
|
|
497
|
-
...(nativeSubagentDefaultsWarning
|
|
498
|
-
|
|
709
|
+
...(nativeSubagentDefaultsWarning
|
|
710
|
+
? { nativeSubagentDefaultsWarning }
|
|
711
|
+
: {}),
|
|
712
|
+
message:
|
|
713
|
+
`⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
499
714
|
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
500
715
|
` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
|
|
501
716
|
`${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
|
|
@@ -508,23 +723,37 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
508
723
|
// root routing key: inserting that key ahead of a marker-owned first table
|
|
509
724
|
// would otherwise separate the table marker from its header. Ambiguous
|
|
510
725
|
// markers fail closed without writing config, profile, or journal state.
|
|
511
|
-
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
726
|
+
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
727
|
+
rawContent,
|
|
728
|
+
null,
|
|
729
|
+
);
|
|
512
730
|
if (!nativeDefaultsBaseline.ok) {
|
|
513
731
|
return {
|
|
514
732
|
success: false,
|
|
515
|
-
message:
|
|
516
|
-
|
|
733
|
+
message:
|
|
734
|
+
`Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
|
|
735
|
+
`No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
|
|
517
736
|
};
|
|
518
737
|
}
|
|
519
738
|
const baselineContent = nativeDefaultsBaseline.content;
|
|
520
739
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
740
|
+
/*
|
|
741
|
+
* The journal write used to happen HERE, before the transforms. It now happens
|
|
742
|
+
* inside the write lock further down, and the transforms were hoisted above it
|
|
743
|
+
* rather than the lock being narrowed to the three file writes.
|
|
744
|
+
*
|
|
745
|
+
* Why: the lock's witness hashes the CANDIDATE BYTES, and those are not final
|
|
746
|
+
* until `profileContent` and the EOL-applied `content` exist. Opening the lock
|
|
747
|
+
* before them would leave nothing to hash; keeping the journal outside the
|
|
748
|
+
* lock would leave the first artifact-creating write unserialized, which is
|
|
749
|
+
* the hole this edge exists to close.
|
|
750
|
+
*
|
|
751
|
+
* The move is safe because the region between here and the writes performs no
|
|
752
|
+
* filesystem mutation — its only touch is `existsSync` on the catalog paths
|
|
753
|
+
* (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
|
|
754
|
+
* with `configContent`, so it snapshots the baseline it is handed rather than
|
|
755
|
+
* rereading `config.toml` underneath the transforms.
|
|
756
|
+
*/
|
|
528
757
|
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
529
758
|
const eol = dominantEol(rawContent);
|
|
530
759
|
let content = applyEol(baselineContent, "\n");
|
|
@@ -535,17 +764,22 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
535
764
|
// Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
|
|
536
765
|
// marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
|
|
537
766
|
content = stripInjectedOpenaiBaseUrl(content);
|
|
538
|
-
if (content
|
|
767
|
+
if (hasOcxProviderTable(content)) {
|
|
539
768
|
content = removeOcxSection(content);
|
|
540
769
|
}
|
|
541
770
|
content = removeProfileSection(content);
|
|
542
771
|
content = stripExistingModelProvider(content);
|
|
543
772
|
content = stripRootContextWindowOverrides(content);
|
|
544
773
|
content = normalizeServiceTier(content);
|
|
545
|
-
content = ensureFastModeFeature(content);
|
|
774
|
+
content = ensureFastModeFeature(content, config?.fastMode);
|
|
546
775
|
|
|
547
|
-
const catalogPath = chooseCatalogPathForInjection(
|
|
548
|
-
|
|
776
|
+
const catalogPath = chooseCatalogPathForInjection(
|
|
777
|
+
content,
|
|
778
|
+
options.catalogPath,
|
|
779
|
+
);
|
|
780
|
+
content = catalogPath
|
|
781
|
+
? setRootModelCatalogPath(content, catalogPath)
|
|
782
|
+
: stripOpencodexCatalogPath(content);
|
|
549
783
|
|
|
550
784
|
const legacyMode = shouldInjectApiAuthHeader(config);
|
|
551
785
|
let keptUserBaseUrl = false;
|
|
@@ -555,7 +789,15 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
555
789
|
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
556
790
|
content = setRootModelProvider(content);
|
|
557
791
|
// 2) Provider table appended at EOF (position-independent).
|
|
558
|
-
content =
|
|
792
|
+
content =
|
|
793
|
+
content.trimEnd() +
|
|
794
|
+
"\n" +
|
|
795
|
+
buildProviderTableBlock(
|
|
796
|
+
port,
|
|
797
|
+
websocketsEnabled(config ?? {}),
|
|
798
|
+
true,
|
|
799
|
+
config?.hostname,
|
|
800
|
+
);
|
|
559
801
|
} else {
|
|
560
802
|
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
561
803
|
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
@@ -566,66 +808,309 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
566
808
|
}
|
|
567
809
|
|
|
568
810
|
const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
|
|
569
|
-
const routingOwnershipWarning =
|
|
570
|
-
|
|
571
|
-
|
|
811
|
+
const routingOwnershipWarning =
|
|
812
|
+
keptUserBaseUrl && desiredSubagentDefaults
|
|
813
|
+
? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
|
|
814
|
+
: undefined;
|
|
572
815
|
const managedDefaults = transformManagedSubagentDefaults(
|
|
573
816
|
content,
|
|
574
817
|
keptUserBaseUrl ? null : desiredSubagentDefaults,
|
|
575
818
|
);
|
|
576
819
|
let nativeSubagentDefaultsWarning = routingOwnershipWarning;
|
|
577
|
-
let managedDefaultsMessage = routingOwnershipWarning
|
|
820
|
+
let managedDefaultsMessage = routingOwnershipWarning
|
|
821
|
+
? ` ⚠️ ${routingOwnershipWarning}\n`
|
|
822
|
+
: "";
|
|
578
823
|
if (managedDefaults.ok) {
|
|
579
824
|
content = managedDefaults.content;
|
|
580
825
|
if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
|
|
581
|
-
const keys = managedDefaults.conflicts
|
|
826
|
+
const keys = managedDefaults.conflicts
|
|
827
|
+
.map((conflict) => `agents.${conflict.key}`)
|
|
828
|
+
.join(", ");
|
|
582
829
|
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
|
|
583
830
|
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
584
831
|
}
|
|
585
832
|
} else {
|
|
586
|
-
const action =
|
|
587
|
-
|
|
588
|
-
|
|
833
|
+
const action =
|
|
834
|
+
desiredSubagentDefaults && !keptUserBaseUrl
|
|
835
|
+
? "were not injected"
|
|
836
|
+
: "could not be safely removed";
|
|
589
837
|
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
|
|
590
838
|
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
591
839
|
}
|
|
592
840
|
|
|
593
|
-
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname);
|
|
841
|
+
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
|
|
594
842
|
content = applyEol(content, eol);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
843
|
+
|
|
844
|
+
/*
|
|
845
|
+
* The witness, built from the FINAL bytes. Everything it hashes is either the
|
|
846
|
+
* output about to be written or evidence that can be re-read under the lock;
|
|
847
|
+
* ownership rides along as recorded context because it is not re-observed
|
|
848
|
+
* there — see `write-coordination.ts`.
|
|
849
|
+
*/
|
|
850
|
+
const persisted = readConfigAdmissionSnapshot();
|
|
851
|
+
const persistedIdentity =
|
|
852
|
+
persisted.kind === "read" ? persisted.contentSha256 : "unreadable";
|
|
853
|
+
const observedGeneration = observeConfigGeneration();
|
|
854
|
+
const generation =
|
|
855
|
+
observedGeneration.kind === "ready"
|
|
856
|
+
? { present: true, value: observedGeneration.generation.value }
|
|
857
|
+
: { present: false, value: 0 };
|
|
858
|
+
const candidate = {
|
|
859
|
+
configBytes: content,
|
|
860
|
+
profileBytes: profileContent,
|
|
861
|
+
catalogPath,
|
|
862
|
+
};
|
|
863
|
+
const witness = buildInjectWitness(
|
|
864
|
+
candidate,
|
|
865
|
+
rawContent,
|
|
866
|
+
persistedIdentity,
|
|
867
|
+
generation,
|
|
868
|
+
"unknown",
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
/*
|
|
872
|
+
* THE COORDINATED SECTION.
|
|
873
|
+
*
|
|
874
|
+
* This is the write lock's first production caller. Everything above is
|
|
875
|
+
* classification and pure transformation; everything from here to the end of
|
|
876
|
+
* the callback replaces files, and two processes doing it at once is the
|
|
877
|
+
* interruption hazard this substrate exists to close.
|
|
878
|
+
*
|
|
879
|
+
* The witness hashes the bytes about to be written rather than the inputs that
|
|
880
|
+
* produced them, so two operations intending different output cannot share an
|
|
881
|
+
* id no matter which input differed.
|
|
882
|
+
*/
|
|
883
|
+
/*
|
|
884
|
+
* Eligibility BEFORE acquisition, never "try and fall back".
|
|
885
|
+
*
|
|
886
|
+
* A home routed before this substrate existed cannot have its first
|
|
887
|
+
* coordinator row created — the guard that refuses is correct — and that
|
|
888
|
+
* describes every pre-substrate install. Attempting the lock there would enter
|
|
889
|
+
* a refusal path on the entire installed base, so the decision happens first
|
|
890
|
+
* and those homes keep the write sequence they have always used.
|
|
891
|
+
*/
|
|
892
|
+
const eligibility = codexWriteCoordinationEligibility({
|
|
893
|
+
coordinatorPath: () =>
|
|
894
|
+
resolveCodexCoordinatorDatabasePath(
|
|
895
|
+
resolveEffectiveUserIdentity(),
|
|
896
|
+
getCodexHome(),
|
|
897
|
+
),
|
|
898
|
+
residue: () => classifyNativeRoutedResidue(),
|
|
899
|
+
integrationRecord: () => readIntegrationRecord(),
|
|
900
|
+
});
|
|
901
|
+
if (eligibility.kind === "refused") {
|
|
902
|
+
return {
|
|
903
|
+
success: false,
|
|
904
|
+
message: `Codex configuration was not written: ${eligibility.reason}.`,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (options.validateOnly) {
|
|
909
|
+
return {
|
|
910
|
+
success: true,
|
|
911
|
+
message: "Codex config injection preflight passed; no files were changed.",
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const applyNativeArtifacts = (): void => {
|
|
916
|
+
writeJournal({
|
|
917
|
+
currentStateIsNative: !hasInjectedCodexRouting(rawContent),
|
|
918
|
+
configContent: baselineContent,
|
|
919
|
+
});
|
|
920
|
+
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
921
|
+
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
922
|
+
markJournalInjectedState(content, profileContent, {
|
|
923
|
+
// A root override is ours only in loopback Design B when no user-owned value won.
|
|
924
|
+
injectedOpenaiBaseUrl: legacyMode || keptUserBaseUrl
|
|
925
|
+
? null
|
|
926
|
+
: rootTomlString(content, "openai_base_url"),
|
|
927
|
+
// This is the catalog artifact selected for this injection, even when config.toml
|
|
928
|
+
// already points at that path and therefore needs no textual rewrite.
|
|
929
|
+
injectedCatalogPath: catalogPath,
|
|
930
|
+
});
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
/*
|
|
934
|
+
* Set only on the coordinated path: the generation/txId the transition just
|
|
935
|
+
* committed. The terminal history update CASes against this, so a job that
|
|
936
|
+
* was overtaken cannot overwrite the winner. Stays undefined for a
|
|
937
|
+
* legacy-uncoordinated home, which publishes no transition to resolve.
|
|
938
|
+
*/
|
|
939
|
+
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
940
|
+
|
|
941
|
+
if (eligibility.kind === "legacy-uncoordinated") {
|
|
942
|
+
// Unchanged behavior for homes the coordinator cannot yet adopt. Stated
|
|
943
|
+
// rather than implied: this is the boundary, and adoption is its own phase.
|
|
944
|
+
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
945
|
+
return {
|
|
946
|
+
success: true,
|
|
947
|
+
status: "skipped",
|
|
948
|
+
skippedReason: "desired_disabled",
|
|
949
|
+
message: "Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
applyNativeArtifacts();
|
|
953
|
+
} else {
|
|
954
|
+
const coordinated = await withCodexWriteLock(
|
|
955
|
+
{
|
|
956
|
+
timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
957
|
+
...(eligibility.kind === "adopt" ? { adoption: { direction: "apply" as const } } : {}),
|
|
958
|
+
admitted: { authoritySnapshotId: witness.comparisonId },
|
|
959
|
+
readAdmissionUnderLock: () => ({
|
|
960
|
+
authoritySnapshotId: recomputeInjectWitness({
|
|
961
|
+
candidate: witness.candidate,
|
|
962
|
+
canonicalTargets: witness.evidence.canonicalTargets,
|
|
963
|
+
persistedIdentity,
|
|
964
|
+
generation,
|
|
965
|
+
observedOwnership: witness.observedOwnership,
|
|
966
|
+
}).comparisonId,
|
|
967
|
+
}),
|
|
968
|
+
},
|
|
969
|
+
(ctx) => {
|
|
970
|
+
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
971
|
+
throw new CodexWriteLockSkipped("desired_disabled");
|
|
972
|
+
}
|
|
973
|
+
/*
|
|
974
|
+
* Publish BEFORE touching the filesystem. `assertPublished` runs after this
|
|
975
|
+
* callback returns and throws unless a transition was recorded, so writing
|
|
976
|
+
* first would replace every file and only then fail — with SQLite rolling
|
|
977
|
+
* back and the filesystem staying changed.
|
|
978
|
+
*
|
|
979
|
+
* `beginTransition` returns a conflict rather than throwing, so its result
|
|
980
|
+
* is checked here; ignoring it would reach the same failure by a slower
|
|
981
|
+
* route.
|
|
982
|
+
*/
|
|
983
|
+
const published = ctx.coordinator.beginTransition(
|
|
984
|
+
{
|
|
985
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
986
|
+
currentTxId: ctx.currentTxId,
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
txId: ctx.expectation.txId,
|
|
990
|
+
direction: "apply",
|
|
991
|
+
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
992
|
+
nextRetryAt: new Date().toISOString(),
|
|
993
|
+
},
|
|
994
|
+
);
|
|
995
|
+
if (published.kind !== "updated") {
|
|
996
|
+
throw new CodexWriteConflictError(
|
|
997
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/*
|
|
1002
|
+
* Exact pre-images, captured under the lock and used for compensation.
|
|
1003
|
+
*
|
|
1004
|
+
* A rolled-back coordinator row is not a rolled-back filesystem: each
|
|
1005
|
+
* `atomicWriteFile` is atomic alone, never across the three together, so a
|
|
1006
|
+
* failure partway leaves earlier replacements in place. `restoreJournalState`
|
|
1007
|
+
* cannot be the undo — it restores whichever journal occupies the path,
|
|
1008
|
+
* which need not be the one this operation wrote.
|
|
1009
|
+
*/
|
|
1010
|
+
const preImages = captureCodexPreImages();
|
|
1011
|
+
try {
|
|
1012
|
+
applyNativeArtifacts();
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
// Compensate, then ALWAYS throw. Returning a partial result would let the
|
|
1015
|
+
// lock commit a row describing an apply that did not finish.
|
|
1016
|
+
const restored = restoreCodexPreImages(preImages);
|
|
1017
|
+
if (!restored.complete) {
|
|
1018
|
+
throw new CodexPartialWriteError(restored.unrestored);
|
|
1019
|
+
}
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
kind: "applied" as const,
|
|
1024
|
+
preImages,
|
|
1025
|
+
/*
|
|
1026
|
+
* The receipt the terminal update matches on. The transition commits
|
|
1027
|
+
* when the callback returns, so this pair is what the post-job
|
|
1028
|
+
* `updateCodexHistoryTransition` CASes against — an overtaken job
|
|
1029
|
+
* cannot overwrite a winner.
|
|
1030
|
+
*/
|
|
1031
|
+
receipt: {
|
|
1032
|
+
nativeGeneration: ctx.expectation.nativeAfter,
|
|
1033
|
+
currentTxId: ctx.expectation.txId,
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
},
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
if (coordinated.status !== "acquired") {
|
|
1040
|
+
return codexInjectLockOutcome(coordinated);
|
|
1041
|
+
}
|
|
1042
|
+
recordCodexNativeTransactionProvenance(
|
|
1043
|
+
coordinated.value.preImages,
|
|
1044
|
+
coordinated.value.receipt.currentTxId,
|
|
1045
|
+
);
|
|
1046
|
+
transitionReceipt = coordinated.value.receipt;
|
|
1047
|
+
}
|
|
598
1048
|
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
599
1049
|
// the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
|
|
600
1050
|
// machinery; cheap no-op when there is nothing to migrate).
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1051
|
+
// History runs in a Worker under H, not on this thread.
|
|
1052
|
+
//
|
|
1053
|
+
// The three surfaces it touches — the SQLite rows, the backup manifest, and the
|
|
1054
|
+
// rollout files — do not share a transaction, so a busy timeout only ever
|
|
1055
|
+
// serialized one of them and an opposite-direction process could overtake
|
|
1056
|
+
// through the other two. The operation is derived from admitted intent here and
|
|
1057
|
+
// handed down fixed; the Worker never takes a direction from its caller.
|
|
1058
|
+
const historyOutcome = await runCodexHistoryJob({
|
|
1059
|
+
...resolveCodexHistoryJobTarget(),
|
|
1060
|
+
expectedDesiredEnabled: true,
|
|
1061
|
+
operation: deriveCodexHistoryOperation({
|
|
1062
|
+
direction: "apply",
|
|
1063
|
+
resumeHistory: config?.syncResumeHistory !== false,
|
|
1064
|
+
legacyMode,
|
|
1065
|
+
}),
|
|
1066
|
+
});
|
|
1067
|
+
// A blocked or failed unit is reported, not silently counted as zero work:
|
|
1068
|
+
// `failed` is what makes the caller's message say so.
|
|
1069
|
+
const history: { rows: number; files: number; failed?: true } =
|
|
1070
|
+
historyOutcome.kind === "converged"
|
|
1071
|
+
? { rows: historyOutcome.rows, files: historyOutcome.files }
|
|
1072
|
+
: historyOutcome.kind === "skipped"
|
|
1073
|
+
? { rows: 0, files: 0 }
|
|
1074
|
+
: { rows: 0, files: 0, failed: true };
|
|
1075
|
+
|
|
1076
|
+
/*
|
|
1077
|
+
* Resolve the transition this job belongs to, on the coordinated path only.
|
|
1078
|
+
*
|
|
1079
|
+
* `updateCodexHistoryTransition` had no production caller since it was
|
|
1080
|
+
* written, so every completed or skipped job left the row permanently
|
|
1081
|
+
* `pending` — the transition was published and never resolved. This is the
|
|
1082
|
+
* first time the durable row reflects what actually happened. The CAS on the
|
|
1083
|
+
* receipt means an overtaken job's late write loses and is not overwritten.
|
|
1084
|
+
*/
|
|
1085
|
+
if (transitionReceipt) {
|
|
1086
|
+
resolveCodexHistoryTransition(transitionReceipt, historyOutcome);
|
|
1087
|
+
}
|
|
604
1088
|
|
|
605
1089
|
const catalogMessage = catalogPath
|
|
606
1090
|
? ` Codex model catalog: ${catalogPath}\n`
|
|
607
1091
|
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
? (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
? ` Codex resume history: ${migratedRows} legacy opencodex-tagged thread(s) migrated back to openai (one-time).\n`
|
|
621
|
-
: ` Codex resume history: untouched (threads keep their native openai tag).\n`;
|
|
1092
|
+
const ejected = (history as { ejectedRows?: number }).ejectedRows ?? 0;
|
|
1093
|
+
const migratedRows = (history.rows ?? 0) + ejected;
|
|
1094
|
+
const historyMessage =
|
|
1095
|
+
config?.syncResumeHistory === false
|
|
1096
|
+
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
1097
|
+
: history.failed
|
|
1098
|
+
? formatApplyHistoryFailure(historyOutcome, legacyMode)
|
|
1099
|
+
: legacyMode
|
|
1100
|
+
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
1101
|
+
: migratedRows > 0
|
|
1102
|
+
? ` Codex resume history: restored original provider metadata for ${migratedRows} manifest-backed thread(s) (one-time).\n`
|
|
1103
|
+
: ` Codex resume history: no backed-up metadata pending; untracked routed history left unchanged.\n`;
|
|
622
1104
|
// A user-owned root openai_base_url means we did NOT install routing — say so honestly
|
|
623
1105
|
// instead of claiming the proxy route is active (catalog/fast_mode were still written).
|
|
624
1106
|
if (keptUserBaseUrl) {
|
|
625
1107
|
return {
|
|
626
1108
|
success: true,
|
|
627
|
-
...(nativeSubagentDefaultsWarning
|
|
628
|
-
|
|
1109
|
+
...(nativeSubagentDefaultsWarning
|
|
1110
|
+
? { nativeSubagentDefaultsWarning }
|
|
1111
|
+
: {}),
|
|
1112
|
+
message:
|
|
1113
|
+
`⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
|
|
629
1114
|
catalogMessage +
|
|
630
1115
|
historyMessage +
|
|
631
1116
|
managedDefaultsMessage +
|
|
@@ -639,7 +1124,8 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
639
1124
|
return {
|
|
640
1125
|
success: true,
|
|
641
1126
|
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
642
|
-
message:
|
|
1127
|
+
message:
|
|
1128
|
+
headline +
|
|
643
1129
|
catalogMessage +
|
|
644
1130
|
historyMessage +
|
|
645
1131
|
managedDefaultsMessage +
|
|
@@ -652,19 +1138,47 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
652
1138
|
};
|
|
653
1139
|
}
|
|
654
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Sub-table headers like `[model_providers.opencodex.env_http_headers]` appear when a Codex app
|
|
1143
|
+
* config rewrite re-serializes the provider's inline `env_http_headers` table. They define the
|
|
1144
|
+
* same `model_providers.opencodex` provider, so cleanup must remove them too — otherwise the
|
|
1145
|
+
* provider survives with no `name` and Codex rejects the whole config
|
|
1146
|
+
* ("provider name must not be empty"). The dot terminator keeps a user's
|
|
1147
|
+
* `[model_providers.opencodex_backup]`-style tables out of scope.
|
|
1148
|
+
*/
|
|
1149
|
+
function isOcxProviderHeaderLine(trimmedLine: string): boolean {
|
|
1150
|
+
// Root form matched by regex, not equality: TOML v1.0 allows a trailing comment
|
|
1151
|
+
// (`[model_providers.opencodex] # comment`), and an exact compare would miss that form.
|
|
1152
|
+
// The sub-table prefix check already tolerates trailing comments by construction.
|
|
1153
|
+
return (
|
|
1154
|
+
/^\[model_providers\.opencodex\]\s*(?:#.*)?$/.test(trimmedLine) ||
|
|
1155
|
+
trimmedLine.startsWith("[model_providers.opencodex.")
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function hasOcxProviderTable(content: string): boolean {
|
|
1160
|
+
return content
|
|
1161
|
+
.split("\n")
|
|
1162
|
+
.some((line) => isOcxProviderHeaderLine(line.trim()));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
655
1165
|
function removeOcxSection(content: string): string {
|
|
656
1166
|
const lines = content.split("\n");
|
|
657
1167
|
const filtered: string[] = [];
|
|
658
1168
|
let inOcxSection = false;
|
|
659
1169
|
for (const line of lines) {
|
|
660
|
-
if (
|
|
1170
|
+
if (
|
|
1171
|
+
line.includes(OCX_SECTION_MARKER) ||
|
|
1172
|
+
isOcxProviderHeaderLine(line.trim())
|
|
1173
|
+
) {
|
|
661
1174
|
inOcxSection = true;
|
|
662
1175
|
continue;
|
|
663
1176
|
}
|
|
664
1177
|
if (inOcxSection) {
|
|
665
|
-
// End the injected section at the next table header that ISN'T our own
|
|
666
|
-
//
|
|
667
|
-
|
|
1178
|
+
// End the injected section at the next table header that ISN'T our own. Exact match on the
|
|
1179
|
+
// provider name (plus our own sub-tables) so a user's
|
|
1180
|
+
// "[model_providers.opencodex_backup]" (or similar) is preserved, not swallowed.
|
|
1181
|
+
if (/^\s*\[/.test(line) && !isOcxProviderHeaderLine(line.trim())) {
|
|
668
1182
|
inOcxSection = false;
|
|
669
1183
|
filtered.push(line);
|
|
670
1184
|
}
|
|
@@ -672,7 +1186,12 @@ function removeOcxSection(content: string): string {
|
|
|
672
1186
|
}
|
|
673
1187
|
filtered.push(line);
|
|
674
1188
|
}
|
|
675
|
-
return
|
|
1189
|
+
return (
|
|
1190
|
+
filtered
|
|
1191
|
+
.join("\n")
|
|
1192
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1193
|
+
.trimEnd() + "\n"
|
|
1194
|
+
);
|
|
676
1195
|
}
|
|
677
1196
|
|
|
678
1197
|
interface StripOpencodexConfigResult {
|
|
@@ -685,18 +1204,30 @@ interface StripOpencodexConfigResult {
|
|
|
685
1204
|
* ambiguous: keep the associated value, but return the transform error so the
|
|
686
1205
|
* caller cannot report a complete restore.
|
|
687
1206
|
*/
|
|
688
|
-
function stripOpencodexConfigResult(
|
|
1207
|
+
function stripOpencodexConfigResult(
|
|
1208
|
+
content: string,
|
|
1209
|
+
journaledBaseUrl: string | null = null,
|
|
1210
|
+
): StripOpencodexConfigResult {
|
|
689
1211
|
let out = content;
|
|
690
|
-
const hadRootOcxProvider =
|
|
691
|
-
|
|
1212
|
+
const hadRootOcxProvider =
|
|
1213
|
+
readRootTomlString(out, "model_provider") === "opencodex";
|
|
1214
|
+
// #1798: marker adjacency is FORMATTING evidence, and a Codex app rewrite keeps values
|
|
1215
|
+
// while dropping comments. Fall back to VALUE evidence -- the exact URL we recorded
|
|
1216
|
+
// writing -- so an app-rewritten config is still recognized as ours.
|
|
1217
|
+
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out)
|
|
1218
|
+
|| (journaledBaseUrl !== null && rootTomlString(out, "openai_base_url") === journaledBaseUrl);
|
|
692
1219
|
out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
|
|
693
|
-
|
|
1220
|
+
out = stripJournaledOpenaiBaseUrl(out, journaledBaseUrl);
|
|
1221
|
+
if (hasOcxProviderTable(out)) {
|
|
694
1222
|
out = removeOcxSection(out);
|
|
695
1223
|
}
|
|
696
1224
|
out = removeProfileSection(out);
|
|
697
1225
|
// Regex (not exact-string) removal so compact `model_provider="opencodex"` is stripped too —
|
|
698
1226
|
// must match the detection regex above, or a detected line could survive un-removed.
|
|
699
|
-
out = out
|
|
1227
|
+
out = out
|
|
1228
|
+
.split("\n")
|
|
1229
|
+
.filter((l) => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l))
|
|
1230
|
+
.join("\n");
|
|
700
1231
|
// Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
|
|
701
1232
|
// them — strip on both the legacy re-tag form and the Design B injected-base-url form.
|
|
702
1233
|
if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
|
|
@@ -715,14 +1246,19 @@ export function stripOpencodexConfig(content: string): string {
|
|
|
715
1246
|
}
|
|
716
1247
|
|
|
717
1248
|
function hasOpencodexRouting(content: string): boolean {
|
|
718
|
-
return
|
|
719
|
-
|
|
720
|
-
|
|
1249
|
+
return (
|
|
1250
|
+
hasOcxProviderTable(content) ||
|
|
1251
|
+
/^\s*model_provider\s*=\s*"opencodex"/m.test(content) ||
|
|
1252
|
+
hasInjectedOpenaiBaseUrl(content)
|
|
1253
|
+
);
|
|
721
1254
|
}
|
|
722
1255
|
|
|
723
|
-
export function removeCodexConfig(
|
|
1256
|
+
export function removeCodexConfig(
|
|
1257
|
+
options: { preserveProfile?: boolean } = {},
|
|
1258
|
+
): { success: boolean; message: string } {
|
|
724
1259
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
725
|
-
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1260
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1261
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
726
1262
|
return {
|
|
727
1263
|
success: true,
|
|
728
1264
|
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
@@ -733,20 +1269,28 @@ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}):
|
|
|
733
1269
|
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
734
1270
|
const eol = dominantEol(rawContent);
|
|
735
1271
|
const content = applyEol(rawContent, "\n");
|
|
736
|
-
|
|
737
|
-
|
|
1272
|
+
// Read the recorded injection once: the strip below consumes it, and so does the
|
|
1273
|
+
// ownership verdict, which must agree with what was actually removed.
|
|
1274
|
+
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl();
|
|
1275
|
+
const had = hasOpencodexRouting(content)
|
|
1276
|
+
|| (journaledBaseUrl !== null && rootTomlString(content, "openai_base_url") === journaledBaseUrl);
|
|
1277
|
+
const stripped = stripOpencodexConfigResult(content, journaledBaseUrl);
|
|
738
1278
|
if (had || stripped.content !== content) {
|
|
739
1279
|
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
|
|
740
1280
|
}
|
|
741
|
-
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1281
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1282
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
742
1283
|
const removedMessage = had
|
|
743
1284
|
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
744
1285
|
: "opencodex not present in Codex config.";
|
|
745
1286
|
if (stripped.managedDefaultsError) {
|
|
746
|
-
const routingMessage = had
|
|
1287
|
+
const routingMessage = had
|
|
1288
|
+
? removedMessage
|
|
1289
|
+
: "No opencodex routing was present in Codex config.";
|
|
747
1290
|
return {
|
|
748
1291
|
success: false,
|
|
749
|
-
message:
|
|
1292
|
+
message:
|
|
1293
|
+
`${routingMessage} Native Codex sub-agent defaults could not be safely removed: ${stripped.managedDefaultsError}. ` +
|
|
750
1294
|
"The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.",
|
|
751
1295
|
};
|
|
752
1296
|
}
|
|
@@ -756,22 +1300,395 @@ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}):
|
|
|
756
1300
|
};
|
|
757
1301
|
}
|
|
758
1302
|
|
|
1303
|
+
export type CodexRestoreArtifactState = "ok" | "skipped" | "failed";
|
|
1304
|
+
|
|
1305
|
+
export interface CodexRestoreConfigResult {
|
|
1306
|
+
state: CodexRestoreArtifactState;
|
|
1307
|
+
changed: boolean;
|
|
1308
|
+
action: "journal-restored" | "owned-fields-stripped" | "external-provider-preserved" | "failed";
|
|
1309
|
+
message: string;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export interface CodexRestoreCatalogResult {
|
|
1313
|
+
state: CodexRestoreArtifactState;
|
|
1314
|
+
changed: boolean;
|
|
1315
|
+
removed: number;
|
|
1316
|
+
kept: number;
|
|
1317
|
+
path: string | null;
|
|
1318
|
+
message: string;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
export interface CodexRestoreHistoryResult {
|
|
1322
|
+
state: CodexRestoreArtifactState;
|
|
1323
|
+
changed: boolean;
|
|
1324
|
+
reason?: CodexHistoryFailureReason;
|
|
1325
|
+
rows: number;
|
|
1326
|
+
files: number;
|
|
1327
|
+
ejectedRows: number;
|
|
1328
|
+
message: string;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
export interface CodexNativeRestoreResult {
|
|
1332
|
+
success: boolean;
|
|
1333
|
+
message: string;
|
|
1334
|
+
externalProvider?: string;
|
|
1335
|
+
artifacts: {
|
|
1336
|
+
config: CodexRestoreConfigResult;
|
|
1337
|
+
catalog: CodexRestoreCatalogResult;
|
|
1338
|
+
history: CodexRestoreHistoryResult;
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function failedHistoryRestore(
|
|
1343
|
+
reason?: CodexHistoryFailureReason,
|
|
1344
|
+
detail?: string,
|
|
1345
|
+
progress: { rows?: number; files?: number } = {},
|
|
1346
|
+
): CodexRestoreHistoryResult {
|
|
1347
|
+
const rows = progress.rows ?? 0;
|
|
1348
|
+
const files = progress.files ?? 0;
|
|
1349
|
+
const changed = rows > 0 || files > 0;
|
|
1350
|
+
return {
|
|
1351
|
+
state: "failed",
|
|
1352
|
+
changed,
|
|
1353
|
+
...(reason ? { reason } : {}),
|
|
1354
|
+
rows,
|
|
1355
|
+
files,
|
|
1356
|
+
ejectedRows: 0,
|
|
1357
|
+
message: reason === "permission"
|
|
1358
|
+
? changed
|
|
1359
|
+
? "Codex resume history changed but did NOT converge because permission was denied while finalizing the backup manifest; the manifest was retained for review and safe retry."
|
|
1360
|
+
: "Codex resume history could NOT be restored because permission was denied."
|
|
1361
|
+
: reason === "busy"
|
|
1362
|
+
? changed
|
|
1363
|
+
? "Codex resume history changed but did NOT converge because backup-manifest finalization remained busy; the manifest was retained for review and safe retry."
|
|
1364
|
+
: detail ?? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
|
|
1365
|
+
: reason === "integrity"
|
|
1366
|
+
? changed
|
|
1367
|
+
? "Codex resume history changed but did NOT converge because the backup or target changed; the manifest was retained for review and safe retry."
|
|
1368
|
+
: "Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged."
|
|
1369
|
+
: detail
|
|
1370
|
+
? `Codex resume history could NOT be restored: ${detail}`
|
|
1371
|
+
: "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Restore failure wording for a Worker outcome.
|
|
1377
|
+
*
|
|
1378
|
+
* Only a genuine busy result blames the Codex app. An unsafe-path refusal, an
|
|
1379
|
+
* unavailable coordinator database, a permission denial, or a dead/timed-out
|
|
1380
|
+
* worker is a different problem; the old collapse made every one of those read
|
|
1381
|
+
* as "the Codex app is holding the database" (issue #1191). `busy` and
|
|
1382
|
+
* `permission` keep the restore-specific sentence built by
|
|
1383
|
+
* `failedHistoryRestore`; every other reason reuses the single formatter so
|
|
1384
|
+
* the two modules cannot drift apart.
|
|
1385
|
+
*/
|
|
1386
|
+
export function failedHistoryRestoreFromOutcome(
|
|
1387
|
+
outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
|
|
1388
|
+
): CodexRestoreHistoryResult {
|
|
1389
|
+
if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
|
|
1390
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") {
|
|
1391
|
+
return failedHistoryRestore(
|
|
1392
|
+
"busy",
|
|
1393
|
+
describeHistoryJobFailure(outcome, "restore"),
|
|
1394
|
+
{ rows: outcome.rows, files: outcome.files },
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
|
|
1398
|
+
return failedHistoryRestore("permission", undefined, { rows: outcome.rows, files: outcome.files });
|
|
1399
|
+
}
|
|
1400
|
+
if (outcome.kind === "failed" && outcome.historyFailureReason === "integrity") {
|
|
1401
|
+
return failedHistoryRestore("integrity", undefined, { rows: outcome.rows, files: outcome.files });
|
|
1402
|
+
}
|
|
1403
|
+
return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
|
|
1407
|
+
const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
|
|
1408
|
+
return {
|
|
1409
|
+
success: true,
|
|
1410
|
+
message,
|
|
1411
|
+
externalProvider: activeProvider,
|
|
1412
|
+
artifacts: {
|
|
1413
|
+
config: { state: "skipped", changed: false, action: "external-provider-preserved", message },
|
|
1414
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1415
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** A foreign service claim is an authority boundary, including explicit CLI restore. */
|
|
1421
|
+
function foreignOwnershipRestoreRefusal(message: string): CodexNativeRestoreResult {
|
|
1422
|
+
return {
|
|
1423
|
+
success: false,
|
|
1424
|
+
message: `Codex native restore refused: ${message}`,
|
|
1425
|
+
artifacts: {
|
|
1426
|
+
config: { state: "skipped", changed: false, action: "failed", message },
|
|
1427
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1428
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1429
|
+
},
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function desiredEnabledRestoreSkip(): CodexNativeRestoreResult {
|
|
1434
|
+
const message = "Codex integration was re-enabled; native restore was skipped.";
|
|
1435
|
+
return skippedRestoreEnvelope(true, message);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* A schema-complete all-skipped envelope for outcomes decided before any
|
|
1440
|
+
* restore machinery runs. Every `restore --json` path must stay shape-stable
|
|
1441
|
+
* with `CodexNativeRestoreResult`; consumers never special-case early exits.
|
|
1442
|
+
*/
|
|
1443
|
+
export function skippedRestoreEnvelope(success: boolean, message: string): CodexNativeRestoreResult {
|
|
1444
|
+
return {
|
|
1445
|
+
success,
|
|
1446
|
+
message,
|
|
1447
|
+
artifacts: {
|
|
1448
|
+
config: { state: "skipped", changed: false, action: "owned-fields-stripped", message },
|
|
1449
|
+
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1450
|
+
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1451
|
+
},
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** The config/profile half of a native restore, reported as one artifact. */
|
|
1456
|
+
function restoreCodexConfigInline(): CodexRestoreConfigResult {
|
|
1457
|
+
try {
|
|
1458
|
+
const journal = restoreJournalState();
|
|
1459
|
+
const restored = journal.configRestored
|
|
1460
|
+
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
1461
|
+
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
1462
|
+
return restored.success
|
|
1463
|
+
? {
|
|
1464
|
+
state: "ok",
|
|
1465
|
+
changed: journal.configRestored || journal.profileRestored || journal.profileChanged || restored.message.startsWith("Removed"),
|
|
1466
|
+
action: journal.configRestored ? "journal-restored" : "owned-fields-stripped",
|
|
1467
|
+
message: restored.message,
|
|
1468
|
+
}
|
|
1469
|
+
: { state: "failed", changed: false, action: "failed", message: restored.message };
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** The catalog half, always inside its own K acquisition. */
|
|
1476
|
+
/**
|
|
1477
|
+
* The catalog half, always inside its own K acquisition.
|
|
1478
|
+
*
|
|
1479
|
+
* `journaledCatalogPath` must be captured by the CALLER, before the config half runs: a
|
|
1480
|
+
* successful journal restore deletes the journal, and a config restore can remove
|
|
1481
|
+
* `model_catalog_json`. Reading it here would be too late in both cases (#1798).
|
|
1482
|
+
*/
|
|
1483
|
+
function restoreCodexCatalogArtifact(
|
|
1484
|
+
revalidateDesiredState: boolean,
|
|
1485
|
+
journaledCatalogPath: string | null,
|
|
1486
|
+
): CodexRestoreCatalogResult {
|
|
1487
|
+
const owningCodexHome = getCodexHome();
|
|
1488
|
+
try {
|
|
1489
|
+
const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
1490
|
+
revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
|
|
1491
|
+
? null
|
|
1492
|
+
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
|
|
1493
|
+
return restored.kind === "completed" && restored.value !== null
|
|
1494
|
+
? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
|
|
1495
|
+
: restored.kind === "completed"
|
|
1496
|
+
? {
|
|
1497
|
+
state: "skipped", changed: false, removed: 0, kept: 0, path: null,
|
|
1498
|
+
message: "Codex integration was re-enabled; native catalog restoration was skipped.",
|
|
1499
|
+
}
|
|
1500
|
+
: {
|
|
1501
|
+
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
1502
|
+
message: `Codex catalog could not be restored: ${restored.reason}.`,
|
|
1503
|
+
};
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
return {
|
|
1506
|
+
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
1507
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
759
1512
|
/**
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
1513
|
+
* Restore native Codex, running history in a Worker under H.
|
|
1514
|
+
*
|
|
1515
|
+
* On a coordinated home the config/profile restore happens INSIDE the Codex
|
|
1516
|
+
* write lock, publishing a `remove` transition — the same serialization inject
|
|
1517
|
+
* uses. Without it, an older restore could overwrite a config a concurrent
|
|
1518
|
+
* enable had just written under the lock, and then honestly report success
|
|
1519
|
+
* while desired intent said ON. The desired-state re-read under the lock turns
|
|
1520
|
+
* that lost race into the discriminated `desired_enabled` skip.
|
|
763
1521
|
*/
|
|
764
|
-
export function
|
|
1522
|
+
export async function restoreNativeCodexAsync(
|
|
1523
|
+
options: { revalidateDesiredState?: boolean } = {},
|
|
1524
|
+
): Promise<CodexNativeRestoreResult> {
|
|
1525
|
+
const activeProvider = currentExternalCodexModelProvider();
|
|
1526
|
+
if (activeProvider) {
|
|
1527
|
+
// External-provider courtesy: only the stale journal is removed. The
|
|
1528
|
+
// history worker must not launch — it would turn a read-mostly courtesy
|
|
1529
|
+
// result into a history mutation on a home we do not own.
|
|
1530
|
+
removeJournal();
|
|
1531
|
+
return externalProviderRestoreResult(activeProvider);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// `restore` normally honours a human request even when an unrelated
|
|
1535
|
+
// service-manager probe is unavailable. A recorded FOREIGN home is not an
|
|
1536
|
+
// unrelated probe: it is positive evidence another installation owns these
|
|
1537
|
+
// native artifacts, so do not create profile/claim locks before refusing.
|
|
1538
|
+
if (options.revalidateDesiredState) {
|
|
1539
|
+
const ownership = inspectNativeCodexOwnership();
|
|
1540
|
+
if (ownership.ownership === "foreign") return foreignOwnershipRestoreRefusal(ownership.reason);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const eligibility = codexWriteCoordinationEligibility({
|
|
1544
|
+
coordinatorPath: () =>
|
|
1545
|
+
resolveCodexCoordinatorDatabasePath(resolveEffectiveUserIdentity(), getCodexHome()),
|
|
1546
|
+
residue: () => classifyNativeRoutedResidue(),
|
|
1547
|
+
integrationRecord: () => readIntegrationRecord(),
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
1551
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
1552
|
+
// catalog we actually wrote (#1798).
|
|
1553
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
1554
|
+
let config: CodexRestoreConfigResult;
|
|
1555
|
+
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
1556
|
+
|
|
1557
|
+
if (eligibility.kind === "coordinated" || eligibility.kind === "adopt") {
|
|
1558
|
+
// The restore has no candidate bytes to witness; freshness comes from the
|
|
1559
|
+
// filesystem reads and the desired-state re-read performed under the lock.
|
|
1560
|
+
const witness = { authoritySnapshotId: "codex-native-restore" };
|
|
1561
|
+
const coordinated = await withCodexWriteLock(
|
|
1562
|
+
{
|
|
1563
|
+
timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
1564
|
+
...(eligibility.kind === "adopt" ? { adoption: { direction: "remove" as const } } : {}),
|
|
1565
|
+
admitted: witness,
|
|
1566
|
+
readAdmissionUnderLock: () => witness,
|
|
1567
|
+
},
|
|
1568
|
+
(ctx) => {
|
|
1569
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1570
|
+
throw new CodexWriteLockSkipped("desired_enabled");
|
|
1571
|
+
}
|
|
1572
|
+
const published = ctx.coordinator.beginTransition(
|
|
1573
|
+
{
|
|
1574
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
1575
|
+
currentTxId: ctx.currentTxId,
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
txId: ctx.expectation.txId,
|
|
1579
|
+
direction: "remove",
|
|
1580
|
+
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
1581
|
+
nextRetryAt: new Date().toISOString(),
|
|
1582
|
+
},
|
|
1583
|
+
);
|
|
1584
|
+
if (published.kind !== "updated") {
|
|
1585
|
+
throw new CodexWriteConflictError(
|
|
1586
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const preImages = captureCodexPreImages();
|
|
1590
|
+
let restored: CodexRestoreConfigResult;
|
|
1591
|
+
try {
|
|
1592
|
+
restored = restoreCodexConfigInline();
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
const compensated = restoreCodexPreImages(preImages);
|
|
1595
|
+
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
config: restored,
|
|
1600
|
+
preImages,
|
|
1601
|
+
receipt: {
|
|
1602
|
+
nativeGeneration: ctx.expectation.nativeAfter,
|
|
1603
|
+
currentTxId: ctx.expectation.txId,
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
},
|
|
1607
|
+
);
|
|
1608
|
+
if (coordinated.status === "skipped") return desiredEnabledRestoreSkip();
|
|
1609
|
+
if (coordinated.status !== "acquired") {
|
|
1610
|
+
config = {
|
|
1611
|
+
state: "failed",
|
|
1612
|
+
changed: false,
|
|
1613
|
+
action: "failed",
|
|
1614
|
+
message: coordinated.status === "busy"
|
|
1615
|
+
? `Another process is writing Codex configuration right now (waited ${coordinated.waitedMs}ms). Retry shortly.`
|
|
1616
|
+
: `Codex configuration was not restored: ${coordinated.message}`,
|
|
1617
|
+
};
|
|
1618
|
+
} else {
|
|
1619
|
+
recordCodexNativeTransactionProvenance(
|
|
1620
|
+
coordinated.value.preImages,
|
|
1621
|
+
coordinated.value.receipt.currentTxId,
|
|
1622
|
+
);
|
|
1623
|
+
config = coordinated.value.config;
|
|
1624
|
+
transitionReceipt = coordinated.value.receipt;
|
|
1625
|
+
}
|
|
1626
|
+
} else {
|
|
1627
|
+
// Legacy-uncoordinated (or unresolvable) homes keep the unserialized path
|
|
1628
|
+
// they have always had; restore is the escape hatch and must not strand
|
|
1629
|
+
// them. The plain re-read still honors an intervening re-enable.
|
|
1630
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1631
|
+
return desiredEnabledRestoreSkip();
|
|
1632
|
+
}
|
|
1633
|
+
config = restoreCodexConfigInline();
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
1637
|
+
const outcome = await runCodexHistoryJob({
|
|
1638
|
+
...resolveCodexHistoryJobTarget(),
|
|
1639
|
+
...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
|
|
1640
|
+
operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
|
|
1641
|
+
});
|
|
1642
|
+
if (transitionReceipt) {
|
|
1643
|
+
resolveCodexHistoryTransition(transitionReceipt, outcome);
|
|
1644
|
+
}
|
|
1645
|
+
const history: CodexRestoreHistoryResult = outcome.kind === "converged"
|
|
1646
|
+
? {
|
|
1647
|
+
state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
1648
|
+
message: outcome.rows > 0
|
|
1649
|
+
? `Resume history metadata restored from opencodex backup (${outcome.rows} thread(s)); original providers preserved.`
|
|
1650
|
+
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
1651
|
+
}
|
|
1652
|
+
: outcome.kind === "skipped"
|
|
1653
|
+
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
|
|
1654
|
+
: outcome.kind === "blocked" && (outcome.reason === "desired_disabled" || outcome.reason === "desired_enabled")
|
|
1655
|
+
? {
|
|
1656
|
+
state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
|
|
1657
|
+
message: outcome.reason === "desired_disabled"
|
|
1658
|
+
? "Codex integration was disabled; history restoration was skipped."
|
|
1659
|
+
: "Codex integration was enabled; history restoration was skipped.",
|
|
1660
|
+
}
|
|
1661
|
+
: outcome.kind === "blocked" || outcome.kind === "failed"
|
|
1662
|
+
? failedHistoryRestoreFromOutcome(outcome)
|
|
1663
|
+
: failedHistoryRestore();
|
|
1664
|
+
const base = catalog.removed > 0
|
|
1665
|
+
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
1666
|
+
: config.message;
|
|
1667
|
+
const success = config.state !== "failed"
|
|
1668
|
+
&& catalog.state !== "failed"
|
|
1669
|
+
&& history.state !== "failed";
|
|
1670
|
+
return {
|
|
1671
|
+
success,
|
|
1672
|
+
message: `${base}${history.state === "failed" ? ` ⚠️ ${history.message}` : ""}`,
|
|
1673
|
+
artifacts: { config, catalog, history },
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateDesiredState?: boolean } = {}): CodexNativeRestoreResult {
|
|
765
1678
|
const activeProvider = currentExternalCodexModelProvider();
|
|
766
1679
|
if (activeProvider) {
|
|
767
1680
|
removeJournal();
|
|
768
|
-
return
|
|
1681
|
+
return externalProviderRestoreResult(activeProvider);
|
|
1682
|
+
}
|
|
1683
|
+
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
1684
|
+
return desiredEnabledRestoreSkip();
|
|
769
1685
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
1686
|
+
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
1687
|
+
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
1688
|
+
// catalog we actually wrote (#1798).
|
|
1689
|
+
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
1690
|
+
const config = restoreCodexConfigInline();
|
|
1691
|
+
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
775
1692
|
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
776
1693
|
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
777
1694
|
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
@@ -779,21 +1696,66 @@ export function restoreNativeCodex(): { success: boolean; message: string } {
|
|
|
779
1696
|
let skipWhenProvablyNoop = false;
|
|
780
1697
|
try {
|
|
781
1698
|
skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
|
|
782
|
-
} catch {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
?
|
|
789
|
-
:
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1699
|
+
} catch {
|
|
1700
|
+
/* unreadable config: keep the conservative write-open restore */
|
|
1701
|
+
}
|
|
1702
|
+
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
1703
|
+
// native files come down here, and history runs in the Worker under H.
|
|
1704
|
+
const rawHistory = options.skipHistory
|
|
1705
|
+
? { rows: 0, files: 0 }
|
|
1706
|
+
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
1707
|
+
skipWhenProvablyNoop,
|
|
1708
|
+
});
|
|
1709
|
+
const history: CodexRestoreHistoryResult = options.skipHistory
|
|
1710
|
+
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
1711
|
+
: rawHistory.failed
|
|
1712
|
+
? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
|
|
1713
|
+
: {
|
|
1714
|
+
state: "ok",
|
|
1715
|
+
changed: rawHistory.rows > 0 || rawHistory.files > 0 || (rawHistory.ejectedRows ?? 0) > 0,
|
|
1716
|
+
rows: rawHistory.rows,
|
|
1717
|
+
files: rawHistory.files,
|
|
1718
|
+
ejectedRows: rawHistory.ejectedRows ?? 0,
|
|
1719
|
+
message: rawHistory.rows > 0
|
|
1720
|
+
? `Resume history metadata restored from opencodex backup (${rawHistory.rows} thread(s)); original providers preserved.`
|
|
1721
|
+
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
1722
|
+
};
|
|
1723
|
+
const message = catalog.removed > 0
|
|
1724
|
+
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
1725
|
+
: config.message;
|
|
1726
|
+
return {
|
|
1727
|
+
success: config.state !== "failed" && catalog.state !== "failed" && history.state !== "failed",
|
|
1728
|
+
message,
|
|
1729
|
+
artifacts: { config, catalog, history },
|
|
1730
|
+
};
|
|
795
1731
|
}
|
|
796
1732
|
|
|
797
1733
|
export function getCodexConfigPath(): string {
|
|
798
1734
|
return CODEX_CONFIG_PATH;
|
|
799
1735
|
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Frame one failed apply history job honestly.
|
|
1739
|
+
*
|
|
1740
|
+
* A genuine lock keeps the established deferred/SKIPPED wording; any other
|
|
1741
|
+
* reason names itself instead of blaming the Codex app/IDE.
|
|
1742
|
+
*/
|
|
1743
|
+
export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legacyMode: boolean): string {
|
|
1744
|
+
// A busy database is a deferral no matter which half observed it: the lock
|
|
1745
|
+
// contended (blocked/busy), or the worker acquired the lock and then found
|
|
1746
|
+
// SQLite busy (failed with a busy history reason). Only those keep the
|
|
1747
|
+
// deferred headline; every other failure is a real "NOT changed".
|
|
1748
|
+
const busy =
|
|
1749
|
+
(outcome.kind === "blocked" && outcome.reason === "busy") ||
|
|
1750
|
+
(outcome.kind === "failed" && outcome.historyFailureReason === "busy");
|
|
1751
|
+
const partiallyChanged = outcome.kind === "failed"
|
|
1752
|
+
&& ((outcome.rows ?? 0) > 0 || (outcome.files ?? 0) > 0);
|
|
1753
|
+
const headline = partiallyChanged
|
|
1754
|
+
? "Codex resume history changed but did not converge"
|
|
1755
|
+
: legacyMode
|
|
1756
|
+
? "Codex resume history sync SKIPPED"
|
|
1757
|
+
: busy
|
|
1758
|
+
? "Codex resume history metadata restore deferred"
|
|
1759
|
+
: "Codex resume history NOT changed";
|
|
1760
|
+
return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
|
|
1761
|
+
}
|