@bitkyc08/opencodex 2.84.0 → 2.87.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +91 -11
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +16 -3
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +40 -11
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +37 -10
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +44 -3
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +223 -0
- package/src/advisor/context.ts +238 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +353 -0
- package/src/advisor/settings.ts +144 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +72 -7
- package/src/claude/desktop-3p.ts +75 -19
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +154 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +51 -19
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +165 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +23 -2
- package/src/cli/capabilities-base.ts +39 -11
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +30 -5
- package/src/cli/doctor.ts +15 -1
- package/src/cli/ensure-desktop-cold-start.ts +382 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +4 -2
- package/src/cli/index.ts +14 -5
- package/src/cli/integration-plan-dto.ts +3 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +41 -9
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +3 -1
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +35 -48
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +81 -7
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +19 -2
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +42 -19
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +35 -86
- package/src/codex/catalog/provider-models.ts +40 -34
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +33 -16
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +63 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +25 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +73 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +182 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/features.ts +85 -45
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +2 -2
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +35 -7
- package/src/config/live-reconcile.ts +51 -2
- package/src/config/load-degrade.ts +23 -1
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/paths.ts +10 -3
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +39 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +39 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +962 -350
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +2 -0
- package/src/integrations/registry.ts +15 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +72 -0
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +26 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/redact.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +62 -12
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +188 -111
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/provider-summary.ts +2 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +5 -0
- package/src/providers/model-rename-migration.ts +7 -5
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +44 -14
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +78 -22
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/responses/state.ts +32 -29
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +254 -221
- package/src/routing/history/ingest.ts +242 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +21 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +15 -0
- package/src/server/chat-completions.ts +82 -10
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +164 -433
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +11 -3
- package/src/server/index/startup-warnings.ts +11 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +14 -11
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +202 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +77 -6
- package/src/server/management/context.ts +4 -0
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +13 -8
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +21 -1
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +106 -22
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +13 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +48 -19
- package/src/server/responses/adapter-delivery.ts +43 -8
- package/src/server/responses/adapter-dispatch.ts +48 -4
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +368 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +12 -1
- package/src/server/responses/core-combo.ts +141 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +7 -2
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +51 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +25 -6
- package/src/server/responses/core.ts +3 -116
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +19 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -16
- package/src/server/responses/request-prepare.ts +144 -17
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +120 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +166 -38
- package/src/server/responses/sidecar-execution.ts +98 -5
- package/src/server/responses/terminal-guard.ts +4 -17
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +99 -61
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +135 -0
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +124 -6
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +39 -6
- package/src/types/request.ts +38 -2
- package/src/types/tools.ts +2 -0
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/npm-invocation.mjs +12 -2
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +15 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { dlopen, ptr, toArrayBuffer, type Pointer } from "bun:ffi";
|
|
4
|
+
import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsSystemDirectory } from "../../lib/windows-elevation";
|
|
5
|
+
|
|
6
|
+
const MAX_INPUT = 65_536;
|
|
7
|
+
const MAX_OUTPUT = 131_072;
|
|
8
|
+
const PURPOSE = "opencodex/codex-desktop-compatibility/authority/v1";
|
|
9
|
+
const ENTROPY = new TextEncoder().encode(PURPOSE);
|
|
10
|
+
// Only the PowerShell fallback is bounded by a clock. A cold Windows PowerShell that must also load
|
|
11
|
+
// System.Security has been measured past 10s on a loaded machine (Windows CI run 38591626343).
|
|
12
|
+
const DPAPI_TIMEOUT_MS = 20_000;
|
|
13
|
+
const SCRIPT = [
|
|
14
|
+
"$ErrorActionPreference='Stop'",
|
|
15
|
+
"Add-Type -AssemblyName System.Security",
|
|
16
|
+
"$r=[Console]::In.ReadToEnd()|ConvertFrom-Json",
|
|
17
|
+
"$data=[Convert]::FromBase64String($r.data)",
|
|
18
|
+
`if($data.Length -eq 0 -or $data.Length -gt ${MAX_INPUT}){throw 'Invalid data size'}`,
|
|
19
|
+
`$entropy=[Text.Encoding]::UTF8.GetBytes('${PURPOSE}')`,
|
|
20
|
+
"$scope=[Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
21
|
+
"if($r.operation -eq 'protect'){",
|
|
22
|
+
" $out=[Security.Cryptography.ProtectedData]::Protect($data,$entropy,$scope)",
|
|
23
|
+
"}elseif($r.operation -eq 'unprotect'){",
|
|
24
|
+
" $out=[Security.Cryptography.ProtectedData]::Unprotect($data,$entropy,$scope)",
|
|
25
|
+
"}else{throw 'Invalid operation'}",
|
|
26
|
+
"[Console]::Out.Write([Convert]::ToBase64String($out))",
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
type Operation = "protect" | "unprotect";
|
|
30
|
+
|
|
31
|
+
export interface AuthorityKeyProtection {
|
|
32
|
+
protect(cleartext: Uint8Array): Promise<Uint8Array>;
|
|
33
|
+
unprotect(ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// CRYPTPROTECT_UI_FORBIDDEN and no CRYPTPROTECT_LOCAL_MACHINE: the CurrentUser scope that
|
|
37
|
+
// ProtectedData.Protect(..., CurrentUser) requests, so blobs written by either path read in the other.
|
|
38
|
+
const CRYPTPROTECT_UI_FORBIDDEN = 0x1;
|
|
39
|
+
// DATA_BLOB on x64: DWORD cbData, four bytes of padding, BYTE* pbData.
|
|
40
|
+
const DATA_BLOB_BYTES = 16;
|
|
41
|
+
const DPAPI_SIGNATURE = { args: ["ptr", "ptr", "ptr", "ptr", "ptr", "u32", "ptr"], returns: "i32" } as const;
|
|
42
|
+
|
|
43
|
+
type NativeDpapi = (operation: Operation, data: Uint8Array) => Uint8Array;
|
|
44
|
+
let nativeDpapi: NativeDpapi | undefined;
|
|
45
|
+
|
|
46
|
+
function dataBlob(bytes: Uint8Array): Buffer {
|
|
47
|
+
const blob = Buffer.alloc(DATA_BLOB_BYTES);
|
|
48
|
+
blob.writeUInt32LE(bytes.byteLength, 0);
|
|
49
|
+
blob.writeBigUInt64LE(BigInt(ptr(bytes)), 8);
|
|
50
|
+
return blob;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* In-process CryptProtectData/CryptUnprotectData. Spawning Windows PowerShell for every call cost
|
|
55
|
+
* seconds of cold start (past 20s on a loaded CI runner) for a call that takes milliseconds.
|
|
56
|
+
* crypt32 is loaded from the GetSystemDirectoryW path so the DLL search order cannot substitute it.
|
|
57
|
+
* Returns null where Bun's FFI is unavailable (Windows ARM64), leaving the PowerShell helper.
|
|
58
|
+
*/
|
|
59
|
+
function loadNativeDpapi(): NativeDpapi | null {
|
|
60
|
+
if (nativeDpapi) return nativeDpapi;
|
|
61
|
+
if (process.platform !== "win32" || process.arch !== "x64") return null;
|
|
62
|
+
let crypt32;
|
|
63
|
+
let kernel32;
|
|
64
|
+
try {
|
|
65
|
+
const systemDirectory = resolveTrustedWindowsSystemDirectory();
|
|
66
|
+
crypt32 = dlopen(join(systemDirectory, "crypt32.dll"), {
|
|
67
|
+
CryptProtectData: DPAPI_SIGNATURE,
|
|
68
|
+
CryptUnprotectData: DPAPI_SIGNATURE,
|
|
69
|
+
}).symbols;
|
|
70
|
+
kernel32 = dlopen(join(systemDirectory, "kernel32.dll"), {
|
|
71
|
+
LocalFree: { args: ["ptr"], returns: "ptr" },
|
|
72
|
+
// Wipes by address, so clearing never depends on a JS view having been created.
|
|
73
|
+
RtlZeroMemory: { args: ["ptr", "u64"], returns: "void" },
|
|
74
|
+
}).symbols;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const { CryptProtectData, CryptUnprotectData } = crypt32;
|
|
79
|
+
const { LocalFree, RtlZeroMemory } = kernel32;
|
|
80
|
+
nativeDpapi = (operation, data) => {
|
|
81
|
+
const input = dataBlob(data);
|
|
82
|
+
const entropy = dataBlob(ENTROPY);
|
|
83
|
+
const output = Buffer.alloc(DATA_BLOB_BYTES);
|
|
84
|
+
const call = operation === "protect" ? CryptProtectData : CryptUnprotectData;
|
|
85
|
+
// Description, reserved and prompt are null; Windows reports no detail worth propagating.
|
|
86
|
+
if (!call(ptr(input), null, ptr(entropy), null, null, CRYPTPROTECT_UI_FORBIDDEN, ptr(output))) {
|
|
87
|
+
throw new Error("desktop_compatibility_key_protection_failed");
|
|
88
|
+
}
|
|
89
|
+
const length = output.readUInt32LE(0);
|
|
90
|
+
const address = Number(output.readBigUInt64LE(8)) as Pointer;
|
|
91
|
+
// Every step after a successful call sits inside try so the native buffer is always
|
|
92
|
+
// released, even if creating the view over it throws.
|
|
93
|
+
let view: Uint8Array | null = null;
|
|
94
|
+
try {
|
|
95
|
+
view = length > 0 ? new Uint8Array(toArrayBuffer(address, 0, length)) : null;
|
|
96
|
+
if (length === 0 || length > MAX_INPUT) throw new Error("desktop_compatibility_key_size");
|
|
97
|
+
return Uint8Array.from(view!);
|
|
98
|
+
} finally {
|
|
99
|
+
// The unprotected buffer is cleartext key material; wipe it natively before LocalFree
|
|
100
|
+
// releases it, including when the size check or view creation fails. LocalFree still
|
|
101
|
+
// runs if wiping throws.
|
|
102
|
+
try {
|
|
103
|
+
if (operation === "unprotect" && length > 0) RtlZeroMemory(address, length);
|
|
104
|
+
} finally {
|
|
105
|
+
LocalFree(address);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
return nativeDpapi;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The payload travels on stdin, never in process arguments, environment or logs. */
|
|
113
|
+
function runPowerShellDpapi(operation: Operation, data: Uint8Array, timeoutMs = DPAPI_TIMEOUT_MS): Promise<Uint8Array> {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
|
|
116
|
+
timeout: timeoutMs, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
|
|
117
|
+
}, (error, stdout) => {
|
|
118
|
+
// Do not include PowerShell stderr, command output or the input in a propagated error.
|
|
119
|
+
if (error || !/^[A-Za-z0-9+/]+={0,2}$/.test(stdout.trim())) {
|
|
120
|
+
reject(new Error("desktop_compatibility_key_protection_failed")); return;
|
|
121
|
+
}
|
|
122
|
+
const bytes = Buffer.from(stdout.trim(), "base64");
|
|
123
|
+
if (bytes.length === 0 || bytes.length > MAX_INPUT) {
|
|
124
|
+
reject(new Error("desktop_compatibility_key_size")); return;
|
|
125
|
+
}
|
|
126
|
+
resolve(bytes);
|
|
127
|
+
});
|
|
128
|
+
child.stdin?.on("error", () => { /* execFile's completion reports the failed child. */ });
|
|
129
|
+
child.stdin?.end(JSON.stringify({ operation, data: Buffer.from(data).toString("base64") }));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validated(data: Uint8Array): Uint8Array {
|
|
134
|
+
if (process.platform !== "win32") throw new Error("desktop_compatibility_windows_required");
|
|
135
|
+
if (data.byteLength === 0 || data.byteLength > MAX_INPUT) throw new Error("desktop_compatibility_key_size");
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runDpapi(operation: Operation, data: Uint8Array): Promise<Uint8Array> {
|
|
140
|
+
validated(data);
|
|
141
|
+
const native = loadNativeDpapi();
|
|
142
|
+
return native ? native(operation, data) : runPowerShellDpapi(operation, data);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** CurrentUser protects against other OS identities, not other processes of the same user. */
|
|
146
|
+
export const windowsAuthorityKeyProtection: AuthorityKeyProtection = {
|
|
147
|
+
protect: bytes => runDpapi("protect", bytes),
|
|
148
|
+
unprotect: bytes => runDpapi("unprotect", bytes),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Test-only: the PowerShell helper (Windows ARM64, and every key stored before the native path).
|
|
153
|
+
* The caller owns the bound because a compatibility check must not depend on cold-start latency.
|
|
154
|
+
*/
|
|
155
|
+
export function powerShellAuthorityKeyProtectionForTests(timeoutMs: number): AuthorityKeyProtection {
|
|
156
|
+
return {
|
|
157
|
+
protect: async bytes => runPowerShellDpapi("protect", validated(bytes), timeoutMs),
|
|
158
|
+
unprotect: async bytes => runPowerShellDpapi("unprotect", validated(bytes), timeoutMs),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Test-only: whether this process protects keys in-process rather than through PowerShell. */
|
|
163
|
+
export function usesNativeDpapiForTests(): boolean {
|
|
164
|
+
return process.platform === "win32" && loadNativeDpapi() !== null;
|
|
165
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DesktopAppInstall, DesktopExec, DesktopProcess } from "../desktop-app/types";
|
|
2
|
+
import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
|
|
3
|
+
import { WINDOWS_ACTIVATION_SOURCE } from "./windows-activation-source";
|
|
4
|
+
import { readDesktopCompatibilityLaunch } from "./runtime-ownership";
|
|
5
|
+
|
|
6
|
+
/** Only the feature's loopback PAC endpoint may become a launch argument. */
|
|
7
|
+
export function validatedCompatibilityPacUrl(value: string): string {
|
|
8
|
+
let url: URL;
|
|
9
|
+
try { url = new URL(value); } catch { throw new Error("desktop_compatibility_invalid_pac_url"); }
|
|
10
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || Number(url.port) < 1
|
|
11
|
+
|| url.username || url.password || url.search || url.hash
|
|
12
|
+
|| !/^\/[a-zA-Z0-9-]{16,80}\/proxy\.pac$/.test(url.pathname)
|
|
13
|
+
|| value !== url.href) throw new Error("desktop_compatibility_invalid_pac_url");
|
|
14
|
+
return url.href;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function literal(value: string): string { return `'${value.replaceAll("'", "''")}'`; }
|
|
18
|
+
|
|
19
|
+
export function compatibilityActivationScript(install: DesktopAppInstall, pacUrl: string): string {
|
|
20
|
+
const pac = validatedCompatibilityPacUrl(pacUrl);
|
|
21
|
+
if (!/^OpenAI\.Codex(?:Beta)?_[A-Za-z0-9]+$/.test(install.id) || install.relaunch !== `${install.id}!App`) {
|
|
22
|
+
throw new Error("desktop_compatibility_invalid_package");
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
"$ErrorActionPreference='Stop'",
|
|
26
|
+
`$family=${literal(install.id)}; $root=${literal(install.root)}; $pac=${literal(pac)}`,
|
|
27
|
+
`$p=Get-AppxPackage -Name ${literal(install.id.split("_")[0]!)}`,
|
|
28
|
+
"$p=@($p|Where-Object {$_.PackageFamilyName -eq $family -and $_.InstallLocation -ieq $root})",
|
|
29
|
+
"if($p.Count -ne 1){throw 'Package changed'}; $p=$p[0]",
|
|
30
|
+
"$manifest=Get-AppxPackageManifest -Package $p.PackageFullName",
|
|
31
|
+
"$entry=@($manifest.Package.Applications.Application|Where-Object {$_.Id -eq 'App' -and $_.Executable.Replace('/','\\') -ieq 'app\\ChatGPT.exe'})",
|
|
32
|
+
"if($entry.Count -ne 1){throw 'Application entry changed'}",
|
|
33
|
+
"Add-Type -TypeDefinition @'", WINDOWS_ACTIVATION_SOURCE, "'@",
|
|
34
|
+
"$pidValue=[OpenCodexPackageActivation]::Activate($family+'!App','--proxy-pac-url='+$pac)",
|
|
35
|
+
"if([OpenCodexPackageActivation]::PackageOf($pidValue) -ne $p.PackageFullName){throw 'Package identity missing'}",
|
|
36
|
+
"$running=Get-CimInstance Win32_Process -Filter ('ProcessId='+$pidValue)",
|
|
37
|
+
"$expected=Join-Path $p.InstallLocation 'app\\ChatGPT.exe'",
|
|
38
|
+
"if(-not $running -or $running.ExecutablePath -ine $expected -or $running.CommandLine -notmatch ('(?:^|[\\s\"])--proxy-pac-url='+[regex]::Escape($pac)+'(?:$|[\\s\"])')){throw 'Launch arguments not observed'}",
|
|
39
|
+
"[pscustomobject]@{pid=[int]$pidValue;packageFullName=$p.PackageFullName;verified=$true}|ConvertTo-Json -Compress",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/** Preserve only the current runtime's PAC and lifetime, never trust a URL shape alone. */
|
|
45
|
+
export function captureWindowsCompatibilityContext(processes: readonly DesktopProcess[]): Record<string, string> {
|
|
46
|
+
const members = new Set(processes.map(entry => entry.pid));
|
|
47
|
+
const urls = new Set<string>();
|
|
48
|
+
for (const entry of processes.filter(value => !members.has(value.parentPid))) {
|
|
49
|
+
if (entry.commandLine !== undefined && entry.commandLine.trim() === "") {
|
|
50
|
+
throw new Error("desktop_compatibility_launch_context_unavailable");
|
|
51
|
+
}
|
|
52
|
+
for (const match of (entry.commandLine ?? "").matchAll(/(?:^|\s)"?--proxy-pac-url=([^"\s]+)"?/g)) {
|
|
53
|
+
const url = match[1]!;
|
|
54
|
+
if (url.startsWith("http://127.0.0.1:")) urls.add(validatedCompatibilityPacUrl(url));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (urls.size > 1) throw new Error("desktop_compatibility_conflicting_launch_context");
|
|
58
|
+
if (urls.size === 0) return {};
|
|
59
|
+
const pacUrl = [...urls][0]!, owner = readDesktopCompatibilityLaunch();
|
|
60
|
+
if (!owner || owner.pacUrl !== pacUrl) throw new Error("desktop_compatibility_launch_owner_unverified");
|
|
61
|
+
return { codexCompatibilityPacUrl: pacUrl, codexCompatibilityGeneration: owner.generation };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Recheck after the asynchronous stop ladder: identical endpoints can belong to a new runtime. */
|
|
65
|
+
export function assertWindowsCompatibilityContext(context: Record<string, string>): void {
|
|
66
|
+
const owner = readDesktopCompatibilityLaunch();
|
|
67
|
+
if (!owner || owner.pacUrl !== context.codexCompatibilityPacUrl || owner.generation !== context.codexCompatibilityGeneration) {
|
|
68
|
+
throw new Error("desktop_compatibility_launch_owner_unverified");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function activateWindowsCodexCompatibility(exec: DesktopExec, install: DesktopAppInstall, pacUrl: string): { pid: number; packageFullName: string } {
|
|
73
|
+
const script = compatibilityActivationScript(install, pacUrl);
|
|
74
|
+
let text: string;
|
|
75
|
+
try { text = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 15_000, windowsHide: true }); }
|
|
76
|
+
catch { throw new Error("desktop_compatibility_activation_unverified"); }
|
|
77
|
+
let result: unknown;
|
|
78
|
+
try { result = JSON.parse(text.trim().split(/\r?\n/).at(-1) ?? ""); }
|
|
79
|
+
catch { throw new Error("desktop_compatibility_activation_unverified"); }
|
|
80
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("desktop_compatibility_activation_unverified");
|
|
81
|
+
const value = result as Record<string, unknown>;
|
|
82
|
+
if (value.verified !== true || !Number.isSafeInteger(value.pid) || Number(value.pid) <= 0
|
|
83
|
+
|| typeof value.packageFullName !== "string" || !value.packageFullName.startsWith(install.id.split("_")[0]! + "_")
|
|
84
|
+
|| !value.packageFullName.endsWith("_" + install.id.split("_")[1]!)) throw new Error("desktop_compatibility_activation_unverified");
|
|
85
|
+
return { pid: Number(value.pid), packageFullName: value.packageFullName };
|
|
86
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
|
|
2
|
+
import type { DesktopExec } from "../desktop-app/types";
|
|
3
|
+
import { activateWindowsCodexCompatibility, validatedCompatibilityPacUrl } from "./windows-package-command";
|
|
4
|
+
export { compatibilityActivationScript, validatedCompatibilityPacUrl } from "./windows-package-command";
|
|
5
|
+
|
|
6
|
+
export interface CompatibilityLaunchIo {
|
|
7
|
+
exec?: DesktopExec;
|
|
8
|
+
platform?: string;
|
|
9
|
+
}
|
|
10
|
+
export type CompatibilityLaunchResult =
|
|
11
|
+
| { status: "started"; pid: number; packageFullName: string }
|
|
12
|
+
| { status: "refused"; reason: "unsupported_platform" | "test_environment" | "package_unavailable" | "app_running" | "process_probe_failed" | "activation_failed" };
|
|
13
|
+
|
|
14
|
+
/** Never quits an app, launches the raw EXE, changes login or installs a watcher. */
|
|
15
|
+
export function launchWindowsCodexCompatibility(pacUrl: string, io: CompatibilityLaunchIo = {}): CompatibilityLaunchResult {
|
|
16
|
+
validatedCompatibilityPacUrl(pacUrl);
|
|
17
|
+
if ((io.platform ?? process.platform) !== "win32") return { status: "refused", reason: "unsupported_platform" };
|
|
18
|
+
if (process.env.OCX_TEST_HOME_GUARD === "1" && !io.exec) return { status: "refused", reason: "test_environment" };
|
|
19
|
+
const exec = io.exec ?? windowsDefaultExec;
|
|
20
|
+
const install = windowsDesktopAppAdapter.discover(exec);
|
|
21
|
+
if (!install) return { status: "refused", reason: "package_unavailable" };
|
|
22
|
+
const processes = windowsDesktopAppAdapter.listProcesses(exec, install);
|
|
23
|
+
if (processes === null) return { status: "refused", reason: "process_probe_failed" };
|
|
24
|
+
if (processes.length > 0) return { status: "refused", reason: "app_running" };
|
|
25
|
+
try {
|
|
26
|
+
return { status: "started", ...activateWindowsCodexCompatibility(exec, install, pacUrl) };
|
|
27
|
+
} catch { return { status: "refused", reason: "activation_failed" }; }
|
|
28
|
+
}
|
package/src/codex/features.ts
CHANGED
|
@@ -44,6 +44,10 @@ import { resolveCodexHomeDir } from "./home";
|
|
|
44
44
|
import { resolveAndPersistCodexRuntime } from "./runtime";
|
|
45
45
|
import { decodeOverlayState } from "./shim-state-file";
|
|
46
46
|
import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy";
|
|
47
|
+
import { rootAssignmentKey, sourceText, splitSourceLines } from "./toml-source-lines";
|
|
48
|
+
|
|
49
|
+
const AGENTS_THREAD_LIMIT_KEY = "max_concurrent_threads_per_session";
|
|
50
|
+
const LEGACY_AGENTS_THREAD_LIMIT_KEY = "max_threads";
|
|
47
51
|
|
|
48
52
|
/** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
|
|
49
53
|
export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input";
|
|
@@ -328,36 +332,31 @@ export function isDefaultModeRequestUserInputEnabled(configPath?: string): boole
|
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* set when features.multi_agent_v2 is enabled", core/src/config/mod.rs:1421). The
|
|
334
|
-
* `ocx v2 on` flow warns about it instead of editing config itself.
|
|
335
|
+
* Whether either spelling of the [agents] limit is present, including invalid
|
|
336
|
+
* values. Keep the exported compatibility name; migrations must remove both.
|
|
335
337
|
*/
|
|
336
338
|
export function hasAgentsMaxThreads(configPath?: string): boolean {
|
|
337
339
|
const content = readConfigText(configPath);
|
|
338
340
|
if (content === null) return false;
|
|
339
341
|
const parsed = parsedTomlTable(content, "agents");
|
|
340
|
-
if (parsed !== null) return Object.hasOwn(parsed,
|
|
341
|
-
|
|
342
|
-
if (agents === null) return false;
|
|
343
|
-
return /^\s*max_threads\s*=/m.test(agents);
|
|
342
|
+
if (parsed !== null) return Object.hasOwn(parsed, AGENTS_THREAD_LIMIT_KEY) || Object.hasOwn(parsed, LEGACY_AGENTS_THREAD_LIMIT_KEY);
|
|
343
|
+
return agentsThreadLimitEntries(content).entries.length > 0;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
/**
|
|
346
|
+
/** Canonical [agents] limit, falling back to max_threads only when absent. */
|
|
347
347
|
export function getAgentsMaxThreads(configPath?: string): number | null {
|
|
348
348
|
const content = readConfigText(configPath);
|
|
349
349
|
if (content === null) return null;
|
|
350
350
|
const parsed = parsedTomlTable(content, "agents");
|
|
351
351
|
if (parsed !== null) {
|
|
352
|
-
const value = parsed
|
|
352
|
+
const value = Object.hasOwn(parsed, AGENTS_THREAD_LIMIT_KEY) ? parsed[AGENTS_THREAD_LIMIT_KEY] : parsed[LEGACY_AGENTS_THREAD_LIMIT_KEY];
|
|
353
353
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null;
|
|
354
354
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return Number.isInteger(value) && value >= 1 ? value : null;
|
|
355
|
+
// Unrelated valid 64-bit integers can make Bun reject the whole document.
|
|
356
|
+
// Decode the same isolated structural assignment the editor uses, including
|
|
357
|
+
// quoted keys and non-decimal integers, rather than treating unreadable as unset.
|
|
358
|
+
const entries = agentsThreadLimitEntries(content).entries;
|
|
359
|
+
return (entries.find(entry => entry.key === AGENTS_THREAD_LIMIT_KEY) ?? entries[0])?.value ?? null;
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
/**
|
|
@@ -1310,33 +1309,59 @@ function editAgentsMaxThreads(value: number | null, configPath?: string, migrate
|
|
|
1310
1309
|
const path = configPath ?? activeCodexConfigPath();
|
|
1311
1310
|
return editCodexConfigToml(path, (content): TomlEditOutcome => {
|
|
1312
1311
|
const eol = dominantEol(content);
|
|
1313
|
-
const lines = content
|
|
1314
|
-
const headerIdx = lines.findIndex(l => /^\s*\[agents\]\s*(?:#.*)?$/.test(l));
|
|
1312
|
+
const { lines, headerIdx, entries } = agentsThreadLimitEntries(content);
|
|
1315
1313
|
if (headerIdx === -1) {
|
|
1316
1314
|
if (value === null) return { next: content };
|
|
1317
|
-
const separator =
|
|
1318
|
-
|
|
1319
|
-
return { next: applyEol(lines.join("\n"), eol) };
|
|
1315
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? eol : "";
|
|
1316
|
+
return { next: `${content}${separator}${eol}[agents]${eol}${AGENTS_THREAD_LIMIT_KEY} = ${value}${migratedComment ?? ""}${eol}` };
|
|
1320
1317
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1318
|
+
const selected = entries.find(entry => entry.key === AGENTS_THREAD_LIMIT_KEY) ?? entries[0];
|
|
1319
|
+
if (entries.length === 1 && selected?.key === AGENTS_THREAD_LIMIT_KEY && selected.value === value
|
|
1320
|
+
&& value !== null && mergeTrailingComments(selected.comment, migratedComment) === (selected.comment ?? "")) return { next: content };
|
|
1321
|
+
let comment = selected?.comment;
|
|
1322
|
+
for (const entry of entries) comment = mergeTrailingComments(comment, entry.comment);
|
|
1323
|
+
comment = mergeTrailingComments(comment, migratedComment);
|
|
1324
|
+
for (const entry of [...entries].reverse()) {
|
|
1325
|
+
if (value !== null && entry === selected) {
|
|
1326
|
+
const line = lines[entry.index]!;
|
|
1327
|
+
line.text = `${line.text.match(/^\s*/)?.[0] ?? ""}${AGENTS_THREAD_LIMIT_KEY} = ${value}${comment}`;
|
|
1328
|
+
} else lines.splice(entry.index, 1);
|
|
1324
1329
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (!m) continue;
|
|
1329
|
-
if (value === null) lines.splice(i, 1);
|
|
1330
|
-
else if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { next: content };
|
|
1331
|
-
else lines[i] = `${m[1]}max_threads = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
|
|
1332
|
-
return { next: applyEol(lines.join("\n"), eol) };
|
|
1330
|
+
if (value !== null && !selected) {
|
|
1331
|
+
if (!lines[headerIdx]!.eol) lines[headerIdx]!.eol = eol;
|
|
1332
|
+
lines.splice(headerIdx + 1, 0, { text: `${AGENTS_THREAD_LIMIT_KEY} = ${value}${comment}`, eol, structural: true });
|
|
1333
1333
|
}
|
|
1334
|
-
|
|
1335
|
-
lines.splice(headerIdx + 1, 0, `max_threads = ${value}${migratedComment ?? ""}`);
|
|
1336
|
-
return { next: applyEol(lines.join("\n"), eol) };
|
|
1334
|
+
return { next: sourceText(lines) };
|
|
1337
1335
|
});
|
|
1338
1336
|
}
|
|
1339
1337
|
|
|
1338
|
+
/** Only structural assignments in the supported agents table are editable. */
|
|
1339
|
+
function agentsThreadLimitEntries(content: string) {
|
|
1340
|
+
const lines = splitSourceLines(content);
|
|
1341
|
+
const headerIdx = lines.findIndex(line => line.structural && /^\s*\[agents\]\s*(?:#.*)?$/.test(line.text));
|
|
1342
|
+
const entries: Array<{ index: number; key: string; value: number | null; comment?: string; editable: boolean }> = [];
|
|
1343
|
+
if (headerIdx !== -1) {
|
|
1344
|
+
for (let index = headerIdx + 1; index < lines.length; index++) {
|
|
1345
|
+
const line = lines[index]!;
|
|
1346
|
+
if (!line.structural) continue;
|
|
1347
|
+
if (/^\s*\[/.test(line.text)) break;
|
|
1348
|
+
const key = rootAssignmentKey(line.text);
|
|
1349
|
+
if (key !== AGENTS_THREAD_LIMIT_KEY && key !== LEGACY_AGENTS_THREAD_LIMIT_KEY) continue;
|
|
1350
|
+
const assignment = findTomlAssignment(line.text, key);
|
|
1351
|
+
const editable = assignment !== null && (index + 1 === lines.length || lines[index + 1]!.structural);
|
|
1352
|
+
let value: unknown;
|
|
1353
|
+
try { value = (Bun.TOML.parse(line.text) as Record<string, unknown>)[key]; } catch {
|
|
1354
|
+
// Match the reader's large-integer fallback without widening edit syntax.
|
|
1355
|
+
const integer = assignment && /^\d+$/.exec(line.text.slice(assignment.valueStart, assignment.valueEnd).trim());
|
|
1356
|
+
if (integer) value = Number(integer[0]);
|
|
1357
|
+
}
|
|
1358
|
+
entries.push({ index, key, editable, value: typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null,
|
|
1359
|
+
comment: assignment ? line.text.slice(assignment.valueEnd).match(/\s*#.*$/)?.[0] : undefined });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return { lines, headerIdx, entries };
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1340
1365
|
function removeMaxConcurrentThreads(configPath?: string): ConfigEditResult {
|
|
1341
1366
|
const path = configPath ?? activeCodexConfigPath();
|
|
1342
1367
|
return editCodexConfigToml(path, (content): TomlEditOutcome => {
|
|
@@ -1439,7 +1464,9 @@ function discoverStoredThreadLimit(configPath?: string): { value: number; units:
|
|
|
1439
1464
|
}
|
|
1440
1465
|
|
|
1441
1466
|
function activeThreadComment(content: string, v2Enabled: boolean): string | undefined {
|
|
1442
|
-
const
|
|
1467
|
+
const entries = agentsThreadLimitEntries(content).entries;
|
|
1468
|
+
let legacy = (entries.find(entry => entry.key === AGENTS_THREAD_LIMIT_KEY) ?? entries[0])?.comment;
|
|
1469
|
+
for (const entry of entries) legacy = mergeTrailingComments(legacy, entry.comment) || undefined;
|
|
1443
1470
|
const dedicated = tomlTableBody(content, "features.multi_agent_v2")
|
|
1444
1471
|
?.match(/^\s*max_concurrent_threads_per_session\s*=\s*\d+(\s*#.*)$/m)?.[1];
|
|
1445
1472
|
const features = tomlTableBody(content, "features");
|
|
@@ -1503,8 +1530,8 @@ export type MultiAgentV2TransitionResult =
|
|
|
1503
1530
|
| { ok: true; changed: boolean; threadLimit: number | null }
|
|
1504
1531
|
| { ok: false; error: string; retryable?: boolean };
|
|
1505
1532
|
|
|
1506
|
-
function transitionConfigError(content: string): string | null {
|
|
1507
|
-
if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.max_threads)\s*=/m.test(content)) {
|
|
1533
|
+
function transitionConfigError(content: string, explicitThreadLimit: boolean): string | null {
|
|
1534
|
+
if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.(?:max_threads|max_concurrent_threads_per_session))\s*=/m.test(content)) {
|
|
1508
1535
|
return "dotted multi-agent config keys are not supported for automatic migration";
|
|
1509
1536
|
}
|
|
1510
1537
|
const dedicatedTables = content.match(/^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/gm) ?? [];
|
|
@@ -1521,9 +1548,18 @@ function transitionConfigError(content: string): string | null {
|
|
|
1521
1548
|
if (features && /^\s*multi_agent_v2\.(?:enabled|max_concurrent_threads_per_session)\s*=/m.test(features)) {
|
|
1522
1549
|
return "dotted multi_agent_v2 fields are not supported for automatic migration";
|
|
1523
1550
|
}
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1551
|
+
const entries = agentsThreadLimitEntries(content).entries;
|
|
1552
|
+
const parsedAgents = parsedTomlTable(content, "agents");
|
|
1553
|
+
for (const key of [AGENTS_THREAD_LIMIT_KEY, LEGACY_AGENTS_THREAD_LIMIT_KEY]) {
|
|
1554
|
+
const matches = entries.filter(entry => entry.key === key);
|
|
1555
|
+
if (matches.length > 1) return `duplicate agents.${key} definitions cannot be migrated safely`;
|
|
1556
|
+
if (parsedAgents && Object.hasOwn(parsedAgents, key) && matches.length === 0) {
|
|
1557
|
+
return "unsupported agents thread-limit storage cannot be migrated safely";
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (entries.some(entry => !entry.editable)) return "unsupported agents thread-limit value cannot be migrated safely";
|
|
1561
|
+
if (!explicitThreadLimit && (entries.some(entry => entry.value === null) || (entries.length === 2 && entries[0]!.value !== entries[1]!.value))) {
|
|
1562
|
+
return "conflicting or invalid agents thread-limit aliases require an explicit thread limit";
|
|
1527
1563
|
}
|
|
1528
1564
|
const dedicated = tomlTableBody(content, "features.multi_agent_v2");
|
|
1529
1565
|
if ((dedicated?.match(/^\s*max_concurrent_threads_per_session\s*=/gm) ?? []).length > 1) {
|
|
@@ -1542,15 +1578,15 @@ export function transitionMultiAgentV2(
|
|
|
1542
1578
|
toggleFeature: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void,
|
|
1543
1579
|
options: { configPath?: string; threadLimit?: number; heldConfigWriteLock?: LockHandle } = {},
|
|
1544
1580
|
): MultiAgentV2TransitionResult {
|
|
1545
|
-
if (options.threadLimit !== undefined && (!Number.
|
|
1546
|
-
return { ok: false, error: "thread limit must be
|
|
1581
|
+
if (options.threadLimit !== undefined && (!Number.isSafeInteger(options.threadLimit) || options.threadLimit < 1)) {
|
|
1582
|
+
return { ok: false, error: "thread limit must be a safe integer >= 1" };
|
|
1547
1583
|
}
|
|
1548
1584
|
const path = options.configPath ?? activeCodexConfigPath();
|
|
1549
1585
|
if (codexHomeIsAbsent(dirname(path))) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
1550
1586
|
const runTransition = (held: LockHandle): MultiAgentV2TransitionResult => {
|
|
1551
1587
|
const original = readConfigText(path);
|
|
1552
1588
|
if (original === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
1553
|
-
const preflightError = transitionConfigError(original);
|
|
1589
|
+
const preflightError = transitionConfigError(original, options.threadLimit !== undefined);
|
|
1554
1590
|
if (preflightError) return { ok: false, error: preflightError };
|
|
1555
1591
|
const beforeEnabled = isMultiAgentV2Enabled(path);
|
|
1556
1592
|
// A caller-supplied limit is already in the DESTINATION backend's units and is
|
|
@@ -1573,6 +1609,9 @@ export function transitionMultiAgentV2(
|
|
|
1573
1609
|
? v1ChildLimitToV2TotalLimit(discovered.value)
|
|
1574
1610
|
: v2TotalLimitToV1ChildLimit(discovered.value);
|
|
1575
1611
|
}
|
|
1612
|
+
if (threadLimit !== null && !Number.isSafeInteger(threadLimit)) {
|
|
1613
|
+
return { ok: false, error: "stored thread limit cannot be written safely; supply an explicit safe integer limit" };
|
|
1614
|
+
}
|
|
1576
1615
|
const migratedComment = activeThreadComment(original, beforeEnabled);
|
|
1577
1616
|
try {
|
|
1578
1617
|
return withConfigWriteRecovery(path, held, () => {
|
|
@@ -1606,7 +1645,8 @@ export function transitionMultiAgentV2(
|
|
|
1606
1645
|
return editAgentsMaxThreads(threadLimit, tempPath, migratedComment);
|
|
1607
1646
|
});
|
|
1608
1647
|
if (!target.ok) throw new Error(target.error);
|
|
1609
|
-
|
|
1648
|
+
const agents = parsedTomlTable(readConfigText(path) ?? "", "agents");
|
|
1649
|
+
if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit || (agents && Object.hasOwn(agents, LEGACY_AGENTS_THREAD_LIMIT_KEY))) throw new Error("v1 thread-limit migration postcondition failed");
|
|
1610
1650
|
}
|
|
1611
1651
|
return { ok: true, changed: readConfigText(path) !== original, threadLimit };
|
|
1612
1652
|
});
|
|
@@ -1831,7 +1831,7 @@ export function countPendingOpencodexHistory(
|
|
|
1831
1831
|
: { pendingRows: 0, backupEntries };
|
|
1832
1832
|
}
|
|
1833
1833
|
try {
|
|
1834
|
-
const db =
|
|
1834
|
+
const db = openCodexStateForPreflight(stateDbPath);
|
|
1835
1835
|
try {
|
|
1836
1836
|
db.exec("PRAGMA busy_timeout = 100");
|
|
1837
1837
|
// Prove the expected history schema is readable without counting unowned routed rows.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AtomicWriteHooks } from "../../config/atomic-write";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { withConfigMutationLockSync } from "../../config";
|
|
4
|
-
import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
|
|
4
|
+
import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, removeConfigWriteEntry, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
|
|
5
5
|
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "../paths";
|
|
6
6
|
import { getCodexHome } from "../paths";
|
|
7
7
|
import { codexHomeIsAbsent } from "../codex-home-owner";
|
|
@@ -16,6 +16,9 @@ export function publishCodexArtifact<T>(path: string, held: LockHandle | undefin
|
|
|
16
16
|
if (!held) throw new Error("Codex artifact publication requires a held configuration lock");
|
|
17
17
|
return publishConfigWriteTarget(CODEX_CONFIG_PATH, held, path, run);
|
|
18
18
|
}
|
|
19
|
+
export function removeCodexProfileEntry(held: LockHandle): void {
|
|
20
|
+
removeConfigWriteEntry(CODEX_CONFIG_PATH, held, CODEX_PROFILE_PATH);
|
|
21
|
+
}
|
|
19
22
|
/** Courtesy cleanup re-reads provider ownership inside the same config section. */
|
|
20
23
|
export function cleanExternalProviderJournal(beforeClientWrite?: () => void): void {
|
|
21
24
|
if (codexHomeIsAbsent(getCodexHome())) return;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describeHistoryJobFailure, type CodexHistoryJobOutcome } from "../history-job";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Frame one failed apply history job honestly.
|
|
5
|
+
*
|
|
6
|
+
* A genuine lock keeps the established deferred/SKIPPED wording; any other
|
|
7
|
+
* reason names itself instead of blaming the Codex app/IDE.
|
|
8
|
+
*/
|
|
9
|
+
export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legacyMode: boolean): string {
|
|
10
|
+
// A busy database is a deferral no matter which half observed it: the lock
|
|
11
|
+
// contended (blocked/busy), or the worker acquired the lock and then found
|
|
12
|
+
// SQLite busy (failed with a busy history reason). Only those keep the
|
|
13
|
+
// deferred headline; every other failure is a real "NOT changed".
|
|
14
|
+
const busy =
|
|
15
|
+
(outcome.kind === "blocked" && outcome.reason === "busy") ||
|
|
16
|
+
(outcome.kind === "failed" && outcome.historyFailureReason === "busy");
|
|
17
|
+
const partiallyChanged = outcome.kind === "failed"
|
|
18
|
+
&& ((outcome.rows ?? 0) > 0 || (outcome.files ?? 0) > 0);
|
|
19
|
+
const headline = partiallyChanged
|
|
20
|
+
? "Codex resume history changed but did not converge"
|
|
21
|
+
: legacyMode
|
|
22
|
+
? "Codex resume history sync SKIPPED"
|
|
23
|
+
: busy
|
|
24
|
+
? "Codex resume history metadata restore deferred"
|
|
25
|
+
: "Codex resume history NOT changed";
|
|
26
|
+
return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
|
|
27
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { beginCodexWriteSection, publishCodexArtifact } from "./config-write-section";
|
|
2
|
-
import { existsSync,
|
|
1
|
+
import { beginCodexWriteSection, publishCodexArtifact, removeCodexProfileEntry } from "./config-write-section";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { normalizeStructuralWhitespace, rootAssignmentKey, rootSourceLines, sourceAssignment, sourceText } from "../toml-source-lines";
|
|
4
4
|
import { atomicWriteFile } from "../../config";
|
|
5
|
-
import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld } from "../config-write-lock";
|
|
5
|
+
import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld, withConfigWriteRecovery } from "../config-write-lock";
|
|
6
6
|
import type { LockHandle } from "../config-write-lock";
|
|
7
7
|
import {
|
|
8
8
|
REALTIME_WS_BASE_URL_KEY,
|
|
@@ -206,15 +206,26 @@ export function removeCodexConfig(
|
|
|
206
206
|
}
|
|
207
207
|
const locked = withConfigWriteLockHeld(CODEX_CONFIG_PATH, options.heldConfigWriteLock, held => {
|
|
208
208
|
beginCodexWriteSection(held);
|
|
209
|
+
if (!options.preserveProfile) {
|
|
210
|
+
try {
|
|
211
|
+
if (lstatSync(CODEX_PROFILE_PATH).isSymbolicLink()) {
|
|
212
|
+
return { success: false, message: "Codex profile is a symbolic link; configuration and recovery evidence were preserved." };
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
209
218
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
210
219
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
211
|
-
|
|
220
|
+
removeCodexProfileEntry(held);
|
|
212
221
|
return {
|
|
213
222
|
success: true,
|
|
214
223
|
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
215
224
|
};
|
|
216
225
|
}
|
|
217
|
-
const
|
|
226
|
+
const preimage = readFileSync(CODEX_CONFIG_PATH);
|
|
227
|
+
return withConfigWriteRecovery(CODEX_CONFIG_PATH, held, () => {
|
|
228
|
+
const rawContent = preimage.toString("utf-8");
|
|
218
229
|
// Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
|
|
219
230
|
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
220
231
|
const eol = dominantEol(rawContent);
|
|
@@ -248,7 +259,7 @@ export function removeCodexConfig(
|
|
|
248
259
|
publishCodexArtifact(CODEX_CONFIG_PATH, held, (path, hooks) => atomicWriteFile(path, applyEol(finalContent, eol), undefined, hooks));
|
|
249
260
|
}
|
|
250
261
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
251
|
-
|
|
262
|
+
removeCodexProfileEntry(held);
|
|
252
263
|
const retainedNote = retainedBlock === null
|
|
253
264
|
? ""
|
|
254
265
|
: " Kept [model_providers.opencodex] so conversations already tagged opencodex still open;"
|
|
@@ -272,6 +283,7 @@ export function removeCodexConfig(
|
|
|
272
283
|
message: removedMessage,
|
|
273
284
|
...(retainedBlock === null ? {} : { retainedProviderTable: retainedBlock.replace(/\n+$/, "").split("\n") }),
|
|
274
285
|
};
|
|
286
|
+
}, { preimage, requireMatchingPreimage: true });
|
|
275
287
|
});
|
|
276
288
|
if (!locked.ok) return { success: false, retryable: locked.error === "locked", message: configWriteLockFailureMessage(locked) };
|
|
277
289
|
return locked.value;
|