@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
package/src/update/index.ts
CHANGED
|
@@ -17,13 +17,10 @@ import { unprivilegedOwnershipMutationEnvironment } from "../service/ownership-m
|
|
|
17
17
|
import { withUpdateOwnershipLease, readUpdateRuntimeTarget } from "./ownership-transaction";
|
|
18
18
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
19
19
|
import { pnpmInvocation, pnpmInvocationForPath, resolvePnpmCommands } from "./pnpm-invocation.mjs";
|
|
20
|
-
import {
|
|
21
|
-
detectInstallFromPath,
|
|
22
|
-
detectInstallOwnershipFromPath,
|
|
23
|
-
} from "./install-detection.mjs";
|
|
20
|
+
import { detectInstallOwnershipFromPath } from "./install-detection.mjs";
|
|
24
21
|
import type {
|
|
25
22
|
DetectedInstall,
|
|
26
|
-
InstallOwnership,
|
|
23
|
+
InstallOwnership as PackageInstallOwnership,
|
|
27
24
|
MiseInstallOwner,
|
|
28
25
|
} from "./install-detection.d.mts";
|
|
29
26
|
import {
|
|
@@ -42,8 +39,13 @@ import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-updat
|
|
|
42
39
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
43
40
|
import { withoutSiblingMarker } from "../codex/sibling-start";
|
|
44
41
|
import { packageVersion } from "../lib/package-version";
|
|
42
|
+
import { isStandaloneModuleUrl } from "../lib/standalone";
|
|
45
43
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
46
44
|
import { PNPM_READ_CWD, withPnpmCommandCwd, pnpmReadEnvironment } from "./pnpm-read-policy.mjs";
|
|
45
|
+
import {
|
|
46
|
+
freshServingPid, hasRestartableServiceManager, readParentPid, readServiceManagerPid, restartServiceManagerJob,
|
|
47
|
+
supervisionVerdict, unsettledManagerWarning, unsupervisedRuntimeWarning,
|
|
48
|
+
} from "./supervised-recovery.mjs";
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* A `codex-history-backup-*.json` surviving a stop means the native-history restore was
|
|
@@ -62,18 +64,21 @@ export function historyRestoreIncomplete(configDir = getConfigDir()): boolean {
|
|
|
62
64
|
export const PKG = "@bitkyc08/opencodex";
|
|
63
65
|
const HERE = dirname(fileURLToPath(import.meta.url)); // .../opencodex/src/update
|
|
64
66
|
|
|
65
|
-
export type Installer = DetectedInstall;
|
|
67
|
+
export type Installer = DetectedInstall | "standalone";
|
|
68
|
+
export const STANDALONE_UPDATE_HINT = "Update via OpenCodex Desktop (Check for Updates…) or reinstall the standalone binary from GitHub Releases.";
|
|
66
69
|
export type Channel = "latest" | "preview";
|
|
67
|
-
export type {
|
|
70
|
+
export type InstallOwnership = PackageInstallOwnership | { installer: "standalone" };
|
|
71
|
+
export type { MiseInstallOwner };
|
|
68
72
|
|
|
69
73
|
/** Infer how opencodex is installed from the running module's path. */
|
|
70
|
-
export function detectInstall(): Installer {
|
|
71
|
-
return
|
|
74
|
+
export function detectInstall(moduleUrl = import.meta.url): Installer {
|
|
75
|
+
return detectInstallOwnership(moduleUrl).installer;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/** Resolve installer ownership and verified mise update guidance for this package. */
|
|
75
|
-
export function detectInstallOwnership(): InstallOwnership {
|
|
76
|
-
|
|
79
|
+
export function detectInstallOwnership(moduleUrl = import.meta.url): InstallOwnership {
|
|
80
|
+
if (isStandaloneModuleUrl(moduleUrl)) return { installer: "standalone" };
|
|
81
|
+
return detectInstallOwnershipFromPath(dirname(fileURLToPath(moduleUrl)), { exists: existsSync });
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
export function miseUpdateCommand(
|
|
@@ -234,6 +239,7 @@ export function registrySpawnTarget(
|
|
|
234
239
|
args: readonly string[],
|
|
235
240
|
owner?: PnpmGlobalOwner,
|
|
236
241
|
): SpawnTarget | null {
|
|
242
|
+
if (installer === "standalone") return null;
|
|
237
243
|
// A pnpm command without an owner would silently fall back to the first PATH
|
|
238
244
|
// candidate. That is unsafe when multiple PNPM_HOME installations expose the
|
|
239
245
|
// same version, so registry queries use the same hard binding as mutation.
|
|
@@ -310,6 +316,7 @@ export function latestVersion(
|
|
|
310
316
|
|
|
311
317
|
/** The global-install command opencodex would run to update on this channel. */
|
|
312
318
|
export function updateCommand(installer: Installer, tag: Channel, resolvedVersion?: string | null): { bin: string; args: string[] } {
|
|
319
|
+
if (installer === "standalone") throw new Error(STANDALONE_UPDATE_HINT);
|
|
313
320
|
if (installer === "mise") {
|
|
314
321
|
throw new Error("mise-owned installations must be upgraded through mise");
|
|
315
322
|
}
|
|
@@ -427,6 +434,12 @@ export async function runUpdate(): Promise<void> {
|
|
|
427
434
|
return;
|
|
428
435
|
}
|
|
429
436
|
|
|
437
|
+
if (installer === "standalone") {
|
|
438
|
+
console.error(STANDALONE_UPDATE_HINT);
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
430
443
|
if (installer === "source") {
|
|
431
444
|
console.log("Running from a source checkout — update with: git pull && bun install");
|
|
432
445
|
return;
|
|
@@ -551,7 +564,8 @@ export async function runUpdate(): Promise<void> {
|
|
|
551
564
|
return before?.owner === after?.owner && before?.installId === after?.installId
|
|
552
565
|
&& before?.consentGeneration === after?.consentGeneration;
|
|
553
566
|
};
|
|
554
|
-
|
|
567
|
+
/** `"vetoed"`: another owner holds the runtime, so not starting is the correct outcome. */
|
|
568
|
+
const startProxyDirectly = async (): Promise<boolean | "vetoed"> => {
|
|
555
569
|
const ownership = await resolvedRuntimeOwnership();
|
|
556
570
|
const supervision = observeSupervision();
|
|
557
571
|
const plan = planUpdateRuntimeHandling({ ...ownership, serviceInstalled: false, supervision });
|
|
@@ -559,7 +573,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
559
573
|
console.warn(supervision
|
|
560
574
|
? "OpenCodex Desktop supervises the proxy; no CLI runtime was restored."
|
|
561
575
|
: plan.notice);
|
|
562
|
-
return
|
|
576
|
+
return "vetoed";
|
|
563
577
|
}
|
|
564
578
|
if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) return false;
|
|
565
579
|
// An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
|
|
@@ -582,6 +596,51 @@ export async function runUpdate(): Promise<void> {
|
|
|
582
596
|
console.warn("⚠️ Recovery did not report a healthy proxy before its deadline; run 'ocx service repair' or 'ocx start'.");
|
|
583
597
|
return false;
|
|
584
598
|
};
|
|
599
|
+
// #7043: before any detached start, ask launchd/systemd to restart the job it already holds,
|
|
600
|
+
// and count it only when the serving proxy is that job. Runs unleased: the manager's child
|
|
601
|
+
// cannot join this lease (#5760). `"unsettled"` means the manager may still finish the restart,
|
|
602
|
+
// so no competing proxy may be started.
|
|
603
|
+
const restartThroughServiceManager = async (): Promise<"supervised" | "failed" | "unsettled"> => {
|
|
604
|
+
if (!hasRestartableServiceManager()) return "failed";
|
|
605
|
+
console.log("🔁 Asking the service manager to restart the background service...");
|
|
606
|
+
const before = readUpdateRuntimeTarget(getRuntimePortPath(), capturedListen.hostname);
|
|
607
|
+
const staleRecordPid = before.kind === "target" ? before.target.pid : null;
|
|
608
|
+
const restarted = restartServiceManagerJob();
|
|
609
|
+
const unsettled = !restarted.ok && restarted.unsettled === true;
|
|
610
|
+
let verdict: ReturnType<typeof supervisionVerdict> | { kind: "failed"; reason: string };
|
|
611
|
+
if (!restarted.ok && !unsettled) {
|
|
612
|
+
verdict = { kind: "failed", reason: restarted.reason };
|
|
613
|
+
} else {
|
|
614
|
+
let servingPid: number | null = null;
|
|
615
|
+
const deadline = Date.now() + 30_000;
|
|
616
|
+
while (servingPid === null && Date.now() < deadline) {
|
|
617
|
+
const current = readUpdateRuntimeTarget(getRuntimePortPath(), capturedListen.hostname);
|
|
618
|
+
if (current.kind === "target" && current.target.port === capturedListen.port
|
|
619
|
+
&& freshServingPid(current.target.pid, staleRecordPid) !== null
|
|
620
|
+
&& await proxyIdentityAt(current.target.port, { hostname: current.target.hostname, expectedPid: current.target.pid },
|
|
621
|
+
{ timeoutMs: 1500, deadlineAt: deadline, attempts: 1 })) servingPid = current.target.pid;
|
|
622
|
+
else await Bun.sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
623
|
+
}
|
|
624
|
+
verdict = supervisionVerdict({ servingPid, managerPid: readServiceManagerPid(), parentPid: pid => readParentPid(pid) });
|
|
625
|
+
}
|
|
626
|
+
if (verdict.kind === "supervised") {
|
|
627
|
+
console.log(`✅ Proxy is healthy on port ${capturedListen.port} under the service manager (pid ${verdict.managerPid}).`);
|
|
628
|
+
return "supervised";
|
|
629
|
+
}
|
|
630
|
+
if (unsettled) {
|
|
631
|
+
console.warn(unsettledManagerWarning(capturedListen.port));
|
|
632
|
+
return "unsettled";
|
|
633
|
+
}
|
|
634
|
+
console.warn(`⚠️ The service manager did not recover a supervised proxy: ${verdict.reason}.`);
|
|
635
|
+
return "failed";
|
|
636
|
+
};
|
|
637
|
+
/** Direct start as the last resort for a managed install: never reported as full recovery. */
|
|
638
|
+
const startUnsupervisedFallback = async (): Promise<"vetoed" | "started" | "failed"> => {
|
|
639
|
+
const started = await startProxyDirectly();
|
|
640
|
+
if (started === "vetoed") return "vetoed";
|
|
641
|
+
if (started) console.warn(unsupervisedRuntimeWarning(capturedListen.port));
|
|
642
|
+
return started ? "started" : "failed";
|
|
643
|
+
};
|
|
585
644
|
const recoverStoppedRuntime = async (reason: string): Promise<void> => {
|
|
586
645
|
try {
|
|
587
646
|
const planRecovery = async () => {
|
|
@@ -617,6 +676,12 @@ export async function runUpdate(): Promise<void> {
|
|
|
617
676
|
env: mutation.controlEnvironment({ ...process.env, OCX_BAKE_PORT: String(capturedListen.port) }),
|
|
618
677
|
});
|
|
619
678
|
if (service.status !== 0 || !isServiceViable()) {
|
|
679
|
+
// Ownership may have moved while the refresh ran unleased; only a recovery plan that
|
|
680
|
+
// still names the service may restart the manager's job.
|
|
681
|
+
if ((await planRecovery()).action === "service") {
|
|
682
|
+
const managed = await restartThroughServiceManager();
|
|
683
|
+
if (managed !== "failed") return;
|
|
684
|
+
}
|
|
620
685
|
// The refresh ran unleased; a claim could have landed meanwhile, so take
|
|
621
686
|
// the lease back before re-reading ownership and authorizing a direct start.
|
|
622
687
|
mutation.reacquire();
|
|
@@ -626,7 +691,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
626
691
|
liveness: currentPackageRuntimeLiveness(), serviceInstalled: false,
|
|
627
692
|
launcherUsable: postUpdateLauncherUsable && existsSync(postUpdateLauncher), hadRuntimeState,
|
|
628
693
|
});
|
|
629
|
-
if (fallback.action === "direct") await
|
|
694
|
+
if (fallback.action === "direct") await startUnsupervisedFallback();
|
|
630
695
|
else console.warn("⚠️ Service recovery was not confirmed; no second proxy was started.");
|
|
631
696
|
}
|
|
632
697
|
} else if (recovery.action === "direct") {
|
|
@@ -902,6 +967,12 @@ export async function runUpdate(): Promise<void> {
|
|
|
902
967
|
}
|
|
903
968
|
}
|
|
904
969
|
if (!serviceRefreshed || !serviceViable) {
|
|
970
|
+
// #7043: the manager first. Only a proxy it owns counts as recovered; everything
|
|
971
|
+
// below this line leaves an unsupervised runtime and exits non-zero.
|
|
972
|
+
if (!await mayRefresh()) return;
|
|
973
|
+
const managed = await restartThroughServiceManager();
|
|
974
|
+
if (managed === "supervised") return;
|
|
975
|
+
if (managed === "unsettled") return 1;
|
|
905
976
|
// Repair normally avoids elevation for a healthy scheduler task, but a stale
|
|
906
977
|
// definition may require guarded create/elevation. It can also fail — or exit 0
|
|
907
978
|
// while leaving stale/missing assets that never start
|
|
@@ -956,7 +1027,9 @@ export async function runUpdate(): Promise<void> {
|
|
|
956
1027
|
console.warn(process.platform === "win32"
|
|
957
1028
|
? " Run 'ocx service repair' to refresh the background service."
|
|
958
1029
|
: " Run 'ocx service repair' to refresh the background service and see why it failed.");
|
|
959
|
-
|
|
1030
|
+
// An ownership veto is the correct outcome, not a degraded one.
|
|
1031
|
+
if (await startUnsupervisedFallback() === "vetoed") return;
|
|
1032
|
+
return 1;
|
|
960
1033
|
}
|
|
961
1034
|
}
|
|
962
1035
|
} finally {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readRuntimePort } from "../config/process-state";
|
|
2
|
+
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
3
|
+
import {
|
|
4
|
+
freshServingPid, hasRestartableServiceManager, readParentPid, readServiceManagerPid, restartServiceManagerJob,
|
|
5
|
+
supervisionVerdict, unsettledManagerWarning, unsupervisedRuntimeWarning,
|
|
6
|
+
} from "./supervised-recovery.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manager-first recovery for the dashboard update worker (#7043), the `src/update/job.ts` side of
|
|
10
|
+
* what #7074 did for `ocx update`. When the post-update service refresh fails, is not viable, or
|
|
11
|
+
* never serves, the worker asks launchd/systemd to restart the job it already holds before any
|
|
12
|
+
* detached `ocx start`, and counts it only when the serving proxy is that job. It lives beside the
|
|
13
|
+
* worker because `src/update/job.ts` sits at the repository's size cap.
|
|
14
|
+
*/
|
|
15
|
+
export type ManagerRestartOutcome = "supervised" | "failed" | "unsettled";
|
|
16
|
+
|
|
17
|
+
export interface ManagerRestartDeps {
|
|
18
|
+
hasManager?: () => boolean;
|
|
19
|
+
restartJob?: () => { ok: true } | { ok: false; reason: string; unsettled?: true };
|
|
20
|
+
managerPid?: () => number | null;
|
|
21
|
+
parentPid?: (pid: number) => number | null;
|
|
22
|
+
readRecord?: () => { pid: number; port: number; hostname?: string } | null;
|
|
23
|
+
probeServing?: (port: number, hostname: string, pid: number, deadlineAt: number) => Promise<boolean>;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
sleepMs?: (ms: number) => Promise<void>;
|
|
26
|
+
deadlineMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ManagerRestartFn = (
|
|
30
|
+
target: { port: number; hostname: string; release?: () => void },
|
|
31
|
+
log: (line: string) => void,
|
|
32
|
+
) => Promise<ManagerRestartOutcome>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ask the manager to restart its job and prove the serving proxy is that job. Never throws.
|
|
36
|
+
* `release` frees the update lease first: the manager's child cannot join it (#5760), and the
|
|
37
|
+
* worker reaches this point without a release when it skipped the service repair.
|
|
38
|
+
*/
|
|
39
|
+
export async function restartServiceManagerAfterUpdate(
|
|
40
|
+
target: { port: number; hostname: string; release?: () => void },
|
|
41
|
+
log: (line: string) => void,
|
|
42
|
+
deps: ManagerRestartDeps = {},
|
|
43
|
+
): Promise<ManagerRestartOutcome> {
|
|
44
|
+
if (!(deps.hasManager ?? hasRestartableServiceManager)()) return "failed";
|
|
45
|
+
target.release?.();
|
|
46
|
+
log("Asking the service manager to restart the background service...");
|
|
47
|
+
const readRecord = deps.readRecord ?? (() => readRuntimePort());
|
|
48
|
+
const probe = deps.probeServing ?? (async (port: number, hostname: string, pid: number, deadlineAt: number) => (
|
|
49
|
+
!!(await proxyIdentityAt(port, { hostname, expectedPid: pid }, { timeoutMs: 1500, deadlineAt, attempts: 1 }))
|
|
50
|
+
));
|
|
51
|
+
const now = deps.now ?? (() => Date.now());
|
|
52
|
+
const sleep = deps.sleepMs ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
53
|
+
const staleRecordPid = readRecord()?.pid ?? null;
|
|
54
|
+
const restarted = (deps.restartJob ?? restartServiceManagerJob)();
|
|
55
|
+
const unsettled = !restarted.ok && restarted.unsettled === true;
|
|
56
|
+
let verdict: ReturnType<typeof supervisionVerdict>;
|
|
57
|
+
if (!restarted.ok && !unsettled) {
|
|
58
|
+
// The refusal reason is the manager's stderr: external text, so it never reaches the persisted job.
|
|
59
|
+
verdict = { kind: "failed", reason: "the service manager refused the restart" };
|
|
60
|
+
} else {
|
|
61
|
+
let servingPid: number | null = null;
|
|
62
|
+
const deadline = now() + (deps.deadlineMs ?? 30_000);
|
|
63
|
+
while (servingPid === null && now() < deadline) {
|
|
64
|
+
const current = readRecord();
|
|
65
|
+
if (current && current.port === target.port && freshServingPid(current.pid, staleRecordPid) !== null
|
|
66
|
+
&& await probe(current.port, current.hostname ?? target.hostname, current.pid, deadline)) servingPid = current.pid;
|
|
67
|
+
else await sleep(Math.min(100, Math.max(0, deadline - now())));
|
|
68
|
+
}
|
|
69
|
+
verdict = supervisionVerdict({
|
|
70
|
+
servingPid,
|
|
71
|
+
managerPid: (deps.managerPid ?? readServiceManagerPid)(),
|
|
72
|
+
parentPid: deps.parentPid ?? (pid => readParentPid(pid)),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (verdict.kind === "supervised") {
|
|
76
|
+
log(`Proxy is healthy on port ${target.port} under the service manager (pid ${verdict.managerPid}).`);
|
|
77
|
+
return "supervised";
|
|
78
|
+
}
|
|
79
|
+
if (unsettled) return "unsettled";
|
|
80
|
+
log(`The service manager did not recover a supervised proxy: ${verdict.reason}.`);
|
|
81
|
+
return "failed";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Job patch and log line when the manager may still finish its restart: no second proxy is started. */
|
|
85
|
+
export function unsettledManagerRestartFailure(port: number) {
|
|
86
|
+
const patch = {
|
|
87
|
+
status: "failed" as const,
|
|
88
|
+
restarted: false,
|
|
89
|
+
error: "the service manager did not finish restarting the proxy; no second proxy was started",
|
|
90
|
+
};
|
|
91
|
+
return [patch, oneLine(unsettledManagerWarning(port))] as const;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Job patch and log line when only a detached fallback ran for a service-managed install. */
|
|
95
|
+
export function unsupervisedRestartFailure(port: number) {
|
|
96
|
+
const patch = {
|
|
97
|
+
status: "failed" as const,
|
|
98
|
+
restarted: false,
|
|
99
|
+
error: `the proxy on port ${port} is running UNSUPERVISED after the update; run 'ocx service repair'`,
|
|
100
|
+
};
|
|
101
|
+
return [patch, oneLine(unsupervisedRuntimeWarning(port))] as const;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The job store withholds multi-line text as possible vendor output, so join the shared warning. */
|
|
105
|
+
function oneLine(text: string): string {
|
|
106
|
+
return text.split("\n").map(line => line.trim()).join(" ");
|
|
107
|
+
}
|
package/src/update/job.ts
CHANGED
|
@@ -2,18 +2,8 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, join, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
getConfigDir,
|
|
8
|
-
loadConfig,
|
|
9
|
-
} from "../config";
|
|
10
|
-
import {
|
|
11
|
-
readPid,
|
|
12
|
-
readRuntimePort,
|
|
13
|
-
removePid,
|
|
14
|
-
removeRuntimePort,
|
|
15
|
-
verifyPidIdentity,
|
|
16
|
-
} from "../config/process-state";
|
|
5
|
+
import { atomicWriteFile, getConfigDir, loadConfig } from "../config";
|
|
6
|
+
import { readPid, readRuntimePort, removePid, removeRuntimePort, verifyPidIdentity } from "../config/process-state";
|
|
17
7
|
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
18
8
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
19
9
|
import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
|
|
@@ -33,9 +23,10 @@ import {
|
|
|
33
23
|
} from "../server/proxy-liveness";
|
|
34
24
|
import { isServiceInstalled, isServiceViable, readServiceBackend, stopWindows } from "../service";
|
|
35
25
|
import { runUpdateRestartWithOwnershipLease, type ServiceOwnershipResolution } from "./restart-ownership";
|
|
26
|
+
import { restartServiceManagerAfterUpdate, unsettledManagerRestartFailure, unsupervisedRestartFailure, type ManagerRestartFn } from "./job-supervised-restart";
|
|
36
27
|
import {
|
|
37
28
|
type Channel, type Installer,
|
|
38
|
-
PKG,
|
|
29
|
+
PKG, STANDALONE_UPDATE_HINT,
|
|
39
30
|
checkUpdatePackageIntegrity,
|
|
40
31
|
currentVersion,
|
|
41
32
|
defaultUpdateTag,
|
|
@@ -492,17 +483,17 @@ export function checkForUpdate(
|
|
|
492
483
|
const ownership = deps.detectInstallOwnership?.();
|
|
493
484
|
const installer = ownership?.installer ?? deps.detectInstall();
|
|
494
485
|
const channel = requestedChannel ?? normalizeUpdateChannel(null, current);
|
|
495
|
-
const latest = installer === "source" ? null : deps.latestVersion(channel);
|
|
486
|
+
const latest = installer === "source" || installer === "standalone" ? null : deps.latestVersion(channel);
|
|
496
487
|
const updateAvailable = !!latest && isNewer(latest, current, channel);
|
|
497
488
|
let reason: string | undefined;
|
|
498
|
-
let command = installer === "source"
|
|
489
|
+
let command = installer === "standalone" ? STANDALONE_UPDATE_HINT : installer === "source"
|
|
499
490
|
? manualSourceCommand()
|
|
500
491
|
: installer === "mise"
|
|
501
492
|
? (ownership && deps.miseUpdateCommand?.(ownership)) ?? ""
|
|
502
493
|
: updateExecutionCommand(installer, channel).display;
|
|
503
494
|
|
|
504
|
-
if (installer === "source") {
|
|
505
|
-
reason = "source_checkout";
|
|
495
|
+
if (installer === "source" || installer === "standalone") {
|
|
496
|
+
reason = installer === "source" ? "source_checkout" : "standalone_install";
|
|
506
497
|
} else if (installer === "mise") {
|
|
507
498
|
reason = command ? "externally_managed" : "external_ownership_invalid";
|
|
508
499
|
} else if (!latest) {
|
|
@@ -517,7 +508,7 @@ export function checkForUpdate(
|
|
|
517
508
|
channel,
|
|
518
509
|
installer,
|
|
519
510
|
updateAvailable,
|
|
520
|
-
canUpdate: installer !== "source" && installer !== "mise" && updateAvailable,
|
|
511
|
+
canUpdate: installer !== "source" && installer !== "standalone" && installer !== "mise" && updateAvailable,
|
|
521
512
|
command,
|
|
522
513
|
releaseNotesUrl: RELEASE_NOTES_URL,
|
|
523
514
|
...(reason ? { reason } : {}),
|
|
@@ -1005,6 +996,7 @@ export interface RestartIo {
|
|
|
1005
996
|
releaseForServiceManagerFn?: () => void;
|
|
1006
997
|
/** Re-acquire the lease for the direct-start fallthrough, then re-run the veto; a notice stops the restart. */
|
|
1007
998
|
reacquireForDirectStartFn?: () => { readonly notice: string; readonly failed: boolean } | null;
|
|
999
|
+
restartServiceManagerFn?: ManagerRestartFn; // launchd/systemd restart before a managed direct fallback (#7043)
|
|
1008
1000
|
probeProxy?: (port: number, hostname?: string) => Promise<boolean>;
|
|
1009
1001
|
/** Richer /healthz read for update-correlated restart evidence (pid + version). */
|
|
1010
1002
|
probeProxyIdentity?: (port: number, hostname?: string) => Promise<RestartProxyIdentity | null>;
|
|
@@ -1035,7 +1027,7 @@ export interface RestartIo {
|
|
|
1035
1027
|
job: UpdateJobState,
|
|
1036
1028
|
captured?: { port: number; hostname: string; oldPid?: number },
|
|
1037
1029
|
io?: RestartIo,
|
|
1038
|
-
) => Promise<false | void>;
|
|
1030
|
+
) => Promise<false | void | "unsupervised">;
|
|
1039
1031
|
/**
|
|
1040
1032
|
* PIDs currently LISTENing on the captured port. Used to widen the post-update
|
|
1041
1033
|
* kill allowlist beyond the pre-update PID (Windows often leaves a respawned
|
|
@@ -1098,7 +1090,7 @@ async function restartAfterUpdate(
|
|
|
1098
1090
|
job: UpdateJobState,
|
|
1099
1091
|
captured?: { port: number; hostname: string; oldPid?: number },
|
|
1100
1092
|
io: RestartIo = {},
|
|
1101
|
-
): Promise<false | void> {
|
|
1093
|
+
): Promise<false | void | "unsupervised"> {
|
|
1102
1094
|
const serviceInstalled = (io.serviceInstalledFn ?? isServiceInstalled)();
|
|
1103
1095
|
const config = loadConfig();
|
|
1104
1096
|
// Stop-first update already cleared pid/runtime state, so the pre-update capture is
|
|
@@ -1250,6 +1242,12 @@ async function restartAfterUpdate(
|
|
|
1250
1242
|
}
|
|
1251
1243
|
}
|
|
1252
1244
|
|
|
1245
|
+
// #7043: a managed install asks launchd/systemd first; only the manager's own job counts as recovered.
|
|
1246
|
+
const managed = !serviceInstalled ? "failed" : await (io.restartServiceManagerFn ?? restartServiceManagerAfterUpdate)(
|
|
1247
|
+
{ port, hostname, release: io.releaseForServiceManagerFn }, line => updateJob(job, {}, line));
|
|
1248
|
+
if (managed === "supervised") return;
|
|
1249
|
+
if (managed === "unsettled") { updateJob(job, ...unsettledManagerRestartFailure(port)); return false; }
|
|
1250
|
+
const fallback = serviceInstalled ? "unsupervised" as const : undefined; // every exit below is unproven ownership
|
|
1253
1251
|
// The service refresh ran outside the lease; take it back so the veto and the kills/start below stay serialized.
|
|
1254
1252
|
const restartVeto = io.reacquireForDirectStartFn?.();
|
|
1255
1253
|
if (restartVeto) {
|
|
@@ -1282,7 +1280,7 @@ async function restartAfterUpdate(
|
|
|
1282
1280
|
);
|
|
1283
1281
|
if (liveHolders.length > 0) {
|
|
1284
1282
|
updateJob(job, {}, `Live holder(s) remain on port ${port}; not starting on another port. Retry 'ocx start --port ${port}'.`);
|
|
1285
|
-
return;
|
|
1283
|
+
return fallback;
|
|
1286
1284
|
}
|
|
1287
1285
|
// Dead PIDs can still own LISTEN rows. SetTcpEntry needs elevation (rc 317 on a
|
|
1288
1286
|
// normal update worker), so poll until netstat is empty and the start runtime can bind.
|
|
@@ -1306,7 +1304,7 @@ async function restartAfterUpdate(
|
|
|
1306
1304
|
+ `${formatPortHolders(port, listPids, verifyOcx, directAllow)} `
|
|
1307
1305
|
+ `Retry 'ocx start --port ${port}'.`,
|
|
1308
1306
|
);
|
|
1309
|
-
return;
|
|
1307
|
+
return fallback;
|
|
1310
1308
|
}
|
|
1311
1309
|
}
|
|
1312
1310
|
}
|
|
@@ -1314,7 +1312,7 @@ async function restartAfterUpdate(
|
|
|
1314
1312
|
// retries on missing /healthz after prepare + ghost-LISTEN clear.
|
|
1315
1313
|
if (io.spawnStart) {
|
|
1316
1314
|
io.spawnStart(job, job.installer, port, launcher);
|
|
1317
|
-
return;
|
|
1315
|
+
return fallback;
|
|
1318
1316
|
}
|
|
1319
1317
|
const sleep = io.sleepMs ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
1320
1318
|
const probe = io.probeProxy ?? (async (p: number, host?: string) => (
|
|
@@ -1334,7 +1332,7 @@ async function restartAfterUpdate(
|
|
|
1334
1332
|
{},
|
|
1335
1333
|
`Proxy already healthy on ${hostname}:${port} at ${expectedVersion}; skipping pinned start.`,
|
|
1336
1334
|
);
|
|
1337
|
-
return;
|
|
1335
|
+
return fallback;
|
|
1338
1336
|
}
|
|
1339
1337
|
}
|
|
1340
1338
|
const attempts = 3;
|
|
@@ -1393,12 +1391,13 @@ async function restartAfterUpdate(
|
|
|
1393
1391
|
child.once("close", retireChild);
|
|
1394
1392
|
const healthDeadline = now() + perAttemptHealthMs;
|
|
1395
1393
|
while (now() < healthDeadline) {
|
|
1396
|
-
if (await probe(port, hostname)) return;
|
|
1394
|
+
if (await probe(port, hostname)) return fallback;
|
|
1397
1395
|
await sleep(500);
|
|
1398
1396
|
}
|
|
1399
1397
|
}
|
|
1400
1398
|
// Exhausted retries: do not leave a hung pinned-start child owning the port.
|
|
1401
1399
|
killSpawnAttempt(lastChild);
|
|
1400
|
+
return fallback;
|
|
1402
1401
|
}
|
|
1403
1402
|
|
|
1404
1403
|
/** Compact listen-holder summary for update-job logs when reclaim fails. */
|
|
@@ -1454,7 +1453,7 @@ export function restartAfterUpdateForTests(
|
|
|
1454
1453
|
job: UpdateJobState,
|
|
1455
1454
|
captured: { port: number; hostname: string; oldPid?: number },
|
|
1456
1455
|
io: RestartIo,
|
|
1457
|
-
): Promise<false | void> {
|
|
1456
|
+
): Promise<false | void | "unsupervised"> {
|
|
1458
1457
|
return restartAfterUpdate(job, captured, io);
|
|
1459
1458
|
}
|
|
1460
1459
|
|
|
@@ -1729,12 +1728,13 @@ export async function finishGuiUpdateRestart(
|
|
|
1729
1728
|
}
|
|
1730
1729
|
}
|
|
1731
1730
|
const restartFn = io.restartAfterUpdateFn ?? restartAfterUpdate;
|
|
1732
|
-
|
|
1733
|
-
if (
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1731
|
+
const restarted = await restartFn(job, captured, io);
|
|
1732
|
+
if (restarted === false) return false;
|
|
1733
|
+
// Bun/source: health alone remains enough unless a richer identity probe is supplied.
|
|
1734
|
+
const confirm = !usesNodeLauncher(installer) && !io.probeProxyIdentity ? confirmRestartedProxy : confirmPackageManagerExplicitRestart;
|
|
1735
|
+
const confirmed = await confirm(job, captured, io);
|
|
1736
|
+
if (confirmed && restarted === "unsupervised") updateJob(job, ...unsupervisedRestartFailure(captured.port)); // #7043
|
|
1737
|
+
return confirmed && restarted !== "unsupervised";
|
|
1738
1738
|
}
|
|
1739
1739
|
|
|
1740
1740
|
/**
|
package/src/update/notify.ts
CHANGED
|
@@ -152,7 +152,7 @@ export function interactiveGuardOk(): boolean {
|
|
|
152
152
|
*/
|
|
153
153
|
export function shouldConsider(): { channel: Channel; current: string } | null {
|
|
154
154
|
const installer = detectInstall();
|
|
155
|
-
if (installer === "source" || installer === "mise") return null;
|
|
155
|
+
if (installer === "source" || installer === "standalone" || installer === "mise") return null;
|
|
156
156
|
const current = currentVersion();
|
|
157
157
|
if (current === "?" || isSourceBuildVersion(current)) return null;
|
|
158
158
|
if (!interactiveGuardOk()) return null;
|
|
@@ -95,12 +95,22 @@ export function resolveNpmCommand(
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function shellEnvValue(env, name) {
|
|
99
|
+
// Copying process.env loses Windows' case-insensitive property access. Keep
|
|
100
|
+
// the existing exact-key precedence; do not choose between conflicting aliases.
|
|
101
|
+
if (env[name] !== undefined) return env[name];
|
|
102
|
+
const values = new Set(Object.entries(env)
|
|
103
|
+
.filter(([key, value]) => key.toLowerCase() === name.toLowerCase() && value !== undefined)
|
|
104
|
+
.map(([, value]) => value));
|
|
105
|
+
return values.size === 1 ? values.values().next().value : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
function systemCommandProcessor(env) {
|
|
99
|
-
const systemRoot = env
|
|
109
|
+
const systemRoot = shellEnvValue(env, "SystemRoot") ?? shellEnvValue(env, "windir");
|
|
100
110
|
if (systemRoot && win32.isAbsolute(systemRoot)) {
|
|
101
111
|
return win32.join(systemRoot, "System32", "cmd.exe");
|
|
102
112
|
}
|
|
103
|
-
const comSpec = env
|
|
113
|
+
const comSpec = shellEnvValue(env, "ComSpec");
|
|
104
114
|
return comSpec && win32.isAbsolute(comSpec) ? win32.resolve(comSpec) : null;
|
|
105
115
|
}
|
|
106
116
|
|
|
@@ -53,7 +53,7 @@ export function createRefreshScheduler(deps: RefreshDeps = defaults) {
|
|
|
53
53
|
const version = deps.current();
|
|
54
54
|
const installer = deps.install();
|
|
55
55
|
return !deps.disabled() && version !== "?" && !isSourceBuildVersion(version)
|
|
56
|
-
&& installer !== "source" && installer !== "mise";
|
|
56
|
+
&& installer !== "source" && installer !== "standalone" && installer !== "mise";
|
|
57
57
|
};
|
|
58
58
|
const channel = () => defaultUpdateTag(deps.current());
|
|
59
59
|
const stale = (tag: Channel) => {
|
|
@@ -123,7 +123,7 @@ export function createRefreshScheduler(deps: RefreshDeps = defaults) {
|
|
|
123
123
|
},
|
|
124
124
|
async check(tag: Channel): Promise<UpdateCheckResult> {
|
|
125
125
|
const installer = deps.install();
|
|
126
|
-
const latest = installer === "source" || installer === "mise" ? null : await lookup(tag, false);
|
|
126
|
+
const latest = installer === "source" || installer === "standalone" || installer === "mise" ? null : await lookup(tag, false);
|
|
127
127
|
return checkForUpdate(tag, {
|
|
128
128
|
currentVersion: deps.current,
|
|
129
129
|
detectInstall: deps.install,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Declarations for the plain-ESM manager recovery shared with `bin/ocx.mjs` (#7043). */
|
|
2
|
+
export type ServiceManagerRun = (file: string, args: string[]) => { ok: boolean; stdout: string; stderr: string; timedOut?: boolean };
|
|
3
|
+
export interface ServiceManagerDeps {
|
|
4
|
+
platform?: NodeJS.Platform;
|
|
5
|
+
uid?: number;
|
|
6
|
+
run?: ServiceManagerRun;
|
|
7
|
+
}
|
|
8
|
+
export type SupervisionVerdict =
|
|
9
|
+
| { kind: "supervised"; managerPid: number; servingPid: number }
|
|
10
|
+
| { kind: "failed"; reason: string };
|
|
11
|
+
export declare const LAUNCHD_SERVICE_LABEL: string;
|
|
12
|
+
export declare const SYSTEMD_SERVICE_UNIT: string;
|
|
13
|
+
export declare function parseLaunchdJobPid(printOutput: string): number | null;
|
|
14
|
+
export declare function parseSystemdMainPid(showOutput: string): number | null;
|
|
15
|
+
export declare function hasRestartableServiceManager(deps?: ServiceManagerDeps): boolean;
|
|
16
|
+
export declare function restartServiceManagerJob(deps?: ServiceManagerDeps):
|
|
17
|
+
{ ok: true } | { ok: false; reason: string; unsettled?: true };
|
|
18
|
+
export declare function freshServingPid(recordPid: number, staleRecordPid: number | null): number | null;
|
|
19
|
+
export declare function unsettledManagerWarning(port: number): string;
|
|
20
|
+
export declare function readServiceManagerPid(deps?: ServiceManagerDeps): number | null;
|
|
21
|
+
export declare function readParentPid(pid: number, deps?: ServiceManagerDeps): number | null;
|
|
22
|
+
export declare function supervisionVerdict(input: {
|
|
23
|
+
servingPid: number | null;
|
|
24
|
+
managerPid: number | null;
|
|
25
|
+
parentPid: (pid: number) => number | null;
|
|
26
|
+
}): SupervisionVerdict;
|
|
27
|
+
export declare function unsupervisedRuntimeWarning(port: number): string;
|