@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
package/bin/ocx.mjs
CHANGED
|
@@ -12,6 +12,21 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
12
12
|
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../src/update/stop-contract.mjs";
|
|
13
13
|
import { probeProxyLiveness } from "../src/update/proxy-liveness-probe.mjs";
|
|
14
14
|
import { decidePostStopUpdate } from "../src/update/stop-decision.mjs";
|
|
15
|
+
import {
|
|
16
|
+
inspectPackageRuntimeLiveness,
|
|
17
|
+
planStoppedRuntimeRecovery,
|
|
18
|
+
planUpdateRuntimeHandling,
|
|
19
|
+
} from "../src/update/runtime-ownership.mjs";
|
|
20
|
+
import {
|
|
21
|
+
inspectInstallStateBytes,
|
|
22
|
+
selectAuthoritativeServiceState,
|
|
23
|
+
serviceStateFilesFor,
|
|
24
|
+
} from "../src/service/install-state-contract.mjs";
|
|
25
|
+
import {
|
|
26
|
+
acquireOwnershipMutationLease,
|
|
27
|
+
ownershipMutationLeaseChildEnvironment,
|
|
28
|
+
unprivilegedOwnershipMutationEnvironment,
|
|
29
|
+
} from "../src/service/ownership-mutation-lease.mjs";
|
|
15
30
|
import { randomBytes } from "node:crypto";
|
|
16
31
|
import { createRequire } from "node:module";
|
|
17
32
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
@@ -41,6 +56,9 @@ import {
|
|
|
41
56
|
} from "../src/update/codex-cli-update-launch-policy.mjs";
|
|
42
57
|
|
|
43
58
|
const PKG = "@bitkyc08/opencodex";
|
|
59
|
+
const UPDATE_RECOVERY_READY_MS = 30_000;
|
|
60
|
+
const UPDATE_RECOVERY_POLL_MS = 100;
|
|
61
|
+
const UPDATE_RECOVERY_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
44
62
|
try {
|
|
45
63
|
process.cwd();
|
|
46
64
|
} catch {
|
|
@@ -256,8 +274,45 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
256
274
|
|
|
257
275
|
// Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
|
|
258
276
|
// unloads it, so a successful update must refresh and restart it afterwards.
|
|
259
|
-
const
|
|
260
|
-
|
|
277
|
+
const allServiceStatePaths = serviceStateFilesFor(configDir(), join(homedir(), ".opencodex"));
|
|
278
|
+
// The test guard's legacy path is the developer's real home. Production always reads the
|
|
279
|
+
// same active-home + default-home observations as the Bun resolver.
|
|
280
|
+
const serviceStatePaths = process.env.OCX_TEST_HOME_GUARD === "1"
|
|
281
|
+
? allServiceStatePaths.slice(0, 1)
|
|
282
|
+
: allServiceStatePaths;
|
|
283
|
+
const serviceWasInstalled = serviceStatePaths.some(path => existsSync(path));
|
|
284
|
+
// What this update may do to the runtime. The same rule the Bun updater applies, from the
|
|
285
|
+
// same module: a desktop takeover vetoes both the stop and the service refresh below.
|
|
286
|
+
const readServiceState = () => selectAuthoritativeServiceState(
|
|
287
|
+
serviceStatePaths.map(path => inspectInstallStateBytes(path, at => readFileSync(at, "utf8"))),
|
|
288
|
+
);
|
|
289
|
+
const readOwnership = () => {
|
|
290
|
+
const selected = readServiceState();
|
|
291
|
+
if (selected.kind === "unknown") return { ownership: null, ownershipUnknown: true, subjectToken: "unknown" };
|
|
292
|
+
if (selected.kind === "none") return {
|
|
293
|
+
ownership: null, ownershipUnknown: false, subjectToken: JSON.stringify(["none", selected.revision]),
|
|
294
|
+
};
|
|
295
|
+
const ownership = selected.state.ownership ?? null;
|
|
296
|
+
return {
|
|
297
|
+
ownership,
|
|
298
|
+
ownershipUnknown: false,
|
|
299
|
+
subjectToken: JSON.stringify(ownership
|
|
300
|
+
? ["owned", selected.revision, ownership]
|
|
301
|
+
: ["none", selected.revision]),
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
const ownershipIdentity = observation => observation.ownershipUnknown
|
|
305
|
+
? null
|
|
306
|
+
: JSON.stringify(observation.ownership
|
|
307
|
+
? ["owned", observation.ownership.owner, observation.ownership.installId, observation.ownership.consentGeneration]
|
|
308
|
+
: ["none"]);
|
|
309
|
+
const initialOwnership = readOwnership();
|
|
310
|
+
let runtimePlan = planUpdateRuntimeHandling({ ...initialOwnership, serviceInstalled: serviceWasInstalled });
|
|
311
|
+
if (runtimePlan.notice) console.log(runtimePlan.notice);
|
|
312
|
+
if (!runtimePlan.mayReplacePackage) {
|
|
313
|
+
console.error("opencodex: update stopped before tray handoff, runtime stop, or package replacement because runtime ownership is unknown.");
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
261
316
|
const trayBeforeUpdate = planWindowsTrayUpdate(
|
|
262
317
|
process.platform === "win32" ? trayInstallState() : { installed: false, running: false },
|
|
263
318
|
);
|
|
@@ -271,10 +326,11 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
271
326
|
}
|
|
272
327
|
/** Register from scratch, preserving the recorded backend. Only for a genuinely absent service. */
|
|
273
328
|
function serviceInstallArgs() {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
329
|
+
const selected = readServiceState();
|
|
330
|
+
if (selected.kind === "unknown") throw new Error(`service backend is unknown: ${selected.reason}`);
|
|
331
|
+
if (selected.kind === "state" && selected.state.backend === "native") {
|
|
332
|
+
return [postUpdateLauncher, "service", "install", "--native"];
|
|
333
|
+
}
|
|
278
334
|
return [postUpdateLauncher, "service", "install"];
|
|
279
335
|
}
|
|
280
336
|
/**
|
|
@@ -302,39 +358,45 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
302
358
|
}
|
|
303
359
|
}
|
|
304
360
|
|
|
305
|
-
|
|
306
|
-
|
|
361
|
+
function readCurrentRuntimeTarget() {
|
|
362
|
+
let raw;
|
|
363
|
+
try {
|
|
364
|
+
raw = readFileSync(join(configDir(), "runtime-port.json"), "utf8");
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
367
|
+
? { kind: "absent" }
|
|
368
|
+
: { kind: "unknown" };
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const rt = JSON.parse(raw);
|
|
372
|
+
const pid = Number(rt?.pid);
|
|
373
|
+
if (!Number.isFinite(rt?.port) || rt.port <= 0 || rt.port > 65535
|
|
374
|
+
|| !Number.isSafeInteger(pid) || pid <= 0) return { kind: "unknown" };
|
|
375
|
+
return { kind: "target", target: {
|
|
376
|
+
pid,
|
|
377
|
+
port: Math.trunc(rt.port),
|
|
378
|
+
hostname: typeof rt.hostname === "string" && rt.hostname.trim() !== ""
|
|
379
|
+
? rt.hostname.trim()
|
|
380
|
+
: null,
|
|
381
|
+
} };
|
|
382
|
+
} catch { return { kind: "unknown" }; }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Capture the recovery target before stop clears runtime-port.json. Replacement safety
|
|
386
|
+
// re-reads this record under the mutation lease instead of trusting this snapshot.
|
|
307
387
|
let bakePort = 10100;
|
|
308
388
|
// The hostname travels with the port: a proxy bound to ::1 or a specific interface is
|
|
309
389
|
// invisible to a probe that assumes 127.0.0.1, and "no answer" would then read as
|
|
310
390
|
// "stopped" for exactly the proxy the probe exists to find.
|
|
311
391
|
let bakeHostname = "127.0.0.1";
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (Number.isSafeInteger(rtPid) && rtPid > 0) {
|
|
321
|
-
try {
|
|
322
|
-
process.kill(rtPid, 0);
|
|
323
|
-
runtimeLive = true;
|
|
324
|
-
} catch (e) {
|
|
325
|
-
if (e && typeof e === "object" && "code" in e && e.code === "EPERM") runtimeLive = true;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (runtimeLive) {
|
|
329
|
-
bakePort = Math.trunc(rt.port);
|
|
330
|
-
if (typeof rt?.hostname === "string" && rt.hostname.trim() !== "") {
|
|
331
|
-
bakeHostname = rt.hostname.trim();
|
|
332
|
-
sawRuntimeHostname = true;
|
|
333
|
-
}
|
|
334
|
-
sawRuntimePort = true;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
} catch { /* fall through to config */ }
|
|
392
|
+
const initialRuntimeObservation = readCurrentRuntimeTarget();
|
|
393
|
+
const initialRuntimeTarget = initialRuntimeObservation.kind === "target" ? initialRuntimeObservation.target : null;
|
|
394
|
+
let sawRuntimePort = initialRuntimeTarget !== null;
|
|
395
|
+
let sawRuntimeHostname = initialRuntimeTarget?.hostname !== null && initialRuntimeTarget?.hostname !== undefined;
|
|
396
|
+
if (initialRuntimeTarget) {
|
|
397
|
+
bakePort = initialRuntimeTarget.port;
|
|
398
|
+
if (initialRuntimeTarget.hostname) bakeHostname = initialRuntimeTarget.hostname;
|
|
399
|
+
}
|
|
338
400
|
// Port and hostname resolve INDEPENDENTLY: a legacy runtime record carries a port and no
|
|
339
401
|
// hostname, and skipping config in that case probed 127.0.0.1 for a proxy bound to ::1.
|
|
340
402
|
if (!sawRuntimePort || bakeHostname === "127.0.0.1") {
|
|
@@ -350,6 +412,18 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
350
412
|
}
|
|
351
413
|
// Wildcard and bracketed-IPv6 normalization lives in probeProxyLiveness, so both lanes
|
|
352
414
|
// get it from one place.
|
|
415
|
+
function currentPackageRuntimeLiveness() {
|
|
416
|
+
return inspectPackageRuntimeLiveness({
|
|
417
|
+
capturedTarget: { port: bakePort, hostname: bakeHostname },
|
|
418
|
+
readCurrentTarget: () => {
|
|
419
|
+
const current = readCurrentRuntimeTarget();
|
|
420
|
+
return current.kind === "target"
|
|
421
|
+
? { kind: "target", target: { port: current.target.port, hostname: current.target.hostname ?? bakeHostname } }
|
|
422
|
+
: current;
|
|
423
|
+
},
|
|
424
|
+
probe: target => probeProxyLiveness(target.port, target.hostname),
|
|
425
|
+
}).overall;
|
|
426
|
+
}
|
|
353
427
|
|
|
354
428
|
const launcher = fileURLToPath(import.meta.url);
|
|
355
429
|
// The pnpm owner preflight has verified this package tree and global group. Keep that exact
|
|
@@ -359,13 +433,17 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
359
433
|
? join(owner.packagePath, "bin", "ocx.mjs")
|
|
360
434
|
: launcher;
|
|
361
435
|
let postUpdateLauncherUsable = true;
|
|
436
|
+
let delegatedOwnershipMutationToken = null;
|
|
437
|
+
const mutationChildEnvironment = () => delegatedOwnershipMutationToken
|
|
438
|
+
? ownershipMutationLeaseChildEnvironment(process.env, delegatedOwnershipMutationToken)
|
|
439
|
+
: unprivilegedOwnershipMutationEnvironment(process.env);
|
|
362
440
|
|
|
363
441
|
function startProxyDirectly() {
|
|
364
442
|
if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) {
|
|
365
443
|
console.error("opencodex: cannot restart the proxy because the launcher is missing; reinstall opencodex manually.");
|
|
366
|
-
return;
|
|
444
|
+
return false;
|
|
367
445
|
}
|
|
368
|
-
const env =
|
|
446
|
+
const env = mutationChildEnvironment();
|
|
369
447
|
delete env.OCX_SERVICE;
|
|
370
448
|
console.log(`Attempting to restart the proxy on port ${bakePort}.`);
|
|
371
449
|
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(bakePort)], {
|
|
@@ -378,13 +456,24 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
378
456
|
console.error(`opencodex: direct proxy restart failed: ${error.message}`);
|
|
379
457
|
});
|
|
380
458
|
child.unref();
|
|
459
|
+
const deadline = Date.now() + UPDATE_RECOVERY_READY_MS;
|
|
460
|
+
while (Date.now() < deadline) {
|
|
461
|
+
const current = readCurrentRuntimeTarget();
|
|
462
|
+
if (current.kind === "target"
|
|
463
|
+
&& probeProxyLiveness(current.target.port, current.target.hostname ?? bakeHostname) === "live") return true;
|
|
464
|
+
Atomics.wait(UPDATE_RECOVERY_SLEEP, 0, 0, UPDATE_RECOVERY_POLL_MS);
|
|
465
|
+
}
|
|
466
|
+
console.error("opencodex: the recovery proxy did not publish a healthy runtime before the recovery deadline.");
|
|
467
|
+
return false;
|
|
381
468
|
}
|
|
382
469
|
|
|
383
470
|
function refreshBackgroundServiceOrStartDirect() {
|
|
384
471
|
const prevBake = process.env.OCX_BAKE_PORT;
|
|
385
472
|
process.env.OCX_BAKE_PORT = String(bakePort);
|
|
386
473
|
try {
|
|
387
|
-
let svc = spawnSync(process.execPath, serviceRefreshArgs(), {
|
|
474
|
+
let svc = spawnSync(process.execPath, serviceRefreshArgs(), {
|
|
475
|
+
stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
|
|
476
|
+
});
|
|
388
477
|
// `serviceWasInstalled` is inferred from service-state.json alone, which can be
|
|
389
478
|
// STALE — present while the registration is gone. Repair refuses that case by
|
|
390
479
|
// design, and its thrown Error is indistinguishable from any other failure at
|
|
@@ -395,7 +484,9 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
395
484
|
// could re-register a service the user just uninstalled.
|
|
396
485
|
if (svc.status !== 0 && readServiceInstalledFromStatus(postUpdateLauncher) === false) {
|
|
397
486
|
console.log("No registered service found — installing it instead.");
|
|
398
|
-
svc = spawnSync(process.execPath, serviceInstallArgs(), {
|
|
487
|
+
svc = spawnSync(process.execPath, serviceInstallArgs(), {
|
|
488
|
+
stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
|
|
489
|
+
});
|
|
399
490
|
}
|
|
400
491
|
let needDirectStart = svc.status !== 0;
|
|
401
492
|
if (!needDirectStart) {
|
|
@@ -421,6 +512,14 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
421
512
|
}
|
|
422
513
|
}
|
|
423
514
|
if (needDirectStart) {
|
|
515
|
+
// Re-read rather than reuse the plan from before the package install: the app can
|
|
516
|
+
// claim the runtime during an update that takes minutes, and the refusal that repair
|
|
517
|
+
// just returned is indistinguishable from any other failure at this layer.
|
|
518
|
+
const nowOwned = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: true });
|
|
519
|
+
if (!nowOwned.mayStopRuntime) {
|
|
520
|
+
console.warn(nowOwned.notice ?? "opencodex: the background runtime is owned elsewhere; not starting a second proxy.");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
424
523
|
// Repair normally avoids elevation for a healthy registration, but a stale Windows
|
|
425
524
|
// scheduler definition can require it. It can also fail — or exit 0 while leaving
|
|
426
525
|
// a non-viable manager. Fall back to a direct detached proxy start so the
|
|
@@ -439,189 +538,264 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
439
538
|
}
|
|
440
539
|
}
|
|
441
540
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
541
|
+
const updateLease = acquireOwnershipMutationLease(serviceStatePaths);
|
|
542
|
+
delegatedOwnershipMutationToken = updateLease.token;
|
|
543
|
+
let updateLeaseReleased = false;
|
|
544
|
+
const releaseUpdateLease = () => {
|
|
545
|
+
if (updateLeaseReleased) return;
|
|
546
|
+
updateLeaseReleased = true;
|
|
547
|
+
delegatedOwnershipMutationToken = null;
|
|
548
|
+
updateLease.release();
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
let res;
|
|
552
|
+
try {
|
|
553
|
+
// Stop authority is decided under the same lease the child joins. A takeover between the
|
|
554
|
+
// earlier preflight and this boundary therefore blocks stop before it is sent.
|
|
555
|
+
const lockedOwnership = readOwnership();
|
|
556
|
+
const lockedPlan = planUpdateRuntimeHandling({ ...lockedOwnership, serviceInstalled: serviceWasInstalled });
|
|
557
|
+
if (lockedOwnership.subjectToken !== initialOwnership.subjectToken || !lockedPlan.mayReplacePackage) {
|
|
558
|
+
releaseUpdateLease();
|
|
559
|
+
console.error(lockedPlan.notice
|
|
560
|
+
?? "opencodex: update stopped because runtime ownership changed before stop authorization; rerun from the beginning.");
|
|
458
561
|
process.exit(1);
|
|
459
562
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
|
|
563
|
+
runtimePlan = lockedPlan;
|
|
564
|
+
const stoppedOwnershipIdentity = ownershipIdentity(lockedOwnership);
|
|
463
565
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
566
|
+
// Never replace package files under a live proxy — stop it first (full `ocx stop`
|
|
567
|
+
// semantics: graceful drain, service stop, native Codex restore). Gate on the service
|
|
568
|
+
// and the runtime-port record too: a service-managed or orphaned proxy can be live
|
|
569
|
+
// while ocx.pid is stale/missing.
|
|
570
|
+
if (trayBeforeUpdate.stopBeforeReplacement) {
|
|
571
|
+
console.log("⏹ Handing off the Windows tray before updating...");
|
|
572
|
+
try {
|
|
573
|
+
handoffWindowsTrayForUpdate(trayBeforeUpdate, {
|
|
574
|
+
stop: () => {
|
|
575
|
+
const stopped = runTrayLifecycle(launcher, "stop");
|
|
576
|
+
return { exitStatus: stopped.status, running: trayInstallState().running };
|
|
577
|
+
},
|
|
578
|
+
start: () => runTrayLifecycle(launcher, "start"),
|
|
579
|
+
});
|
|
580
|
+
} catch {
|
|
581
|
+
releaseUpdateLease();
|
|
582
|
+
console.error("opencodex: could not stop the Windows tray; aborting before package replacement.");
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
468
585
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
586
|
+
const hasRuntimeState =
|
|
587
|
+
existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
|
|
588
|
+
let stopAttempted = false;
|
|
589
|
+
|
|
590
|
+
function recoverStoppedRuntimeAfterFailure(reason) {
|
|
591
|
+
const recoveryOwnership = readOwnership();
|
|
592
|
+
const recoveryLiveness = currentPackageRuntimeLiveness();
|
|
593
|
+
const recovery = planStoppedRuntimeRecovery({
|
|
594
|
+
stopAttempted,
|
|
595
|
+
...recoveryOwnership,
|
|
596
|
+
sameOwner: ownershipIdentity(recoveryOwnership) === stoppedOwnershipIdentity,
|
|
597
|
+
liveness: recoveryLiveness,
|
|
598
|
+
serviceInstalled: serviceWasInstalled,
|
|
599
|
+
launcherUsable: postUpdateLauncherUsable,
|
|
600
|
+
hadRuntimeState: hasRuntimeState,
|
|
601
|
+
});
|
|
602
|
+
if (recovery.reason === "ownership-unknown") {
|
|
603
|
+
console.error(`opencodex: ${reason}; runtime ownership is unknown, so automatic recovery was refused. Run 'ocx status --json' and repair the service-state record before retrying.`);
|
|
604
|
+
} else if (recovery.reason === "ownership-transferred") {
|
|
605
|
+
console.log("opencodex: runtime ownership moved to another installation; the stopped CLI runtime was not revived.");
|
|
606
|
+
} else if (recovery.reason.startsWith("runtime-")) {
|
|
607
|
+
console.error(`opencodex: ${reason}; package runtime liveness is ${recoveryLiveness}, so automatic recovery was refused.`);
|
|
608
|
+
} else if (recovery.reason === "launcher-unavailable") {
|
|
609
|
+
console.error("opencodex: no verified active launcher remains for automatic recovery; reinstall opencodex manually.");
|
|
610
|
+
} else if (recovery.action === "service") {
|
|
611
|
+
console.warn(`opencodex: ${reason} after stopping the proxy — restoring the previous background service.`);
|
|
612
|
+
refreshBackgroundServiceOrStartDirect();
|
|
613
|
+
} else if (recovery.action === "direct") {
|
|
614
|
+
console.warn(`opencodex: ${reason} after stopping the proxy — restarting the previous version directly.`);
|
|
615
|
+
startProxyDirectly();
|
|
616
|
+
}
|
|
617
|
+
return recovery;
|
|
475
618
|
}
|
|
476
|
-
}
|
|
477
619
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// A history-only failure means teardown succeeded and a backup manifest is waiting for
|
|
491
|
-
// review: the proxy is down and replacing package files is safe. Every other nonzero
|
|
492
|
-
// status is a stop that did not finish, and a signal kill (status null) says nothing
|
|
493
|
-
// about whether it did - both abort, because replacing files under a live server
|
|
494
|
-
// leaves it running mixed old and new modules (#3008).
|
|
495
|
-
// The same decision the Bun updater makes, from the same module (#3008). Absent PID and
|
|
496
|
-
// runtime files are weak evidence, so the captured endpoint is asked; "unknown" aborts
|
|
497
|
-
// because a silent listener is exactly the state where replacing files is dangerous.
|
|
498
|
-
const decision = decidePostStopUpdate({
|
|
499
|
-
status: stopRes.status,
|
|
500
|
-
hasRuntimeState: stillHasRuntimeState,
|
|
501
|
-
// Re-checked AFTER the stop: a quarantined receipt lets the stop itself succeed
|
|
502
|
-
// (there is nothing left to stop), so a pre-stop check alone let the retry install
|
|
503
|
-
// over a teardown that never ran.
|
|
504
|
-
teardownOutstanding: hasPendingTeardownIn(readdirSync, configDir()),
|
|
505
|
-
liveness: probeProxyLiveness(bakePort, bakeHostname),
|
|
506
|
-
});
|
|
507
|
-
const historyOnlyStop = decision.reason === "history-only";
|
|
508
|
-
if (!decision.proceed) {
|
|
509
|
-
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
|
|
510
|
-
if (decision.reason === "teardown-outstanding") {
|
|
511
|
-
console.error("opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.");
|
|
512
|
-
console.error("opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.");
|
|
513
|
-
} else console.error(decision.reason === "proxy-unknown"
|
|
514
|
-
? `opencodex: could not confirm the proxy on ${bakeHostname}:${bakePort} is stopped; aborting the update. Run 'ocx stop' and retry.`
|
|
515
|
-
: "opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
|
|
620
|
+
// An outstanding pending-teardown receipt is a fourth reason to run the stop. After a
|
|
621
|
+
// parent crashed mid-deferral the service, pid and runtime records can all be absent
|
|
622
|
+
// while the shared client config still points at a proxy that is gone; installing over
|
|
623
|
+
// that silently skips the recovery the receipt was written to trigger (#3008). Presence
|
|
624
|
+
// is the whole test here — the launcher cannot parse it, and `ocx stop` is what decides
|
|
625
|
+
// whether the obligation is safe to finish.
|
|
626
|
+
const hasPendingTeardown = hasPendingTeardownIn(readdirSync, configDir());
|
|
627
|
+
const stopNeeded = serviceWasInstalled || hasRuntimeState || hasPendingTeardown;
|
|
628
|
+
if (stopNeeded && !runtimePlan.mayStopRuntime) {
|
|
629
|
+
releaseUpdateLease();
|
|
630
|
+
console.error(runtimePlan.notice
|
|
631
|
+
?? "opencodex: update stopped because this installation may not stop the current runtime.");
|
|
516
632
|
process.exit(1);
|
|
517
633
|
}
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
);
|
|
634
|
+
if (stopNeeded) {
|
|
635
|
+
stopAttempted = true;
|
|
636
|
+
console.log("⏹ Stopping the running proxy before updating...");
|
|
637
|
+
const stopRes = spawnSync(process.execPath, [launcher, "stop"], {
|
|
638
|
+
stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
|
|
639
|
+
});
|
|
640
|
+
const stillHasRuntimeState =
|
|
641
|
+
existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
|
|
642
|
+
// A history-only failure means teardown succeeded and a backup manifest is waiting for
|
|
643
|
+
// review: the proxy is down and replacing package files is safe. Every other nonzero
|
|
644
|
+
// status is a stop that did not finish, and a signal kill (status null) says nothing
|
|
645
|
+
// about whether it did - both abort, because replacing files under a live server
|
|
646
|
+
// leaves it running mixed old and new modules (#3008).
|
|
647
|
+
// The same decision the Bun updater makes, from the same module (#3008). Absent PID and
|
|
648
|
+
// runtime files are weak evidence, so the captured endpoint is asked; "unknown" aborts
|
|
649
|
+
// because a silent listener is exactly the state where replacing files is dangerous.
|
|
650
|
+
const decision = decidePostStopUpdate({
|
|
651
|
+
status: stopRes.status,
|
|
652
|
+
hasRuntimeState: stillHasRuntimeState,
|
|
653
|
+
// Re-checked AFTER the stop: a quarantined receipt lets the stop itself succeed
|
|
654
|
+
// (there is nothing left to stop), so a pre-stop check alone let the retry install
|
|
655
|
+
// over a teardown that never ran.
|
|
656
|
+
teardownOutstanding: hasPendingTeardownIn(readdirSync, configDir()),
|
|
657
|
+
liveness: probeProxyLiveness(bakePort, bakeHostname),
|
|
658
|
+
});
|
|
659
|
+
const historyOnlyStop = decision.reason === "history-only";
|
|
660
|
+
if (!decision.proceed) {
|
|
661
|
+
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
|
|
662
|
+
if (decision.reason === "teardown-outstanding") {
|
|
663
|
+
console.error("opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.");
|
|
664
|
+
console.error("opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.");
|
|
665
|
+
} else console.error(decision.reason === "proxy-unknown"
|
|
666
|
+
? `opencodex: could not confirm the proxy on ${bakeHostname}:${bakePort} is stopped; aborting the update. Run 'ocx stop' and retry.`
|
|
667
|
+
: "opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
|
|
668
|
+
releaseUpdateLease();
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
if (historyOnlyStop || historyRestoreIncomplete()) {
|
|
672
|
+
console.warn(
|
|
673
|
+
"opencodex: WARNING — Codex resume-history metadata restore is incomplete (a backup manifest remains).\n" +
|
|
674
|
+
" The DB may be busy or the manifest/target may need review; untracked routed history is intentionally unchanged.\n" +
|
|
675
|
+
" After the update: close the Codex app, run 'ocx doctor', then run 'ocx stop' once to retry.",
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
if (decision.reason === "history-deferred") {
|
|
679
|
+
// The reported #4718 path is this lane. Nothing was restored, so this is a different
|
|
680
|
+
// sentence from the manifest warning above: an operator told "history metadata is
|
|
681
|
+
// incomplete" would assume config and catalog already came back.
|
|
682
|
+
console.warn(
|
|
683
|
+
"opencodex: WARNING — the shared teardown was refused by the Codex history preflight and restored nothing.\n" +
|
|
684
|
+
" Config, catalog, history and provenance were preserved, and the teardown receipt was kept.\n" +
|
|
685
|
+
" The proxy is down, so the update continues; close the Codex app and run 'ocx stop' once afterwards to finish the restore.",
|
|
686
|
+
);
|
|
687
|
+
}
|
|
524
688
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
);
|
|
689
|
+
|
|
690
|
+
const replacementOwnership = readOwnership();
|
|
691
|
+
const replacementPlan = planUpdateRuntimeHandling({ ...replacementOwnership, serviceInstalled: serviceWasInstalled });
|
|
692
|
+
const replacementLiveness = currentPackageRuntimeLiveness();
|
|
693
|
+
if (replacementOwnership.subjectToken !== initialOwnership.subjectToken
|
|
694
|
+
|| !replacementPlan.mayReplacePackage
|
|
695
|
+
|| replacementLiveness !== "dead") {
|
|
696
|
+
recoverStoppedRuntimeAfterFailure("replacement was refused");
|
|
697
|
+
releaseUpdateLease();
|
|
698
|
+
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
|
|
699
|
+
console.error(replacementPlan.notice
|
|
700
|
+
?? "opencodex: update stopped because runtime ownership or liveness changed after the stop decision; rerun from the beginning.");
|
|
701
|
+
process.exit(1);
|
|
534
702
|
}
|
|
535
|
-
}
|
|
536
703
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
} else {
|
|
576
|
-
const update = runPnpmGlobalUpdate({
|
|
577
|
-
packageName: PKG,
|
|
578
|
-
currentVersion: current,
|
|
579
|
-
targetVersion: latest || undefined,
|
|
580
|
-
tag,
|
|
581
|
-
owner,
|
|
582
|
-
runningPackagePath: resolve(here, ".."),
|
|
583
|
-
runPnpm: (args, capture = false) => {
|
|
584
|
-
const invocation = pnpmOwnerInvocation(owner, args);
|
|
585
|
-
if (!invocation) return { status: 1 };
|
|
586
|
-
return spawnSync(invocation.file, invocation.args, {
|
|
587
|
-
stdio: capture ? "pipe" : "inherit",
|
|
588
|
-
encoding: "utf8",
|
|
589
|
-
timeout: 180000,
|
|
590
|
-
windowsHide: true,
|
|
591
|
-
env: invocation.env,
|
|
592
|
-
...invocation.options,
|
|
593
|
-
});
|
|
594
|
-
},
|
|
595
|
-
log: line => console.log(line),
|
|
596
|
-
});
|
|
597
|
-
if (update.ok) {
|
|
598
|
-
// pnpm switches the active global group and updates its shim. Continue recovery
|
|
599
|
-
// through that fresh package tree, not the old group whose launcher is still
|
|
600
|
-
// executing this update.
|
|
601
|
-
postUpdateLauncher = join(update.path, "bin", "ocx.mjs");
|
|
602
|
-
res = { status: 0 };
|
|
704
|
+
// npm keeps the existing stage -> verify -> swap -> rollback flow. pnpm owns a
|
|
705
|
+
// content-addressable store and generated global shims, so its path uses pnpm's own
|
|
706
|
+
// global update operation and verifies the active group instead of renaming files.
|
|
707
|
+
console.log(`Updating${latest ? ` to v${latest}` : ""} (${manager === "npm" ? "transactional" : "pnpm-managed"})...`);
|
|
708
|
+
try {
|
|
709
|
+
if (manager === "npm") {
|
|
710
|
+
const packageDir = resolve(here, "..");
|
|
711
|
+
const tx = transactionalNpmUpdate({
|
|
712
|
+
packageDir,
|
|
713
|
+
pkgName: PKG,
|
|
714
|
+
targetVersion: latest || undefined,
|
|
715
|
+
tag,
|
|
716
|
+
runNpm: (args) => {
|
|
717
|
+
const invocation = npmInvocation(args);
|
|
718
|
+
if (!invocation) return { status: 1 };
|
|
719
|
+
return spawnSync(invocation.file, invocation.args, {
|
|
720
|
+
...invocation.options,
|
|
721
|
+
stdio: "inherit",
|
|
722
|
+
timeout: 180000,
|
|
723
|
+
windowsHide: true,
|
|
724
|
+
env: unprivilegedOwnershipMutationEnvironment(invocation.options?.env ?? process.env),
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
log: (line) => console.log(line),
|
|
728
|
+
});
|
|
729
|
+
postUpdateLauncherUsable = tx.ok
|
|
730
|
+
|| tx.rolledBack === true
|
|
731
|
+
|| ["stage", "verify", "swap-backup"].includes(tx.phase);
|
|
732
|
+
if (tx.ok) {
|
|
733
|
+
res = { status: 0 };
|
|
734
|
+
} else if (tx.phase === "stage" || tx.phase === "verify") {
|
|
735
|
+
// Live tree untouched: report and stop. Nothing to roll back.
|
|
736
|
+
console.error(`opencodex: update aborted before touching the live install (${tx.phase}): ${tx.error}`);
|
|
737
|
+
res = { status: 1 };
|
|
738
|
+
} else {
|
|
739
|
+
console.error(`opencodex: update failed (${tx.phase}): ${tx.error}${tx.rolledBack ? " — previous version restored." : ""}`);
|
|
740
|
+
res = { status: 1 };
|
|
741
|
+
}
|
|
603
742
|
} else {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
743
|
+
const update = runPnpmGlobalUpdate({
|
|
744
|
+
packageName: PKG,
|
|
745
|
+
currentVersion: current,
|
|
746
|
+
targetVersion: latest || undefined,
|
|
747
|
+
tag,
|
|
748
|
+
owner,
|
|
749
|
+
runningPackagePath: resolve(here, ".."),
|
|
750
|
+
runPnpm: (args, capture = false) => {
|
|
751
|
+
const invocation = pnpmOwnerInvocation(owner, args);
|
|
752
|
+
if (!invocation) return { status: 1 };
|
|
753
|
+
return spawnSync(invocation.file, invocation.args, {
|
|
754
|
+
...invocation.options,
|
|
755
|
+
stdio: capture ? "pipe" : "inherit",
|
|
756
|
+
encoding: "utf8",
|
|
757
|
+
timeout: 180000,
|
|
758
|
+
windowsHide: true,
|
|
759
|
+
env: unprivilegedOwnershipMutationEnvironment(invocation.env ?? process.env),
|
|
760
|
+
});
|
|
761
|
+
},
|
|
762
|
+
log: line => console.log(line),
|
|
763
|
+
});
|
|
764
|
+
if (update.ok) {
|
|
765
|
+
// pnpm switches the active global group and updates its shim. Continue recovery
|
|
766
|
+
// through that fresh package tree, not the old group whose launcher is still
|
|
767
|
+
// executing this update.
|
|
768
|
+
postUpdateLauncher = join(update.path, "bin", "ocx.mjs");
|
|
769
|
+
res = { status: 0 };
|
|
770
|
+
} else {
|
|
771
|
+
console.error(`opencodex: ${update.error}${update.rolledBack ? "." : " Manual recovery may be required."}`);
|
|
772
|
+
postUpdateLauncherUsable = Boolean(update.activePath);
|
|
773
|
+
if (update.activePath) postUpdateLauncher = join(update.activePath, "bin", "ocx.mjs");
|
|
774
|
+
res = { status: 1 };
|
|
775
|
+
}
|
|
608
776
|
}
|
|
777
|
+
} catch (error) {
|
|
778
|
+
// An unexpected throw means we cannot prove the live tree is untouched, so the
|
|
779
|
+
// legacy in-place install (which deletes live first) is exactly the wrong rescue —
|
|
780
|
+
// it recreates the #1849 destruction path. Report and stop; the boot probe and the
|
|
781
|
+
// recovery marker cover the swap-window states.
|
|
782
|
+
const manual = manager === "pnpm"
|
|
783
|
+
? `pnpm add -g --allow-build=bun ${PKG}@${tag}`
|
|
784
|
+
: `npm install -g --allow-scripts=bun ${PKG}@${tag}`;
|
|
785
|
+
// An unexpected exception leaves the active package path unproven for either manager.
|
|
786
|
+
// Do not run service/tray/proxy recovery through a possibly half-swapped tree.
|
|
787
|
+
postUpdateLauncherUsable = false;
|
|
788
|
+
console.error(`opencodex: ${manager} update failed unexpectedly (${error?.message ?? error}). ` +
|
|
789
|
+
`The live install was not knowingly modified; run 'ocx update' again or reinstall with ${manual}.`);
|
|
790
|
+
res = { status: 1 };
|
|
609
791
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
const manual = manager === "pnpm"
|
|
616
|
-
? `pnpm add -g --allow-build=bun ${PKG}@${tag}`
|
|
617
|
-
: `npm install -g --allow-scripts=bun ${PKG}@${tag}`;
|
|
618
|
-
// An unexpected exception leaves the active package path unproven for either manager.
|
|
619
|
-
// Do not run service/tray/proxy recovery through a possibly half-swapped tree.
|
|
620
|
-
postUpdateLauncherUsable = false;
|
|
621
|
-
console.error(`opencodex: ${manager} update failed unexpectedly (${error?.message ?? error}). ` +
|
|
622
|
-
`The live install was not knowingly modified; run 'ocx update' again or reinstall with ${manual}.`);
|
|
623
|
-
res = { status: 1 };
|
|
792
|
+
if (res.status !== 0) recoverStoppedRuntimeAfterFailure("update failed");
|
|
793
|
+
} finally {
|
|
794
|
+
// Expected aborts release before process.exit(); this covers every thrown or newly-added
|
|
795
|
+
// path and keeps token restoration coupled to the lease itself.
|
|
796
|
+
releaseUpdateLease();
|
|
624
797
|
}
|
|
798
|
+
const postInstallPlan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: serviceWasInstalled });
|
|
625
799
|
if (res.status === 0) {
|
|
626
800
|
console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`);
|
|
627
801
|
repairCodexShimIfNeeded(postUpdateLauncher);
|
|
@@ -637,16 +811,15 @@ function runPackageManagerSelfUpdate(manager) {
|
|
|
637
811
|
}
|
|
638
812
|
// The stop above unloaded any managed service; refresh via the freshly-installed
|
|
639
813
|
// launcher so the new files write the baked paths and the service restarts.
|
|
640
|
-
if (
|
|
814
|
+
if (postInstallPlan.mayRestoreService) {
|
|
641
815
|
console.log("Refreshing the background service with the updated files...");
|
|
642
816
|
refreshBackgroundServiceOrStartDirect();
|
|
643
|
-
} else {
|
|
817
|
+
} else if (postInstallPlan.mayStopRuntime) {
|
|
644
818
|
console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, bakePort)}`);
|
|
645
819
|
}
|
|
646
820
|
process.exit(0);
|
|
647
821
|
}
|
|
648
822
|
if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable) runTrayLifecycle(postUpdateLauncher, "start");
|
|
649
|
-
recoverStoppedRuntimeAfterFailure();
|
|
650
823
|
const manual = manager === "pnpm"
|
|
651
824
|
? `pnpm add -g --allow-build=bun ${PKG}@${tag}`
|
|
652
825
|
: `npm install -g --allow-scripts=bun ${PKG}@${tag}`;
|