@bitkyc08/opencodex 2.86.0 → 2.88.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/gui/dist/assets/App-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- 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/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- 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-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 +4 -2
- 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 +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- 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/outbound.ts +46 -13
- 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 +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- 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 +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -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 +28 -8
- 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 +2 -1
- package/src/clients/config-export.ts +18 -44
- 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/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 +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -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 +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- 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 +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- 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/main-account-hard-lock.ts +30 -1
- 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 +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- 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/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- 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 +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -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 +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -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/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- 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 +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- 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/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- 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 +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- 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/opencode-zen-rate-limit.ts +7 -2
- 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 +37 -8
- 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 +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- 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/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- 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 +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- 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 +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -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 +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- 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/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 +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- 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 +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -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 +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- 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 +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -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/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- 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/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -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 +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- 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/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -206,6 +206,27 @@ export function listCodexDesktopAppPids(io: DesktopAppRestartIo = {}): number[]
|
|
|
206
206
|
return processes === null ? null : processes.map(entry => entry.pid);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Pids of the running desktop-app ROOT shells, or null when discovery or the probe failed.
|
|
211
|
+
*
|
|
212
|
+
* Read-only. The shell is the process whose renderer built the model picker, so its start
|
|
213
|
+
* time is what tells `performCodexRestart` whether the picker predates the catalog (#4761).
|
|
214
|
+
* `[]` means the probe ran and the app is not running.
|
|
215
|
+
*/
|
|
216
|
+
export function listCodexDesktopShellPids(io: DesktopAppRestartIo = {}): number[] | null {
|
|
217
|
+
const platform = io.platform ?? process.platform;
|
|
218
|
+
const selected = ADAPTERS[platform];
|
|
219
|
+
const adapter = io.adapter ?? selected?.adapter;
|
|
220
|
+
if (!adapter) return null;
|
|
221
|
+
const exec = io.execFile ?? selected?.exec;
|
|
222
|
+
if (!exec) return null;
|
|
223
|
+
const install = adapter.discover(exec);
|
|
224
|
+
if (!install) return null;
|
|
225
|
+
const processes = adapter.listProcesses(exec, install);
|
|
226
|
+
if (processes === null) return null;
|
|
227
|
+
return rootShells(processes, install, adapter).map(entry => entry.pid);
|
|
228
|
+
}
|
|
229
|
+
|
|
209
230
|
export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopAppRestartResult {
|
|
210
231
|
const skipped = (reason: DesktopAppRestartReason): DesktopAppRestartResult => ({
|
|
211
232
|
attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason,
|
|
@@ -8,11 +8,21 @@ import type { DesktopAppInstall } from "../desktop-app/types";
|
|
|
8
8
|
export const DESKTOP_COMPATIBILITY_ASSESSED_VERSION = "26.924.2738.0";
|
|
9
9
|
export const DESKTOP_COMPATIBILITY_ASSESSED_FAMILY = "OpenAI.Codex_2p2nqsd0c76g0";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** Last completed probe outcome. Only public package facts: never paths, keys or account data. */
|
|
12
|
+
export interface DesktopBuildAssessment {
|
|
13
|
+
assessedVersion: string;
|
|
14
|
+
observedVersion: string | null;
|
|
15
|
+
verified: boolean;
|
|
16
|
+
completedAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assessInstalledPackage(install: DesktopAppInstall): { observedVersion: string | null; verified: boolean } {
|
|
12
20
|
const name = win32.basename(install.root);
|
|
13
21
|
const parts = /^OpenAI\.Codex_(\d+\.\d+\.\d+\.\d+)_(x64|arm64|x86|neutral)_([A-Za-z0-9.-]*)_([A-Za-z0-9]+)$/.exec(name);
|
|
14
|
-
|
|
15
|
-
&& !!parts &&
|
|
22
|
+
const identified = install.id === DESKTOP_COMPATIBILITY_ASSESSED_FAMILY && install.relaunch === `${install.id}!App`
|
|
23
|
+
&& !!parts && `OpenAI.Codex_${parts[4]}` === install.id;
|
|
24
|
+
const observedVersion = identified ? parts![1] : null;
|
|
25
|
+
return { observedVersion, verified: identified && observedVersion === DESKTOP_COMPATIBILITY_ASSESSED_VERSION };
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
/** Resolve only after the exact asynchronous child has closed, including abort/timeout. */
|
|
@@ -34,17 +44,25 @@ function queryInstalledPackage(signal: AbortSignal): Promise<DesktopAppInstall |
|
|
|
34
44
|
/** Coalesce concurrent fresh probes; never cache a positive verdict across requests. */
|
|
35
45
|
export function createInstalledBuildProbe(query: (signal: AbortSignal) => Promise<DesktopAppInstall | null> = queryInstalledPackage) {
|
|
36
46
|
let pending: Promise<boolean> | null = null, controller: AbortController | null = null, closed = false;
|
|
47
|
+
let lastResult: DesktopBuildAssessment | null = null;
|
|
48
|
+
const record = (install: DesktopAppInstall | null) => {
|
|
49
|
+
const outcome = install ? assessInstalledPackage(install) : { observedVersion: null, verified: false };
|
|
50
|
+
lastResult = { assessedVersion: DESKTOP_COMPATIBILITY_ASSESSED_VERSION, ...outcome, completedAt: Date.now() };
|
|
51
|
+
return outcome.verified;
|
|
52
|
+
};
|
|
37
53
|
return {
|
|
38
54
|
check(): Promise<boolean> {
|
|
39
55
|
if (closed) return Promise.resolve(false);
|
|
40
56
|
if (pending) return pending;
|
|
41
57
|
const current = new AbortController(); controller = current;
|
|
42
58
|
pending = Promise.resolve().then(() => current.signal.aborted ? null : query(current.signal))
|
|
43
|
-
.then(install =>
|
|
44
|
-
|
|
45
|
-
.
|
|
59
|
+
.then(install => closed || current.signal.aborted ? false : record(install))
|
|
60
|
+
.catch(() => closed || current.signal.aborted ? false : record(null))
|
|
61
|
+
.finally(() => { pending = null; controller = null; });
|
|
46
62
|
return pending;
|
|
47
63
|
},
|
|
64
|
+
/** Last completed check only: an aborted or in-flight query never rewrites it. */
|
|
65
|
+
lastResult(): DesktopBuildAssessment | null { return lastResult ? { ...lastResult } : null; },
|
|
48
66
|
async close(): Promise<void> { closed = true; controller?.abort(); await pending; },
|
|
49
67
|
};
|
|
50
68
|
}
|
|
@@ -9,7 +9,7 @@ const record = (value: unknown): value is Record<string, unknown> => !!value &&
|
|
|
9
9
|
// changes revoke the observed context even if the loopback URL stays the same.
|
|
10
10
|
const ROUTING_KEYS = ["providers", "defaultProvider", "defaultModelAliases", "customModels", "combos", "routingProfiles",
|
|
11
11
|
"subagentModelFallback", "subagentModelFallbackByModel", "injectionModel", "compactionRouting", "compactionRecovery",
|
|
12
|
-
"blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels"] as const satisfies readonly (keyof OcxConfig)[];
|
|
12
|
+
"blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels", "codexMainAccountHardLockFallback"] as const satisfies readonly (keyof OcxConfig)[];
|
|
13
13
|
function routingDigest(text: string, owner: NativeCompatibilityOwner): string {
|
|
14
14
|
const native = Bun.TOML.parse(text) as Record<string, unknown>;
|
|
15
15
|
// Desktop refreshes MCP endpoints after launch. Tool transport metadata and TOML
|
|
@@ -16,7 +16,7 @@ import { launchWindowsCodexCompatibility } from "./windows-package-launch";
|
|
|
16
16
|
import { acquireDesktopCompatibilityRuntime, bindDesktopCompatibilityLaunch } from "./runtime-ownership";
|
|
17
17
|
import { createDesktopConnectionStore, type DesktopConnectionStore } from "./connection-store";
|
|
18
18
|
import { createNativeRoutingVerifier } from "./routing-preflight";
|
|
19
|
-
import { createInstalledBuildProbe } from "./installed-build";
|
|
19
|
+
import { createInstalledBuildProbe, type DesktopBuildAssessment } from "./installed-build";
|
|
20
20
|
|
|
21
21
|
export { DESKTOP_COMPATIBILITY_ASSESSED_VERSION, isAssessedDesktopInstalled } from "./installed-build";
|
|
22
22
|
export interface DesktopRuntimeIo {
|
|
@@ -25,6 +25,8 @@ export interface DesktopRuntimeIo {
|
|
|
25
25
|
trust?: (authority: StoredDesktopAuthority) => Promise<"trusted" | "not-trusted" | "unknown">;
|
|
26
26
|
identity?: ReturnType<typeof createNativeIdentityReader>;
|
|
27
27
|
buildSupported?: () => boolean | Promise<boolean>;
|
|
28
|
+
/** Internal fixture probe factory; requires testOnly and all sandbox identity/CA seams. */
|
|
29
|
+
testOnlyBuildProbe?: () => ReturnType<typeof createInstalledBuildProbe>;
|
|
28
30
|
upstreamFetch?: typeof fetch;
|
|
29
31
|
connectionStore?: DesktopConnectionStore;
|
|
30
32
|
routingSupported?: () => boolean;
|
|
@@ -36,6 +38,7 @@ export interface DesktopRuntimeIo {
|
|
|
36
38
|
export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
|
|
37
39
|
const platform = io.platform ?? process.platform;
|
|
38
40
|
let buildProbe: ReturnType<typeof createInstalledBuildProbe> | undefined;
|
|
41
|
+
let previousBuildAssessment: DesktopBuildAssessment | null = null;
|
|
39
42
|
const buildSupported = io.buildSupported ?? (() => buildProbe?.check() ?? Promise.resolve(false));
|
|
40
43
|
let routingSupported = io.routingSupported ?? (() => false);
|
|
41
44
|
let contextFailure: "build_unverified" | "native_routing_unverified" | null = null;
|
|
@@ -54,6 +57,7 @@ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
|
|
|
54
57
|
let pendingCleanup: (() => Promise<void>) | null = null;
|
|
55
58
|
let releaseOwner: (() => void) | null = null;
|
|
56
59
|
const status = () => ({ phase, supported: platform === "win32", running: owned !== null, contextFailure,
|
|
60
|
+
...(buildProbe ? { buildAssessment: buildProbe.lastResult() ?? (previousBuildAssessment ? { ...previousBuildAssessment } : null) } : {}),
|
|
57
61
|
...(owned ? { fingerprint: owned.fingerprint, safetyDeadline: owned.deadline, usage: owned.controller.snapshot() } : {}) });
|
|
58
62
|
async function stop(): Promise<ReturnType<typeof status>> {
|
|
59
63
|
if (phase === "starting") throw new Error("desktop_compatibility_busy");
|
|
@@ -67,11 +71,11 @@ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
|
|
|
67
71
|
async function start() {
|
|
68
72
|
if (phase !== "off") throw new Error("desktop_compatibility_busy");
|
|
69
73
|
if (platform !== "win32") throw new Error("desktop_compatibility_unsupported");
|
|
70
|
-
if (
|
|
74
|
+
if (io.testOnlyBuildProbe && (!io.testOnly || io.buildSupported)) throw new Error("desktop_compatibility_test_environment");
|
|
75
|
+
if ((isTestHomeGuardArmed() || io.testOnlyBuildProbe) && !(io.testOnly && io.identity && io.loadAuthority && io.trust && (io.buildSupported || io.testOnlyBuildProbe) && io.connectionStore && io.routingSupported)) throw new Error("desktop_compatibility_test_environment");
|
|
71
76
|
if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
|
|
72
77
|
try { desktopProxyFor(new URL("https://chatgpt.com")); } catch { throw new Error("desktop_compatibility_egress_proxy_invalid"); }
|
|
73
78
|
releaseOwner = acquireDesktopCompatibilityRuntime(); phase = "starting";
|
|
74
|
-
if (!io.buildSupported) buildProbe = createInstalledBuildProbe();
|
|
75
79
|
let relay: Awaited<ReturnType<typeof startDesktopRelay>> | undefined, proxy: ConnectProxyHandle | undefined;
|
|
76
80
|
let pac: ReturnType<typeof Bun.serve> | undefined, timer: ReturnType<typeof setInterval> | undefined;
|
|
77
81
|
let detach: (() => void) | undefined, unbindLaunch: (() => void) | undefined, controller: UsageRelayController | undefined;
|
|
@@ -84,6 +88,11 @@ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
|
|
|
84
88
|
};
|
|
85
89
|
pendingCleanup = cleanup;
|
|
86
90
|
try {
|
|
91
|
+
if (!io.buildSupported) {
|
|
92
|
+
// A fresh probe owns this start; diagnostics retain the last completed observation.
|
|
93
|
+
previousBuildAssessment = buildProbe?.lastResult() ?? previousBuildAssessment;
|
|
94
|
+
buildProbe = io.testOnlyBuildProbe ? io.testOnlyBuildProbe() : createInstalledBuildProbe();
|
|
95
|
+
}
|
|
87
96
|
if (!io.routingSupported) routingSupported = createNativeRoutingVerifier((await import("../paths")).getCodexHome());
|
|
88
97
|
if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
|
|
89
98
|
const directory = join(getConfigDir(), "codex-desktop-compatibility");
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { dlopen, ptr, toArrayBuffer, type Pointer } from "bun:ffi";
|
|
4
|
+
import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsSystemDirectory } from "../../lib/windows-elevation";
|
|
3
5
|
|
|
4
6
|
const MAX_INPUT = 65_536;
|
|
5
7
|
const MAX_OUTPUT = 131_072;
|
|
6
8
|
const PURPOSE = "opencodex/codex-desktop-compatibility/authority/v1";
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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).
|
|
10
12
|
const DPAPI_TIMEOUT_MS = 20_000;
|
|
11
13
|
const SCRIPT = [
|
|
12
14
|
"$ErrorActionPreference='Stop'",
|
|
@@ -24,18 +26,94 @@ const SCRIPT = [
|
|
|
24
26
|
"[Console]::Out.Write([Convert]::ToBase64String($out))",
|
|
25
27
|
].join("\n");
|
|
26
28
|
|
|
29
|
+
type Operation = "protect" | "unprotect";
|
|
30
|
+
|
|
27
31
|
export interface AuthorityKeyProtection {
|
|
28
32
|
protect(cleartext: Uint8Array): Promise<Uint8Array>;
|
|
29
33
|
unprotect(ciphertext: Uint8Array): Promise<Uint8Array>;
|
|
30
34
|
}
|
|
31
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
|
+
|
|
32
112
|
/** The payload travels on stdin, never in process arguments, environment or logs. */
|
|
33
|
-
function
|
|
34
|
-
if (process.platform !== "win32") return Promise.reject(new Error("desktop_compatibility_windows_required"));
|
|
35
|
-
if (data.byteLength === 0 || data.byteLength > MAX_INPUT) return Promise.reject(new Error("desktop_compatibility_key_size"));
|
|
113
|
+
function runPowerShellDpapi(operation: Operation, data: Uint8Array, timeoutMs = DPAPI_TIMEOUT_MS): Promise<Uint8Array> {
|
|
36
114
|
return new Promise((resolve, reject) => {
|
|
37
|
-
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
|
|
38
|
-
timeout:
|
|
115
|
+
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
|
|
116
|
+
timeout: timeoutMs, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
|
|
39
117
|
}, (error, stdout) => {
|
|
40
118
|
// Do not include PowerShell stderr, command output or the input in a propagated error.
|
|
41
119
|
if (error || !/^[A-Za-z0-9+/]+={0,2}$/.test(stdout.trim())) {
|
|
@@ -52,8 +130,36 @@ function runDpapi(operation: "protect" | "unprotect", data: Uint8Array): Promise
|
|
|
52
130
|
});
|
|
53
131
|
}
|
|
54
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
|
+
|
|
55
145
|
/** CurrentUser protects against other OS identities, not other processes of the same user. */
|
|
56
146
|
export const windowsAuthorityKeyProtection: AuthorityKeyProtection = {
|
|
57
147
|
protect: bytes => runDpapi("protect", bytes),
|
|
58
148
|
unprotect: bytes => runDpapi("unprotect", bytes),
|
|
59
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
|
+
}
|
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
|
});
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { restoreApprovedCodexProviders } from "../provider-routing";
|
|
1
2
|
import { beginCodexWriteSection, cleanExternalProviderJournal, publishCodexArtifact } from "./config-write-section";
|
|
2
3
|
import { assertCodexHomeOwner, codexHomeIsAbsent, codexHomeOwnerBlocksCompensation, CodexHomeOwnerRefusal, type CodexHomeOwnerRefusalReason } from "../codex-home-owner";
|
|
3
4
|
import { loadConfig } from "../../config";
|
|
@@ -115,6 +116,7 @@ export interface CodexRestoreConfigResult {
|
|
|
115
116
|
| "owned-fields-stripped"
|
|
116
117
|
| "routing-restored-provider-retained"
|
|
117
118
|
| "external-provider-preserved"
|
|
119
|
+
| "approved-provider-restored"
|
|
118
120
|
| "failed";
|
|
119
121
|
message: string;
|
|
120
122
|
retained?: RetainedCodexProviderTable;
|
|
@@ -236,6 +238,26 @@ export function failedHistoryRestoreFromOutcome(
|
|
|
236
238
|
return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
export function finishApprovedProviderRestore(result: CodexNativeRestoreResult, revalidateDesiredState = false): CodexNativeRestoreResult {
|
|
242
|
+
if (!result.success || revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) return result;
|
|
243
|
+
try {
|
|
244
|
+
if (!restoreApprovedCodexProviders()) return result;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error instanceof CodexHomeOwnerRefusal) throw error;
|
|
247
|
+
const message = `Approved Codex provider URL restoration failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
248
|
+
return { ...result, success: false, message: `${result.message} ${message}`, artifacts: {
|
|
249
|
+
...result.artifacts, config: { ...result.artifacts.config, state: "failed", action: "failed", message },
|
|
250
|
+
} };
|
|
251
|
+
}
|
|
252
|
+
const message = "Approved Codex provider URLs restored; identity, other settings and history preserved.";
|
|
253
|
+
return { ...result, message: result.externalProvider ? message : `${result.message} ${message}`, artifacts: {
|
|
254
|
+
...result.artifacts,
|
|
255
|
+
config: { ...result.artifacts.config, state: result.artifacts.config.state === "partial" ? "partial" : "ok", changed: true,
|
|
256
|
+
action: result.artifacts.config.changed ? result.artifacts.config.action : "approved-provider-restored",
|
|
257
|
+
message: result.artifacts.config.state === "partial" ? `${result.artifacts.config.message} ${message}` : message },
|
|
258
|
+
} };
|
|
259
|
+
}
|
|
260
|
+
|
|
239
261
|
function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
|
|
240
262
|
const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
|
|
241
263
|
return {
|
|
@@ -553,7 +575,7 @@ export async function restoreNativeCodexAsync(
|
|
|
553
575
|
if (sibling) return sibling;
|
|
554
576
|
try {
|
|
555
577
|
assertCodexHomeOwner(getCodexHome());
|
|
556
|
-
return await restoreNativeCodexAsyncImpl(options);
|
|
578
|
+
return finishApprovedProviderRestore(await restoreNativeCodexAsyncImpl(options), options.revalidateDesiredState);
|
|
557
579
|
} catch (error) {
|
|
558
580
|
if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
|
|
559
581
|
if (!(error instanceof CodexRestoreRefusal)) throw error;
|
|
@@ -782,7 +804,7 @@ export function restoreNativeCodex(
|
|
|
782
804
|
if (sibling) return sibling;
|
|
783
805
|
try {
|
|
784
806
|
assertCodexHomeOwner(getCodexHome());
|
|
785
|
-
return restoreNativeCodexImpl(options);
|
|
807
|
+
return finishApprovedProviderRestore(restoreNativeCodexImpl(options), options.revalidateDesiredState);
|
|
786
808
|
} catch (error) {
|
|
787
809
|
if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
|
|
788
810
|
throw error;
|