@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -19,6 +19,7 @@ import { codexPlanKey } from "../plan";
|
|
|
19
19
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
|
|
20
20
|
import type { OcxConfig } from "../../types";
|
|
21
21
|
import { CODEX_FAILURE_WINDOW_MS, computeCodexUsageScore } from "./cooldown-math";
|
|
22
|
+
import { failureWindowSteersNewThreads } from "./failure-window";
|
|
22
23
|
import {
|
|
23
24
|
codexPoolKeyForScope,
|
|
24
25
|
dropSpentCredentialFailure,
|
|
@@ -211,6 +212,7 @@ export function getEligiblePoolAccounts(
|
|
|
211
212
|
&& (!skipFailoverReadyCandidates || !shouldFailover(config, account.id, now)))
|
|
212
213
|
.filter(account => getCodexQuotaHealthSnapshot(account.id, quotaScope, now) === null)
|
|
213
214
|
.filter(account => !isCodexAccountSoftAvoided(account.id, now))
|
|
215
|
+
.filter(account => !failureWindowSteersNewThreads(config, account.id, now))
|
|
214
216
|
.filter(account => !isCodexQuotaAvoided(account.id, quotaScope, now))
|
|
215
217
|
.filter(account => !isCodexPoolRefreshCooling(account.id, now))
|
|
216
218
|
.filter(account => isCodexAccountUsable(config, account.id, selectionOptions))
|
|
@@ -222,6 +224,7 @@ export function getEligiblePoolAccounts(
|
|
|
222
224
|
&& !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
223
225
|
&& getCodexQuotaHealthSnapshot(MAIN_CODEX_ACCOUNT_ID, quotaScope, now) === null
|
|
224
226
|
&& !isCodexAccountSoftAvoided(MAIN_CODEX_ACCOUNT_ID, now)
|
|
227
|
+
&& !failureWindowSteersNewThreads(config, MAIN_CODEX_ACCOUNT_ID, now)
|
|
225
228
|
// The main login is not in `config.codexAccounts`, so it never passes through the
|
|
226
229
|
// filters above and this is the only place an avoidance window can exclude it. Without
|
|
227
230
|
// this the window a refusal announced applies to the pool but not to the account that
|
|
@@ -863,7 +866,7 @@ export function applyFailureFailover(
|
|
|
863
866
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
864
867
|
commitSharedSelection = true,
|
|
865
868
|
): string {
|
|
866
|
-
if (!shouldFailover(config, active, now)) return active;
|
|
869
|
+
if (!shouldFailover(config, active, now) && !failureWindowSteersNewThreads(config, active, now)) return active;
|
|
867
870
|
const best = pickAlternateCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
868
871
|
if (best) {
|
|
869
872
|
// The scope still routes away from the failing account — that is this request's
|
package/src/codex/routing.ts
CHANGED
|
@@ -19,6 +19,13 @@ import type { OcxConfig } from "../types";
|
|
|
19
19
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
20
20
|
import { recordUpstreamHostFailure } from "./upstream-host-health";
|
|
21
21
|
import type { CodexThreadLineage } from "./lineage";
|
|
22
|
+
import {
|
|
23
|
+
failureWindowSteersNewThreads,
|
|
24
|
+
forgetCodexFailureWindow,
|
|
25
|
+
forgetCodexFailureWindows,
|
|
26
|
+
noteCodexFailureWindowSample,
|
|
27
|
+
settleCodexSuccessStreak,
|
|
28
|
+
} from "./routing/failure-window";
|
|
22
29
|
|
|
23
30
|
import { isCodexPoolRefreshCooling } from "./pool-refresh-backoff";
|
|
24
31
|
import {
|
|
@@ -238,6 +245,7 @@ export function clearCodexUpstreamHealth(): void {
|
|
|
238
245
|
clearAllManualPreferences();
|
|
239
246
|
clearIdleWindowSteering();
|
|
240
247
|
clearUpstreamHealthState();
|
|
248
|
+
forgetCodexFailureWindows();
|
|
241
249
|
forgetRuntimeActiveCodexAccount();
|
|
242
250
|
// The reconcile watermark is part of this state, not something that outlives it. Keeping
|
|
243
251
|
// it across a full reset is incoherent: there is no health left to protect, yet
|
|
@@ -249,6 +257,7 @@ export function clearCodexUpstreamHealth(): void {
|
|
|
249
257
|
|
|
250
258
|
export function clearCodexUpstreamHealthForAccount(accountId: string): void {
|
|
251
259
|
deleteAllHealthForAccount(accountId);
|
|
260
|
+
forgetCodexFailureWindow(accountId);
|
|
252
261
|
// Deletion is the third operator exit, next to pause and exclusion, and it is the one
|
|
253
262
|
// with no reconcile path behind it: once the account is gone nothing can succeed on it,
|
|
254
263
|
// so an unspent preference naming it would suppress the automatic cursor for every other
|
|
@@ -829,7 +838,7 @@ export function previewCodexAccountForRequest(
|
|
|
829
838
|
active = pickLowerUsageAccount(config, active, usage, now, quotaScope, selectionOptions);
|
|
830
839
|
}
|
|
831
840
|
}
|
|
832
|
-
if (shouldFailover(config, active, now)) {
|
|
841
|
+
if (shouldFailover(config, active, now) || failureWindowSteersNewThreads(config, active, now)) {
|
|
833
842
|
const best = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
|
834
843
|
if (best) active = best;
|
|
835
844
|
}
|
|
@@ -1296,6 +1305,7 @@ export function recordCodexUpstreamOutcome(
|
|
|
1296
1305
|
*/
|
|
1297
1306
|
dropSpentCredentialFailure(accountId);
|
|
1298
1307
|
if (outcomeClass === "success") {
|
|
1308
|
+
noteCodexFailureWindowSample(config, accountId, false, now);
|
|
1299
1309
|
// The operator's one-shot is spent by a dispatch that actually worked, and only by that.
|
|
1300
1310
|
// A failed lookup leaves it unspent so the intent survives the failure.
|
|
1301
1311
|
consumeManualPreference(accountId, codexPoolKeyForScope(quotaScope));
|
|
@@ -1336,22 +1346,7 @@ export function recordCodexUpstreamOutcome(
|
|
|
1336
1346
|
// Non-owners keep every hard-cooldown field, including someone else's live lease.
|
|
1337
1347
|
const base = ownsProbeLease(current, meta) ? withProbeLeaseReleased(current!, now) : current;
|
|
1338
1348
|
const preserved = preservedCooldownFields(base);
|
|
1339
|
-
|
|
1340
|
-
if (failoverEnabled && current && current.consecutiveFailures >= 2) {
|
|
1341
|
-
const consecutiveSuccesses = (current.consecutiveSuccesses ?? 0) + 1;
|
|
1342
|
-
if (consecutiveSuccesses < 2) {
|
|
1343
|
-
setAccountHealth(accountId, {
|
|
1344
|
-
...base!,
|
|
1345
|
-
...preserved,
|
|
1346
|
-
consecutiveSuccesses,
|
|
1347
|
-
});
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
// Level 1 clears immediately; escalated accounts need two consecutive healthy terminals.
|
|
1352
|
-
// Hard quota cooldown intentionally survives either recovery path.
|
|
1353
|
-
if (cooldownUntil) setAccountHealth(accountId, { consecutiveFailures: 0, ...preserved });
|
|
1354
|
-
else deleteAccountHealth(accountId);
|
|
1349
|
+
settleCodexSuccessStreak(config, accountId, current, base, preserved, cooldownUntil);
|
|
1355
1350
|
return;
|
|
1356
1351
|
}
|
|
1357
1352
|
if (outcomeClass === "caller") {
|
|
@@ -1560,6 +1555,7 @@ export function recordCodexUpstreamOutcome(
|
|
|
1560
1555
|
// A transient failure concludes an owning probe; an unrelated 5xx must not
|
|
1561
1556
|
// consume someone else's live lease or drop hard-cooldown bookkeeping (#433).
|
|
1562
1557
|
const transientBase = ownsProbeLease(current, meta) ? withProbeLeaseReleased(current!, now) : current;
|
|
1558
|
+
noteCodexFailureWindowSample(config, accountId, true, now);
|
|
1563
1559
|
const stale = current?.lastFailureAt ? now - current.lastFailureAt > CODEX_FAILURE_WINDOW_MS : false;
|
|
1564
1560
|
const hardCooldownUntil = getCodexAccountCooldownUntil(accountId, now) ?? undefined;
|
|
1565
1561
|
// Soft avoid + affinity clears are part of failover. When threshold is 0, leave
|
package/src/codex/runtime.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { codexExecInvocation, isSpawnableCodexCandidate } from "./exec-invocatio
|
|
|
7
7
|
import { resolveCodexHomeDir } from "./home";
|
|
8
8
|
import { decodeOverlayState } from "./shim-state-file";
|
|
9
9
|
import { redactSecretString, redactUserPath } from "../lib/redact";
|
|
10
|
+
import { SUBPROCESS_KILL_GRACE_MS, waitForSubprocessExit } from "../lib/bounded-subprocess";
|
|
10
11
|
|
|
11
12
|
export type CodexRuntimeSource =
|
|
12
13
|
| "environment"
|
|
@@ -68,6 +69,8 @@ export type RuntimeExecFile = (
|
|
|
68
69
|
shell?: boolean;
|
|
69
70
|
windowsVerbatimArguments?: boolean;
|
|
70
71
|
env?: NodeJS.ProcessEnv;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
maxBuffer?: number;
|
|
71
74
|
},
|
|
72
75
|
) => string;
|
|
73
76
|
|
|
@@ -77,6 +80,7 @@ export type RuntimeExecFileAsync = (
|
|
|
77
80
|
) => Promise<string>;
|
|
78
81
|
|
|
79
82
|
export interface ResolveCodexRuntimeDeps {
|
|
83
|
+
signal?: AbortSignal;
|
|
80
84
|
env?: NodeJS.ProcessEnv;
|
|
81
85
|
platform?: NodeJS.Platform;
|
|
82
86
|
configDir?: string;
|
|
@@ -487,23 +491,62 @@ function probeVersion(command: string, deps: ResolveCodexRuntimeDeps): VersionPr
|
|
|
487
491
|
}
|
|
488
492
|
}
|
|
489
493
|
|
|
490
|
-
const
|
|
494
|
+
export const execCodexFileAsync: RuntimeExecFileAsync = (file, args, options) =>
|
|
491
495
|
new Promise((resolve, reject) => {
|
|
496
|
+
if (options.signal?.aborted) { reject(new Error("Codex subprocess aborted")); return; }
|
|
492
497
|
// execFile always pipes stdout/stderr; it takes no stdio option.
|
|
493
|
-
|
|
494
|
-
|
|
498
|
+
// Own abort/deadline termination; execFile's built-in observer can settle the
|
|
499
|
+
// callback while a SIGTERM-ignoring child is still alive.
|
|
500
|
+
const { stdio: _stdio, signal: _signal, timeout: _timeout, ...execOptions } = options;
|
|
501
|
+
let settled = false;
|
|
502
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
503
|
+
let abort = () => {};
|
|
504
|
+
const finish = (error: Error | null, stdout = "") => {
|
|
505
|
+
if (settled) return;
|
|
506
|
+
settled = true;
|
|
507
|
+
clearTimeout(timer);
|
|
508
|
+
options.signal?.removeEventListener("abort", abort);
|
|
509
|
+
if (error) reject(error); else resolve(stdout);
|
|
510
|
+
};
|
|
511
|
+
const child = execFile(file, args, execOptions, (error, stdout) => {
|
|
512
|
+
if (error && child.exitCode === null && child.signalCode === null) stop(error);
|
|
513
|
+
else finish(error, String(stdout));
|
|
514
|
+
});
|
|
515
|
+
const exited = new Promise<number>(resolveExit => {
|
|
516
|
+
child.once("exit", code => resolveExit(code ?? 1));
|
|
517
|
+
// A kill error is not exit proof; only failed spawn has no child to reap.
|
|
518
|
+
child.once("error", () => { if (!child.pid) resolveExit(1); });
|
|
519
|
+
});
|
|
520
|
+
// A reaper owns actual exit independently of caller-facing promise settlement.
|
|
521
|
+
let stopping = false;
|
|
522
|
+
const stop = (error: Error) => {
|
|
523
|
+
if (stopping) return;
|
|
524
|
+
stopping = true;
|
|
525
|
+
try { child.kill(); } catch { /* exit/error observation still owns reaping */ }
|
|
526
|
+
child.stdout?.destroy();
|
|
527
|
+
child.stderr?.destroy();
|
|
528
|
+
void waitForSubprocessExit({ exited, kill: () => child.kill("SIGKILL") }, SUBPROCESS_KILL_GRACE_MS);
|
|
529
|
+
finish(error);
|
|
530
|
+
};
|
|
531
|
+
abort = () => stop(new Error("Codex subprocess aborted"));
|
|
532
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
533
|
+
if (options.timeout > 0) {
|
|
534
|
+
timer = setTimeout(() => stop(new Error("Codex subprocess deadline exceeded")), options.timeout);
|
|
535
|
+
timer.unref?.();
|
|
536
|
+
}
|
|
537
|
+
if (options.signal?.aborted) abort();
|
|
495
538
|
});
|
|
496
539
|
|
|
497
540
|
/** probeVersion with the same sandbox and outcomes, but the exec never holds the event loop. */
|
|
498
541
|
async function probeVersionAsync(command: string, deps: ResolveCodexRuntimeDeps): Promise<VersionProbeResult> {
|
|
499
542
|
const settled = versionProbePrecheck(command, deps);
|
|
500
543
|
if (settled) return settled;
|
|
501
|
-
const exec = deps.execFile ??
|
|
544
|
+
const exec = deps.execFile ?? execCodexFileAsync;
|
|
502
545
|
let probeHome: string | undefined;
|
|
503
546
|
try {
|
|
504
547
|
probeHome = mkdtempSync(join(tmpdir(), "ocx-codex-probe-"));
|
|
505
548
|
const { file, args, options } = versionProbeInvocation(command, deps, probeHome);
|
|
506
|
-
return versionProbeOutput(await exec(file, args, options));
|
|
549
|
+
return versionProbeOutput(await exec(file, args, { ...options, signal: deps.signal }));
|
|
507
550
|
} catch (error) {
|
|
508
551
|
return versionProbeError(error, probeHome);
|
|
509
552
|
} finally {
|
|
@@ -782,9 +825,7 @@ function persistedRuntimeCacheStamp(deps: ResolveCodexRuntimeDeps): string {
|
|
|
782
825
|
const configDir = deps.configDir ?? getConfigDir();
|
|
783
826
|
const read = deps.readFileSync ?? ((path, encoding) => readFileSync(path, encoding));
|
|
784
827
|
try {
|
|
785
|
-
|
|
786
|
-
if (!raw) return "";
|
|
787
|
-
return `${raw.command}|${raw.selectedVersion ?? ""}|${raw.updatedAt ?? ""}`;
|
|
828
|
+
return read(codexRuntimeStatePath(configDir), "utf8");
|
|
788
829
|
} catch {
|
|
789
830
|
return "";
|
|
790
831
|
}
|
|
@@ -793,7 +834,8 @@ function persistedRuntimeCacheStamp(deps: ResolveCodexRuntimeDeps): string {
|
|
|
793
834
|
function resolveCacheKey(deps: ResolveCodexRuntimeDeps): string | null {
|
|
794
835
|
// Only memoize uninjected process-env resolves (settings/status hot paths).
|
|
795
836
|
if (
|
|
796
|
-
deps.
|
|
837
|
+
deps.signal
|
|
838
|
+
|| deps.execFileSync
|
|
797
839
|
|| deps.execFile
|
|
798
840
|
|| deps.existsSync
|
|
799
841
|
|| deps.readFileSync
|
|
@@ -804,8 +846,14 @@ function resolveCacheKey(deps: ResolveCodexRuntimeDeps): string | null {
|
|
|
804
846
|
) {
|
|
805
847
|
return null;
|
|
806
848
|
}
|
|
849
|
+
return codexRuntimeSelectionIdentity(deps);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Exec-free input identity, including pin changes made by another process. */
|
|
853
|
+
export function codexRuntimeSelectionIdentity(deps: ResolveCodexRuntimeDeps = {}): string {
|
|
807
854
|
const env = deps.env ?? process.env;
|
|
808
855
|
return JSON.stringify({
|
|
856
|
+
configDir: deps.configDir ?? getConfigDir(),
|
|
809
857
|
cli: env.CODEX_CLI_PATH ?? "",
|
|
810
858
|
path: env.PATH ?? "",
|
|
811
859
|
platform: deps.platform ?? process.platform,
|
|
@@ -859,7 +907,12 @@ export function resolveCodexRuntime(deps: ResolveCodexRuntimeDeps = {}): Resolve
|
|
|
859
907
|
return cloneAndDeepFreeze(result);
|
|
860
908
|
}
|
|
861
909
|
|
|
862
|
-
|
|
910
|
+
interface AsyncRuntimeObservation {
|
|
911
|
+
readonly result: ResolveCodexRuntimeResult;
|
|
912
|
+
readonly inputs: string;
|
|
913
|
+
readonly epoch: number | null;
|
|
914
|
+
}
|
|
915
|
+
let asyncResolveInflight: { key: string; epoch: number; promise: Promise<AsyncRuntimeObservation> } | null = null;
|
|
863
916
|
|
|
864
917
|
/**
|
|
865
918
|
* resolveCodexRuntime for server request paths: same selection and process memo, but every
|
|
@@ -869,16 +922,29 @@ let asyncResolveInflight: { key: string; epoch: number; promise: Promise<Resolve
|
|
|
869
922
|
export async function resolveCodexRuntimeAsync(
|
|
870
923
|
deps: ResolveCodexRuntimeDeps = {},
|
|
871
924
|
): Promise<ResolveCodexRuntimeResult> {
|
|
925
|
+
return cloneAndDeepFreeze((await resolveCodexRuntimeObservationAsync(deps)).result);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function resolveCodexRuntimeObservationAsync(deps: ResolveCodexRuntimeDeps): Promise<AsyncRuntimeObservation> {
|
|
929
|
+
const inputs = codexRuntimeSelectionIdentity(deps);
|
|
930
|
+
const observed = (result: ResolveCodexRuntimeResult, epoch: number | null): AsyncRuntimeObservation => ({ result, inputs, epoch });
|
|
872
931
|
// A deferred selection never execs, so the sync path is already nonblocking.
|
|
873
|
-
if (deps.probeVersion === false) return resolveCodexRuntime(deps);
|
|
874
|
-
const cacheKey = resolveCacheKey(deps);
|
|
932
|
+
if (deps.probeVersion === false) return observed(resolveCodexRuntime(deps), resolveCacheEpoch);
|
|
933
|
+
const cacheKey = resolveCacheKey({ ...deps, signal: undefined });
|
|
875
934
|
if (cacheKey && resolveCache && resolveCache.key === cacheKey && Date.now() - resolveCache.at < RESOLVE_CACHE_MS) {
|
|
876
|
-
return cloneAndDeepFreeze(resolveCache.value);
|
|
935
|
+
return observed(cloneAndDeepFreeze(resolveCache.value), resolveCacheEpoch);
|
|
877
936
|
}
|
|
878
|
-
if (!cacheKey) return cloneAndDeepFreeze(await resolveCodexRuntimeUncachedAsync(deps));
|
|
879
937
|
const startedEpoch = resolveCacheEpoch;
|
|
938
|
+
// A cancellable miss owns its probes; a warm memo remains usable without a new child.
|
|
939
|
+
if (!cacheKey || deps.signal) {
|
|
940
|
+
const result = await resolveCodexRuntimeUncachedAsync(deps);
|
|
941
|
+
const current = !deps.signal?.aborted && resolveCacheEpoch === startedEpoch
|
|
942
|
+
&& codexRuntimeSelectionIdentity(deps) === inputs;
|
|
943
|
+
if (current && cacheKey) publishResolveCache(cacheKey, Date.now(), result);
|
|
944
|
+
return observed(result, current ? resolveCacheEpoch : null);
|
|
945
|
+
}
|
|
880
946
|
if (asyncResolveInflight?.key === cacheKey && asyncResolveInflight.epoch === startedEpoch) {
|
|
881
|
-
return
|
|
947
|
+
return asyncResolveInflight.promise;
|
|
882
948
|
}
|
|
883
949
|
const promise = resolveCodexRuntimeUncachedAsync(deps).then(result => {
|
|
884
950
|
// A persist or clear while the probes ran makes this answer the previous selection's;
|
|
@@ -886,13 +952,14 @@ export async function resolveCodexRuntimeAsync(
|
|
|
886
952
|
// rewrite codex-runtime.json without touching this epoch, so the key is read again too.
|
|
887
953
|
if (resolveCacheEpoch === startedEpoch && resolveCacheKey(deps) === cacheKey) {
|
|
888
954
|
publishResolveCache(cacheKey, Date.now(), result);
|
|
955
|
+
return observed(result, resolveCacheEpoch);
|
|
889
956
|
}
|
|
890
|
-
return result;
|
|
957
|
+
return observed(result, null);
|
|
891
958
|
}).finally(() => {
|
|
892
959
|
if (asyncResolveInflight?.promise === promise) asyncResolveInflight = null;
|
|
893
960
|
});
|
|
894
961
|
asyncResolveInflight = { key: cacheKey, epoch: startedEpoch, promise };
|
|
895
|
-
return
|
|
962
|
+
return promise;
|
|
896
963
|
}
|
|
897
964
|
|
|
898
965
|
/**
|
|
@@ -1087,7 +1154,10 @@ function resolveCodexRuntimeUncached(deps: ResolveCodexRuntimeDeps = {}): Resolv
|
|
|
1087
1154
|
async function resolveCodexRuntimeUncachedAsync(deps: ResolveCodexRuntimeDeps): Promise<ResolveCodexRuntimeResult> {
|
|
1088
1155
|
const steps = resolveCodexRuntimeSteps(deps);
|
|
1089
1156
|
let step = steps.next();
|
|
1090
|
-
while (!step.done)
|
|
1157
|
+
while (!step.done) {
|
|
1158
|
+
if (deps.signal?.aborted) throw new Error("Codex runtime resolution aborted");
|
|
1159
|
+
step = steps.next(await probeVersionAsync(step.value.command, deps));
|
|
1160
|
+
}
|
|
1091
1161
|
return step.value;
|
|
1092
1162
|
}
|
|
1093
1163
|
|
|
@@ -1096,6 +1166,24 @@ export function resolveAndPersistCodexRuntime(
|
|
|
1096
1166
|
deps: ResolveCodexRuntimeDeps = {},
|
|
1097
1167
|
): ResolveCodexRuntimeResult {
|
|
1098
1168
|
const result = resolveCodexRuntime(deps);
|
|
1169
|
+
return persistResolvedCodexRuntime(result, deps);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** Same selection/persistence as the CLI driver, with stale async results rejected. */
|
|
1173
|
+
export async function resolveAndPersistCodexRuntimeAsync(
|
|
1174
|
+
deps: ResolveCodexRuntimeDeps = {},
|
|
1175
|
+
isCurrent: () => boolean = () => true,
|
|
1176
|
+
): Promise<ResolveCodexRuntimeResult | null> {
|
|
1177
|
+
const observation = await resolveCodexRuntimeObservationAsync(deps);
|
|
1178
|
+
if (!isCurrent() || observation.epoch === null || observation.epoch !== resolveCacheEpoch
|
|
1179
|
+
|| observation.inputs !== codexRuntimeSelectionIdentity(deps)) return null;
|
|
1180
|
+
return cloneAndDeepFreeze(persistResolvedCodexRuntime(observation.result, deps));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function persistResolvedCodexRuntime(
|
|
1184
|
+
result: ResolveCodexRuntimeResult,
|
|
1185
|
+
deps: ResolveCodexRuntimeDeps,
|
|
1186
|
+
): ResolveCodexRuntimeResult {
|
|
1099
1187
|
// Only WRITE when the selection actually changed. persistCodexRuntime() clears the
|
|
1100
1188
|
// in-process resolve memo and persistedRuntimeCacheStamp() folds `updatedAt` into the
|
|
1101
1189
|
// cache key, so an unconditional rewrite made every caller re-run the ~1s
|
package/src/codex/shim.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
|
|
12
|
-
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
12
|
+
import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
|
|
13
|
+
import { assertSelectedRuntimeWritable, RuntimePreflightError, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
|
|
14
|
+
import { getConfigDir } from "../config";
|
|
13
15
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
14
16
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
15
17
|
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
@@ -83,7 +85,9 @@ export function lastCodexDiscoveryError(): string | null {
|
|
|
83
85
|
return lastShimDiscoveryError;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
interface InstallCodexShimInternalOptions {
|
|
88
|
+
interface InstallCodexShimInternalOptions extends RuntimePreflightDeps {
|
|
89
|
+
runtime?: DurableBunRuntime;
|
|
90
|
+
runtimeAdmitted?: boolean;
|
|
87
91
|
expectedReplacements?: ReadonlyMap<string, ShimPathFingerprint>;
|
|
88
92
|
allowFreshInstall: boolean;
|
|
89
93
|
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
@@ -105,12 +109,11 @@ export type CodexShimAutoRestoreResult =
|
|
|
105
109
|
| { status: "ineligible" | "deferred"; message?: string }
|
|
106
110
|
| { status: "restored"; message: string };
|
|
107
111
|
|
|
108
|
-
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
112
|
+
function cliEntry(runtime: DurableBunRuntime = durableBunRuntime()): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
109
113
|
// Bundled Bun path (survives `ocx update`); all three shim builders
|
|
110
114
|
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
|
|
111
115
|
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
|
|
112
116
|
// Path and provenance resolve together so the marker always describes this binary.
|
|
113
|
-
const runtime = durableBunRuntime();
|
|
114
117
|
return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "..", "cli", "index.ts") };
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -345,8 +348,8 @@ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry
|
|
|
345
348
|
* updater's wrapper can carry them, and a replacement landing between the write
|
|
346
349
|
* and the observation is otherwise indistinguishable from our own file.
|
|
347
350
|
*/
|
|
348
|
-
function writeShim(wrapperPath: string, realCodexPath: string): { dev: number; ino: number } | undefined {
|
|
349
|
-
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
351
|
+
function writeShim(wrapperPath: string, realCodexPath: string, runtime?: DurableBunRuntime): { dev: number; ino: number } | undefined {
|
|
352
|
+
const { bun, bunRuntimeSource, cli } = cliEntry(runtime);
|
|
350
353
|
if (process.platform === "win32") {
|
|
351
354
|
const lower = wrapperPath.toLowerCase();
|
|
352
355
|
if (lower.endsWith(".ps1")) {
|
|
@@ -458,7 +461,7 @@ function replaceOwnedBackup(sourcePath: string, backupPath: string): void {
|
|
|
458
461
|
}
|
|
459
462
|
}
|
|
460
463
|
|
|
461
|
-
function refreshShimFile(file: ShimFileState): boolean {
|
|
464
|
+
function refreshShimFile(file: ShimFileState, runtime: DurableBunRuntime): boolean {
|
|
462
465
|
if (file.preserveOnly) {
|
|
463
466
|
if (existsSync(file.originalPath) && !isShim(file.originalPath)) {
|
|
464
467
|
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
@@ -474,10 +477,10 @@ function refreshShimFile(file: ShimFileState): boolean {
|
|
|
474
477
|
file,
|
|
475
478
|
expectedReplacement: replacement.fingerprint,
|
|
476
479
|
sourcePath: file.wrapperPath,
|
|
477
|
-
}]);
|
|
480
|
+
}], undefined, undefined, runtime);
|
|
478
481
|
}
|
|
479
482
|
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
480
|
-
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
483
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath, runtime);
|
|
481
484
|
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
482
485
|
if (!writtenWrapper || !writtenWrapper.prefix.includes(SHIM_MARKER)) {
|
|
483
486
|
return false;
|
|
@@ -503,7 +506,7 @@ function refreshShimFile(file: ShimFileState): boolean {
|
|
|
503
506
|
}
|
|
504
507
|
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
505
508
|
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
506
|
-
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
509
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath, runtime);
|
|
507
510
|
return true;
|
|
508
511
|
}
|
|
509
512
|
return false;
|
|
@@ -610,6 +613,7 @@ function applyGuardedRefreshTransaction(
|
|
|
610
613
|
operations: readonly GuardedRefreshOperation[],
|
|
611
614
|
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void,
|
|
612
615
|
commitState?: () => void,
|
|
616
|
+
runtime?: DurableBunRuntime,
|
|
613
617
|
): boolean {
|
|
614
618
|
const journal: GuardedRefreshJournalEntry[] = [];
|
|
615
619
|
let applyError: Error | null = null;
|
|
@@ -643,7 +647,7 @@ function applyGuardedRefreshTransaction(
|
|
|
643
647
|
if (!movedReplacement) throw new Error("Codex shim guarded refresh could not fingerprint the staged launcher");
|
|
644
648
|
entry.movedReplacementFingerprint = movedReplacement.fingerprint;
|
|
645
649
|
entry.wrapperWriteStarted = true;
|
|
646
|
-
const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
650
|
+
const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath, runtime);
|
|
647
651
|
// Claim our own partial write before the hook can fail (see fresh install).
|
|
648
652
|
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
649
653
|
codexShimGuardedWriteHookForTests?.();
|
|
@@ -747,7 +751,7 @@ type ObsoleteUnixShimRefreshResult =
|
|
|
747
751
|
| { installed: true; message: string }
|
|
748
752
|
| { installed: false; deferred: boolean; message: string };
|
|
749
753
|
|
|
750
|
-
function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnixShimRefreshResult {
|
|
754
|
+
function refreshObsoleteUnixShims(files: readonly ShimFileState[], runtime?: DurableBunRuntime): ObsoleteUnixShimRefreshResult {
|
|
751
755
|
if (process.platform === "win32") {
|
|
752
756
|
return { installed: false, deferred: false, message: "Codex autostart shim is already current." };
|
|
753
757
|
}
|
|
@@ -786,7 +790,7 @@ function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnix
|
|
|
786
790
|
throw new Error("Codex autostart shim upgrade could not fingerprint the staged wrapper");
|
|
787
791
|
}
|
|
788
792
|
entry.wrapperWriteStarted = true;
|
|
789
|
-
const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
793
|
+
const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath, runtime);
|
|
790
794
|
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
791
795
|
if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) {
|
|
792
796
|
throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper");
|
|
@@ -876,6 +880,19 @@ function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnix
|
|
|
876
880
|
|
|
877
881
|
function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
|
|
878
882
|
const existing = readState();
|
|
883
|
+
const filesBefore = existing ? stateFiles(existing) : [];
|
|
884
|
+
if (filesBefore.length > 0 && filesBefore.every(file => {
|
|
885
|
+
if (file.preserveOnly) return existsSync(file.backupPath) && !existsSync(file.originalPath);
|
|
886
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
887
|
+
return hasUsableBackingPath(file) && probe !== null && isHealthyShimProbe(probe, process.platform);
|
|
888
|
+
})) {
|
|
889
|
+
return { installed: false, message: "Codex autostart shim is already installed." };
|
|
890
|
+
}
|
|
891
|
+
const runtime = options.runtime ?? Object.freeze({ ...(options.selectRuntime ?? durableBunRuntime)() });
|
|
892
|
+
if (!options.runtimeAdmitted) {
|
|
893
|
+
const configDir = (options.configDir ?? getConfigDir)();
|
|
894
|
+
(options.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { rootWasAbsent: !existsSync(configDir) });
|
|
895
|
+
}
|
|
879
896
|
if (existing) {
|
|
880
897
|
const files = stateFiles(existing);
|
|
881
898
|
if (!options.expectedReplacements && process.platform !== "win32") {
|
|
@@ -907,7 +924,7 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
907
924
|
throw writeError;
|
|
908
925
|
}
|
|
909
926
|
};
|
|
910
|
-
if (!applyGuardedRefreshTransaction(operations, options.beforeGuardedRefresh, commitState)) {
|
|
927
|
+
if (!applyGuardedRefreshTransaction(operations, options.beforeGuardedRefresh, commitState, runtime)) {
|
|
911
928
|
return { installed: false, message: "Codex shim auto-restore deferred because tracked launchers changed." };
|
|
912
929
|
}
|
|
913
930
|
return {
|
|
@@ -916,7 +933,7 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
916
933
|
};
|
|
917
934
|
}
|
|
918
935
|
let refreshed = false;
|
|
919
|
-
for (const file of files) refreshed = refreshShimFile(file) || refreshed;
|
|
936
|
+
for (const file of files) refreshed = refreshShimFile(file, runtime) || refreshed;
|
|
920
937
|
const allInstalled = files.every(file => file.preserveOnly
|
|
921
938
|
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
922
939
|
: existsSync(file.wrapperPath)
|
|
@@ -988,7 +1005,7 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
988
1005
|
}
|
|
989
1006
|
if (!target.preserveOnly) {
|
|
990
1007
|
entry.wrapperWriteStarted = true;
|
|
991
|
-
const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
|
|
1008
|
+
const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath, runtime);
|
|
992
1009
|
entry.writtenWrapperInode = writtenInode;
|
|
993
1010
|
codexShimFreshWriteHookForTests?.();
|
|
994
1011
|
if (process.platform !== "win32") {
|
|
@@ -1061,12 +1078,12 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1061
1078
|
};
|
|
1062
1079
|
}
|
|
1063
1080
|
|
|
1064
|
-
export function installCodexShim(): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
|
|
1081
|
+
export function installCodexShim(deps: RuntimePreflightDeps = {}): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
|
|
1065
1082
|
if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath);
|
|
1066
|
-
return installCodexShimInternal({ allowFreshInstall: true });
|
|
1083
|
+
return installCodexShimInternal({ ...deps, allowFreshInstall: true });
|
|
1067
1084
|
}
|
|
1068
1085
|
|
|
1069
|
-
export function autoRestoreCodexShim(options: {
|
|
1086
|
+
export function autoRestoreCodexShim(options: RuntimePreflightDeps & {
|
|
1070
1087
|
enabled: () => boolean;
|
|
1071
1088
|
stabilitySleep?: (ms: number) => void;
|
|
1072
1089
|
/** Narrow deterministic seam used to hold the interprocess lock in tests. */
|
|
@@ -1133,6 +1150,14 @@ export function autoRestoreCodexShim(options: {
|
|
|
1133
1150
|
};
|
|
1134
1151
|
}
|
|
1135
1152
|
|
|
1153
|
+
const runtime = Object.freeze({ ...(options.selectRuntime ?? durableBunRuntime)() });
|
|
1154
|
+
try {
|
|
1155
|
+
const configDir = (options.configDir ?? getConfigDir)();
|
|
1156
|
+
(options.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { rootWasAbsent: !existsSync(configDir) });
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
if (error instanceof RuntimePreflightError) return { status: "deferred", message: error.message };
|
|
1159
|
+
throw error;
|
|
1160
|
+
}
|
|
1136
1161
|
const lock = tryAcquireShimRestoreLock(options.beforeStaleRestoreLockDelete);
|
|
1137
1162
|
if (!lock) return { status: "deferred" };
|
|
1138
1163
|
try {
|
|
@@ -1159,6 +1184,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1159
1184
|
expectedReplacements.set(path, secondProbe.fingerprint);
|
|
1160
1185
|
}
|
|
1161
1186
|
const result = installCodexShimInternal({
|
|
1187
|
+
runtime, runtimeAdmitted: true,
|
|
1162
1188
|
allowFreshInstall: false,
|
|
1163
1189
|
expectedReplacements,
|
|
1164
1190
|
beforeGuardedRefresh: options.beforeGuardedRefresh,
|