@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +5 -1
  13. package/src/adapters/anthropic.ts +16 -0
  14. package/src/adapters/coding-agent/protocol.ts +36 -6
  15. package/src/adapters/coding-agent/turn.ts +10 -2
  16. package/src/adapters/command-code.ts +2 -1
  17. package/src/adapters/cursor/catalog.ts +51 -7
  18. package/src/adapters/cursor/protobuf-request.ts +6 -3
  19. package/src/adapters/cursor/request-builder.ts +13 -3
  20. package/src/adapters/cursor.ts +11 -2
  21. package/src/adapters/declaration-carrier.ts +45 -0
  22. package/src/adapters/devin.ts +75 -23
  23. package/src/adapters/google-antigravity-wire.ts +5 -2
  24. package/src/adapters/google-errors.ts +7 -1
  25. package/src/adapters/google.ts +29 -5
  26. package/src/adapters/image.ts +4 -1
  27. package/src/adapters/input-media-guard.ts +21 -9
  28. package/src/adapters/kiro/usage.ts +3 -2
  29. package/src/adapters/kiro-tool-fallback.ts +1 -1
  30. package/src/adapters/ollama-native.ts +6 -0
  31. package/src/adapters/openai-chat/developer-role.ts +61 -0
  32. package/src/adapters/openai-chat/messages.ts +46 -27
  33. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  34. package/src/adapters/openai-chat/passthrough.ts +33 -9
  35. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  36. package/src/adapters/openai-chat.ts +18 -57
  37. package/src/adapters/openai-responses/passthrough.ts +2 -0
  38. package/src/adapters/registry.ts +3 -2
  39. package/src/adapters/run-turn-queue.ts +178 -29
  40. package/src/adapters/xai-web-search.ts +16 -1
  41. package/src/bridge/errors.ts +8 -2
  42. package/src/bridge/response-json.ts +9 -1
  43. package/src/bridge/sse.ts +10 -0
  44. package/src/chat/inbound.ts +141 -5
  45. package/src/claude/desktop-3p.ts +7 -1
  46. package/src/claude/desktop-first-party.ts +183 -0
  47. package/src/claude/desktop-gateway-state.ts +41 -0
  48. package/src/claude/inbound-content-options.ts +6 -0
  49. package/src/claude/inbound.ts +32 -6
  50. package/src/claude/intercept/connect-proxy.ts +179 -0
  51. package/src/claude/intercept/listener.ts +122 -0
  52. package/src/claude/intercept/local-ca.ts +298 -0
  53. package/src/claude/intercept/runtime.ts +98 -0
  54. package/src/claude/intercept/settings.ts +189 -0
  55. package/src/cli/access.ts +87 -0
  56. package/src/cli/account-auth.ts +19 -0
  57. package/src/cli/capabilities.ts +31 -0
  58. package/src/cli/claude-desktop.ts +206 -16
  59. package/src/cli/codex-shim-autorestore.ts +3 -0
  60. package/src/cli/companion.ts +56 -0
  61. package/src/cli/dispatch.ts +43 -4
  62. package/src/cli/ensure-desired-integrations.ts +43 -5
  63. package/src/cli/help.ts +7 -9
  64. package/src/cli/index.ts +200 -61
  65. package/src/cli/init.ts +8 -0
  66. package/src/cli/integrations.ts +7 -1
  67. package/src/cli/registry.ts +41 -2
  68. package/src/cli/resolve.ts +230 -0
  69. package/src/cli/root.ts +24 -1
  70. package/src/cli/start-ownership-publication.ts +56 -0
  71. package/src/cli/status-probes.ts +2 -18
  72. package/src/cli/status.ts +62 -0
  73. package/src/cli/stop-report.ts +143 -0
  74. package/src/cli/uninstall-plan.ts +9 -0
  75. package/src/client/machine-listener.ts +2 -5
  76. package/src/clients/aside-profiles.ts +4 -0
  77. package/src/clients/config-export/zcode-store.ts +157 -0
  78. package/src/clients/config-export.ts +36 -0
  79. package/src/codex/app-server-processes.ts +72 -40
  80. package/src/codex/auth-api/login-flow.ts +6 -1
  81. package/src/codex/autostart-health.ts +28 -0
  82. package/src/codex/catalog/build-entries.ts +2 -2
  83. package/src/codex/catalog/effort.ts +3 -3
  84. package/src/codex/catalog/provider-models.ts +24 -15
  85. package/src/codex/catalog/retained-sync.ts +2 -2
  86. package/src/codex/convergence.ts +2 -2
  87. package/src/codex/history-provider.ts +12 -1
  88. package/src/codex/inject/config-toml.ts +41 -6
  89. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  90. package/src/codex/inject.ts +18 -15
  91. package/src/codex/injected-marker.ts +18 -0
  92. package/src/codex/main-account.ts +6 -0
  93. package/src/codex/model-cache.ts +52 -6
  94. package/src/codex/model-entitlement-admission.ts +59 -0
  95. package/src/codex/model-entitlements.ts +87 -44
  96. package/src/codex/native-main-admission.ts +83 -0
  97. package/src/codex/routing/health-store.ts +39 -0
  98. package/src/codex/routing/selection.ts +37 -1
  99. package/src/codex/routing.ts +5 -41
  100. package/src/codex/shim-templates.ts +29 -3
  101. package/src/companion/settings.ts +132 -0
  102. package/src/config/atomic-write.ts +117 -5
  103. package/src/config/load-degrade.ts +34 -7
  104. package/src/config/process-state.ts +1 -1
  105. package/src/config/schema/config-schema.ts +27 -1
  106. package/src/config/schema/leaf-validators.ts +47 -0
  107. package/src/config.ts +1 -1
  108. package/src/generated/compatibility-version.json +418 -174
  109. package/src/integrations/config-io.ts +44 -10
  110. package/src/integrations/merge.ts +120 -13
  111. package/src/integrations/mutation-plan.ts +124 -18
  112. package/src/integrations/registry.ts +38 -0
  113. package/src/integrations/state.ts +78 -45
  114. package/src/integrations/target.ts +208 -0
  115. package/src/integrations/writer.ts +49 -11
  116. package/src/lab/conformance/fixture-provider.ts +5 -0
  117. package/src/lib/browser-launch-notice.ts +59 -0
  118. package/src/lib/bun-runtime.ts +6 -2
  119. package/src/lib/debug.ts +40 -0
  120. package/src/lib/open-url.ts +51 -7
  121. package/src/lib/package-tree-integrity.ts +2 -1
  122. package/src/lib/package-version.ts +8 -0
  123. package/src/lib/provider-egress.ts +310 -0
  124. package/src/lib/provider-outbound.ts +59 -14
  125. package/src/lib/proxy-env.ts +82 -7
  126. package/src/lib/request-execution-budget.ts +72 -0
  127. package/src/lib/request-failure-attribution.ts +183 -0
  128. package/src/lib/request-failure-model.ts +236 -0
  129. package/src/lib/request-resend-gate.ts +138 -0
  130. package/src/lib/standalone.ts +16 -0
  131. package/src/lib/upstream-retry.ts +167 -16
  132. package/src/lib/winsw.ts +2 -2
  133. package/src/oauth/index.ts +24 -1
  134. package/src/oauth/login-cli.ts +80 -29
  135. package/src/providers/api-key-resolve.ts +133 -0
  136. package/src/providers/api-key-selection.ts +5 -1
  137. package/src/providers/key-failover.ts +31 -1
  138. package/src/providers/key-store.ts +34 -110
  139. package/src/providers/model-rename-fields.ts +147 -0
  140. package/src/providers/model-rename-migration.ts +124 -37
  141. package/src/providers/quota/vendor-probes-key.ts +37 -22
  142. package/src/providers/reasoning-metadata.ts +43 -18
  143. package/src/providers/registry/entries-core.ts +9 -4
  144. package/src/providers/registry/entries-extended.ts +29 -4
  145. package/src/providers/registry/model-seeds.ts +47 -10
  146. package/src/providers/xai-transport.ts +12 -1
  147. package/src/reasoning-effort.ts +8 -0
  148. package/src/responses/function-call-compat.ts +38 -1
  149. package/src/responses/inline-document.ts +65 -0
  150. package/src/responses/input-media.ts +42 -8
  151. package/src/responses/muse-tool-name-alias.ts +19 -0
  152. package/src/responses/parser-content.ts +8 -2
  153. package/src/responses/parser-tools.ts +3 -0
  154. package/src/responses/parser.ts +3 -1
  155. package/src/responses/schema.ts +3 -0
  156. package/src/router.ts +17 -2
  157. package/src/server/admission-model-scope.ts +219 -0
  158. package/src/server/audio-live.ts +9 -3
  159. package/src/server/audio-upstream.ts +18 -0
  160. package/src/server/auth-cors.ts +26 -0
  161. package/src/server/chat-completions.ts +55 -2
  162. package/src/server/chat-native.ts +19 -4
  163. package/src/server/claude-messages.ts +55 -17
  164. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  165. package/src/server/gui-freshness.ts +103 -0
  166. package/src/server/gui-static.ts +7 -9
  167. package/src/server/images.ts +59 -6
  168. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  169. package/src/server/index/serve-options.ts +56 -10
  170. package/src/server/index/spend-ledger-lifecycle.ts +34 -8
  171. package/src/server/index/startup-warnings.ts +24 -0
  172. package/src/server/index.ts +21 -28
  173. package/src/server/lifecycle.ts +4 -4
  174. package/src/server/live-call-bindings.ts +6 -0
  175. package/src/server/live.ts +88 -3
  176. package/src/server/management/agent-settings-routes.ts +121 -36
  177. package/src/server/management/companion-routes.ts +77 -0
  178. package/src/server/management/logs-usage-routes.ts +19 -0
  179. package/src/server/management/native-integration-routes.ts +103 -6
  180. package/src/server/management/oauth-account-routes.ts +45 -7
  181. package/src/server/management/route-registry.ts +6 -0
  182. package/src/server/management/shared.ts +18 -1
  183. package/src/server/management/usage-timeline-routes.ts +44 -0
  184. package/src/server/management-api.ts +8 -9
  185. package/src/server/proxy-liveness.ts +75 -0
  186. package/src/server/relay.ts +19 -2
  187. package/src/server/request-log-failure-attribution.ts +99 -0
  188. package/src/server/request-log.ts +114 -0
  189. package/src/server/request-metrics.ts +92 -30
  190. package/src/server/responses/codex-ws-wire.ts +34 -8
  191. package/src/server/responses/combo-stream-preflight.ts +168 -6
  192. package/src/server/responses/compact.ts +11 -0
  193. package/src/server/responses/core-opaque-recovery.ts +90 -0
  194. package/src/server/responses/fetch-helpers.ts +124 -8
  195. package/src/server/responses/input-admission.ts +10 -0
  196. package/src/server/responses/passthrough-delivery.ts +14 -1
  197. package/src/server/responses/passthrough-dispatch.ts +179 -35
  198. package/src/server/responses/passthrough-error.ts +27 -8
  199. package/src/server/responses/request-prepare.ts +42 -1
  200. package/src/server/responses/request-send-budget.ts +12 -0
  201. package/src/server/responses/request-transport.ts +24 -4
  202. package/src/server/responses/reset-replay.ts +108 -0
  203. package/src/server/responses-request-tool-scope.ts +214 -0
  204. package/src/server/responses-undeclared-tool-guard.ts +4 -1
  205. package/src/server/search.ts +25 -1
  206. package/src/server/usage-ledger-retention.ts +73 -0
  207. package/src/service/cli.ts +48 -2
  208. package/src/service/health.ts +3 -2
  209. package/src/service/install-state-contract.d.mts +27 -0
  210. package/src/service/install-state-contract.mjs +34 -0
  211. package/src/service/launchd.ts +1 -1
  212. package/src/service/orchestration.ts +2 -4
  213. package/src/service/ownership-compatibility.ts +164 -0
  214. package/src/service/ownership-mutation-lease.d.mts +32 -0
  215. package/src/service/ownership-mutation-lease.mjs +211 -0
  216. package/src/service/repair.ts +45 -1
  217. package/src/service/state-lock.ts +269 -0
  218. package/src/service/state-record.d.mts +36 -0
  219. package/src/service/state-record.mjs +138 -0
  220. package/src/service/state.ts +582 -68
  221. package/src/service/windows-taskxml.ts +11 -10
  222. package/src/service.ts +7 -3
  223. package/src/tray/windows-tray.ps1 +1 -1
  224. package/src/types/config.ts +37 -0
  225. package/src/types/provider.ts +73 -0
  226. package/src/types/request.ts +28 -2
  227. package/src/types/tools.ts +19 -0
  228. package/src/types.ts +3 -0
  229. package/src/update/index.ts +207 -63
  230. package/src/update/job.ts +9 -5
  231. package/src/update/ownership-transaction.ts +47 -0
  232. package/src/update/restart-ownership.ts +54 -0
  233. package/src/update/runtime-ownership.d.mts +40 -0
  234. package/src/update/runtime-ownership.mjs +122 -0
  235. package/src/usage/attempt-delivery.ts +198 -0
  236. package/src/usage/cache-diagnostic.ts +305 -0
  237. package/src/usage/failure-fingerprint.ts +118 -0
  238. package/src/usage/failure-projection-cache.ts +174 -0
  239. package/src/usage/failure-projection.ts +174 -0
  240. package/src/usage/ledger-retention.ts +165 -0
  241. package/src/usage/log.ts +126 -79
  242. package/src/usage/request-outcome.ts +150 -0
  243. package/src/usage/retention-contract.ts +28 -0
  244. package/src/usage/summary.ts +2 -2
  245. package/src/usage/telemetry-contract.ts +237 -0
  246. package/src/usage/timeline.ts +236 -0
  247. package/src/web-search/alpha-search.ts +21 -1
  248. package/gui/dist/assets/index-BTuCbqQd.css +0 -1
  249. package/gui/dist/assets/index-DoBVdPHP.js +0 -134
@@ -7,8 +7,12 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { getConfigDir, loadConfig } from "../config";
10
- import { readPid, readRuntimePort } from "../config/process-state";
10
+ import { readPid, readRuntimePort, getRuntimePortPath } from "../config/process-state";
11
11
  import { pendingTeardownOutstanding } from "../config/pending-teardown";
12
+ import type { ServiceOwnership } from "../service/state";
13
+ import { planUpdateRuntimeHandling, planStoppedRuntimeRecovery, inspectPackageRuntimeLiveness } from "./runtime-ownership.mjs";
14
+ import { unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
15
+ import { withUpdateOwnershipLease, readUpdateRuntimeTarget } from "./ownership-transaction";
12
16
  import { npmInvocation } from "./npm-invocation.mjs";
13
17
  import { pnpmInvocation, pnpmInvocationForPath, resolvePnpmCommands } from "./pnpm-invocation.mjs";
14
18
  import { detectInstallFromPath } from "./install-detection.mjs";
@@ -26,6 +30,7 @@ import {
26
30
  } from "./npm-cache-preflight.mjs";
27
31
  import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
28
32
  import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
33
+ import { packageVersion } from "../lib/package-version";
29
34
  import { selfLaunchArgv } from "../lib/self-launch-argv";
30
35
 
31
36
  /**
@@ -79,6 +84,7 @@ function runPnpmCandidate(
79
84
  encoding: "utf8",
80
85
  timeout: 20_000,
81
86
  windowsHide: true,
87
+ env: unprivilegedOwnershipMutationEnvironment(process.env),
82
88
  ...invocation.options,
83
89
  });
84
90
  }
@@ -121,7 +127,7 @@ function runOwnedPnpm(
121
127
  encoding: "utf8",
122
128
  timeout: 180_000,
123
129
  windowsHide: true,
124
- env: target.env,
130
+ env: unprivilegedOwnershipMutationEnvironment(target.env),
125
131
  ...target.options,
126
132
  });
127
133
  }
@@ -143,11 +149,7 @@ export function resolvePnpmActiveLauncher(owner: PnpmGlobalOwner): string | null
143
149
  }
144
150
 
145
151
  export function currentVersion(): string {
146
- try {
147
- return (JSON.parse(readFileSync(join(HERE, "..", "..", "package.json"), "utf8")).version as string) ?? "?";
148
- } catch {
149
- return "?";
150
- }
152
+ return packageVersion("?");
151
153
  }
152
154
 
153
155
  export function defaultUpdateTag(current: string): Channel {
@@ -259,7 +261,7 @@ export function latestVersion(
259
261
  encoding: "utf8",
260
262
  timeout: 12000,
261
263
  windowsHide: true,
262
- ...(manager.env ? { env: manager.env } : {}),
264
+ env: unprivilegedOwnershipMutationEnvironment(manager.env ?? process.env),
263
265
  ...manager.options,
264
266
  });
265
267
  return r.status === 0 && typeof r.stdout === "string" ? (r.stdout.trim() || null) : null;
@@ -313,13 +315,46 @@ export function checkUpdatePackageIntegrity(
313
315
  encoding: "utf8",
314
316
  timeout: 12000,
315
317
  windowsHide: true,
316
- ...(target.env ? { env: target.env } : {}),
318
+ env: unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
317
319
  ...target.options,
318
320
  });
319
321
  });
320
322
  return result;
321
323
  }
322
324
 
325
+ /**
326
+ * The recorded runtime owner, in the shape the shared update rule reads.
327
+ *
328
+ * Fails CLOSED. A resolution this process could not obtain is not evidence that nobody owns
329
+ * the runtime, and treating it as such is how an unreadable record reactivates the npm
330
+ * launcher over a takeover the user consented to.
331
+ */
332
+ interface RuntimeOwnershipObservation {
333
+ readonly ownership: ServiceOwnership | null;
334
+ readonly ownershipUnknown: boolean;
335
+ readonly subjectToken: string;
336
+ }
337
+
338
+ async function resolvedRuntimeOwnership(): Promise<RuntimeOwnershipObservation> {
339
+ try {
340
+ const { resolveServiceOwnership } = await import("../service");
341
+ const resolution = resolveServiceOwnership();
342
+ if (resolution.kind === "owned") return {
343
+ ownership: resolution.ownership,
344
+ ownershipUnknown: false,
345
+ subjectToken: JSON.stringify(["owned", resolution.revision, resolution.ownership]),
346
+ };
347
+ if (resolution.kind === "none") return {
348
+ ownership: null,
349
+ ownershipUnknown: false,
350
+ subjectToken: JSON.stringify(["none", resolution.revision]),
351
+ };
352
+ return { ownership: null, ownershipUnknown: true, subjectToken: "unknown" };
353
+ } catch {
354
+ return { ownership: null, ownershipUnknown: true, subjectToken: "unknown" };
355
+ }
356
+ }
357
+
323
358
  /**
324
359
  * `ocx update` fallback for source checkouts and Bun global installs. npm and pnpm global installs
325
360
  * are updated in the Node bin launcher before Bun starts, so Windows does not replace the running
@@ -378,6 +413,8 @@ export async function runUpdate(): Promise<void> {
378
413
  process.exit(1);
379
414
  }
380
415
 
416
+ const { serviceStatePaths } = await import("../service");
417
+ const updateExitCode = await withUpdateOwnershipLease(serviceStatePaths(), async mutation => {
381
418
  // Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
382
419
  // unloads it, so a successful update must repair/restart it afterwards.
383
420
  let serviceWasInstalled = false;
@@ -385,6 +422,18 @@ export async function runUpdate(): Promise<void> {
385
422
  const { isServiceInstalled } = await import("../service");
386
423
  serviceWasInstalled = isServiceInstalled();
387
424
  } catch { /* best-effort */ }
425
+ // What this update may do to the runtime. A desktop takeover vetoes both the stop and the
426
+ // service refresh below; see `planUpdateRuntimeHandling` for why each half is wrong.
427
+ const initialOwnership = await resolvedRuntimeOwnership();
428
+ const runtimePlan = planUpdateRuntimeHandling({
429
+ ...initialOwnership,
430
+ serviceInstalled: serviceWasInstalled,
431
+ });
432
+ if (runtimePlan.notice) console.log(runtimePlan.notice);
433
+ if (!runtimePlan.mayReplacePackage) {
434
+ console.error("⚠️ Update stopped before tray handoff, runtime stop, or package replacement because runtime ownership is unknown.");
435
+ return 1;
436
+ }
388
437
  let trayWasInstalled = false;
389
438
  let trayWasRunning = false;
390
439
  if (process.platform === "win32") {
@@ -402,7 +451,7 @@ export async function runUpdate(): Promise<void> {
402
451
  trayWasRunning = trayPlan.restoreOnFailure;
403
452
  } catch (error) {
404
453
  console.error(`⚠️ Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`);
405
- process.exit(1);
454
+ return 1;
406
455
  }
407
456
  }
408
457
 
@@ -421,6 +470,79 @@ export async function runUpdate(): Promise<void> {
421
470
  ...(runtimeTrusted && livePid ? { oldPid: livePid } : {}),
422
471
  };
423
472
 
473
+ const currentPackageRuntimeLiveness = () => inspectPackageRuntimeLiveness({
474
+ capturedTarget: capturedListen,
475
+ readCurrentTarget: () => readUpdateRuntimeTarget(getRuntimePortPath(), capturedListen.hostname),
476
+ probe: target => probeProxyLiveness(target.port, target.hostname),
477
+ }).overall;
478
+ const hadRuntimeState = serviceWasInstalled || !!livePid || !!preUpdateRt;
479
+
480
+ let stopAttempted = false;
481
+ let postUpdateLauncher = installer === "pnpm" && owner
482
+ ? join(owner.packagePath, "bin", "ocx.mjs")
483
+ : join(packageRoot(), "bin", "ocx.mjs");
484
+ let postUpdateLauncherUsable = true;
485
+ const sameOwner = (current: RuntimeOwnershipObservation) => {
486
+ const before = initialOwnership.ownership, after = current.ownership;
487
+ return before?.owner === after?.owner && before?.installId === after?.installId
488
+ && before?.consentGeneration === after?.consentGeneration;
489
+ };
490
+ const startProxyDirectly = async (): Promise<boolean> => {
491
+ if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) return false;
492
+ const env = mutation.controlEnvironment();
493
+ delete env.OCX_SERVICE;
494
+ const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
495
+ detached: true, stdio: "ignore", windowsHide: true, env: withProcessRuntimeProvenance(env),
496
+ });
497
+ let failed = false;
498
+ child.once("error", () => { failed = true; });
499
+ child.unref();
500
+ const deadline = Date.now() + 30_000;
501
+ while (!failed && Date.now() < deadline) {
502
+ const current = readUpdateRuntimeTarget(getRuntimePortPath(), capturedListen.hostname);
503
+ if (current.kind === "target" && current.target.port === capturedListen.port
504
+ && await proxyIdentityAt(current.target.port, { hostname: current.target.hostname, expectedPid: current.target.pid },
505
+ { timeoutMs: 1500, deadlineAt: deadline, attempts: 1 })) return true;
506
+ await Bun.sleep(Math.min(100, Math.max(0, deadline - Date.now())));
507
+ }
508
+ console.warn("⚠️ Recovery did not report a healthy proxy before its deadline; run 'ocx service repair' or 'ocx start'.");
509
+ return false;
510
+ };
511
+ const recoverStoppedRuntime = async (reason: string): Promise<void> => {
512
+ try {
513
+ const current = await resolvedRuntimeOwnership();
514
+ const recovery = planStoppedRuntimeRecovery({
515
+ stopAttempted, ...current, sameOwner: sameOwner(current),
516
+ liveness: currentPackageRuntimeLiveness(), serviceInstalled: serviceWasInstalled,
517
+ launcherUsable: postUpdateLauncherUsable && existsSync(postUpdateLauncher), hadRuntimeState,
518
+ });
519
+ if (recovery.action === "manual") {
520
+ console.warn(`⚠️ ${reason}; runtime recovery requires manual review (${recovery.reason}).`);
521
+ } else if (recovery.action === "service") {
522
+ const { serviceReinstallArgs, isServiceViable } = await import("../service");
523
+ const service = spawnSync(process.execPath, [postUpdateLauncher, ...serviceReinstallArgs()], {
524
+ stdio: updateChildStdio(), windowsHide: true,
525
+ env: mutation.controlEnvironment({ ...process.env, OCX_BAKE_PORT: String(capturedListen.port) }),
526
+ });
527
+ if (service.status !== 0 || !isServiceViable()) {
528
+ const nowOwned = await resolvedRuntimeOwnership();
529
+ const fallback = planStoppedRuntimeRecovery({
530
+ stopAttempted, ...nowOwned, sameOwner: sameOwner(nowOwned),
531
+ liveness: currentPackageRuntimeLiveness(), serviceInstalled: false,
532
+ launcherUsable: postUpdateLauncherUsable && existsSync(postUpdateLauncher), hadRuntimeState,
533
+ });
534
+ if (fallback.action === "direct") await startProxyDirectly();
535
+ else console.warn("⚠️ Service recovery was not confirmed; no second proxy was started.");
536
+ }
537
+ } else if (recovery.action === "direct") {
538
+ await startProxyDirectly();
539
+ }
540
+ } catch {
541
+ console.warn("⚠️ Runtime recovery could not be verified; run 'ocx service repair' or 'ocx start' after checking ownership.");
542
+ }
543
+ };
544
+
545
+ try {
424
546
  // Never replace package files under a live proxy: the running server dynamic-imports
425
547
  // modules after startup, so an in-place update leaves it executing mixed old/new code.
426
548
  // Gate on the service and the runtime-port record too, not just the pid file — a
@@ -431,12 +553,13 @@ export async function runUpdate(): Promise<void> {
431
553
  // shared client config still points at a proxy that is gone; installing over that
432
554
  // silently skips the recovery the receipt was written to trigger (#3008).
433
555
  // Full `ocx stop` semantics (drain, service stop, restore).
434
- let stopAttempted = false;
435
- if (serviceWasInstalled || readPid() || readRuntimePort() || pendingTeardownOutstanding()) {
556
+
557
+ if (runtimePlan.mayStopRuntime && (serviceWasInstalled || readPid() || readRuntimePort() || pendingTeardownOutstanding())) {
436
558
  stopAttempted = true;
437
559
  console.log("⏹ Stopping the running proxy before updating...");
438
560
  const stopStdio = updateChildStdio();
439
561
  const stop = spawnSync(process.execPath, selfLaunchArgv(["stop"]), {
562
+ env: mutation.controlEnvironment(),
440
563
  stdio: stopStdio,
441
564
  encoding: stopStdio === "pipe" ? "utf8" : undefined,
442
565
  windowsHide: true,
@@ -472,7 +595,7 @@ export async function runUpdate(): Promise<void> {
472
595
  ? `⚠️ Could not confirm the proxy on ${capturedListen.hostname}:${capturedListen.port} is stopped; aborting the update. Run 'ocx stop' and retry.`
473
596
  : "⚠️ Could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
474
597
  }
475
- process.exit(1);
598
+ return 1;
476
599
  }
477
600
  if (historyOnlyStop || historyRestoreIncomplete()) {
478
601
  console.warn(
@@ -493,26 +616,34 @@ export async function runUpdate(): Promise<void> {
493
616
  }
494
617
  }
495
618
 
496
- console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
497
-
498
619
  const installStdio = updateChildStdio();
499
- // Every post-update action below receives this path. For pnpm it is replaced only
500
- // by a path returned after tree+shim verification; on rollback, activePath is
501
- // likewise returned only after the old group has been verified again.
502
- let postUpdateLauncher = join(packageRoot(), "bin", "ocx.mjs");
503
- // The pnpm owner preflight has verified this package tree and global group. Keep that exact
504
- // package path as the recovery starting point; the path returned by the update transaction
505
- // replaces it only after post-update tree+shim verification succeeds.
506
- if (installer === "pnpm" && owner) {
507
- postUpdateLauncher = join(owner.packagePath, "bin", "ocx.mjs");
508
- }
509
- let postUpdateLauncherUsable = true;
510
620
  let r: {
511
621
  status: number | null;
512
622
  signal?: NodeJS.Signals | null;
513
623
  stdout?: string | Buffer | null;
514
624
  stderr?: string | Buffer | null;
515
- };
625
+ } | null = null;
626
+ let replacementRefusal: string | null = null;
627
+ // Ownership can change while registry work is in flight. Unknown at this exact
628
+ // boundary blocks replacement; a confirmed desktop claim still permits updating the idle
629
+ // npm installation while leaving the bundled sidecar alone.
630
+ const replacementOwnership = await resolvedRuntimeOwnership();
631
+ const replacementPlan = planUpdateRuntimeHandling({
632
+ ...replacementOwnership,
633
+ serviceInstalled: serviceWasInstalled,
634
+ });
635
+ const replacementLiveness = currentPackageRuntimeLiveness();
636
+ if (replacementOwnership.subjectToken !== initialOwnership.subjectToken
637
+ || !replacementPlan.mayReplacePackage
638
+ || replacementLiveness !== "dead") {
639
+ replacementRefusal = replacementPlan.notice
640
+ ?? (replacementLiveness === "live"
641
+ ? "⚠️ Update stopped because a proxy became live after the stop decision; rerun from the beginning."
642
+ : "⚠️ Update stopped because runtime ownership or liveness changed after the stop decision; rerun from the beginning.");
643
+ } else {
644
+ console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
645
+
646
+ // Every post-update action below receives the verified active launcher.
516
647
  if (installer === "pnpm") {
517
648
  let update: ReturnType<typeof runPnpmGlobalUpdate>;
518
649
  try {
@@ -558,9 +689,27 @@ export async function runUpdate(): Promise<void> {
558
689
  timeout: 180000,
559
690
  windowsHide: true,
560
691
  ...target.options,
692
+ env: mutation.unprivilegedEnvironment(),
561
693
  });
562
694
  }
563
- if (installStdio === "pipe") logSpawnOutput("", r);
695
+ if (r && installStdio === "pipe") logSpawnOutput("", r);
696
+ }
697
+ if (replacementRefusal) {
698
+ await recoverStoppedRuntime("package replacement was refused");
699
+ if (trayWasRunning) {
700
+ try {
701
+ const { startWindowsTray } = await import("../tray/windows");
702
+ startWindowsTray();
703
+ } catch { /* preserve the ownership refusal */ }
704
+ }
705
+ console.error(replacementRefusal);
706
+ return 1;
707
+ }
708
+ if (!r) throw new Error("update replacement returned no result");
709
+ const postInstallPlan = planUpdateRuntimeHandling({
710
+ ...(await resolvedRuntimeOwnership()),
711
+ serviceInstalled: serviceWasInstalled,
712
+ });
564
713
  if (r.status === 0) {
565
714
  console.log(`\n✅ Updated${latest ? ` to v${latest}` : ""}.`);
566
715
  // Re-enter through the verified active package launcher. This keeps the Codex
@@ -572,6 +721,7 @@ export async function runUpdate(): Promise<void> {
572
721
  const shim = spawnSync(process.execPath, [postUpdateLauncher, "codex-shim", "install"], {
573
722
  stdio: "inherit",
574
723
  windowsHide: true,
724
+ env: mutation.unprivilegedEnvironment(),
575
725
  });
576
726
  if (shim.status !== 0) console.warn("⚠️ Shim repair skipped: run 'ocx codex-shim install'.");
577
727
  }
@@ -580,18 +730,18 @@ export async function runUpdate(): Promise<void> {
580
730
  }
581
731
  if (trayWasInstalled) {
582
732
  const trayArgs = planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs;
583
- const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayArgs], { stdio: "inherit", windowsHide: true });
733
+ const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayArgs], { stdio: "inherit", windowsHide: true, env: mutation.unprivilegedEnvironment() });
584
734
  if (tray.status === 0) {
585
735
  console.log("🔧 Refreshed Windows tray startup paths.");
586
736
  } else {
587
737
  console.warn("⚠️ Windows tray refresh failed. Run 'ocx tray install'.");
588
- if (trayWasRunning) spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true });
738
+ if (trayWasRunning) spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true, env: mutation.unprivilegedEnvironment() });
589
739
  }
590
740
  }
591
741
  // The stop above unloaded any managed service; repair it with the NEW files
592
742
  // (spawn the fresh cli.ts so updated code writes the baked paths) so a
593
743
  // launchd/schtasks/systemd user isn't left with the background proxy down.
594
- if (serviceWasInstalled) {
744
+ if (postInstallPlan.mayRestoreService) {
595
745
  console.log("🔁 Refreshing the background service with the updated files...");
596
746
  const { serviceReinstallArgs } = await import("../service");
597
747
  const { reclaimListenPort } = await import("../server/port-reclaim");
@@ -610,6 +760,7 @@ export async function runUpdate(): Promise<void> {
610
760
  try {
611
761
  const svcStdio = updateChildStdio();
612
762
  const svc = spawnSync(process.execPath, [postUpdateLauncher, ...serviceReinstallArgs()], {
763
+ env: mutation.controlEnvironment(),
613
764
  stdio: svcStdio,
614
765
  encoding: svcStdio === "pipe" ? "utf8" : undefined,
615
766
  windowsHide: true,
@@ -641,6 +792,17 @@ export async function runUpdate(): Promise<void> {
641
792
  ? ` Run 'ocx service repair', then 'ocx start --port ${capturedListen.port}'.`
642
793
  : ` Run 'ocx service repair' to see the reason, then 'ocx start --port ${capturedListen.port}'.`);
643
794
  } else {
795
+ // Re-read rather than reuse the plan from before the package install: the app can
796
+ // claim the runtime during an update that takes minutes, and the refusal the
797
+ // repair above just returned is indistinguishable from any other failure here.
798
+ const nowOwned = planUpdateRuntimeHandling({
799
+ ...(await resolvedRuntimeOwnership()),
800
+ serviceInstalled: true,
801
+ });
802
+ if (!nowOwned.mayStopRuntime) {
803
+ console.warn(nowOwned.notice ?? "⚠️ The background runtime is owned elsewhere; not starting a second proxy.");
804
+ return;
805
+ }
644
806
  console.warn(
645
807
  serviceRefreshed
646
808
  ? "⚠️ Service refresh left a non-viable manager (stale or missing assets) — starting the proxy directly instead."
@@ -652,49 +814,31 @@ export async function runUpdate(): Promise<void> {
652
814
  console.warn(process.platform === "win32"
653
815
  ? " Run 'ocx service repair' to refresh the background service."
654
816
  : " Run 'ocx service repair' to refresh the background service and see why it failed.");
655
- const env = { ...process.env };
656
- delete env.OCX_SERVICE;
657
- const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
658
- detached: true,
659
- stdio: "ignore",
660
- windowsHide: true,
661
- env: withProcessRuntimeProvenance(env),
662
- });
663
- child.unref();
664
- console.log(`✅ Proxy starting on port ${capturedListen.port}.`);
817
+ if (await startProxyDirectly()) console.log(`✅ Proxy is healthy on port ${capturedListen.port}.`);
665
818
  }
666
819
  }
667
820
  } finally {
668
821
  if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
669
822
  else process.env.OCX_BAKE_PORT = prevBake;
670
823
  }
671
- } else {
824
+ } else if (postInstallPlan.mayStopRuntime) {
672
825
  console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, capturedListen.port)}`);
673
826
  }
674
827
  } else {
675
828
  if (stopAttempted && trayWasRunning && postUpdateLauncherUsable) {
676
- spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true });
677
- }
678
- if (stopAttempted && serviceWasInstalled && postUpdateLauncherUsable) {
679
- const service = spawnSync(process.execPath, [postUpdateLauncher, "service", "repair"], {
680
- stdio: "inherit",
681
- windowsHide: true,
682
- });
683
- if (service.status !== 0) console.warn("⚠️ Previous background service could not be restored; run 'ocx service repair'.");
684
- } else if (stopAttempted && postUpdateLauncherUsable) {
685
- const env = { ...process.env };
686
- delete env.OCX_SERVICE;
687
- const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
688
- detached: true,
689
- stdio: "ignore",
690
- windowsHide: true,
691
- env: withProcessRuntimeProvenance(env),
692
- });
693
- child.unref();
694
- } else if (stopAttempted) {
695
- console.error("opencodex: no verified active launcher remains for automatic recovery; reinstall opencodex manually.");
829
+ spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true, env: mutation.unprivilegedEnvironment() });
696
830
  }
831
+ await recoverStoppedRuntime("package update failed");
697
832
  console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`);
698
- process.exit(1);
833
+ return 1;
699
834
  }
835
+ } catch (error) {
836
+ await recoverStoppedRuntime("update failed unexpectedly");
837
+ if (trayWasRunning) {
838
+ try { const { startWindowsTray } = await import("../tray/windows"); startWindowsTray(); } catch { /* retain original failure */ }
839
+ }
840
+ throw error;
841
+ }
842
+ });
843
+ if (updateExitCode) process.exit(updateExitCode);
700
844
  }
package/src/update/job.ts CHANGED
@@ -26,6 +26,7 @@ import { listListenPids, reclaimListenPort, scanListenPids, type ListenPidScan }
26
26
  import { dropWindowsTcpRowsForLocalPort } from "../server/windows-tcp-drop";
27
27
  import { isOpencodexHealthz, probeHostname, proxyIdentityAt, type HealthzIdentity } from "../server/proxy-liveness";
28
28
  import { isServiceInstalled, isServiceViable, readServiceBackend, stopWindows } from "../service";
29
+ import { runUpdateRestartWithOwnershipLease, type ServiceOwnershipResolution } from "./restart-ownership";
29
30
  import {
30
31
  type Channel,
31
32
  type Installer,
@@ -1804,6 +1805,8 @@ export interface GuiUpdateWorkerIo {
1804
1805
  resolvePnpmActiveLauncherFn?: (owner: PnpmGlobalOwner) => string | null;
1805
1806
  /** Restart seams used by focused worker tests; the verified launcher is always injected. */
1806
1807
  restartIo?: RestartIo;
1808
+ /** Resolves who owns the runtime; defaults to the shared service install state. */
1809
+ resolveOwnershipFn?: () => ServiceOwnershipResolution;
1807
1810
  runCommandFn?: (
1808
1811
  job: UpdateJobState,
1809
1812
  bin: string,
@@ -1967,11 +1970,12 @@ export async function runGuiUpdateWorker(
1967
1970
  }
1968
1971
 
1969
1972
  if (restart) {
1970
- job = updateJob(job, { status: "restarting" }, "Update installed. Restarting proxy...");
1971
- if (!(await finishGuiUpdateRestart(job, captured, check.installer, {
1972
- ...io.restartIo,
1973
- packageLauncherPathFn: () => activeLauncher,
1974
- }))) return;
1973
+ const outcome = await runUpdateRestartWithOwnershipLease(io.resolveOwnershipFn, async () => {
1974
+ job = updateJob(job!, { status: "restarting" }, "Update installed. Restarting proxy...");
1975
+ return finishGuiUpdateRestart(job!, captured, check.installer, { ...io.restartIo, packageLauncherPathFn: () => activeLauncher });
1976
+ });
1977
+ if (outcome.kind === "veto") { updateJob(job, { status: "succeeded", restarted: false }, outcome.notice); return; }
1978
+ if (!outcome.value) return;
1975
1979
  updateJob(job, { status: "succeeded", restarted: true }, "Restart requested and proxy is healthy.");
1976
1980
  return;
1977
1981
  }
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from "node:fs";
2
+ import {
3
+ acquireOwnershipMutationLease,
4
+ ownershipMutationLeaseChildEnvironment,
5
+ unprivilegedOwnershipMutationEnvironment,
6
+ } from "../service/ownership-mutation-lease.mjs";
7
+
8
+ type Environment = Record<string, string | undefined>;
9
+ export interface UpdateOwnershipTransaction {
10
+ controlEnvironment(environment?: Environment): Environment;
11
+ unprivilegedEnvironment(environment?: Environment): Environment;
12
+ }
13
+
14
+ /** One lease spans awaited stop, replacement and recovery. Never publish its token globally. */
15
+ export async function withUpdateOwnershipLease<T>(
16
+ paths: string[], run: (transaction: UpdateOwnershipTransaction) => Promise<T>,
17
+ ): Promise<T> {
18
+ const lease = acquireOwnershipMutationLease(paths);
19
+ let released = false;
20
+ const release = () => { if (!released) { released = true; lease.release(); } };
21
+ try {
22
+ return await run({
23
+ controlEnvironment: (env = process.env) => ownershipMutationLeaseChildEnvironment(env, lease.token),
24
+ unprivilegedEnvironment: (env = process.env) => unprivilegedOwnershipMutationEnvironment(env),
25
+ });
26
+ } finally { release(); }
27
+ }
28
+
29
+ export type UpdateRuntimeTarget =
30
+ | { kind: "absent" | "unknown" }
31
+ | { kind: "target"; target: { pid: number; port: number; hostname: string } };
32
+
33
+ /** A malformed/unreadable record is uncertainty, never proof that the current runtime is absent. */
34
+ export function readUpdateRuntimeTarget(path: string, fallbackHostname: string): UpdateRuntimeTarget {
35
+ let raw: string;
36
+ try { raw = readFileSync(path, "utf8"); }
37
+ catch (error) { return { kind: (error as NodeJS.ErrnoException).code === "ENOENT" ? "absent" : "unknown" }; }
38
+ try {
39
+ const value = JSON.parse(raw);
40
+ if (!Number.isSafeInteger(value?.pid) || value.pid <= 0
41
+ || !Number.isInteger(value?.port) || value.port <= 0 || value.port > 65535
42
+ || (value.hostname !== undefined && (typeof value.hostname !== "string" || !value.hostname.trim()))) {
43
+ return { kind: "unknown" };
44
+ }
45
+ return { kind: "target", target: { pid: value.pid, port: value.port, hostname: value.hostname?.trim() ?? fallbackHostname } };
46
+ } catch { return { kind: "unknown" }; }
47
+ }
@@ -0,0 +1,54 @@
1
+ import { resolveServiceOwnership } from "../service";
2
+ import type { ServiceOwnershipResolution } from "../service";
3
+ import { serviceStatePaths } from "../service/state";
4
+ import {
5
+ acquireOwnershipMutationLease,
6
+ OWNERSHIP_MUTATION_LEASE_TOKEN_ENV,
7
+ } from "../service/ownership-mutation-lease.mjs";
8
+ import { planUpdateRuntimeHandling } from "./runtime-ownership.mjs";
9
+
10
+ export type { ServiceOwnershipResolution };
11
+
12
+ /**
13
+ * Why a dashboard update must not restart a runtime it did not stop.
14
+ *
15
+ * The worker defaults to restarting. When the package updater it just ran left a
16
+ * foreign-owned runtime alone — which is the correct behaviour under a desktop takeover —
17
+ * the sidecar's unchanged pid fails the worker's restart evidence, so it reclaims the port,
18
+ * runs the repair that now refuses, and falls through to a direct start. The app's runtime
19
+ * ends up replaced by an npm proxy immediately after the update declined to touch it.
20
+ *
21
+ * Returns the line to report, or null when the restart may proceed. It lives beside the
22
+ * worker rather than inside it because `src/update/job.ts` is one line under the repository
23
+ * size cap.
24
+ */
25
+ export function updateRestartVeto(
26
+ resolve: () => ServiceOwnershipResolution = resolveServiceOwnership,
27
+ ): string | null {
28
+ const owner = resolve();
29
+ const plan = planUpdateRuntimeHandling({
30
+ ownership: owner.kind === "owned" ? owner.ownership : null,
31
+ ownershipUnknown: owner.kind === "unknown",
32
+ // The restart decision does not refresh the service; only the stop veto is read here.
33
+ serviceInstalled: false,
34
+ });
35
+ if (plan.mayStopRuntime) return null;
36
+ return plan.notice ?? "The background runtime is owned elsewhere; it was left running.";
37
+ }
38
+
39
+ export async function runUpdateRestartWithOwnershipLease<T>(
40
+ resolve: (() => ServiceOwnershipResolution) | undefined,
41
+ restart: () => Promise<T>,
42
+ ): Promise<{ readonly kind: "veto"; readonly notice: string } | { readonly kind: "ran"; readonly value: T }> {
43
+ const lease = acquireOwnershipMutationLease(serviceStatePaths());
44
+ const previous = process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
45
+ process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = lease.token;
46
+ try {
47
+ const veto = updateRestartVeto(resolve);
48
+ return veto ? { kind: "veto", notice: veto } : { kind: "ran", value: await restart() };
49
+ } finally {
50
+ if (previous === undefined) delete process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
51
+ else process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = previous;
52
+ lease.release();
53
+ }
54
+ }
@@ -0,0 +1,40 @@
1
+ export declare function planUpdateRuntimeHandling(input: {
2
+ ownership: { owner: string; installId: string; consentGeneration: number } | null;
3
+ ownershipUnknown?: boolean;
4
+ serviceInstalled: boolean;
5
+ }): {
6
+ mayReplacePackage: boolean;
7
+ mayStopRuntime: boolean;
8
+ mayRestoreService: boolean;
9
+ notice: string | null;
10
+ };
11
+
12
+ export declare function planStoppedRuntimeRecovery(input: {
13
+ stopAttempted: boolean;
14
+ ownership: { owner: string; installId: string; consentGeneration: number } | null;
15
+ ownershipUnknown?: boolean;
16
+ sameOwner: boolean;
17
+ liveness: "live" | "dead" | "unknown";
18
+ serviceInstalled: boolean;
19
+ launcherUsable: boolean;
20
+ hadRuntimeState: boolean;
21
+ }): {
22
+ action: "none" | "manual" | "service" | "direct";
23
+ reason: string;
24
+ };
25
+
26
+ type RuntimeTarget = { port: number; hostname: string };
27
+ type RuntimeLiveness = "live" | "dead" | "unknown";
28
+
29
+ export declare function inspectPackageRuntimeLiveness(input: {
30
+ capturedTarget: RuntimeTarget;
31
+ readCurrentTarget():
32
+ | { kind: "target"; target: RuntimeTarget }
33
+ | { kind: "absent" }
34
+ | { kind: "unknown" };
35
+ probe(target: RuntimeTarget): RuntimeLiveness;
36
+ }): {
37
+ current: RuntimeLiveness | "absent";
38
+ captured: RuntimeLiveness;
39
+ overall: RuntimeLiveness;
40
+ };