@bitkyc08/opencodex 2.81.0 → 2.82.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.
Files changed (212) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  11. package/src/adapters/openai-responses/passthrough.ts +9 -4
  12. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  13. package/src/claude/agents-inject.ts +14 -3
  14. package/src/claude/context-windows.ts +19 -17
  15. package/src/claude/desktop-3p.ts +88 -12
  16. package/src/claude/desktop-profile.ts +2 -1
  17. package/src/claude/gateway-cache.ts +18 -1
  18. package/src/claude/inbound.ts +77 -3
  19. package/src/claude/intercept/cli-catalog.ts +3 -0
  20. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  21. package/src/claude/intercept/picker-models.ts +14 -6
  22. package/src/claude/intercept/settings.ts +3 -0
  23. package/src/claude/long-context.ts +66 -0
  24. package/src/claude/model-info.ts +19 -16
  25. package/src/claude/outbound.ts +94 -9
  26. package/src/claude/subagent-model.ts +40 -11
  27. package/src/cli/agent.ts +9 -5
  28. package/src/cli/aside-profile-recovery.ts +33 -0
  29. package/src/cli/capabilities-base.ts +5 -5
  30. package/src/cli/capabilities-integrations.ts +2 -2
  31. package/src/cli/cli-path-diagnostics.ts +132 -0
  32. package/src/cli/desktop-runtime-guidance.ts +65 -0
  33. package/src/cli/dispatch.ts +5 -0
  34. package/src/cli/doctor.ts +16 -3
  35. package/src/cli/index.ts +14 -9
  36. package/src/cli/integration-aside-sync.ts +2 -1
  37. package/src/cli/integrations.ts +16 -3
  38. package/src/cli/resolve.ts +25 -2
  39. package/src/cli/root.ts +4 -2
  40. package/src/cli/status.ts +64 -12
  41. package/src/cli/update-restart-child.ts +4 -2
  42. package/src/cli/update-restart-home.ts +28 -10
  43. package/src/cli/update-restart-service-record.ts +116 -0
  44. package/src/cli/update-restart-supervision.ts +96 -0
  45. package/src/cli/update-restart.ts +59 -13
  46. package/src/cli/v2.ts +27 -20
  47. package/src/cli/version-skew-notice.ts +77 -0
  48. package/src/clients/lazycodex.ts +1 -1
  49. package/src/clients/omo-role-models.ts +32 -7
  50. package/src/codex/auth-api/account-selection.ts +107 -0
  51. package/src/codex/auth-api/routes.ts +10 -49
  52. package/src/codex/auth-context.ts +20 -1
  53. package/src/codex/autostart-health.ts +12 -3
  54. package/src/codex/catalog/build-entries.ts +18 -0
  55. package/src/codex/catalog/bundled.ts +155 -26
  56. package/src/codex/catalog/combo-member.ts +3 -2
  57. package/src/codex/catalog/effort.ts +26 -5
  58. package/src/codex/catalog/gather-capture.ts +5 -0
  59. package/src/codex/catalog/model-hints.ts +15 -8
  60. package/src/codex/catalog/model-visibility.ts +3 -1
  61. package/src/codex/catalog/operator-tiers.ts +38 -0
  62. package/src/codex/catalog/provider-models.ts +43 -35
  63. package/src/codex/catalog/retained-sync.ts +7 -5
  64. package/src/codex/catalog/routed-gather.ts +15 -1
  65. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  66. package/src/codex/catalog-auto-refresh.ts +19 -0
  67. package/src/codex/catalog.ts +1 -1
  68. package/src/codex/codex-home-owner.ts +3 -0
  69. package/src/codex/config-write-lock.ts +333 -0
  70. package/src/codex/convergence.ts +2 -1
  71. package/src/codex/features.ts +257 -200
  72. package/src/codex/inject/config-write-section.ts +34 -0
  73. package/src/codex/inject/multi-agent-v2.ts +14 -6
  74. package/src/codex/inject/remove.ts +36 -11
  75. package/src/codex/inject/restore.ts +85 -20
  76. package/src/codex/inject-coordination.ts +15 -6
  77. package/src/codex/inject.ts +62 -20
  78. package/src/codex/journal.ts +62 -17
  79. package/src/codex/main-account-cache.ts +43 -0
  80. package/src/codex/paths.ts +2 -0
  81. package/src/codex/prepared-home.ts +18 -0
  82. package/src/codex/prompt-journal.ts +25 -12
  83. package/src/codex/prompt-layers.ts +49 -26
  84. package/src/codex/prompt-lock-claim.ts +134 -0
  85. package/src/codex/prompt-lock-evidence.ts +26 -0
  86. package/src/codex/prompt-lock-io.ts +16 -0
  87. package/src/codex/prompt-lock-owner.ts +130 -0
  88. package/src/codex/prompt-lock.ts +84 -53
  89. package/src/codex/routing/failure-window.ts +146 -0
  90. package/src/codex/routing/selection.ts +4 -1
  91. package/src/codex/routing.ts +13 -17
  92. package/src/codex/runtime.ts +106 -18
  93. package/src/codex/shim.ts +45 -19
  94. package/src/combos/jev-service-exchange.ts +281 -0
  95. package/src/combos/jev.ts +34 -220
  96. package/src/config/atomic-write.ts +1 -1
  97. package/src/config/codex-ws-reuse-setting.ts +10 -0
  98. package/src/config/live-reconcile.ts +17 -0
  99. package/src/config/load-degrade.ts +8 -0
  100. package/src/config/rebase-provenance.ts +11 -0
  101. package/src/config/schema/config-schema.ts +4 -0
  102. package/src/generated/compatibility-version.json +303 -171
  103. package/src/generated/model-metadata.ts +2 -2
  104. package/src/grok/inject.ts +50 -4
  105. package/src/grok/sync.ts +2 -1
  106. package/src/images/loop.ts +29 -11
  107. package/src/integrations/aside-profiles.ts +10 -3
  108. package/src/integrations/catalog-refresh.ts +18 -2
  109. package/src/integrations/writer.ts +9 -3
  110. package/src/lib/bun-path-runtime.d.mts +32 -0
  111. package/src/lib/bun-path-runtime.mjs +95 -0
  112. package/src/lib/bun-runtime-preflight.ts +140 -0
  113. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  114. package/src/lib/desktop-cli-handoff.mjs +92 -0
  115. package/src/lib/desktop-cli-record.d.mts +29 -0
  116. package/src/lib/desktop-cli-record.mjs +102 -0
  117. package/src/lib/request-execution-budget.ts +8 -0
  118. package/src/lib/synced-state-location.ts +46 -6
  119. package/src/lib/test-home-guard.ts +87 -1
  120. package/src/lib/translator-budget.ts +17 -4
  121. package/src/lib/windows-atomic-replace.ts +38 -16
  122. package/src/lib/windows-elevation.ts +6 -0
  123. package/src/lib/windows-secret-acl.ts +92 -25
  124. package/src/lib/winsw.ts +2 -3
  125. package/src/oauth/anthropic-account-refusal.ts +39 -1
  126. package/src/oauth/anthropic-routing.ts +1 -1
  127. package/src/oauth/anthropic-send-ownership.ts +3 -3
  128. package/src/oauth/chatgpt.ts +113 -41
  129. package/src/oauth/index.ts +6 -2
  130. package/src/oauth/store.ts +1 -1
  131. package/src/providers/azure-model-metadata.ts +115 -0
  132. package/src/providers/derive.ts +21 -0
  133. package/src/providers/quota/vendor-probes-key.ts +127 -17
  134. package/src/providers/quota.ts +1 -1
  135. package/src/responses/reasoning-envelope.ts +29 -1
  136. package/src/responses/reasoning-replay-cache.ts +17 -1
  137. package/src/responses/spill-store.ts +15 -2
  138. package/src/responses/state/metrics.ts +8 -0
  139. package/src/responses/state/spill-failure.ts +12 -0
  140. package/src/responses/state/spill-queue.ts +12 -5
  141. package/src/responses/state.ts +18 -10
  142. package/src/responses/task-input.ts +17 -0
  143. package/src/server/background-lifecycle.ts +27 -0
  144. package/src/server/chat-completions.ts +1 -0
  145. package/src/server/chat-native.ts +28 -12
  146. package/src/server/claude-messages.ts +43 -6
  147. package/src/server/inbound-body-admission.ts +1 -0
  148. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  149. package/src/server/index/serve-options.ts +10 -2
  150. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  151. package/src/server/index/startup-warnings.ts +19 -0
  152. package/src/server/index.ts +1 -0
  153. package/src/server/management/agent-settings-routes.ts +93 -32
  154. package/src/server/management/codex-agent-role-routes.ts +4 -2
  155. package/src/server/management/codex-prompt-routes.ts +1 -0
  156. package/src/server/management/codex-role-auto-assign.ts +13 -2
  157. package/src/server/management/config-routes.ts +59 -8
  158. package/src/server/management/context.ts +2 -2
  159. package/src/server/management/shared.ts +3 -3
  160. package/src/server/messages-native-oauth.ts +9 -8
  161. package/src/server/messages-native.ts +49 -13
  162. package/src/server/messages-response-headers.ts +14 -0
  163. package/src/server/request-log-conversation.ts +48 -5
  164. package/src/server/request-log.ts +31 -2
  165. package/src/server/responses/adapter-continuation.ts +2 -2
  166. package/src/server/responses/adapter-dispatch.ts +34 -13
  167. package/src/server/responses/codex-ws-exchange.ts +17 -2
  168. package/src/server/responses/codex-ws-pool.ts +100 -13
  169. package/src/server/responses/codex-ws-wire.ts +9 -0
  170. package/src/server/responses/combo-requested-effort.ts +3 -1
  171. package/src/server/responses/core-codex-account.ts +22 -1
  172. package/src/server/responses/core-combo-native.ts +20 -3
  173. package/src/server/responses/core-combo.ts +22 -14
  174. package/src/server/responses/core-lifetime.ts +3 -36
  175. package/src/server/responses/core-options.ts +5 -0
  176. package/src/server/responses/core-replay.ts +73 -0
  177. package/src/server/responses/core.ts +3 -3
  178. package/src/server/responses/fetch-helpers.ts +26 -3
  179. package/src/server/responses/passthrough-delivery.ts +24 -2
  180. package/src/server/responses/passthrough-dispatch.ts +19 -2
  181. package/src/server/responses/request-prepare.ts +4 -0
  182. package/src/server/responses/request-send-budget.ts +112 -49
  183. package/src/server/responses/response-effects.ts +1 -0
  184. package/src/server/responses/run-turn-execution.ts +2 -0
  185. package/src/server/responses/sidecar-execution.ts +25 -16
  186. package/src/server/responses/sidecar-send-budget.ts +58 -0
  187. package/src/server/responses/ws-upstream.ts +19 -6
  188. package/src/server/startup-health-cache.ts +2 -1
  189. package/src/server/workflow-refusal.ts +3 -0
  190. package/src/service/cli.ts +20 -4
  191. package/src/service/desktop-command-guard.ts +32 -0
  192. package/src/service/desktop-startup.ts +66 -40
  193. package/src/service/desktop-supervision.d.mts +26 -0
  194. package/src/service/desktop-supervision.mjs +140 -0
  195. package/src/service/managing-cli.ts +1 -0
  196. package/src/service/orchestration.ts +46 -20
  197. package/src/service/repair.ts +25 -9
  198. package/src/service/state.ts +4 -3
  199. package/src/service/systemd.ts +17 -8
  200. package/src/service/windows-ops.ts +61 -22
  201. package/src/storage/policy-job.ts +5 -1
  202. package/src/storage/policy-worker.ts +3 -1
  203. package/src/storage/policy.ts +9 -0
  204. package/src/types/config.ts +21 -0
  205. package/src/types/request.ts +5 -0
  206. package/src/types/wire.ts +1 -0
  207. package/src/update/index.ts +57 -9
  208. package/src/update/restart-ownership.ts +10 -3
  209. package/src/update/runtime-ownership.d.mts +6 -0
  210. package/src/update/runtime-ownership.mjs +23 -7
  211. package/src/web-search/loop.ts +5 -2
  212. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -37,6 +37,8 @@ import { parseStopApproval, type StopApproval } from "./stop-approval";
37
37
  import type { ResolveArgs } from "./resolve";
38
38
  import type { ClientConnectionState } from "../client/state";
39
39
  import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
40
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
41
+ import { desktopStopNotice } from "./desktop-runtime-guidance";
40
42
 
41
43
  export interface CliDispatchDeps {
42
44
  args: string[];
@@ -51,6 +53,7 @@ export interface CliDispatchDeps {
51
53
  spawnDetached: (argv: readonly string[]) => void;
52
54
  handleStart: () => Promise<void>;
53
55
  handleStop: (approval?: StopApproval) => Promise<StopOutcome>;
56
+ inspectDesktopSupervision?: typeof inspectDesktopSupervision;
54
57
  handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
55
58
  handleResolve: (args: ResolveArgs) => Promise<number>;
56
59
  handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
@@ -112,6 +115,8 @@ const commandRunners: Record<string, CommandRunner> = {
112
115
  // re-start the proxy immediately, so warning there would contradict the next line.
113
116
  const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
114
117
  if (!parsed.json) {
118
+ const notice = desktopStopNotice((deps.inspectDesktopSupervision ?? inspectDesktopSupervision)());
119
+ if (notice) console.error(notice);
115
120
  // handleStop returns the structured outcome now; an object is always truthy, so
116
121
  // the warning must key on .ok — otherwise a failed stop would still claim downtime.
117
122
  if ((await deps.handleStop()).ok) console.log(warning);
package/src/cli/doctor.ts CHANGED
@@ -7,11 +7,13 @@
7
7
  * it never sets proxy env, relocates state dirs, mutates quota, or changes
8
8
  * networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
9
9
  */
10
+ import { cliCommandDoctorChecks, collectCliPathDiagnostics } from "./cli-path-diagnostics";
10
11
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
11
12
  import { homedir } from "node:os";
12
13
  import { dirname, join } from "node:path";
13
14
  import { getConfigDir, getConfigPath, readConfigDiagnostics } from "../config";
14
15
  import { readPid } from "../config/process-state";
16
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
15
17
  import { fetchLiveStartupHealth, probeUncleanExitState, selectStatusStartupHealth } from "./status";
16
18
  import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
17
19
  import { directLocalHttpFetch } from "../server/direct-local-http";
@@ -1324,6 +1326,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1324
1326
  console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
1325
1327
  }
1326
1328
 
1329
+ console.log("\nocx command selection");
1330
+ for (const check of cliCommandDoctorChecks(collectCliPathDiagnostics())) {
1331
+ console.log(` ${check.level === "OK" ? "ok " : "!! "} ${check.message}`);
1332
+ if (check.level === "FAIL") recordDoctorFailure();
1333
+ }
1334
+
1327
1335
  // Runs without the proxy on purpose: the worst accumulation happens when the proxy will
1328
1336
  // not start, which is exactly when the in-process periodic reclaim never ticks.
1329
1337
  const reclaimTemps = args.includes(RECLAIM_RESPONSE_TEMPS_FLAG);
@@ -1379,7 +1387,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1379
1387
  configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
1380
1388
  });
1381
1389
  const liveStartup = live ? await fetchLiveStartupHealth(live) : null;
1382
- const startup = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig));
1390
+ const { startup } = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig),
1391
+ live?.pid != null ? () => inspectDesktopSupervision({ targetPid: live.pid! }) : undefined, live?.pid);
1383
1392
  console.log("\nCodex restart safety");
1384
1393
  console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
1385
1394
  console.log(` ${formatStartupRoutingDetail(startup)}`);
@@ -1685,8 +1694,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1685
1694
  const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
1686
1695
  const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
1687
1696
  if (!startup.rebootSafe) {
1688
- const command = startup.recommendedCommand ?? startup.commands.restoreNative;
1689
- hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`);
1697
+ if (startup.recommendedAction || startup.desktop?.supervisor) {
1698
+ hints.push(`Codex is pinned to the local proxy without persistent startup protection. ${startup.recommendedAction ?? "Reopen OpenCodex and check Start at Login."}`);
1699
+ } else {
1700
+ const command = startup.recommendedCommand ?? startup.commands.restoreNative;
1701
+ hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`);
1702
+ }
1690
1703
  }
1691
1704
  if (anyDrvfs) {
1692
1705
  hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability.");
package/src/cli/index.ts CHANGED
@@ -8,6 +8,8 @@ import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { currentServingCommand, deferServiceChildToNewerRuntime, markDelegatedServiceReady, recordServingRuntime } from "../config/serving-runtimes";
10
10
  import { packageVersion } from "../lib/package-version";
11
+ import { codexHomeIsAbsent } from "../codex/codex-home-owner";
12
+ import { getCodexHome } from "../codex/paths";
11
13
  import { admitUpdateRestartChild } from "./update-restart-child";
12
14
  import { UpdateRestartRequired } from "./update-restart-candidate";
13
15
  import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
@@ -75,7 +77,7 @@ import {
75
77
  pendingTeardownsAreExactly,
76
78
  quarantinePendingTeardown,
77
79
  } from "../config/pending-teardown";
78
- import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
80
+ import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, runtimeSupervisorLine, unusedProxyWarningLines } from "./status";
79
81
  import { endpointsToProve, everyEndpointProvenDownAsync, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
80
82
  import { takeFlag } from "./runtime-api";
81
83
  import { parseStartOptions, StartArgsError } from "./start-args";
@@ -85,11 +87,11 @@ import {
85
87
  recheckRestartFailedStart, reobserveRestartReplacement,
86
88
  restartStartOutcome,
87
89
  waitForProxyReplacement,
88
- runProxyRestart,
89
90
  runTrayProxyStart,
90
91
  type ProxyRestartLive,
91
92
  type ProxyRestartStartOutcome,
92
93
  } from "./tray-proxy";
94
+ import { duplicateRuntimeMessage, runDesktopAwareProxyRestart } from "./desktop-runtime-guidance";
93
95
  import { reportRestartFailure } from "./restart-failure";
94
96
  import { requestBoundSystemRestart } from "./system-restart-client";
95
97
  import { installCrashGuards } from "../lib/crash-guard";
@@ -329,7 +331,7 @@ async function chooseListenPort(
329
331
  throw new StartCommandExit(serviceStayOutExitCode());
330
332
  }
331
333
  if (decision === "refuse-live-proxy") {
332
- console.error(`⚠️ Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}). Use 'ocx stop' first.`);
334
+ console.error(duplicateRuntimeMessage(holder?.pid, preferred));
333
335
  throw new StartCommandExit(1);
334
336
  }
335
337
  if (decision === "refuse-unidentified-holder") {
@@ -444,7 +446,7 @@ async function handleStart(options: { block?: boolean } = {}) {
444
446
  process.exit(serviceStayOutExitCode());
445
447
  }
446
448
  if (decision === "refuse" || decision === "await-parent") {
447
- console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
449
+ console.error(duplicateRuntimeMessage(owner.live.pid ?? owner.pidSnapshot, owner.live.port));
448
450
  process.exit(1);
449
451
  }
450
452
  // Sibling path. The new instance takes over this home's ocx.pid / runtime-port.json while
@@ -654,10 +656,12 @@ async function handleStart(options: { block?: boolean } = {}) {
654
656
  removeRuntimePort(process.pid);
655
657
  if (teardown.restoreNativeCodex && !currentExternalCodexModelProvider()) {
656
658
  try {
657
- const restored = restoreNativeCodex();
658
- if (!restored.success) {
659
- cleanupSucceeded = false;
660
- console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
659
+ if (!codexHomeIsAbsent(getCodexHome())) {
660
+ const restored = restoreNativeCodex();
661
+ if (!restored.success) {
662
+ cleanupSucceeded = false;
663
+ console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
664
+ }
661
665
  }
662
666
  } catch (error) {
663
667
  cleanupSucceeded = false;
@@ -928,7 +932,7 @@ async function handleProxyRestart(
928
932
  startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
929
933
  ): Promise<boolean> {
930
934
  const deadlineAt = Date.now() + PROXY_RESTART_OBSERVE_MS;
931
- const result = await runProxyRestart({
935
+ const result = await runDesktopAwareProxyRestart({
932
936
  findLive: () => discoverStableProxyForRestart({
933
937
  findLive: () => findLiveProxy({ deadlineAt, attempts: 2, acceptPackageTreeFenced: true }),
934
938
  expired: () => Date.now() >= deadlineAt,
@@ -1785,6 +1789,7 @@ async function handleStatus() {
1785
1789
  console.log(` Config: ${status.json.paths.config}${local}`);
1786
1790
  console.log(` PID file: ${status.json.paths.pid}${local}`);
1787
1791
  console.log(` Runtime: ${status.json.paths.runtime}${local}`);
1792
+ if (runtimeSupervisorLine(status.json.startup)) console.log(` ${runtimeSupervisorLine(status.json.startup)}`);
1788
1793
  console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}${local}`);
1789
1794
  // On a client this is the local default, which routing does not use — the hub applies its own.
1790
1795
  console.log(` Default provider: ${status.json.defaultProvider}${local}`);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { redactUserPath } from "../lib/redact";
3
3
  import type { RefusalReason } from "../integrations/mutation-plan";
4
+ import { ASIDE_SYNC_EMPTY_LINES } from "./aside-profile-recovery";
4
5
  import { refreshAsideProfilesThroughServer } from "./aside-profiles";
5
6
  import { runCatalogAction } from "./catalog-command-result";
6
7
  import { CliUsageError, RuntimeApiError, printData, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
@@ -56,7 +57,7 @@ export function handleIntegrationAsideSync(argv: string[], deps: AsideSyncCliDep
56
57
  const parsed = resultsSchema.safeParse(outcomes);
57
58
  if (!parsed.success) throw new Error("Invalid Aside synchronization outcome");
58
59
  const results = parsed.data;
59
- const lines = results.length === 0 ? ["No eligible Aside profiles to synchronize. Check integration status and profile sync preferences."]
60
+ const lines = results.length === 0 ? [...ASIDE_SYNC_EMPTY_LINES]
60
61
  : results.flatMap(row => [
61
62
  `Aside profile ${row.profileId}: ${row.ok ? row.changed ? "updated" : "unchanged" : "failed"}.`,
62
63
  ...(row.reason ? [row.reason] : []),
@@ -13,13 +13,14 @@ import {
13
13
  type RuntimeApiDeps,
14
14
  } from "./runtime-api";
15
15
  import { clientIntegrationPath, validateAsideProfile } from "./integration-input";
16
+ import { asideProfileRecoveryLines } from "./aside-profile-recovery";
16
17
  import type { IntegrationClientId } from "../integrations/registry";
17
18
 
18
19
  const CLAUDE_USAGE = `Usage:
19
20
  ocx claude config [status] [--json]
20
21
  ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>]
21
22
  [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>]
22
- [--compact-window <tokens|default>] [--inject-agents <on|off>]
23
+ [--compact-window <tokens|default>] [--context-accounting <1m|200k>] [--inject-agents <on|off>]
23
24
  [--small-fast-model <id|->] [--model-map <from=to,from=to|->]
24
25
  [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->]
25
26
  [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]
@@ -75,6 +76,7 @@ export async function handleClaudeConfigCommand(argv: string[], deps: RuntimeApi
75
76
  const fastMode = takeBooleanOption(args, "--fast-mode");
76
77
  const autoContext = takeBooleanOption(args, "--auto-context");
77
78
  const compact = takeOption(args, "--compact-window");
79
+ const accounting = takeOption(args, "--context-accounting");
78
80
  const injectAgents = takeBooleanOption(args, "--inject-agents");
79
81
  const smallFastModel = takeOption(args, "--small-fast-model");
80
82
  const modelMap = takeOption(args, "--model-map");
@@ -97,6 +99,11 @@ export async function handleClaudeConfigCommand(argv: string[], deps: RuntimeApi
97
99
  body.autoCompactWindow = value;
98
100
  }
99
101
  }
102
+ if (accounting !== undefined) {
103
+ const value = accounting.toLowerCase();
104
+ if (value !== "1m" && value !== "200k") throw new CliUsageError("--context-accounting must be 1m or 200k", CLAUDE_USAGE);
105
+ body.contextAccounting = value;
106
+ }
100
107
  if (injectAgents !== undefined) body.injectAgents = injectAgents;
101
108
  if (smallFastModel !== undefined) body.smallFastModel = smallFastModel === "-" ? "" : smallFastModel;
102
109
  if (modelMap !== undefined) body.modelMap = parseMap(modelMap);
@@ -194,7 +201,10 @@ function raycastBlock(result: unknown): RaycastStatusBlock | null {
194
201
  */
195
202
  function singleClientStatusLines(result: unknown): string[] {
196
203
  const raycast = raycastBlock(result);
197
- if (!raycast) return summaryLines(result);
204
+ if (!raycast) {
205
+ const aside = (result as { clientId?: unknown } | null)?.clientId === "aside";
206
+ return [...summaryLines(result), ...(aside ? asideProfileRecoveryLines([result]) : [])];
207
+ }
198
208
  const rest = Object.fromEntries(Object.entries(result as Record<string, unknown>).filter(([key]) => key !== "raycast"));
199
209
  const lines = [...summaryLines(rest), `plan: ${raycast.plan}`];
200
210
  if (!raycast.aiDirPresent) {
@@ -252,7 +262,10 @@ export async function handleClientIntegrationCommand(
252
262
  const profiles = (result as { profiles?: Array<Record<string, unknown>> }).profiles;
253
263
  printData(result, wantsJson, profiles
254
264
  ? profiles.length > 0
255
- ? profiles.map(row => `${String(row.profileId)} ${String(row.name ?? "Aside")}: ${row.enabled ? "on" : "off"} (${String(row.state)})${row.current ? " [current]" : ""}`)
265
+ ? [
266
+ ...profiles.map(row => `${String(row.profileId)} ${String(row.name ?? "Aside")}: ${row.enabled ? "on" : "off"} (${String(row.state)})${row.current ? " [current]" : ""}`),
267
+ ...asideProfileRecoveryLines(profiles),
268
+ ]
256
269
  : [String((result as { error?: string }).error ?? "No Aside profiles found.")]
257
270
  : rows
258
271
  /*
@@ -59,6 +59,7 @@ import {
59
59
  } from "../service/ownership-compatibility";
60
60
  import { observeManagingClis } from "../service/managing-cli";
61
61
  import { SERVICE_OWNERSHIP_MINIMUM_CLI_VERSION } from "../service/install-state-contract.mjs";
62
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
62
63
  import { computeVersionSkew, type VersionSkew } from "./version-skew";
63
64
 
64
65
  /** Wire version of the resolve document. Bump only on an incompatible shape change. */
@@ -123,6 +124,12 @@ export interface ResolveJson {
123
124
  * takeover. Additive within ocx-resolve/1; older shells ignore it and read as unknown.
124
125
  */
125
126
  versionSkew?: VersionSkew;
127
+ supervisor?: {
128
+ kind: "desktop" | "none" | "unknown" | "unsupported";
129
+ supervisorPid?: number;
130
+ runtimePid?: number;
131
+ app?: string;
132
+ };
126
133
  }
127
134
 
128
135
  export type ResolveTakeover =
@@ -160,6 +167,7 @@ export interface ResolveIo {
160
167
  observeManagers?: (
161
168
  state: ServiceInstallState | null,
162
169
  ) => Readonly<Record<ManagingCliRole, ManagingCliObservation>>;
170
+ inspectSupervision?: typeof inspectDesktopSupervision;
163
171
  stdout?: { log: (s: string) => void };
164
172
  stderr?: { error: (s: string) => void };
165
173
  }
@@ -187,6 +195,7 @@ export function buildResolveJson(
187
195
  cliVersion: string,
188
196
  ownership: ServiceOwnershipResolution,
189
197
  takeover: ResolveTakeover,
198
+ supervision?: ReturnType<typeof inspectDesktopSupervision>,
190
199
  ): ResolveJson {
191
200
  const configured = config.port ?? RESOLVE_DEFAULT_PORT;
192
201
  return {
@@ -199,6 +208,9 @@ export function buildResolveJson(
199
208
  source: live ? live.source : "config",
200
209
  },
201
210
  liveness: livenessJson(live),
211
+ ...(live && supervision ? { supervisor: supervision.kind === "desktop"
212
+ ? { kind: supervision.kind, supervisorPid: supervision.supervisorPid, runtimePid: supervision.runtimePid, app: supervision.app }
213
+ : { kind: supervision.kind } } : {}),
202
214
  ownership,
203
215
  takeover,
204
216
  // A proven absence has nothing to skew against; the field is omitted there rather
@@ -216,7 +228,8 @@ function reportHuman(json: ResolveJson, stdout: { log: (s: string) => void }): v
216
228
  if (live.status === "live") {
217
229
  const pidText = live.pid === null ? "unknown" : String(live.pid);
218
230
  const versionText = live.version ?? "unknown version";
219
- stdout.log(`Proxy live on port ${json.port.effective} (PID ${pidText}, ${versionText}); effective port ${json.port.effective}.`);
231
+ const supervisor = json.supervisor?.kind === "desktop" ? `; supervisor: desktop (pid ${json.supervisor.supervisorPid})` : "";
232
+ stdout.log(`Proxy live on port ${json.port.effective} (PID ${pidText}, ${versionText}); effective port ${json.port.effective}${supervisor}.`);
220
233
  } else {
221
234
  stdout.log(`No live proxy (absence proven); effective port ${json.port.effective} (configured).`);
222
235
  }
@@ -353,7 +366,17 @@ export async function runResolve(args: ResolveArgs, io: ResolveIo = {}): Promise
353
366
  };
354
367
  }
355
368
  }
356
- const json = buildResolveJson(diagnostics.config, live, configHome, cliVersion(), ownership, takeover);
369
+ let supervision: ReturnType<typeof inspectDesktopSupervision> | undefined;
370
+ if (live) {
371
+ try {
372
+ supervision = live.pid === null ? { kind: "unknown", reason: "pid-unavailable", desktopSeen: false }
373
+ : (io.inspectSupervision ?? inspectDesktopSupervision)({ targetPid: live.pid });
374
+ } catch {
375
+ // Supervision is independent evidence; its failure cannot invalidate established liveness.
376
+ supervision = { kind: "unknown", reason: "probe-failed", desktopSeen: false };
377
+ }
378
+ }
379
+ const json = buildResolveJson(diagnostics.config, live, configHome, cliVersion(), ownership, takeover, supervision);
357
380
  if (args.json) stdout.log(JSON.stringify(json));
358
381
  else reportHuman(json, stdout);
359
382
  return 0;
package/src/cli/root.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * CLI head: version/help early exits, `ocx ready` pre-parse, and the bounded
3
- * Codex-shim auto-restore preflight, in that order (Phase 1 of the CLI
2
+ * CLI head: version/help early exits, ready/resolve pre-parse, lifecycle version
3
+ * notice, then bounded Codex-shim auto-restore preflight (Phase 1 of the CLI
4
4
  * deepening — moved out of src/cli/index.ts).
5
5
  *
6
6
  * `parseCliHead` is pure (no I/O, no process access) so the ordering and the
@@ -16,6 +16,7 @@ import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
16
16
  import { findCommand } from "./registry";
17
17
  import { printUnknownCommand } from "./help-recovery";
18
18
  import { noteCredentialArgv, redactSecretArgs } from "./secret-args";
19
+ import { maybeNoticeVersionSkew } from "./version-skew-notice";
19
20
 
20
21
  export interface CliHead {
21
22
  kind: "version" | "help" | "ready" | "resolve" | "command";
@@ -141,6 +142,7 @@ export async function runCli(argv: string[]): Promise<CliHead> {
141
142
  console.error(uninstallError);
142
143
  process.exit(2);
143
144
  }
145
+ await maybeNoticeVersionSkew(head.command, head.args);
144
146
  maybeAutoRestoreCodexShim(head.command, head.args);
145
147
  return head;
146
148
  }
package/src/cli/status.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { collectCliPathDiagnostics, formatCliStatusHealthLabel, type CliPathDiagnostics } from "./cli-path-diagnostics";
1
2
  import type { CodexMainAccountPolicyHealth } from "../oauth/health";
2
3
  import { durableBunRuntime } from "../lib/bun-runtime";
3
4
  import { existsSync, readFileSync } from "node:fs";
@@ -7,6 +8,7 @@ import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../cod
7
8
  import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
8
9
  import type { OcxConfig } from "../types";
9
10
  import { diagnoseService, serviceLogPath } from "../service";
11
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
10
12
  import { collectStartupHealth, startupHealthReadBudgetMs, type StartupHealth } from "../codex/autostart-health";
11
13
  import { getCodexRoutingKind } from "../codex/inject";
12
14
  import { missingOwnedCatalogPath } from "../codex/inject/config-toml";
@@ -215,6 +217,8 @@ export type CliStatusJson = {
215
217
  * ignores the key is unaffected, and `proxyVersion` is null when nothing is live.
216
218
  */
217
219
  versionSkew: VersionSkew;
220
+ cliCommand: CliPathDiagnostics;
221
+ startupSource: "live" | "local" | "local-supervision-override";
218
222
  };
219
223
 
220
224
  export type CliStatusView = {
@@ -259,12 +263,28 @@ export async function fetchLiveStartupHealth(
259
263
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
260
264
  const row = payload as Record<string, unknown>;
261
265
  if (row.status !== "native" && row.status !== "protected" && row.status !== "at-risk") return null;
262
- if (row.protection !== "service" && row.protection !== "shim" && row.protection !== "none") return null;
266
+ if (row.protection !== "service" && row.protection !== "desktop" && row.protection !== "shim" && row.protection !== "none") return null;
263
267
  if (row.routingKind !== "native" && row.routingKind !== "opencodex-local"
264
268
  && row.routingKind !== "custom-local" && row.routingKind !== "custom-remote" && row.routingKind !== "unknown") return null;
265
269
  if (row.shimCoverage !== "full" && row.shimCoverage !== "cli-only" && row.shimCoverage !== "none") return null;
266
270
  if (typeof row.platform !== "string") return null;
267
271
  if (row.recommendedCommand !== null && typeof row.recommendedCommand !== "string") return null;
272
+ if (row.recommendedAction !== undefined && row.recommendedAction !== null
273
+ && (typeof row.recommendedAction !== "string" || row.recommendedAction.length > 512)) return null;
274
+ if (row.desktop !== undefined) {
275
+ if (!row.desktop || typeof row.desktop !== "object" || Array.isArray(row.desktop)) return null;
276
+ const desktop = row.desktop as Record<string, unknown>;
277
+ for (const key of ["owned", "loginEnabled", "running", "viable"] as const) {
278
+ if (typeof desktop[key] !== "boolean") return null;
279
+ }
280
+ if (desktop.supervisor !== undefined) {
281
+ if (!desktop.supervisor || typeof desktop.supervisor !== "object" || Array.isArray(desktop.supervisor)) return null;
282
+ const supervisor = desktop.supervisor as Record<string, unknown>;
283
+ if (typeof supervisor.supervisorPid !== "number" || !Number.isFinite(supervisor.supervisorPid)
284
+ || typeof supervisor.runtimePid !== "number" || !Number.isFinite(supervisor.runtimePid)
285
+ || typeof supervisor.app !== "string") return null;
286
+ }
287
+ }
268
288
  if (!row.commands || typeof row.commands !== "object" || Array.isArray(row.commands)) return null;
269
289
  for (const key of ["installService", "repairService", "installShim", "restoreNative"] as const) {
270
290
  if (typeof (row.commands as Record<string, unknown>)[key] !== "string") return null;
@@ -286,12 +306,36 @@ export async function fetchLiveStartupHealth(
286
306
  return payload as StartupHealth;
287
307
  }
288
308
 
289
- /** Prefer an attested live verdict and evaluate the local fallback only when live state is absent. */
309
+ /** Older runtimes cannot report supervision; only that missing capability permits a local override. */
290
310
  export function selectStatusStartupHealth(
291
311
  liveStartup: StartupHealth | null,
292
312
  fallback: () => StartupHealth,
293
- ): StartupHealth {
294
- return liveStartup ?? fallback();
313
+ supervision?: () => ReturnType<typeof inspectDesktopSupervision>,
314
+ livePid?: number | null,
315
+ ): { startup: StartupHealth; startupSource: CliStatusJson["startupSource"] } {
316
+ if (!liveStartup) return { startup: fallback(), startupSource: "local" };
317
+ if (liveStartup.desktop === undefined && supervision) {
318
+ let evidence: ReturnType<typeof inspectDesktopSupervision> | undefined;
319
+ try { evidence = supervision(); }
320
+ catch { /* Failed optional evidence cannot replace an attested live verdict. */ }
321
+ if (evidence?.kind === "desktop" && evidence.runtimePid === livePid) {
322
+ const local = fallback();
323
+ const supervisor = local.desktop?.supervisor;
324
+ // The local probe runs later; it must still describe the same unowned process pair.
325
+ if (local.desktop?.owned === false && supervisor
326
+ && supervisor.runtimePid === evidence.runtimePid && supervisor.supervisorPid === evidence.supervisorPid) {
327
+ return { startup: local, startupSource: "local-supervision-override" };
328
+ }
329
+ }
330
+ }
331
+ return { startup: liveStartup, startupSource: "live" };
332
+ }
333
+
334
+ export function runtimeSupervisorLine(startup: StartupHealth): string | null {
335
+ const supervisor = startup.desktop?.supervisor;
336
+ return supervisor
337
+ ? `Runtime supervisor: OpenCodex Desktop (pid ${supervisor.supervisorPid}, ${supervisor.app}); durable owner: ${startup.desktop?.owned ? "desktop" : "none"}`
338
+ : null;
295
339
  }
296
340
 
297
341
  /** Build the service summary from the same startup source that `ocx status` selected. */
@@ -299,18 +343,23 @@ export function statusServiceSummary(
299
343
  liveStartup: StartupHealth | null,
300
344
  service: Pick<ReturnType<typeof diagnoseService>, "installed" | "summary">,
301
345
  live: boolean,
346
+ // Which verdict was selected. A live proxy can still be described by the local reading when
347
+ // the runtime predates supervision reporting, so the label follows the verdict, not liveness.
348
+ startupSource?: CliStatusJson["startupSource"],
302
349
  ): string {
303
- if (liveStartup) {
350
+ if (live && liveStartup) {
351
+ const supervisor = liveStartup.desktop?.supervisor ? "OpenCodex Desktop supervises the running proxy; " : "";
304
352
  if (liveStartup.protection === "service" && liveStartup.serviceViable) {
305
- return `running under the live managed service (logs: ${serviceLogPath()})`;
353
+ return `${supervisor}running under the live managed service (logs: ${serviceLogPath()})`;
306
354
  }
307
355
  const state = [
308
356
  liveStartup.serviceInstalled ? "installed" : "absent",
309
357
  liveStartup.serviceRunning ? "running" : "not running",
310
358
  liveStartup.serviceViable ? "viable" : "not viable",
311
359
  ].join(", ");
312
- const action = liveStartup.recommendedCommand ? `; run '${liveStartup.recommendedCommand}'` : "";
313
- return `live startup reports service ${state}${action} (logs: ${serviceLogPath()})`;
360
+ const action = !liveStartup.desktop?.supervisor && liveStartup.recommendedCommand ? `; run '${liveStartup.recommendedCommand}'` : "";
361
+ const label = (startupSource ?? (live ? "live" : "local")) === "live" ? "live" : "local";
362
+ return `${supervisor}${label} startup reports service ${state}${action} (logs: ${serviceLogPath()})`;
314
363
  }
315
364
  return service.installed && !live
316
365
  ? `${service.summary} — registered but NOT serving; see ${serviceLogPath()} and re-run 'ocx service repair'`
@@ -680,6 +729,7 @@ export function missingCodexCatalogLines(missingCatalogPath: string | null): str
680
729
 
681
730
  /** `mainAccountPolicy`: only `--json` asks; the human report reads the same accounts once via OAuth health. */
682
731
  export async function collectStatus(options: { mainAccountPolicy?: boolean } = {}): Promise<CliStatusView> {
732
+ const cliCommand = collectCliPathDiagnostics();
683
733
  const configDiagnostics = readConfigDiagnostics();
684
734
  const config = configDiagnostics.config;
685
735
  const claudeDesktop = {
@@ -757,14 +807,14 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
757
807
  // A service can be registered and still not serve: the manager reports the job
758
808
  // either way. When the identity-probed live proxy provides an attested startup verdict,
759
809
  // prefer it over a shell-local service-manager probe that lacks the service environment.
760
- const serviceSummary = statusServiceSummary(liveStartup, service, Boolean(live));
761
810
  const codexShim = diagnoseCodexShim();
762
811
  const codexShimSummary = codexShim.summary;
763
- const startup = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(config, {
812
+ const { startup, startupSource } = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(config, {
764
813
  service,
765
814
  shim: codexShim,
766
815
  routingKind: getCodexRoutingKind(),
767
- }));
816
+ }), live?.pid != null ? () => inspectDesktopSupervision({ targetPid: live.pid! }) : undefined, live?.pid);
817
+ const serviceSummary = statusServiceSummary(startup, service, Boolean(live), startupSource);
768
818
  const codexPlugins = diagnoseCodexBundledPlugins();
769
819
  const lastClamp = loadLastEffortClamp();
770
820
  const clampActive = effortClampAppliesToRuntime(lastClamp, resolvedRuntime.runtime);
@@ -869,7 +919,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
869
919
 
870
920
  return {
871
921
  proxyLabel,
872
- healthLabel: health.label,
922
+ healthLabel: formatCliStatusHealthLabel(health.label, cliCommand, remoteHub.connected),
873
923
  json: {
874
924
  ...(mainAccountHardLock ? { mainAccountHardLock } : {}),
875
925
  schemaVersion: 1,
@@ -932,6 +982,8 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
932
982
  // fact about this install, not about the Codex runtime, and filing it there would
933
983
  // print it under the wrong heading (#2701).
934
984
  versionSkew,
985
+ cliCommand,
986
+ startupSource,
935
987
  },
936
988
  };
937
989
  }
@@ -16,7 +16,8 @@ function parseMarker(raw: string): UpdateRestartChildMarker {
16
16
  if (!value || !Number.isSafeInteger(value.deadlineAt) || !Number.isInteger(value.port)
17
17
  || typeof value.hostname !== "string" || value.port < 1 || value.port > 65535 || typeof value.version !== "string"
18
18
  || value.version === "0.0.0" || !parseStrictSemver(value.version) || !value.home
19
- || !Number.isSafeInteger(value.home.revision)) throw new Error("update_restart_child_marker_invalid");
19
+ || !Number.isSafeInteger(value.home.revision) || value.home.serviceRecord?.schema !== 1
20
+ || typeof value.home.serviceRecord.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.home.serviceRecord.digest)) throw new Error("update_restart_child_marker_invalid");
20
21
  for (const directory of [value.home.config, value.home.codex]) {
21
22
  // Windows reports 64-bit NTFS file ids (MFT sequence in the high 16 bits) as doubles above
22
23
  // 2^53. Parent and child read them through the same stat API, so any finite integer is a
@@ -56,7 +57,7 @@ export function admitUpdateRestartChild(argv: string[], io: UpdateRestartChildIo
56
57
  if (now() >= marker.deadlineAt) throw new Error("update_restart_deadline_expired");
57
58
  const bindHost = (host: string) => !host.trim() || /^localhost\.?$/i.test(host.trim()) ? "127.0.0.1" : host.trim();
58
59
  if (port !== marker.port || bindHost(hostname) !== bindHost(marker.hostname) || (io.version ?? packageVersion)() !== marker.version) throw new Error("update_restart_child_identity_changed");
59
- (io.checkHome ?? assertUpdateRestartHome)(marker.home);
60
+ (io.checkHome ?? (home => assertUpdateRestartHome(home, marker.deadlineAt)))(marker.home);
60
61
  (io.checkState ?? (expected => {
61
62
  assertUpdateRestartConfiguration(expected.hostname);
62
63
  const current = readRuntimePort();
@@ -65,6 +66,7 @@ export function admitUpdateRestartChild(argv: string[], io: UpdateRestartChildIo
65
66
  throw new Error("update_restart_competing_runtime");
66
67
  }
67
68
  }))(marker);
69
+ if (now() >= marker.deadlineAt) throw new Error("update_restart_deadline_expired");
68
70
  };
69
71
  check();
70
72
  const lease = (io.acquire ?? (waitMs => acquireOwnershipMutationLease(serviceStatePaths(), { waitMs })))(Math.min(2000, marker.deadlineAt - now()));
@@ -2,12 +2,15 @@ import { realpathSync, statSync } from "node:fs";
2
2
  import { readClientConnectionState } from "../client/state";
3
3
  import { readConfigDiagnostics } from "../config/diagnostics";
4
4
  import { getConfigDir } from "../config/paths";
5
- import { currentCodexHome, resolveServiceOwnership, resolveServiceState } from "../service/state";
5
+ import { currentCodexHome } from "../service/state";
6
+ import { captureUpdateRestartServiceRecord, type UpdateRestartServiceRecord, type UpdateRestartServiceRecordDeps } from "./update-restart-service-record";
7
+ import { probeUpdateRestartSupervision, type UpdateRestartSupervisionDeps } from "./update-restart-supervision";
6
8
 
7
9
  export interface UpdateRestartHome {
8
10
  config: { path: string; dev: number; ino: number };
9
11
  codex: { path: string; dev: number; ino: number };
10
12
  revision: number;
13
+ serviceRecord: UpdateRestartServiceRecord;
11
14
  }
12
15
 
13
16
  function physicalDirectory(path: string): UpdateRestartHome["config"] {
@@ -17,17 +20,32 @@ function physicalDirectory(path: string): UpdateRestartHome["config"] {
17
20
  return { path: canonical, dev: stat.dev, ino: stat.ino };
18
21
  }
19
22
 
20
- /** Only an unclaimed standalone home is eligible for this narrow update path. */
21
- export function readUpdateRestartHome(): UpdateRestartHome {
22
- const state = resolveServiceState();
23
- const owner = resolveServiceOwnership();
24
- if (state.kind !== "none" || owner.kind !== "none") throw new Error("update_restart_owner_unverified");
25
- return { config: physicalDirectory(getConfigDir()), codex: physicalDirectory(currentCodexHome()), revision: owner.revision };
23
+ export interface UpdateRestartHomeDeps {
24
+ record?: UpdateRestartServiceRecordDeps;
25
+ supervision?: UpdateRestartSupervisionDeps;
26
26
  }
27
27
 
28
- export function assertUpdateRestartHome(expected: UpdateRestartHome): void {
29
- const current = readUpdateRestartHome();
30
- if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("update_restart_home_changed");
28
+ /** Capture eligibility from the same bytes that supply the frozen fingerprint. */
29
+ export function readUpdateRestartHome(deps: UpdateRestartHomeDeps = {}): UpdateRestartHome {
30
+ const { serviceRecord, state, owner } = captureUpdateRestartServiceRecord(deps.record);
31
+ const platform = deps.record?.platform ?? process.platform;
32
+ if (owner.kind !== "none" || (state.kind === "state" && platform !== "darwin" && platform !== "linux")) {
33
+ throw new Error("update_restart_owner_unverified");
34
+ }
35
+ return { config: physicalDirectory(getConfigDir()), codex: physicalDirectory(currentCodexHome()), revision: owner.revision, serviceRecord };
36
+ }
37
+
38
+ export function assertUpdateRestartHome(expected: UpdateRestartHome, deadlineAt = Date.now() + 2000, deps: UpdateRestartHomeDeps = {}): void {
39
+ try {
40
+ const current = readUpdateRestartHome(deps);
41
+ if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("changed");
42
+ } catch { throw new Error("update_restart_home_changed"); }
43
+ if (probeUpdateRestartSupervision(deadlineAt, deps.supervision) !== "inactive") throw new Error("update_restart_supervision_unverified");
44
+ // Manager commands may yield to an external writer even though this guard is synchronous.
45
+ try {
46
+ if (JSON.stringify(readUpdateRestartHome(deps)) !== JSON.stringify(expected)) throw new Error("changed");
47
+ } catch { throw new Error("update_restart_home_changed"); }
48
+ if ((deps.supervision?.now ?? Date.now)() >= deadlineAt) throw new Error("update_restart_deadline_expired");
31
49
  }
32
50
 
33
51
  /** Read-only eligibility shared by parent pre-stop and child admission. */