@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -0,0 +1,466 @@
1
+ /**
2
+ * The opt-in service health sentinel process (#6671).
3
+ *
4
+ * The proxy spawns this with its stdin as a pipe whose write end only the proxy holds. When the
5
+ * proxy exits for any reason the operating system closes that pipe and this process sees EOF and
6
+ * leaves. It asks `/healthz` on the cadence in health-sentinel-policy.ts and, when the policy says
7
+ * the proxy has stopped answering, records the recovery and terminates the proxy. The supervisor
8
+ * that started the proxy — the Task Scheduler batch loop, launchd, systemd, WinSW — restarts it as
9
+ * it would after any crash. Nothing is replayed.
10
+ *
11
+ * The kill is bound to the proxy, not to a number:
12
+ * - Windows reuses ids quickly, so the sentinel opens a handle on its parent at startup, accepts it
13
+ * only if that process was created before the sentinel, and terminates through the handle
14
+ * (health-sentinel-windows.ts). While the handle is open the id cannot be reused.
15
+ * - POSIX allocates ids sequentially, and the sentinel's live `process.ppid` changes the moment
16
+ * its parent exits, before the exited parent can be reaped and its id freed. It is the last check
17
+ * before `kill`. Reuse inside that window needs the parent to exit, be reaped, and the id space to
18
+ * wrap completely (99,999 ids on macOS, `pid_max` on Linux) between two adjacent syscalls; that
19
+ * residual is accepted and documented in structure/ops/service-health-recovery.md.
20
+ *
21
+ * SIGKILL rather than SIGTERM: a proxy whose event loop is stuck cannot run its own SIGTERM
22
+ * handler, and systemd counts a SIGTERM death as a clean exit that `Restart=on-failure` does not
23
+ * restart.
24
+ *
25
+ * The final checks and the `intent` write run under the service ownership mutation lease, so the
26
+ * decision cannot interleave with a service install, repair, takeover or startup ownership
27
+ * decision; a busy lease defers. The lease is released before the kill itself: on Windows the
28
+ * sentinel can be terminated together with the proxy, and a lease it still held would block the
29
+ * replacement's startup until reclaimed. Every history update takes the same lease. No synchronous
30
+ * subprocess runs here: a sentinel that can hang protects nothing.
31
+ */
32
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
33
+ import { dirname } from "node:path";
34
+
35
+ import {
36
+ DEFAULT_HEALTH_SENTINEL_TIMING,
37
+ createHealthSentinelPolicy,
38
+ type HealthProbeOutcome,
39
+ type HealthSentinelTiming,
40
+ } from "./health-sentinel-policy";
41
+ import { probeHealth, probeUrlHost, type HealthProbeTarget } from "./health-sentinel-probe";
42
+
43
+ export const HEALTH_SENTINEL_ENV = {
44
+ targetPid: "OCX_HEALTH_SENTINEL_TARGET_PID",
45
+ runtimePath: "OCX_HEALTH_SENTINEL_RUNTIME_PATH",
46
+ historyPath: "OCX_HEALTH_SENTINEL_HISTORY_PATH",
47
+ logPath: "OCX_HEALTH_SENTINEL_LOG_PATH",
48
+ /** JSON array of the service-state paths whose ownership mutation lease guards the kill. */
49
+ statePaths: "OCX_HEALTH_SENTINEL_STATE_PATHS",
50
+ /** Test-only timing override (JSON, partial); production uses the policy defaults. */
51
+ timing: "OCX_HEALTH_SENTINEL_TIMING",
52
+ } as const;
53
+
54
+ const HISTORY_LIMIT = 10;
55
+ const LOG_CAP_BYTES = 256 * 1024;
56
+ const EXIT_WAIT_MS = 10_000;
57
+ const LEASE_WAIT_MS = 500;
58
+
59
+ export type SentinelRuntimeRecord = { pid: number; port: number; hostname?: string; sibling: boolean };
60
+
61
+ /**
62
+ * One recovery. `intent` is written before the kill; the entry is then settled as `terminated`
63
+ * (exit observed), `unconfirmed` (no exit observed in time), `failed` (the kill was refused) or
64
+ * `skipped` (the target had already gone). Every state but `skipped` counts toward the cap.
65
+ */
66
+ export type RecoveryEntry = { at: number; pid: number; state: RecoveryState };
67
+ export type RecoveryState = "intent" | "terminated" | "unconfirmed" | "failed" | "skipped";
68
+ const RECOVERY_STATES: readonly RecoveryState[] = ["intent", "terminated", "unconfirmed", "failed", "skipped"];
69
+
70
+ export type TerminateResult = "terminated" | "not_target" | "failed";
71
+
72
+ export type HealthSentinelDeps = {
73
+ targetPid: number;
74
+ now(): number;
75
+ sleep(ms: number): Promise<void>;
76
+ probe(target: HealthProbeTarget, timeoutMs: number): Promise<HealthProbeOutcome>;
77
+ readRuntime(): SentinelRuntimeRecord | null;
78
+ /** Resolves when the target's pipe closes: the target has exited. */
79
+ targetExited: Promise<void>;
80
+ isTargetGone(): boolean;
81
+ /** Bind to the target once at startup; a failure stands the sentinel down. */
82
+ bindTarget(): { ok: true } | { ok: false; reason: string };
83
+ /** Terminate the bound target, re-proving it is still the target at the last moment. */
84
+ terminateTarget(): TerminateResult;
85
+ /** Run under the ownership mutation lease, released on return; "busy" when it could not be taken promptly. */
86
+ withOwnershipLease<T>(run: () => T): T | "busy";
87
+ /** Earlier recoveries that count toward the cap, or null when the history is unreadable. */
88
+ readHistory(): number[] | null;
89
+ /** Durably write or settle one entry; false when it could not be written. */
90
+ writeHistory(entry: RecoveryEntry): boolean;
91
+ log(line: string): void;
92
+ timing?: Partial<HealthSentinelTiming>;
93
+ };
94
+
95
+ export type HealthSentinelResult =
96
+ | { outcome: "target_exited" }
97
+ | { outcome: "stood_down"; reason: string }
98
+ | { outcome: "recovered"; state: RecoveryState };
99
+
100
+ const GONE = Symbol("gone");
101
+
102
+ /**
103
+ * How late this process's own event loop has run, measured on the wall clock so that a machine
104
+ * sleep counts too. The policy discards a timeout observed while the sentinel itself was late.
105
+ */
106
+ export function startLagMonitor(tickMs = 250, now: () => number = Date.now) {
107
+ let last = now();
108
+ let max = 0;
109
+ const timer = setInterval(() => {
110
+ const at = now();
111
+ max = Math.max(max, at - last - tickMs);
112
+ last = at;
113
+ }, tickMs);
114
+ (timer as { unref?: () => void }).unref?.();
115
+ return {
116
+ reset(): void { max = 0; last = now(); },
117
+ read(): number { return Math.max(max, now() - last - tickMs); },
118
+ stop(): void { clearInterval(timer); },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Race `work` against the target's exit. The exit is one AbortSignal subscribed once per run, and
124
+ * each race detaches its listener when it settles: a sentinel that probes a healthy proxy for
125
+ * months must not accumulate a handler per probe on one never-settling promise.
126
+ */
127
+ async function raceGone<T>(work: Promise<T>, exited: AbortSignal): Promise<T | typeof GONE> {
128
+ if (exited.aborted) return GONE;
129
+ let onExit: () => void = () => {};
130
+ const gone = new Promise<typeof GONE>(resolve => {
131
+ onExit = () => resolve(GONE);
132
+ exited.addEventListener("abort", onExit, { once: true });
133
+ });
134
+ try {
135
+ return await Promise.race([work, gone]);
136
+ } finally {
137
+ exited.removeEventListener("abort", onExit);
138
+ }
139
+ }
140
+
141
+ type Attempt =
142
+ | { kind: "stand_down"; reason: string }
143
+ | { kind: "gone" }
144
+ | { kind: "intent"; entry: RecoveryEntry };
145
+
146
+ function matchesProbeTarget(runtime: SentinelRuntimeRecord | null, pid: number, target: HealthProbeTarget): boolean {
147
+ return runtime !== null && runtime.pid === pid && !runtime.sibling
148
+ && runtime.port === target.port && probeUrlHost(runtime.hostname) === target.hostname;
149
+ }
150
+
151
+ export async function runHealthSentinel(deps: HealthSentinelDeps): Promise<HealthSentinelResult> {
152
+ const timing: HealthSentinelTiming = { ...DEFAULT_HEALTH_SENTINEL_TIMING, ...deps.timing };
153
+ const standDown = (reason: string): HealthSentinelResult => {
154
+ deps.log(`standing down: ${reason} (pid ${deps.targetPid})`);
155
+ return { outcome: "stood_down", reason };
156
+ };
157
+ const exit = new AbortController();
158
+ void deps.targetExited.then(() => exit.abort());
159
+ const exitSignal = exit.signal;
160
+ const bound = deps.bindTarget();
161
+ if (!bound.ok) return standDown(bound.reason);
162
+ const history = deps.readHistory();
163
+ if (history === null) return standDown("the recovery history cannot be read");
164
+ const policy = createHealthSentinelPolicy({
165
+ targetPid: deps.targetPid,
166
+ startedAt: deps.now(),
167
+ recoveryHistory: history,
168
+ timing,
169
+ });
170
+ let pinnedTarget: HealthProbeTarget | null = null;
171
+ for (;;) {
172
+ if (deps.isTargetGone()) return { outcome: "target_exited" };
173
+ const runtime = deps.readRuntime();
174
+ if (!runtime || runtime.pid !== deps.targetPid) return standDown("the runtime record no longer names the watched process");
175
+ if (runtime.sibling) return standDown("the watched process is a sibling proxy");
176
+ const hostname = probeUrlHost(runtime.hostname);
177
+ if (hostname === null) return standDown("the recorded bind address is not numeric; DNS names cannot authorize recovery");
178
+ const target: HealthProbeTarget = pinnedTarget ?? { hostname, port: runtime.port };
179
+ if (!matchesProbeTarget(runtime, deps.targetPid, target)) return standDown("the recorded probe endpoint changed");
180
+ pinnedTarget = target;
181
+ const outcome = await raceGone(deps.probe(target, timing.probeTimeoutMs), exitSignal);
182
+ if (outcome === GONE) return { outcome: "target_exited" };
183
+ const wasArmed = policy.state().armed;
184
+ const decision = policy.observe(outcome);
185
+ if (!wasArmed && policy.state().armed) deps.log(`watching pid ${deps.targetPid} on port ${runtime.port}`);
186
+ if (decision.action === "stand_down") return standDown(decision.reason);
187
+ if (decision.action === "recover") {
188
+ const attempt = deps.withOwnershipLease((): Attempt => {
189
+ // Everything that could have changed during the probe is re-read under the lease.
190
+ if (deps.isTargetGone()) return { kind: "gone" };
191
+ const current = deps.readRuntime();
192
+ if (!matchesProbeTarget(current, deps.targetPid, target)) {
193
+ return { kind: "stand_down", reason: "the runtime record changed while the target was unresponsive" };
194
+ }
195
+ // Persist first: an attempted recovery must count toward the cap, and on Windows this
196
+ // process may be terminated together with the target.
197
+ const entry: RecoveryEntry = { at: deps.now(), pid: deps.targetPid, state: "intent" };
198
+ if (!deps.writeHistory(entry)) return { kind: "stand_down", reason: "the recovery history cannot be written" };
199
+ deps.log(`recovering pid ${deps.targetPid} on port ${runtime.port}: ${decision.reason}; terminating it so its supervisor restarts it (in-flight requests are not replayed)`);
200
+ return { kind: "intent", entry };
201
+ });
202
+ const settle = (entry: RecoveryEntry) => {
203
+ // Best-effort: an unsettled entry stays `intent`, which still counts toward the cap.
204
+ if (deps.withOwnershipLease(() => deps.writeHistory(entry)) === "busy") deps.log("could not settle the recovery entry: the service ownership lease is busy");
205
+ };
206
+ if (attempt === "busy") {
207
+ deps.log("recovery deferred: the service ownership lease is busy");
208
+ } else if (attempt.kind === "gone") {
209
+ return { outcome: "target_exited" };
210
+ } else if (attempt.kind === "stand_down") {
211
+ return standDown(attempt.reason);
212
+ } else {
213
+ const { entry } = attempt;
214
+ // The lease, history and log work above takes time. Re-check that the evidence is still
215
+ // fresh right before the irreversible step: a proxy that recovered meanwhile must not be
216
+ // killed on old observations. A clock that ran backwards makes the evidence stale too. The
217
+ // final runtime-record read is included before the freshness check so time spent on it
218
+ // cannot authorize termination on stale observations.
219
+ const finalRuntime = deps.readRuntime();
220
+ const evidenceAge = deps.now() - outcome.endedAt;
221
+ if (evidenceAge < 0 || evidenceAge > timing.maxEvidenceAgeMs || deps.isTargetGone()) {
222
+ settle({ ...entry, state: "skipped" });
223
+ if (deps.isTargetGone()) return { outcome: "target_exited" };
224
+ deps.log(`discarding stale evidence (${Math.round(evidenceAge / 1000)}s old) for pid ${deps.targetPid}; observing again`);
225
+ if (await raceGone(deps.sleep(policy.nextDelayMs()), exitSignal) === GONE) return { outcome: "target_exited" };
226
+ continue;
227
+ }
228
+ if (!matchesProbeTarget(finalRuntime, deps.targetPid, target)) {
229
+ settle({ ...entry, state: "skipped" });
230
+ return standDown("the recorded probe endpoint changed before termination");
231
+ }
232
+ const result = deps.terminateTarget();
233
+ if (result === "not_target") {
234
+ settle({ ...entry, state: "skipped" });
235
+ return { outcome: "target_exited" };
236
+ }
237
+ if (result === "failed") {
238
+ settle({ ...entry, state: "failed" });
239
+ return standDown("termination was refused");
240
+ }
241
+ const exited = (await raceGone(deps.sleep(EXIT_WAIT_MS), exitSignal)) === GONE || deps.isTargetGone();
242
+ const state: RecoveryState = exited ? "terminated" : "unconfirmed";
243
+ settle({ ...entry, state });
244
+ deps.log(exited ? `pid ${deps.targetPid} exited` : `pid ${deps.targetPid} did not exit within ${EXIT_WAIT_MS / 1000}s`);
245
+ return { outcome: "recovered", state };
246
+ }
247
+ }
248
+ if (await raceGone(deps.sleep(policy.nextDelayMs()), exitSignal) === GONE) return { outcome: "target_exited" };
249
+ }
250
+ }
251
+
252
+ function isRecoveryEntry(value: unknown): value is RecoveryEntry {
253
+ const entry = value as RecoveryEntry | null;
254
+ return typeof entry === "object" && entry !== null
255
+ && typeof entry.at === "number" && Number.isFinite(entry.at)
256
+ && typeof entry.pid === "number" && Number.isSafeInteger(entry.pid)
257
+ && RECOVERY_STATES.includes(entry.state);
258
+ }
259
+
260
+ /** Recovery entries, `[]` when no history exists, null when one exists but cannot be read. */
261
+ export function readRecoveryEntries(path: string): RecoveryEntry[] | null {
262
+ let raw: string;
263
+ try {
264
+ raw = readFileSync(path, "utf8");
265
+ } catch (error) {
266
+ return (error as NodeJS.ErrnoException).code === "ENOENT" ? [] : null;
267
+ }
268
+ try {
269
+ const parsed = JSON.parse(raw) as { recoveries?: unknown };
270
+ if (!Array.isArray(parsed.recoveries) || !parsed.recoveries.every(isRecoveryEntry)) return null;
271
+ return parsed.recoveries.slice(-HISTORY_LIMIT);
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+
277
+ /** Times of recoveries that count toward the cap. */
278
+ export function countedRecoveryTimes(entries: readonly RecoveryEntry[]): number[] {
279
+ return entries.filter(entry => entry.state !== "skipped").map(entry => entry.at);
280
+ }
281
+
282
+ /**
283
+ * Durably write `entry`, replacing the entry with the same time and pid. The temp file is synced
284
+ * before the rename, so a crash leaves either the old history or the new one. Writers are
285
+ * serialized by the ownership lease the runner holds around the intent write.
286
+ */
287
+ export function writeRecoveryEntry(path: string, entry: RecoveryEntry): boolean {
288
+ const existing = readRecoveryEntries(path);
289
+ if (existing === null) return false;
290
+ const others = existing.filter(item => item.at !== entry.at || item.pid !== entry.pid);
291
+ const recoveries = [...others, entry].sort((a, b) => a.at - b.at).slice(-HISTORY_LIMIT);
292
+ const temporary = `${path}.${process.pid}.tmp`;
293
+ let descriptor: number | null = null;
294
+ try {
295
+ mkdirSync(dirname(path), { recursive: true });
296
+ descriptor = openSync(temporary, "w", 0o600);
297
+ writeSync(descriptor, `${JSON.stringify({ recoveries })}\n`);
298
+ fsyncSync(descriptor);
299
+ closeSync(descriptor);
300
+ descriptor = null;
301
+ renameSync(temporary, path);
302
+ // The rename is the publication; a storage error here means it may not survive a crash.
303
+ return syncDirectory(dirname(path));
304
+ } catch {
305
+ if (descriptor !== null) { try { closeSync(descriptor); } catch { /* already closed */ } }
306
+ try { unlinkSync(temporary); } catch { /* absent */ }
307
+ return false;
308
+ }
309
+ }
310
+
311
+ /** Codes that mean "this filesystem does not sync directories", not "the write may be lost". */
312
+ const DIRECTORY_SYNC_UNSUPPORTED = new Set(["EINVAL", "ENOTSUP", "EOPNOTSUPP", "EISDIR", "EBADF"]);
313
+
314
+ /**
315
+ * Publish a rename durably. Windows cannot open a directory for fsync, and some filesystems
316
+ * refuse it; both are treated as supported-as-far-as-possible. Any other failure (EIO, ENOSPC...)
317
+ * is a storage error and is reported, so no termination follows an intent that may be lost.
318
+ */
319
+ export function syncDirectory(path: string, io: { open?: typeof openSync; fsync?: typeof fsyncSync } = {}): boolean {
320
+ if (process.platform === "win32") return true;
321
+ let descriptor: number | null = null;
322
+ try {
323
+ descriptor = (io.open ?? openSync)(path, "r");
324
+ (io.fsync ?? fsyncSync)(descriptor);
325
+ return true;
326
+ } catch (error) {
327
+ return DIRECTORY_SYNC_UNSUPPORTED.has((error as NodeJS.ErrnoException).code ?? "");
328
+ } finally {
329
+ if (descriptor !== null) { try { closeSync(descriptor); } catch { /* already closed */ } }
330
+ }
331
+ }
332
+
333
+ export function appendBoundedLog(path: string, line: string, now = new Date()): void {
334
+ try {
335
+ mkdirSync(dirname(path), { recursive: true });
336
+ let size = 0;
337
+ try { size = statSync(path).size; } catch { /* absent */ }
338
+ const entry = `[${now.toISOString()}] ${line}\n`;
339
+ if (size + entry.length > LOG_CAP_BYTES) writeFileSync(path, entry, "utf8");
340
+ else writeFileSync(path, entry, { encoding: "utf8", flag: "a" });
341
+ } catch {
342
+ // Logging must never stop the sentinel.
343
+ }
344
+ }
345
+
346
+ export function readSentinelRuntime(path: string): SentinelRuntimeRecord | null {
347
+ try {
348
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
349
+ const { pid, port, hostname } = parsed;
350
+ if (hostname !== undefined && typeof hostname !== "string") return null;
351
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return null;
352
+ if (typeof port !== "number" || !Number.isSafeInteger(port) || port <= 0 || port > 65_535) return null;
353
+ return { pid, port, ...(typeof hostname === "string" ? { hostname } : {}), sibling: parsed.siblingOfPort !== undefined };
354
+ } catch {
355
+ return null;
356
+ }
357
+ }
358
+
359
+ /** Partial timing from the test override; anything malformed is ignored. */
360
+ export function parseTimingOverride(raw: string | undefined): Partial<HealthSentinelTiming> {
361
+ if (!raw) return {};
362
+ try {
363
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
364
+ const timing: Partial<HealthSentinelTiming> = {};
365
+ for (const key of Object.keys(DEFAULT_HEALTH_SENTINEL_TIMING) as (keyof HealthSentinelTiming)[]) {
366
+ const value = parsed[key];
367
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) timing[key] = value;
368
+ }
369
+ return timing;
370
+ } catch {
371
+ return {};
372
+ }
373
+ }
374
+
375
+ function parseStatePaths(raw: string | undefined): string[] | null {
376
+ try {
377
+ const parsed = JSON.parse(raw ?? "") as unknown;
378
+ return Array.isArray(parsed) && parsed.length > 0 && parsed.every(item => typeof item === "string") ? parsed : null;
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+
384
+ /** Entry used by `bun health-sentinel-main.ts` and by the compiled binary's private entrypoint. */
385
+ export async function runHealthSentinelProcess(env: NodeJS.ProcessEnv = process.env): Promise<number> {
386
+ const targetPid = Number(env[HEALTH_SENTINEL_ENV.targetPid]);
387
+ const runtimePath = env[HEALTH_SENTINEL_ENV.runtimePath];
388
+ const historyPath = env[HEALTH_SENTINEL_ENV.historyPath];
389
+ const logPath = env[HEALTH_SENTINEL_ENV.logPath];
390
+ const statePaths = parseStatePaths(env[HEALTH_SENTINEL_ENV.statePaths]);
391
+ if (!Number.isSafeInteger(targetPid) || targetPid <= 0 || !runtimePath || !historyPath || !logPath || !statePaths) return 2;
392
+ let gone = false;
393
+ const targetExited = new Promise<void>(resolve => {
394
+ const done = () => { gone = true; resolve(); };
395
+ process.stdin.on("end", done);
396
+ process.stdin.on("close", done);
397
+ process.stdin.on("error", done);
398
+ process.stdin.on("data", () => { /* the proxy never writes; drain anything */ });
399
+ process.stdin.resume();
400
+ });
401
+ const { acquireOwnershipMutationLease } = await import("./ownership-mutation-lease.mjs");
402
+ let windowsHandle: import("./health-sentinel-windows").ParentProcessHandle | null = null;
403
+ let windowsBindFailure: string | null = null;
404
+ if (process.platform === "win32") {
405
+ const { openParentProcessHandle } = await import("./health-sentinel-windows");
406
+ const opened = openParentProcessHandle(targetPid);
407
+ if (opened.ok) windowsHandle = opened.handle;
408
+ else windowsBindFailure = opened.reason;
409
+ }
410
+ const lag = startLagMonitor();
411
+ const result = await runHealthSentinel({
412
+ targetPid,
413
+ now: Date.now,
414
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
415
+ probe: async (target, timeoutMs) => {
416
+ lag.reset();
417
+ const outcome = await probeHealth(target, timeoutMs);
418
+ return outcome.kind === "timeout" ? { ...outcome, sentinelLagMs: lag.read() } : outcome;
419
+ },
420
+ readRuntime: () => readSentinelRuntime(runtimePath),
421
+ targetExited,
422
+ isTargetGone: () => gone || (windowsHandle?.exited() ?? false),
423
+ bindTarget: () => {
424
+ // POSIX: Bun reads the parent id live, and it stops naming the proxy once the proxy exits.
425
+ // Windows: it is the creating parent's id, so the handle below carries the identity.
426
+ if (process.ppid !== targetPid) return { ok: false, reason: "the sentinel is not the watched process's child" };
427
+ if (process.platform !== "win32") return { ok: true };
428
+ return windowsHandle ? { ok: true } : { ok: false, reason: windowsBindFailure ?? "the parent process cannot be opened" };
429
+ },
430
+ terminateTarget: () => {
431
+ if (process.platform === "win32") {
432
+ if (!windowsHandle) return "failed";
433
+ return windowsHandle.terminate(1) ? "terminated" : "failed";
434
+ }
435
+ if (gone || process.ppid !== targetPid) return "not_target";
436
+ try {
437
+ process.kill(targetPid, "SIGKILL");
438
+ return "terminated";
439
+ } catch {
440
+ return "failed";
441
+ }
442
+ },
443
+ withOwnershipLease: run => {
444
+ let lease: { release(): void };
445
+ try {
446
+ lease = acquireOwnershipMutationLease(statePaths, { waitMs: LEASE_WAIT_MS, processImage: () => null });
447
+ } catch {
448
+ return "busy";
449
+ }
450
+ try { return run(); } finally { lease.release(); }
451
+ },
452
+ readHistory: () => {
453
+ const entries = readRecoveryEntries(historyPath);
454
+ return entries === null ? null : countedRecoveryTimes(entries);
455
+ },
456
+ writeHistory: entry => writeRecoveryEntry(historyPath, entry),
457
+ log: line => appendBoundedLog(logPath, line),
458
+ timing: parseTimingOverride(env[HEALTH_SENTINEL_ENV.timing]),
459
+ });
460
+ lag.stop();
461
+ windowsHandle?.close();
462
+ void result;
463
+ return 0;
464
+ }
465
+
466
+ if (import.meta.main) process.exit(await runHealthSentinelProcess());