@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,241 @@
1
+ /**
2
+ * Health-unresponsiveness recovery policy for the opt-in service health sentinel (#6671).
3
+ *
4
+ * Every supervisor this repository installs restarts the proxy only when it exits: the Windows
5
+ * Task Scheduler batch loop, launchd `KeepAlive`/`SuccessfulExit=false`, systemd
6
+ * `Restart=on-failure` and the desktop app's `on_exit`. A proxy that stays alive with its port
7
+ * listening while its event loop answers nothing is invisible to all of them. The sentinel turns
8
+ * that state into an exit, and this module is the whole of its judgement: pure, clock-injected,
9
+ * and free of I/O so every false-positive guard is testable on any platform.
10
+ *
11
+ * The guards, in the order they apply:
12
+ * - nothing counts until the target has answered once as itself and the startup grace is over;
13
+ * the grace is elapsed time, so setting the clock back neither restarts nor extends it;
14
+ * - only a probe that got no complete answer by its deadline counts. Any answer proves the event
15
+ * loop ran, and a refused or failed connection means the listener is closing, not hung;
16
+ * - an answer from another pid means the port is no longer the target's: stand down for good;
17
+ * - a probe that started far later than the cadence allows, that outlived its own deadline by
18
+ * far more than jitter, or during which the sentinel's own event loop ran late (sleep, or the
19
+ * sentinel itself paused or starved), discards the streak, because the timeout may belong to
20
+ * that pause rather than to the target;
21
+ * - once a probe times out the next ones follow back to back, so a run of timeouts is one
22
+ * continuous unresponsive stretch rather than separate stalls that line up with the cadence;
23
+ * - recovery needs both a run of consecutive timeouts and a minimum unhealthy span, which doubles
24
+ * for each recovery in the last hour, and a cap on recoveries per hour stands the sentinel down:
25
+ * a proxy that hangs again right after every restart is not fixed by killing it, and each kill
26
+ * loses the work it held. Nothing is ever replayed.
27
+ */
28
+
29
+ export type HealthProbeOutcome =
30
+ /** A complete status line and headers arrived before the deadline. */
31
+ | { kind: "answered"; pid: number | null; startedAt: number; endedAt: number }
32
+ /**
33
+ * No complete answer by the deadline, whether or not the connection was accepted.
34
+ * `sentinelLagMs` is how late the sentinel's own event loop ran during the probe: a sentinel
35
+ * that was itself paused or starved cannot tell a silent target from its own silence.
36
+ */
37
+ | { kind: "timeout"; startedAt: number; endedAt: number; sentinelLagMs?: number }
38
+ /** The connection was refused: nothing is listening. */
39
+ | { kind: "refused"; startedAt: number; endedAt: number }
40
+ /** Any other failure. */
41
+ | { kind: "error"; startedAt: number; endedAt: number };
42
+
43
+ export type HealthSentinelTiming = {
44
+ /** Time between the end of one answered probe and the start of the next. */
45
+ intervalMs: number;
46
+ /**
47
+ * Time between a timed-out probe and the next. Short on purpose: back-to-back probes make a run
48
+ * of timeouts mean one continuous unresponsive stretch, so recurring shorter stalls that happen
49
+ * to line up with the cadence cannot add up to a recovery.
50
+ */
51
+ failureIntervalMs: number;
52
+ /** A probe without a complete answer by this deadline is a timeout. */
53
+ probeTimeoutMs: number;
54
+ /** Counted from the sentinel's start; nothing counts before it ends. */
55
+ startupGraceMs: number;
56
+ /** Consecutive timeouts needed before a recovery. */
57
+ failureThreshold: number;
58
+ /** Minimum span from the first failing probe's start to the last one's end, before backoff. */
59
+ minUnhealthyMs: number;
60
+ /** Slack over the cadence (and over a probe's deadline) before a pause voids the streak. */
61
+ suspendSlackMs: number;
62
+ /** A timeout during which the sentinel's own loop ran later than this is not evidence. */
63
+ lagToleranceMs: number;
64
+ /** Window over which earlier recoveries raise the backoff and count toward the cap. */
65
+ recoveryWindowMs: number;
66
+ /** Recoveries within the window after which the sentinel stands down. */
67
+ maxRecoveriesPerWindow: number;
68
+ /** Ceiling on the backoff multiplier. */
69
+ maxBackoffMultiplier: number;
70
+ /**
71
+ * Oldest the final failing observation may be when the kill is issued. Lease, history and log
72
+ * work runs between the decision and the kill; evidence older than this is discarded.
73
+ */
74
+ maxEvidenceAgeMs: number;
75
+ };
76
+
77
+ export const DEFAULT_HEALTH_SENTINEL_TIMING: Readonly<HealthSentinelTiming> = Object.freeze({
78
+ intervalMs: 30_000,
79
+ failureIntervalMs: 1_000,
80
+ probeTimeoutMs: 20_000,
81
+ startupGraceMs: 120_000,
82
+ failureThreshold: 3,
83
+ minUnhealthyMs: 60_000,
84
+ suspendSlackMs: 5_000,
85
+ lagToleranceMs: 2_000,
86
+ recoveryWindowMs: 60 * 60_000,
87
+ maxRecoveriesPerWindow: 3,
88
+ maxBackoffMultiplier: 8,
89
+ maxEvidenceAgeMs: 5_000,
90
+ });
91
+
92
+ export type HealthSentinelDecision =
93
+ | { action: "continue"; note?: string }
94
+ | { action: "recover"; reason: string; consecutiveTimeouts: number; unhealthyMs: number }
95
+ /** Stop acting for this target; the reason is logged once. */
96
+ | { action: "stand_down"; reason: string };
97
+
98
+ export type HealthSentinelPolicyState = {
99
+ readonly armed: boolean;
100
+ readonly consecutiveTimeouts: number;
101
+ readonly firstFailureStartedAt: number | null;
102
+ readonly lastObservedAt: number | null;
103
+ readonly stoodDown: string | null;
104
+ };
105
+
106
+ export type HealthSentinelPolicy = {
107
+ observe(outcome: HealthProbeOutcome): HealthSentinelDecision;
108
+ state(): HealthSentinelPolicyState;
109
+ /** Minimum unhealthy span currently required, after backoff. */
110
+ requiredUnhealthyMs(): number;
111
+ /** How long to wait before the next probe: short while a failure streak is open. */
112
+ nextDelayMs(): number;
113
+ };
114
+
115
+ /**
116
+ * Recoveries recorded inside the window ending at `now`. An entry dated after `now` counts as
117
+ * recent: it means the wall clock was set back after it was written, and dropping it would let a
118
+ * clock rollback (or a restart after one) lift the hourly cap. It keeps counting until the clock
119
+ * passes it by a full window, which errs toward not killing.
120
+ */
121
+ export function recentRecoveryCount(history: readonly number[], now: number, windowMs: number): number {
122
+ return history.filter(at => Number.isFinite(at) && now - at < windowMs).length;
123
+ }
124
+
125
+ /** The unhealthy span required after `recent` recoveries: doubled per recovery, capped. */
126
+ export function requiredUnhealthySpan(timing: HealthSentinelTiming, recent: number): number {
127
+ const multiplier = Math.min(timing.maxBackoffMultiplier, 2 ** Math.max(0, recent));
128
+ return timing.minUnhealthyMs * multiplier;
129
+ }
130
+
131
+ export function createHealthSentinelPolicy(input: {
132
+ targetPid: number;
133
+ startedAt: number;
134
+ /** Epoch-ms of earlier recoveries for this installation, newest last. */
135
+ recoveryHistory: readonly number[];
136
+ timing?: Partial<HealthSentinelTiming>;
137
+ }): HealthSentinelPolicy {
138
+ const timing: HealthSentinelTiming = { ...DEFAULT_HEALTH_SENTINEL_TIMING, ...input.timing };
139
+ let armed = false;
140
+ let streak = 0;
141
+ let firstFailureStartedAt: number | null = null;
142
+ let lastObservedAt: number | null = null;
143
+ let stoodDown: string | null = null;
144
+ let required = timing.minUnhealthyMs;
145
+ // Startup grace is measured as elapsed time, accumulated from forward clock movement only, so a
146
+ // wall clock set back after (or during) the grace cannot reopen it and hold off recovery for as
147
+ // long as the clock was moved.
148
+ let graceElapsedMs = 0;
149
+ let graceClock = input.startedAt;
150
+ const noteGraceProgress = (at: number) => {
151
+ if (at > graceClock) graceElapsedMs += at - graceClock;
152
+ graceClock = at;
153
+ };
154
+
155
+ const resetStreak = () => {
156
+ streak = 0;
157
+ firstFailureStartedAt = null;
158
+ };
159
+
160
+ const standDown = (reason: string): HealthSentinelDecision => {
161
+ stoodDown = reason;
162
+ resetStreak();
163
+ return { action: "stand_down", reason };
164
+ };
165
+
166
+ const observe = (outcome: HealthProbeOutcome): HealthSentinelDecision => {
167
+ if (stoodDown !== null) return { action: "stand_down", reason: stoodDown };
168
+ noteGraceProgress(outcome.startedAt);
169
+ const previous = lastObservedAt;
170
+ lastObservedAt = outcome.endedAt;
171
+ let note: string | undefined;
172
+ if (previous !== null && streak > 0 && outcome.startedAt - previous > timing.failureIntervalMs + timing.suspendSlackMs) {
173
+ // The sentinel overslept its cadence (a suspended machine, or the sentinel itself starved),
174
+ // so the timeouts counted so far may belong to that pause rather than to the target.
175
+ resetStreak();
176
+ note = "observation gap; failure streak discarded";
177
+ }
178
+ switch (outcome.kind) {
179
+ case "answered": {
180
+ if (outcome.pid !== null && outcome.pid !== input.targetPid) {
181
+ return standDown("another process answers the health endpoint");
182
+ }
183
+ if (outcome.pid === input.targetPid) armed = true;
184
+ resetStreak();
185
+ return { action: "continue", note };
186
+ }
187
+ case "refused":
188
+ case "error":
189
+ resetStreak();
190
+ return { action: "continue", note };
191
+ case "timeout": {
192
+ if (!armed) return { action: "continue", note: note ?? "not armed: the target has not answered yet" };
193
+ if (graceElapsedMs < timing.startupGraceMs) {
194
+ return { action: "continue", note: note ?? "startup grace" };
195
+ }
196
+ if (
197
+ outcome.endedAt - outcome.startedAt > timing.probeTimeoutMs + timing.suspendSlackMs
198
+ || outcome.endedAt < outcome.startedAt
199
+ || (previous !== null && outcome.startedAt < previous)
200
+ ) {
201
+ // The probe outlived its own deadline by far more than scheduling jitter, or the wall
202
+ // clock ran backwards: its timing spanned a pause or a clock step, so this timeout says
203
+ // nothing about the target.
204
+ resetStreak();
205
+ return { action: "continue", note: "probe spanned a pause; failure streak discarded" };
206
+ }
207
+ if ((outcome.sentinelLagMs ?? 0) > timing.lagToleranceMs) {
208
+ // The sentinel itself stalled during the probe (paused, starved, or the machine slept),
209
+ // so the deadline may have expired while nobody was listening for the answer.
210
+ resetStreak();
211
+ return { action: "continue", note: "probe spanned a pause; failure streak discarded" };
212
+ }
213
+ streak += 1;
214
+ if (firstFailureStartedAt === null) firstFailureStartedAt = outcome.startedAt;
215
+ const unhealthyMs = outcome.endedAt - firstFailureStartedAt;
216
+ const recent = recentRecoveryCount(input.recoveryHistory, outcome.endedAt, timing.recoveryWindowMs);
217
+ required = requiredUnhealthySpan(timing, recent);
218
+ if (streak < timing.failureThreshold || unhealthyMs < required) return { action: "continue", note };
219
+ if (recent >= timing.maxRecoveriesPerWindow) {
220
+ return standDown(`${recent} recoveries in the last ${Math.round(timing.recoveryWindowMs / 60_000)} minutes; not recovering again`);
221
+ }
222
+ const decision: HealthSentinelDecision = {
223
+ action: "recover",
224
+ reason: `no health answer for ${streak} consecutive probes over ${Math.round(unhealthyMs / 1000)}s`,
225
+ consecutiveTimeouts: streak,
226
+ unhealthyMs,
227
+ };
228
+ resetStreak();
229
+ return decision;
230
+ }
231
+ }
232
+ };
233
+
234
+ return {
235
+ observe,
236
+ state: () => ({ armed, consecutiveTimeouts: streak, firstFailureStartedAt, lastObservedAt, stoodDown }),
237
+ requiredUnhealthyMs: () => required,
238
+ nextDelayMs: () => (streak > 0 ? timing.failureIntervalMs : timing.intervalMs),
239
+ };
240
+ }
241
+
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The service health sentinel's one question to the proxy it watches: does `GET /healthz`
3
+ * produce a complete answer before the deadline (#6671)?
4
+ *
5
+ * The request goes through {@link directLocalHttpFetch}: raw `node:net`, so neither proxy
6
+ * environment variables nor the bundled runtime's fetch stack (#6684) are involved, with capped
7
+ * response framing and an absolute deadline. Only the timing decides liveness; the body's `pid`
8
+ * lets the policy tell the target from another process that took the port.
9
+ */
10
+ import { isIP } from "node:net";
11
+
12
+ import { isWildcardHostname } from "../codex/loopback-target";
13
+ import { directLocalHttpFetch } from "../server/direct-local-http";
14
+ import type { HealthProbeOutcome } from "./health-sentinel-policy";
15
+
16
+ export type HealthProbeTarget = { hostname: string; port: number };
17
+
18
+ export type HealthProbeIo = {
19
+ now?: () => number;
20
+ fetch?: (url: string, timeoutMs: number) => Promise<Response>;
21
+ };
22
+
23
+ /**
24
+ * Numeric probe endpoint only; unknown DNS destinations cannot authorize recovery. Wildcard
25
+ * spellings use the server's own predicate (`*`, `0`, `00.0.0.000`, `::0`, ...), so every bind
26
+ * the server accepts as a wildcard is probed on loopback instead of standing the sentinel down.
27
+ */
28
+ export function probeUrlHost(hostname: string | undefined): string | null {
29
+ let value = (hostname ?? "").trim();
30
+ if (isWildcardHostname(value)) return value.includes(":") ? "[::1]" : "127.0.0.1";
31
+ if (value.startsWith("[") || value.endsWith("]")) {
32
+ if (!value.startsWith("[") || !value.endsWith("]")) return null;
33
+ value = value.slice(1, -1);
34
+ if (isIP(value) !== 6) return null;
35
+ }
36
+ if (!value || value === "0.0.0.0" || /^localhost\.?$/i.test(value)) return "127.0.0.1";
37
+ if (isIP(value) === 4) return value;
38
+ if (isIP(value) !== 6 || value.includes("%")) return null;
39
+ const host = new URL(`http://[${value}]/`).hostname;
40
+ return host === "[::]" ? "[::1]" : host;
41
+ }
42
+
43
+ /** `pid` from a `/healthz` JSON body, or null when the body does not carry one. */
44
+ export function healthzPid(body: string): number | null {
45
+ try {
46
+ const parsed = JSON.parse(body) as { pid?: unknown };
47
+ return typeof parsed.pid === "number" && Number.isSafeInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export async function probeHealth(target: HealthProbeTarget, timeoutMs: number, io: HealthProbeIo = {}): Promise<HealthProbeOutcome> {
54
+ const now = io.now ?? Date.now;
55
+ const fetchFn = io.fetch ?? ((url: string, deadlineMs: number) => directLocalHttpFetch(url, { headers: { accept: "application/json" } }, { timeoutMs: deadlineMs }));
56
+ const startedAt = now();
57
+ const hostname = probeUrlHost(target.hostname);
58
+ if (hostname === null) return { kind: "error", startedAt, endedAt: now() };
59
+ try {
60
+ const response = await fetchFn(`http://${hostname}:${target.port}/healthz`, timeoutMs);
61
+ // The framed body is already in memory; reading it cannot wait on the target.
62
+ const body = await response.text().catch(() => "");
63
+ return { kind: "answered", pid: healthzPid(body), startedAt, endedAt: now() };
64
+ } catch (error) {
65
+ const endedAt = now();
66
+ const failure = error as { name?: unknown; code?: unknown };
67
+ if (failure?.name === "TimeoutError") return { kind: "timeout", startedAt, endedAt };
68
+ if (failure?.code === "ECONNREFUSED") return { kind: "refused", startedAt, endedAt };
69
+ return { kind: "error", startedAt, endedAt };
70
+ }
71
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * A Windows process handle on the health sentinel's parent, held for the sentinel's lifetime
3
+ * (#6671).
4
+ *
5
+ * Windows reuses process ids quickly, so "is pid N still my parent" checked just before a kill
6
+ * cannot bind the kill to the proxy: the proxy may exit and its id be handed to another process in
7
+ * between. An open handle can: while any handle to a process object is open, its id is not
8
+ * reused, and `TerminateProcess` acts on exactly that object. The handle is opened once at
9
+ * startup and accepted only if the process it names was created before this sentinel, which a
10
+ * replacement holding a recycled id cannot satisfy. libuv's kill-on-close Job would otherwise be
11
+ * the only tie between the two processes, and libuv ignores a failed Job assignment.
12
+ *
13
+ * Bun exposes no API for a handle on a process it did not spawn, so this calls kernel32 through
14
+ * `bun:ffi`. Nothing here runs on other platforms.
15
+ */
16
+ import { dlopen, FFIType } from "bun:ffi";
17
+
18
+ const PROCESS_TERMINATE = 0x0001;
19
+ const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
20
+ const SYNCHRONIZE = 0x0010_0000;
21
+ const WAIT_OBJECT_0 = 0;
22
+ const WAIT_TIMEOUT = 0x102;
23
+
24
+ export type ParentProcessHandle = {
25
+ /** TerminateProcess on the held handle; true when Windows accepted the request. */
26
+ terminate(exitCode: number): boolean;
27
+ /** True once the process has exited (non-blocking). */
28
+ exited(): boolean;
29
+ close(): void;
30
+ };
31
+
32
+ export type ParentHandleResult =
33
+ | { ok: true; handle: ParentProcessHandle }
34
+ | { ok: false; reason: string };
35
+
36
+ // HANDLEs travel as u64 (bigint), never as `ptr`: a JS number cannot carry every pointer-width
37
+ // value, and a truncated handle would name a different object.
38
+ function kernel32() {
39
+ return dlopen("kernel32.dll", {
40
+ OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.u64 },
41
+ GetProcessTimes: { args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
42
+ TerminateProcess: { args: [FFIType.u64, FFIType.u32], returns: FFIType.i32 },
43
+ WaitForSingleObject: { args: [FFIType.u64, FFIType.u32], returns: FFIType.u32 },
44
+ CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
45
+ }).symbols;
46
+ }
47
+
48
+ /** Open the parent and prove it is the process that created this one. */
49
+ export function openParentProcessHandle(parentPid: number): ParentHandleResult {
50
+ if (process.platform !== "win32") return { ok: false, reason: "not Windows" };
51
+ let k32: ReturnType<typeof kernel32>;
52
+ try {
53
+ k32 = kernel32();
54
+ } catch {
55
+ return { ok: false, reason: "kernel32 is unavailable" };
56
+ }
57
+ const handle = BigInt(k32.OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, parentPid));
58
+ if (handle === 0n) return { ok: false, reason: "the parent process cannot be opened" };
59
+ const close = () => { try { k32.CloseHandle(handle); } catch { /* already closed */ } };
60
+ const created = (target: bigint): bigint | null => {
61
+ const times = new BigUint64Array(4);
62
+ const ok = k32.GetProcessTimes(target, times.subarray(0, 1), times.subarray(1, 2), times.subarray(2, 3), times.subarray(3, 4));
63
+ return ok ? times[0]! : null;
64
+ };
65
+ // This process cannot be reused while it runs, so opening it by id is exact; the pseudo-handle
66
+ // from GetCurrentProcess is avoided because it is -1.
67
+ const self = BigInt(k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process.pid));
68
+ const parentCreated = created(handle);
69
+ const selfCreated = self === 0n ? null : created(self);
70
+ if (self !== 0n) { try { k32.CloseHandle(self); } catch { /* already closed */ } }
71
+ if (parentCreated === null || selfCreated === null || parentCreated === 0n || parentCreated >= selfCreated) {
72
+ close();
73
+ return { ok: false, reason: "the process holding the parent id was not created before this sentinel" };
74
+ }
75
+ if (k32.WaitForSingleObject(handle, 0) !== WAIT_TIMEOUT) {
76
+ close();
77
+ return { ok: false, reason: "the parent process has already exited" };
78
+ }
79
+ return {
80
+ ok: true,
81
+ handle: {
82
+ terminate: exitCode => k32.TerminateProcess(handle, exitCode) !== 0,
83
+ exited: () => k32.WaitForSingleObject(handle, 0) === WAIT_OBJECT_0,
84
+ close,
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,20 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { userInfo } from "node:os";
3
+
4
+ interface LinuxLingerDeps {
5
+ user?: () => string;
6
+ run?: typeof execFileSync;
7
+ }
8
+
9
+ /** Advisory only: never enables linger or changes the user's login settings. */
10
+ export function linuxServiceBootHint(deps: LinuxLingerDeps = {}): string {
11
+ try {
12
+ const user = (deps.user ?? (() => userInfo().username))();
13
+ const output = (deps.run ?? execFileSync)("loginctl", ["show-user", user, "-p", "Linger"], {
14
+ encoding: "utf8", timeout: 750, maxBuffer: 16 * 1024, stdio: ["ignore", "pipe", "pipe"],
15
+ }).trim();
16
+ if (output === "Linger=yes") return " Auto-start on boot: user lingering is already enabled.";
17
+ if (output === "Linger=no") return " For auto-start on boot: loginctl enable-linger $USER";
18
+ } catch { /* Missing loginctl or failed diagnosis is advisory, not an install failure. */ }
19
+ return " User lingering could not be verified. For auto-start on boot, check it and, if disabled, run: loginctl enable-linger $USER";
20
+ }
@@ -0,0 +1,72 @@
1
+ import { closeSync, fstatSync, ftruncateSync, openSync, readSync, renameSync, writeSync } from "node:fs";
2
+ import { formatWithOptions } from "node:util";
3
+ import { dirname } from "node:path";
4
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
5
+
6
+ export const SERVICE_LOG_MAX_BYTES = 20 * 1024 * 1024;
7
+ export const SERVICE_LOG_CHECK_INTERVAL_MS = 60_000;
8
+
9
+ /** Preserve the manager's open append descriptor; retain at most two bounded archives. */
10
+ export function rotateServiceLog(path: string, maxBytes = SERVICE_LOG_MAX_BYTES): boolean {
11
+ let source: number | undefined;
12
+ let archive: number | undefined;
13
+ try {
14
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) return false;
15
+ source = openSync(path, "r+");
16
+ const size = fstatSync(source).size;
17
+ if (size <= maxBytes) return false;
18
+ try { renameSync(`${path}.1`, `${path}.2`); } catch (error) {
19
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false;
20
+ }
21
+ archive = openSync(`${path}.1`, "w", 0o600);
22
+ // A legacy unbounded file may be huge. Copy only its newest cap-sized tail,
23
+ // using fixed memory, before truncating the original inode.
24
+ const buffer = Buffer.alloc(Math.min(64 * 1024, maxBytes));
25
+ let position = size - maxBytes;
26
+ while (position < size) {
27
+ const read = readSync(source, buffer, 0, Math.min(buffer.length, size - position), position);
28
+ if (read === 0) return false;
29
+ let written = 0;
30
+ while (written < read) {
31
+ const count = writeSync(archive, buffer, written, read - written);
32
+ if (count === 0) return false;
33
+ written += count;
34
+ }
35
+ position += read;
36
+ }
37
+ ftruncateSync(source, 0);
38
+ return true;
39
+ } catch {
40
+ return false; // Missing files, permissions and disk failures must never stop the service.
41
+ } finally {
42
+ for (const fd of [archive, source]) {
43
+ if (fd !== undefined) try { closeSync(fd); } catch { /* best-effort */ }
44
+ }
45
+ }
46
+ }
47
+
48
+ /** One sink wrapper covers startup, provider diagnostics and multiline errors. */
49
+ export function startServiceLogging(
50
+ path: string,
51
+ env: NodeJS.ProcessEnv = process.env,
52
+ intervalMs = SERVICE_LOG_CHECK_INTERVAL_MS,
53
+ ): () => void {
54
+ if (!env.OCX_SERVICE) return () => {};
55
+ const restore: (() => void)[] = [];
56
+ for (const method of ["log", "info", "debug", "warn", "error"] as const) {
57
+ const original = console[method];
58
+ console[method] = (...args: unknown[]) => {
59
+ const at = new Date().toISOString();
60
+ const line = formatWithOptions({ colors: false }, ...args);
61
+ original.call(console, line.split("\n").map(part => `${at} ${part}`).join("\n"));
62
+ };
63
+ restore.push(() => { console[method] = original; });
64
+ }
65
+ try {
66
+ for (const suffix of [".1", ".2"]) recordOwnedConfigPath(dirname(path), `${path}${suffix}`);
67
+ } catch { /* ownership bookkeeping must not break logging */ }
68
+ rotateServiceLog(path);
69
+ const timer = setInterval(() => { rotateServiceLog(path); }, intervalMs);
70
+ timer.unref();
71
+ return () => { clearInterval(timer); for (const reset of restore) reset(); };
72
+ }
@@ -201,14 +201,10 @@ function Start-StartupHealthProbe {
201
201
  if ($null -ne $script:startupProbeProcess) {
202
202
  # Process.Start succeeded but the async pipe setup failed: Dispose alone
203
203
  # would leave the Bun child running, so terminate it before cleanup.
204
- try {
205
- $script:startupProbeProcess.Kill()
206
- [void]$script:startupProbeProcess.WaitForExit(3000)
207
- } catch {
208
- Write-ActionLog "startup-health probe launch cleanup failed: $($_.Exception.GetType().Name)"
209
- }
204
+ Stop-StartupHealthProbe
205
+ } else {
206
+ Complete-StartupHealthProbe
210
207
  }
211
- Complete-StartupHealthProbe
212
208
  }
213
209
  }
214
210
 
@@ -217,8 +213,11 @@ function Complete-StartupHealthProbe {
217
213
  $script:startupProbeProcess = $null
218
214
  $script:startupProbeOutputTask = $null
219
215
  $script:startupProbeErrorTask = $null
216
+ $script:startupProbeTerminating = $false
220
217
  if ($null -ne $process) {
221
218
  try {
219
+ $process.StandardOutput.Dispose()
220
+ $process.StandardError.Dispose()
222
221
  $process.Dispose()
223
222
  } catch {
224
223
  Write-ActionLog "startup-health probe dispose failed: $($_.Exception.GetType().Name)"
@@ -226,6 +225,31 @@ function Complete-StartupHealthProbe {
226
225
  }
227
226
  }
228
227
 
228
+ function Stop-StartupHealthProbe([switch]$Shutdown) {
229
+ if ($null -eq $script:startupProbeProcess) { return }
230
+ $script:startupProbeTerminating = $true
231
+ try {
232
+ if (-not $script:startupProbeProcess.HasExited) { $script:startupProbeProcess.Kill() }
233
+ } catch { Write-ActionLog "startup-health probe kill failed: $($_.Exception.GetType().Name)" }
234
+ if ($Shutdown) {
235
+ # Shutdown alone may wait. Timeout ticks keep the child tracked until actual exit.
236
+ try { [void]$script:startupProbeProcess.WaitForExit(500) } catch { $null = $_ }
237
+ $exited = $false
238
+ try { $exited = $script:startupProbeProcess.HasExited } catch { $null = $_ }
239
+ if ($exited) {
240
+ # Exit does not mean both pipe readers finished; give them the same bounded wait.
241
+ $readers = @($script:startupProbeOutputTask, $script:startupProbeErrorTask) | Where-Object { $null -ne $_ }
242
+ if ($readers.Count -gt 0) {
243
+ try { [void][System.Threading.Tasks.Task]::WaitAll([System.Threading.Tasks.Task[]]$readers, 500) } catch { $null = $_ }
244
+ }
245
+ Complete-StartupHealthProbe
246
+ } else {
247
+ # Releasing the only reference would orphan a live child; keep it tracked.
248
+ Write-ActionLog "startup-health probe still running at shutdown; leaving it tracked"
249
+ }
250
+ }
251
+ }
252
+
229
253
  function Initialize-UpdateBadgeReader {
230
254
  if (([System.Management.Automation.PSTypeName]"TrayUpdateBadgeReader").Type) { return }
231
255
  Add-Type -TypeDefinition @'
@@ -433,6 +457,7 @@ $script:startupRefreshMs = 20000L
433
457
  $script:startupProbeProcess = $null
434
458
  $script:startupProbeOutputTask = $null
435
459
  $script:startupProbeErrorTask = $null
460
+ $script:startupProbeTerminating = $false
436
461
  $script:startupProbeStarted = 0L
437
462
  $script:startupProbeTimeoutMs = 30000L
438
463
  $script:updateAvailable = $false
@@ -520,10 +545,16 @@ function Update-TrayState {
520
545
  try {
521
546
  $probeExited = $script:startupProbeProcess.HasExited
522
547
  } catch {
523
- $probeExited = $true
548
+ # Failed inspection is not proof of exit; keep ownership and request termination.
549
+ Stop-StartupHealthProbe
524
550
  }
525
551
  $probeTimedOut = ($now - $script:startupProbeStarted) -gt $script:startupProbeTimeoutMs
526
- if ($probeExited -and $null -ne $script:startupProbeOutputTask -and $script:startupProbeOutputTask.IsCompleted) {
552
+ $probeDrained = ($null -eq $script:startupProbeOutputTask -or $script:startupProbeOutputTask.IsCompleted) -and
553
+ ($null -eq $script:startupProbeErrorTask -or $script:startupProbeErrorTask.IsCompleted)
554
+ if ($script:startupProbeTerminating) {
555
+ if ($probeExited -and $probeDrained) { Complete-StartupHealthProbe }
556
+ elseif (-not $probeExited) { Stop-StartupHealthProbe }
557
+ } elseif ($probeExited -and $probeDrained -and $null -ne $script:startupProbeOutputTask) {
527
558
  $probeStartup = $null
528
559
  try {
529
560
  $probeStartup = Parse-StartupHealthText $script:startupProbeOutputTask.Result
@@ -537,14 +568,8 @@ function Update-TrayState {
537
568
  # A hung diagnostic must not survive its refresh slot: terminate it so the
538
569
  # next refresh cannot stack another orphaned Bun process behind it.
539
570
  Write-ActionLog "startup-health probe timed out; terminating it"
540
- try {
541
- $script:startupProbeProcess.Kill()
542
- [void]$script:startupProbeProcess.WaitForExit(3000)
543
- } catch {
544
- Write-ActionLog "startup-health probe kill failed: $($_.Exception.GetType().Name)"
545
- }
571
+ Stop-StartupHealthProbe
546
572
  $script:startupHealth = $null
547
- Complete-StartupHealthProbe
548
573
  $script:startupHealthCheckedAt = $now
549
574
  }
550
575
  }
@@ -710,14 +735,8 @@ try {
710
735
  # A probe still running when the tray shuts down would outlive it: Dispose()
711
736
  # releases the handle but does not terminate the child, so kill the active
712
737
  # probe, wait briefly for exit, then release the probe state.
713
- try {
714
- Write-ActionLog "terminating active startup-health probe on tray shutdown"
715
- $script:startupProbeProcess.Kill()
716
- [void]$script:startupProbeProcess.WaitForExit(3000)
717
- } catch {
718
- Write-ActionLog "startup-health probe shutdown kill failed: $($_.Exception.GetType().Name)"
719
- }
720
- Complete-StartupHealthProbe
738
+ Write-ActionLog "terminating active startup-health probe on tray shutdown"
739
+ Stop-StartupHealthProbe -Shutdown
721
740
  }
722
741
  Stop-UpdateBadgeProbe -Shutdown
723
742
  $notify.Dispose()