specrails-desktop 2.29.1 → 2.29.2

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 (544) hide show
  1. package/client/dist/assets/ActivityFeedPage-ClpD5CFj.js +1 -0
  2. package/client/dist/assets/AgentBrowserCapture-CSPs9Ofc.js +1 -0
  3. package/client/dist/assets/AgentModeCodePane-DwcyklA5.js +2 -0
  4. package/client/dist/assets/AgentModeJobsPane-C69bqlMa.js +1 -0
  5. package/client/dist/assets/AgentsPage-pRiRr9-W.js +86 -0
  6. package/client/dist/assets/AnalyticsPage-C14exKsC.js +1 -0
  7. package/client/dist/assets/BarChart-nBOMNfiy.js +33 -0
  8. package/client/dist/assets/CodePage-CK76JKm9.js +2 -0
  9. package/client/dist/assets/DesktopAnalyticsPage-Cnv0f3XQ.js +1 -0
  10. package/client/dist/assets/DocsDialog-CHUm_Ym0.js +11 -0
  11. package/client/dist/assets/DocsPage-COI-oDok.js +11 -0
  12. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +1 -0
  13. package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-CypM7R1F.js +19 -0
  15. package/client/dist/assets/JobDetailModal-B_W0H9dw.js +1 -0
  16. package/client/dist/assets/JobDetailPage-CeeXOoKS.js +1 -0
  17. package/client/dist/assets/JobsPage-CwhJG9jw.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-C51t1WOM.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-Y2HgtIJu.js +1 -0
  20. package/client/dist/assets/LoopsPage-Bvn1Kw5V.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-DiFvZDPW.js +1 -0
  22. package/client/dist/assets/PluginsPage-BGPRJpdf.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-D_GTyGLc.js +1 -0
  24. package/client/dist/assets/TemplatePreviewModal-szwBhd7h.js +1 -0
  25. package/client/dist/assets/TicketDetailModal-9UEyK974.js +14 -0
  26. package/client/dist/assets/Trans-B_vKZ-Hb.js +1 -0
  27. package/client/dist/assets/activity-DiyjsgQV.js +1 -0
  28. package/client/dist/assets/addon-image-BRmmRHgQ.js +1 -0
  29. package/client/dist/assets/addon-ligatures-qIZUPvRV.js +2 -0
  30. package/client/dist/assets/{addon-webgl-DMbGeI_u.js → addon-webgl-C2d7lriQ.js} +3 -3
  31. package/client/dist/assets/addspec-Rw8uVBC1.js +1 -0
  32. package/client/dist/assets/agent-4rmXoo_p.js +1 -0
  33. package/client/dist/assets/agent-BxZuNGih.js +1 -0
  34. package/client/dist/assets/agent-C3uXOYv_.js +1 -0
  35. package/client/dist/assets/agent-CA8Y4wbw.js +1 -0
  36. package/client/dist/assets/agent-CASKrhG12.js +1 -0
  37. package/client/dist/assets/agent-Cv-jud5n2.js +1 -0
  38. package/client/dist/assets/agent-DitUxYh2.js +1 -0
  39. package/client/dist/assets/agent-Doow4X7o.js +1 -0
  40. package/client/dist/assets/agents-DhG7mg6-.js +1 -0
  41. package/client/dist/assets/agentstudio-BIUe1Im2.js +1 -0
  42. package/client/dist/assets/aiedit-iIjz2_Qk.js +1 -0
  43. package/client/dist/assets/analytics-DZ8Aoq3I.js +1 -0
  44. package/client/dist/assets/api-B2M7CtDw.js +1 -0
  45. package/client/dist/assets/arrow-left-CpIR4j7G.js +1 -0
  46. package/client/dist/assets/arrow-up-right-9DoMn7fD.js +1 -0
  47. package/client/dist/assets/attachments-B53PG733.js +1 -0
  48. package/client/dist/assets/brain-DI9lHkKY.js +1 -0
  49. package/client/dist/assets/browser-CXUcnTMI.js +1 -0
  50. package/client/dist/assets/button-BUIIBQPT.js +1 -0
  51. package/client/dist/assets/chat-BGD7MXAy.js +1 -0
  52. package/client/dist/assets/chevron-right-CViMxucY.js +1 -0
  53. package/client/dist/assets/chunk-KS7C4IRE-Cx5sqopX.js +3 -0
  54. package/client/dist/assets/circle-x-CMYsN4ZH.js +1 -0
  55. package/client/dist/assets/code-BDXEM4ml.js +1 -0
  56. package/client/dist/assets/commands-DyUZQ1FI.js +1 -0
  57. package/client/dist/assets/common-CD1QHriA2.js +1 -0
  58. package/client/dist/assets/common-CwwPaRxx.js +1 -0
  59. package/client/dist/assets/common-DVQuhs-m.js +1 -0
  60. package/client/dist/assets/common-Dg2jaX7d.js +1 -0
  61. package/client/dist/assets/common-DuBwj6tK.js +1 -0
  62. package/client/dist/assets/common-MRj9MHcc.js +1 -0
  63. package/client/dist/assets/common-O4ZpnMLp.js +1 -0
  64. package/client/dist/assets/common-fyeF0v7C2.js +1 -0
  65. package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +1 -0
  66. package/client/dist/assets/{css.worker-Wv5dxAWO.js → css.worker-yKcq13f2.js} +13 -13
  67. package/client/dist/assets/{cssMode-DBHbkBvI.js → cssMode-BM4z9OX0.js} +1 -1
  68. package/client/dist/assets/dashboard-B_NkSHYm.js +1 -0
  69. package/client/dist/assets/dashboard-ClBb_3pq.js +1 -0
  70. package/client/dist/assets/dashboard-CpCmJvMB.js +1 -0
  71. package/client/dist/assets/dashboard-Czohu35O2.js +1 -0
  72. package/client/dist/assets/dashboard-DcjbD0UK.js +1 -0
  73. package/client/dist/assets/dashboard-Dvx5F3we.js +1 -0
  74. package/client/dist/assets/dashboard-R1bOPjBh2.js +1 -0
  75. package/client/dist/assets/dashboard-owcDucDi.js +1 -0
  76. package/client/dist/assets/dialog-CzIekCw0.js +45 -0
  77. package/client/dist/assets/dist-Cez5CRkX.js +1 -0
  78. package/client/dist/assets/dist-bVqfYmby.js +1 -0
  79. package/client/dist/assets/{dist-js-CgpP6tKI.js → dist-js-CcBCbb32.js} +1 -1
  80. package/client/dist/assets/{dist-js-DHiN-A_E.js → dist-js-VwztRkGj.js} +1 -1
  81. package/client/dist/assets/download-CFHyyHcw.js +1 -0
  82. package/client/dist/assets/editor.api2-4ndOupAA.js +872 -0
  83. package/client/dist/assets/{editor.main-C9J2ikJK.js → editor.main-DxrmGfoq.js} +2 -2
  84. package/client/dist/assets/editor.worker-tytyn86m.js +26 -0
  85. package/client/dist/assets/explore-DgBACn39.js +1 -0
  86. package/client/dist/assets/external-link-BcU5EWZp.js +1 -0
  87. package/client/dist/assets/file-text-CFT-xXKN.js +1 -0
  88. package/client/dist/assets/formatDistanceToNow-ByHIauTQ.js +1 -0
  89. package/client/dist/assets/{freemarker2-J5iIIIK-.js → freemarker2-CELlTDJ2.js} +1 -1
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-BxHAksaU.js +1 -0
  91. package/client/dist/assets/git-branch-BRM0F5GN.js +1 -0
  92. package/client/dist/assets/{handlebars-DSw1l_tw.js → handlebars-NEjHJ7o_.js} +1 -1
  93. package/client/dist/assets/{html-h2-wE1Lj.js → html-CA10zN5A.js} +1 -1
  94. package/client/dist/assets/{html.worker-CQP8QQsS.js → html.worker-DSJ9zmd7.js} +12 -12
  95. package/client/dist/assets/{htmlMode-Cc_ij7lW.js → htmlMode-89rPYiyq.js} +1 -1
  96. package/client/dist/assets/i18n-CgaRxpII.js +2 -0
  97. package/client/dist/assets/index-BflYQpWm.css +2 -0
  98. package/client/dist/assets/index-C1_-BTXy.js +72 -0
  99. package/client/dist/assets/integrations-mB-7zoyT.js +1 -0
  100. package/client/dist/assets/{javascript-C1q3xv6N.js → javascript-B1gDmwzS.js} +1 -1
  101. package/client/dist/assets/jira-CmKWERE0.js +1 -0
  102. package/client/dist/assets/jira-api-BgFIr8eD.js +1 -0
  103. package/client/dist/assets/job-time-BCL7wee9.js +1 -0
  104. package/client/dist/assets/jobs-B-sDHEay2.js +1 -0
  105. package/client/dist/assets/jobs-BCtGfgUA.js +1 -0
  106. package/client/dist/assets/jobs-BHR_EnDI.js +1 -0
  107. package/client/dist/assets/jobs-BMbZJjJJ.js +1 -0
  108. package/client/dist/assets/jobs-CVFD8rar.js +1 -0
  109. package/client/dist/assets/jobs-DoSaFzAa.js +1 -0
  110. package/client/dist/assets/jobs-DzSIR8V62.js +1 -0
  111. package/client/dist/assets/jobs-lk1_Dq1U.js +1 -0
  112. package/client/dist/assets/json.worker-Oa9UrkKE.js +58 -0
  113. package/client/dist/assets/{jsonMode-CeaCdEhr.js → jsonMode-xHLMfzhF.js} +1 -1
  114. package/client/dist/assets/jsx-runtime-zu2_FqZY.js +1 -0
  115. package/client/dist/assets/lib-C7Mhm-gT.js +7 -0
  116. package/client/dist/assets/lib-CGdyxIWg.js +29 -0
  117. package/client/dist/assets/{liquid-eFsVK0V-.js → liquid-BY6hCUXG.js} +1 -1
  118. package/client/dist/assets/loader-circle-DcygOQpC.js +1 -0
  119. package/client/dist/assets/loops-BzfXWEwO.js +1 -0
  120. package/client/dist/assets/{lspLanguageFeatures-DT3-bbhn.js → lspLanguageFeatures-ira2aEJh.js} +1 -1
  121. package/client/dist/assets/mcp-uDkXYu4L.js +1 -0
  122. package/client/dist/assets/{mdx-Fuwnh1cX.js → mdx-BNx0xr3N.js} +1 -1
  123. package/client/dist/assets/monaco.contribution-Bt92R3Vw.js +2 -0
  124. package/client/dist/assets/nav-myFaXRHD.js +1 -0
  125. package/client/dist/assets/pencil-C9RkrIPZ.js +1 -0
  126. package/client/dist/assets/play-DNlrawbS.js +1 -0
  127. package/client/dist/assets/plus-CsnjfkmX.js +1 -0
  128. package/client/dist/assets/preload-helper-Czpn1I53.js +1 -0
  129. package/client/dist/assets/{python-BgoEs6Cj.js → python-DLdbtmNw.js} +1 -1
  130. package/client/dist/assets/{razor-CM1sPMMq.js → razor-B9gdhAbR.js} +1 -1
  131. package/client/dist/assets/react-dom-DiWzBDP9.js +1 -0
  132. package/client/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  133. package/client/dist/assets/save-28sIlCdr.js +1 -0
  134. package/client/dist/assets/search-Cq_gE3Ik.js +1 -0
  135. package/client/dist/assets/settings-B0ypw9uw.js +1 -0
  136. package/client/dist/assets/setup-CmK87LuS.js +1 -0
  137. package/client/dist/assets/sparkles-DrY8hb0X.js +1 -0
  138. package/client/dist/assets/specs-DII0C8gw.js +1 -0
  139. package/client/dist/assets/spending-C4p06nen.js +1 -0
  140. package/client/dist/assets/terminal-BFRZnMPP.js +1 -0
  141. package/client/dist/assets/tickets-B8BuAaj5.js +1 -0
  142. package/client/dist/assets/tickets-B_yh4qQk.js +1 -0
  143. package/client/dist/assets/tickets-Bs37scwY2.js +1 -0
  144. package/client/dist/assets/tickets-CFk2n2Or.js +1 -0
  145. package/client/dist/assets/tickets-DQp7JS2R2.js +1 -0
  146. package/client/dist/assets/tickets-D_yDquEw.js +1 -0
  147. package/client/dist/assets/tickets-DpmdE8tv2.js +1 -0
  148. package/client/dist/assets/tickets-pAUJkDTu.js +1 -0
  149. package/client/dist/assets/trash-2-BOU3WdZZ.js +1 -0
  150. package/client/dist/assets/triangle-alert-H9T4jAmh.js +1 -0
  151. package/client/dist/assets/{ts.worker-METxwbDZ.js → ts.worker-BtjBFNHX.js} +48 -33
  152. package/client/dist/assets/{tsMode-BmT8WogI.js → tsMode-DEjSThU9.js} +1 -1
  153. package/client/dist/assets/{typescript-CQrQPsoW.js → typescript-CRRnD2Ts.js} +1 -1
  154. package/client/dist/assets/upload-DYMjAQ6M.js +1 -0
  155. package/client/dist/assets/useBrowserCaptureSession-2OqX0vap.js +1 -0
  156. package/client/dist/assets/useDesktop-X8YB9xzv.js +1 -0
  157. package/client/dist/assets/useSharedWebSocket-DaPh7QBL.js +1 -0
  158. package/client/dist/assets/utils-BC4DHlz2.js +1 -0
  159. package/client/dist/assets/value-s-gzBeyF.js +1 -0
  160. package/client/dist/assets/{workers-U0UXyLUf.js → workers-uOo7WLaa.js} +1 -1
  161. package/client/dist/assets/x-CBMI9ejn.js +1 -0
  162. package/client/dist/assets/{xml-WRxJs8_I.js → xml-EDREH2X7.js} +1 -1
  163. package/client/dist/assets/{yaml-CjpSTjJU.js → yaml-BjaDVX1E.js} +1 -1
  164. package/client/dist/assets/zap-CHo3aCD1.js +1 -0
  165. package/client/dist/index.html +73 -32
  166. package/docs/guide/en/integrations/5-mcp-server.md +2 -2
  167. package/docs/internals/safe-pr-review-flow.md +190 -132
  168. package/docs/mcp.md +15 -5
  169. package/package.json +11 -7
  170. package/server/dist/active-pr-continuation.js +16 -1
  171. package/server/dist/agent-chat-manager.js +360 -181
  172. package/server/dist/agent-cwd-manager.js +52 -8
  173. package/server/dist/agent-generator.js +90 -48
  174. package/server/dist/agent-mcp-config.js +69 -36
  175. package/server/dist/agent-operator-prompt.js +3 -2
  176. package/server/dist/agent-refine-manager.js +47 -6
  177. package/server/dist/agent-tier.js +13 -11
  178. package/server/dist/artifact-registry.js +33 -2
  179. package/server/dist/auth.js +5 -4
  180. package/server/dist/browser-capture-manager.js +92 -29
  181. package/server/dist/chat-manager.js +302 -46
  182. package/server/dist/contract-refine-runner.js +19 -6
  183. package/server/dist/db.js +665 -8
  184. package/server/dist/desktop-router.js +1 -2
  185. package/server/dist/headroom-manager.js +1310 -121
  186. package/server/dist/headroom-routing.js +120 -2
  187. package/server/dist/index.js +48 -13
  188. package/server/dist/interactive-job-session.js +237 -82
  189. package/server/dist/jira/jira-adf.js +10 -0
  190. package/server/dist/jira/jira-db.js +15 -3
  191. package/server/dist/jira/jira-sync-manager.js +182 -13
  192. package/server/dist/job-listing.js +140 -0
  193. package/server/dist/job-spawn-idempotency.js +67 -0
  194. package/server/dist/loop-run-manager.js +508 -98
  195. package/server/dist/loop-runs-store.js +266 -13
  196. package/server/dist/mcp/agent-capability.js +71 -0
  197. package/server/dist/mcp/guide.js +5 -2
  198. package/server/dist/mcp/mcp-server.js +5 -10
  199. package/server/dist/mcp/mcp-token.js +5 -5
  200. package/server/dist/mcp/tools/jobs.js +20 -13
  201. package/server/dist/mcp/tools/types.js +20 -42
  202. package/server/dist/mobile/mobile-router.js +1 -1
  203. package/server/dist/pr-lifecycle.js +77 -0
  204. package/server/dist/pr-publisher.js +94 -27
  205. package/server/dist/process-admission.js +82 -0
  206. package/server/dist/project-registry.js +572 -174
  207. package/server/dist/project-router-git.js +13 -2
  208. package/server/dist/project-router-jobs.js +111 -55
  209. package/server/dist/project-router-tickets.js +32 -2
  210. package/server/dist/providers/claude-adapter.js +21 -13
  211. package/server/dist/queue-manager.js +2332 -623
  212. package/server/dist/rail-isolated-launch.js +1137 -290
  213. package/server/dist/rail-isolation.js +4 -2
  214. package/server/dist/rail-pr-decision.js +740 -266
  215. package/server/dist/rail-pr-delivery.js +2 -1
  216. package/server/dist/rail-pr-store.js +317 -18
  217. package/server/dist/rail-pr-ticket-effects.js +328 -0
  218. package/server/dist/rail-worktree-release.js +18 -2
  219. package/server/dist/rails-router.js +149 -119
  220. package/server/dist/rails-store.js +106 -3
  221. package/server/dist/ticket-store.js +23 -1
  222. package/server/dist/transient-children.js +13 -0
  223. package/server/dist/util/cli-prompt.js +42 -8
  224. package/server/dist/util/secure-token-file.js +84 -0
  225. package/server/dist/worktree-manager.js +6 -5
  226. package/client/dist/assets/ActivityFeedPage-D8RKRRoM.js +0 -1
  227. package/client/dist/assets/AgentBrowserCapture-BUHPHNSN.js +0 -1
  228. package/client/dist/assets/AgentModeCodePane-GZc8bNGT.js +0 -2
  229. package/client/dist/assets/AgentModeJobsPane-DWtQqj3k.js +0 -1
  230. package/client/dist/assets/AgentsPage-CKPtHkRP.js +0 -86
  231. package/client/dist/assets/AnalyticsPage-CZW8c77k.js +0 -1
  232. package/client/dist/assets/BarChart-DugEoe2q.js +0 -33
  233. package/client/dist/assets/CodePage-CQ-uKETG.js +0 -2
  234. package/client/dist/assets/DesktopAnalyticsPage-Bgo4SyCH.js +0 -1
  235. package/client/dist/assets/DocsDialog-DxpWw_La.js +0 -11
  236. package/client/dist/assets/DocsPage-CmcOkK19.js +0 -11
  237. package/client/dist/assets/ExportDropdown-DvTS2wc7.js +0 -1
  238. package/client/dist/assets/IntegrationsPage-B72WxJ3Z.js +0 -1
  239. package/client/dist/assets/InteractiveJobComposer-B5gh4Qoy.js +0 -19
  240. package/client/dist/assets/JobDetailModal-D-BeqsJ6.js +0 -1
  241. package/client/dist/assets/JobDetailPage-DwdozwLO.js +0 -1
  242. package/client/dist/assets/JobsPage-BDkinklt.js +0 -1
  243. package/client/dist/assets/LoopBuilderPage-CclJ3VZA.js +0 -7
  244. package/client/dist/assets/LoopPreviewModal-Dw4UgoLu.js +0 -1
  245. package/client/dist/assets/LoopsPage-D7ac4M24.js +0 -1
  246. package/client/dist/assets/PluginsPage-wdC-qa40.js +0 -2
  247. package/client/dist/assets/ProjectSettingsDialog-BlTlRtGI.js +0 -1
  248. package/client/dist/assets/TemplatePreviewModal-CeMx2Red.js +0 -1
  249. package/client/dist/assets/activity-Dbl71S-E.js +0 -1
  250. package/client/dist/assets/addon-image-CwoZj17F.js +0 -1
  251. package/client/dist/assets/addon-ligatures-CWDrED80.js +0 -2
  252. package/client/dist/assets/addspec-DrXqP8ip.js +0 -1
  253. package/client/dist/assets/agent-B8BXmlUQ2.js +0 -1
  254. package/client/dist/assets/agent-BEXYxDbs.js +0 -1
  255. package/client/dist/assets/agent-BYc3-6Ln2.js +0 -1
  256. package/client/dist/assets/agent-BbJorPdT.js +0 -1
  257. package/client/dist/assets/agent-CewFImgL.js +0 -1
  258. package/client/dist/assets/agent-DfhYoUmp.js +0 -1
  259. package/client/dist/assets/agent-TK_rrK4d.js +0 -1
  260. package/client/dist/assets/agent-bHjA9R_j2.js +0 -1
  261. package/client/dist/assets/agents-CGDn_3kB.js +0 -1
  262. package/client/dist/assets/agentstudio-Dxe_94PK.js +0 -1
  263. package/client/dist/assets/aiedit-DrfHQnlz.js +0 -1
  264. package/client/dist/assets/analytics-CxDYcBl9.js +0 -1
  265. package/client/dist/assets/attachments-DvxQbBTC.js +0 -1
  266. package/client/dist/assets/brain-C92rjjYP.js +0 -1
  267. package/client/dist/assets/browser-DM8G151q.js +0 -1
  268. package/client/dist/assets/chat-0dg5hOo_.js +0 -1
  269. package/client/dist/assets/chunk-BNv3lrIs.js +0 -1
  270. package/client/dist/assets/code-B62fClk-.js +0 -1
  271. package/client/dist/assets/commands-DCJ-R9E4.js +0 -1
  272. package/client/dist/assets/common-BM3LFLCE2.js +0 -1
  273. package/client/dist/assets/common-Bwafujnc2.js +0 -1
  274. package/client/dist/assets/common-C-vU-j-I.js +0 -1
  275. package/client/dist/assets/common-C5HKSbbz.js +0 -1
  276. package/client/dist/assets/common-DXrmj6WD.js +0 -1
  277. package/client/dist/assets/common-DhWTkocg.js +0 -1
  278. package/client/dist/assets/common-DiPTT7V_2.js +0 -1
  279. package/client/dist/assets/common-DmqBPwZG.js +0 -1
  280. package/client/dist/assets/dashboard-B5ang-ZS.js +0 -1
  281. package/client/dist/assets/dashboard-CHHcOWVL2.js +0 -1
  282. package/client/dist/assets/dashboard-DBD4wPGY2.js +0 -1
  283. package/client/dist/assets/dashboard-DBuLfWo9.js +0 -1
  284. package/client/dist/assets/dashboard-DSnztwWH.js +0 -1
  285. package/client/dist/assets/dashboard-D_aP_InU.js +0 -1
  286. package/client/dist/assets/dashboard-DmBcdl1B.js +0 -1
  287. package/client/dist/assets/dashboard-RIcUKY1Y2.js +0 -1
  288. package/client/dist/assets/editor.api2-C3B0tp2x.js +0 -872
  289. package/client/dist/assets/editor.worker-Bd9IXS8d.js +0 -26
  290. package/client/dist/assets/explore-_XyU5e7R.js +0 -1
  291. package/client/dist/assets/index-BVbgKXUu.css +0 -2
  292. package/client/dist/assets/index-DyY1QyIn.js +0 -158
  293. package/client/dist/assets/integrations-By80EBkf.js +0 -1
  294. package/client/dist/assets/jira-DeIjwCK3.js +0 -1
  295. package/client/dist/assets/jobs-1GV2-4zd2.js +0 -1
  296. package/client/dist/assets/jobs-26F9gGcE.js +0 -1
  297. package/client/dist/assets/jobs-BgQzB0Sx2.js +0 -1
  298. package/client/dist/assets/jobs-BqWui9_f.js +0 -1
  299. package/client/dist/assets/jobs-CE8qeIYj.js +0 -1
  300. package/client/dist/assets/jobs-DrYmsKy5.js +0 -1
  301. package/client/dist/assets/jobs-DzeuASRf.js +0 -1
  302. package/client/dist/assets/jobs-Wh7dn28q2.js +0 -1
  303. package/client/dist/assets/json.worker-DzV-CpCQ.js +0 -58
  304. package/client/dist/assets/lib-0PFDyMxJ.js +0 -7
  305. package/client/dist/assets/loops-CO6q75W8.js +0 -1
  306. package/client/dist/assets/mcp-HwBbQDk0.js +0 -1
  307. package/client/dist/assets/monaco.contribution-Desq7oq4.js +0 -2
  308. package/client/dist/assets/nav-BTg8s-cD.js +0 -1
  309. package/client/dist/assets/preload-helper-DSXbuxSR.js +0 -1
  310. package/client/dist/assets/settings-cJNynGIZ.js +0 -1
  311. package/client/dist/assets/setup-D2CmgAro.js +0 -1
  312. package/client/dist/assets/specs-PGIOzMtc.js +0 -1
  313. package/client/dist/assets/terminal-CFt56A5f.js +0 -1
  314. package/client/dist/assets/tickets-B7zT4IRf.js +0 -1
  315. package/client/dist/assets/tickets-BHv3ewvq.js +0 -1
  316. package/client/dist/assets/tickets-BoXom7l8.js +0 -1
  317. package/client/dist/assets/tickets-C3b8cHhd2.js +0 -1
  318. package/client/dist/assets/tickets-CFXnJsYF.js +0 -1
  319. package/client/dist/assets/tickets-CWYFvx1W2.js +0 -1
  320. package/client/dist/assets/tickets-DTHo9c3C.js +0 -1
  321. package/client/dist/assets/tickets-SuX_HkK4.js +0 -1
  322. package/client/dist/assets/upload-CmR4h9H1.js +0 -1
  323. package/client/dist/assets/value-JA3bf47v.js +0 -1
  324. /package/client/dist/assets/{abap-D0E4FfVW.js → abap-08VXUWAP.js} +0 -0
  325. /package/client/dist/assets/{activity--iN4Ad2R2.js → activity-B-rp4h_v.js} +0 -0
  326. /package/client/dist/assets/{activity-6hM_ZSg9.js → activity-C6kJpm9q2.js} +0 -0
  327. /package/client/dist/assets/{activity-CIOLxGvh.js → activity-CGx4kb_j.js} +0 -0
  328. /package/client/dist/assets/{activity-BZNfOdiS.js → activity-CnoX2bH7.js} +0 -0
  329. /package/client/dist/assets/{activity-BuVo5sPB.js → activity-DAyekdlt2.js} +0 -0
  330. /package/client/dist/assets/{activity-D2r2h3SE2.js → activity-DCxtpT7j.js} +0 -0
  331. /package/client/dist/assets/{activity-DtxrAUMo2.js → activity-JzwRaWPD.js} +0 -0
  332. /package/client/dist/assets/{addspec-BMtLZLW62.js → addspec-BXjvkh5q.js} +0 -0
  333. /package/client/dist/assets/{addspec-CRuvWn8d.js → addspec-C4bwrmpi.js} +0 -0
  334. /package/client/dist/assets/{addspec-D-JLDsUV.js → addspec-CArHXUDM.js} +0 -0
  335. /package/client/dist/assets/{addspec-mgPOt5q02.js → addspec-DTKJcXiR.js} +0 -0
  336. /package/client/dist/assets/{addspec-DfhDbg5c.js → addspec-DbNBbrha2.js} +0 -0
  337. /package/client/dist/assets/{addspec-CP0HRuxY.js → addspec-htHGZHOe2.js} +0 -0
  338. /package/client/dist/assets/{addspec-bsNDdqRm2.js → addspec-lr_LP6jk.js} +0 -0
  339. /package/client/dist/assets/{agents-78bTZGZh.js → agents-6ZOJXWVk2.js} +0 -0
  340. /package/client/dist/assets/{agents-DDtCJ5M02.js → agents-BZ0XQKjg.js} +0 -0
  341. /package/client/dist/assets/{agents-CWo-KyAB.js → agents-BqWklnHR.js} +0 -0
  342. /package/client/dist/assets/{agents-BUdEi3-N.js → agents-Bz-thHkB2.js} +0 -0
  343. /package/client/dist/assets/{agents-bvyRLczL.js → agents-CJclElx6.js} +0 -0
  344. /package/client/dist/assets/{agents-BpoUaoW92.js → agents-D_l9L0VA.js} +0 -0
  345. /package/client/dist/assets/{agents-BLhWCmDS2.js → agents-u_C0jPB5.js} +0 -0
  346. /package/client/dist/assets/{agentstudio-BMQxc_bz.js → agentstudio-BF19hbXj.js} +0 -0
  347. /package/client/dist/assets/{agentstudio-C_Jq8Tl4.js → agentstudio-BLGvxWBx2.js} +0 -0
  348. /package/client/dist/assets/{agentstudio-Dtq3dGYk.js → agentstudio-D9UKiWTj2.js} +0 -0
  349. /package/client/dist/assets/{agentstudio-CWgXG5W52.js → agentstudio-Drs37nIM.js} +0 -0
  350. /package/client/dist/assets/{agentstudio-sI27TpBu2.js → agentstudio-nEnKkcXM.js} +0 -0
  351. /package/client/dist/assets/{agentstudio-jKMTdT70.js → agentstudio-pyFcSBKy.js} +0 -0
  352. /package/client/dist/assets/{agentstudio-COZriimI2.js → agentstudio-zfTzQh3U.js} +0 -0
  353. /package/client/dist/assets/{aiedit-hknZlly82.js → aiedit-B0KUbpB6.js} +0 -0
  354. /package/client/dist/assets/{aiedit-eMN4eQPF.js → aiedit-BoG24nlg2.js} +0 -0
  355. /package/client/dist/assets/{aiedit-pc5JDUq1.js → aiedit-CjlSRQVP.js} +0 -0
  356. /package/client/dist/assets/{aiedit-B03pD_Na.js → aiedit-DCjA7jLJ.js} +0 -0
  357. /package/client/dist/assets/{aiedit-6CHdb_iv2.js → aiedit-DNXxq_Dr.js} +0 -0
  358. /package/client/dist/assets/{aiedit-igmw-ZoZ2.js → aiedit-DQ-0_pvE.js} +0 -0
  359. /package/client/dist/assets/{aiedit-BtRqRxML.js → aiedit-olnv3QSt2.js} +0 -0
  360. /package/client/dist/assets/{analytics-CCprAqk3.js → analytics-BC7mwR4s.js} +0 -0
  361. /package/client/dist/assets/{analytics-CR1TJqUi2.js → analytics-BiNg8edP.js} +0 -0
  362. /package/client/dist/assets/{analytics-Bj-SmKHa.js → analytics-C0s_T2GN.js} +0 -0
  363. /package/client/dist/assets/{analytics-C5dpiS0o.js → analytics-CdsAPAwZ2.js} +0 -0
  364. /package/client/dist/assets/{analytics-BWMUZ2Pq2.js → analytics-Cr1_q-lC.js} +0 -0
  365. /package/client/dist/assets/{analytics-DfNdXR0y2.js → analytics-IEsKZvpW2.js} +0 -0
  366. /package/client/dist/assets/{analytics-rmHuJv0V.js → analytics-SRPf84OT.js} +0 -0
  367. /package/client/dist/assets/{apex-D_X_8J4d.js → apex-BWPQTe0t.js} +0 -0
  368. /package/client/dist/assets/{atom-one-dark-BMDPFwRZ.js → atom-one-dark-qiGhVQxg.js} +0 -0
  369. /package/client/dist/assets/{attachments-BgBNSqQd.js → attachments-D0wXXt9T2.js} +0 -0
  370. /package/client/dist/assets/{attachments-C9GTTJsx2.js → attachments-DenIjuVI.js} +0 -0
  371. /package/client/dist/assets/{attachments-BjyMmrwE.js → attachments-DhO7LPfy.js} +0 -0
  372. /package/client/dist/assets/{attachments-DPCmkFZu2.js → attachments-DoH5w0x4.js} +0 -0
  373. /package/client/dist/assets/{attachments-DCZOL1RU.js → attachments-Q9yNtiEM.js} +0 -0
  374. /package/client/dist/assets/{attachments-rNn1ekRy2.js → attachments-V_WZAYXx2.js} +0 -0
  375. /package/client/dist/assets/{attachments-DL-bPy9E.js → attachments-eaeScpjM.js} +0 -0
  376. /package/client/dist/assets/{auth-DpZq5p1e.js → auth-D_5bQv5W.js} +0 -0
  377. /package/client/dist/assets/{azcli-PZLnZ3jH.js → azcli-Bc_sGQ0U.js} +0 -0
  378. /package/client/dist/assets/{bat-KlBOxpO4.js → bat-i0X4ZdIN.js} +0 -0
  379. /package/client/dist/assets/{bicep-CgG9oF3V.js → bicep-B5-_aFwp.js} +0 -0
  380. /package/client/dist/assets/{browser-BPkhILSJ.js → browser-C25DkQiO.js} +0 -0
  381. /package/client/dist/assets/{browser-BqLqJVVT.js → browser-CiJ7-L32.js} +0 -0
  382. /package/client/dist/assets/{browser-rWKj-RR_2.js → browser-CvgDySd9.js} +0 -0
  383. /package/client/dist/assets/{browser-Cvg-QHvd2.js → browser-CyR1Kp3m2.js} +0 -0
  384. /package/client/dist/assets/{browser-FRv8P02E.js → browser-DwZljcRh2.js} +0 -0
  385. /package/client/dist/assets/{browser-Cc0iDqUi.js → browser-DxP-alBK.js} +0 -0
  386. /package/client/dist/assets/{browser-BijVYG0Y2.js → browser-rRCwi7Y7.js} +0 -0
  387. /package/client/dist/assets/{cameligo-6R45j6Ph.js → cameligo-DMUM7wLl.js} +0 -0
  388. /package/client/dist/assets/{chat-Bom6bqK3.js → chat-2rIB8HhT.js} +0 -0
  389. /package/client/dist/assets/{chat-vylxqR0P2.js → chat-BR8oe53x2.js} +0 -0
  390. /package/client/dist/assets/{chat-Dnwg-NpQ.js → chat-Bua03tty.js} +0 -0
  391. /package/client/dist/assets/{chat-BgL3zv6x2.js → chat-CZpAQan1.js} +0 -0
  392. /package/client/dist/assets/{chat-DsbwJhus.js → chat-CdY5SGvS.js} +0 -0
  393. /package/client/dist/assets/{chat-Bh3NdgJs.js → chat-DZyu3ImY2.js} +0 -0
  394. /package/client/dist/assets/{chat-DdwZjT8K2.js → chat-D_I7nU5K.js} +0 -0
  395. /package/client/dist/assets/{clojure-CLAAMO7J.js → clojure-Cm7r79vr.js} +0 -0
  396. /package/client/dist/assets/{clsx-CNmW0Nip.js → clsx-CjueKrWZ.js} +0 -0
  397. /package/client/dist/assets/{code-UPVvmAKM2.js → code-76gS0IdL.js} +0 -0
  398. /package/client/dist/assets/{code-BRcDlze52.js → code-C1VwxlML.js} +0 -0
  399. /package/client/dist/assets/{code-CeQ0FZpC.js → code-CnMZyJpr.js} +0 -0
  400. /package/client/dist/assets/{code-DgeHz9CB.js → code-D20ef9Cf2.js} +0 -0
  401. /package/client/dist/assets/{code-DzC1rMk12.js → code-DdOXaWRY.js} +0 -0
  402. /package/client/dist/assets/{code-CwnBPPyA.js → code-Duohuf9l.js} +0 -0
  403. /package/client/dist/assets/{code-BdMy3Ffq.js → code-geHmknB92.js} +0 -0
  404. /package/client/dist/assets/{coffee-AruSygo-.js → coffee-Ba7i2nA0.js} +0 -0
  405. /package/client/dist/assets/{commands-DkU0qliq.js → commands-B9RZO4qS.js} +0 -0
  406. /package/client/dist/assets/{commands-u-4eEHCN.js → commands-BEwbAjtD2.js} +0 -0
  407. /package/client/dist/assets/{commands-JWMfGDmr.js → commands-Bbxu8kLU.js} +0 -0
  408. /package/client/dist/assets/{commands-C4Y3C-PF2.js → commands-CDU3qv68.js} +0 -0
  409. /package/client/dist/assets/{commands-xHDDGfcP2.js → commands-CzezgtQj.js} +0 -0
  410. /package/client/dist/assets/{commands-CSDs9q_2.js → commands-DTQBF_q52.js} +0 -0
  411. /package/client/dist/assets/{commands-RHOchN4n2.js → commands-hOtKuOXc.js} +0 -0
  412. /package/client/dist/assets/{cpp-DEE5r4Pw.js → cpp-C7h46wYY.js} +0 -0
  413. /package/client/dist/assets/{csharp-BVXJUvbK.js → csharp-BKxtCVv1.js} +0 -0
  414. /package/client/dist/assets/{csp-cXOF0JoV.js → csp-bTuwJoIa.js} +0 -0
  415. /package/client/dist/assets/{css-C_zQuAoy.js → css-DIMkf-bt.js} +0 -0
  416. /package/client/dist/assets/{cypher-t95m_Q-O.js → cypher-CVaqCwHa.js} +0 -0
  417. /package/client/dist/assets/{dart-B4r1Tg-e.js → dart-onAF5SnQ.js} +0 -0
  418. /package/client/dist/assets/{dockerfile-6mt7zHQw.js → dockerfile-DZFCIeNp.js} +0 -0
  419. /package/client/dist/assets/{ecl-DEY_Fz_Q.js → ecl-D05T4iGw.js} +0 -0
  420. /package/client/dist/assets/{elixir-R6-q1c5t.js → elixir-6RTg0lbw.js} +0 -0
  421. /package/client/dist/assets/{explore-B7fblXBb2.js → explore-BG99VGpL.js} +0 -0
  422. /package/client/dist/assets/{explore-CNBzrEPS.js → explore-C0TjprJW2.js} +0 -0
  423. /package/client/dist/assets/{explore-C0k8HrkN.js → explore-C2dgEo7q.js} +0 -0
  424. /package/client/dist/assets/{explore-BHtEpoNN2.js → explore-C8NDHbob.js} +0 -0
  425. /package/client/dist/assets/{explore-Dahc1lOx.js → explore-CWWgzQGR.js} +0 -0
  426. /package/client/dist/assets/{explore-D_mWBYCS2.js → explore-CcQXpdtS.js} +0 -0
  427. /package/client/dist/assets/{explore-UvW1jEMa.js → explore-DsXAgnmJ2.js} +0 -0
  428. /package/client/dist/assets/{flow9-CNQvHBOt.js → flow9-C5_-GSwl.js} +0 -0
  429. /package/client/dist/assets/{format-command-CuWuRvka.js → format-command-Dfa1o2d-.js} +0 -0
  430. /package/client/dist/assets/{fsharp-vPZkFXwv.js → fsharp-C8Ef5oNN.js} +0 -0
  431. /package/client/dist/assets/{go-KUzBMYJ_.js → go-C-y9NEjX.js} +0 -0
  432. /package/client/dist/assets/{graphql-C3W0rCsD.js → graphql-fmXr3nnJ.js} +0 -0
  433. /package/client/dist/assets/{hcl-BOOMFSNX.js → hcl-CpzslTdj.js} +0 -0
  434. /package/client/dist/assets/{ini-CFat1WJr.js → ini-sBoK_t0W.js} +0 -0
  435. /package/client/dist/assets/{integrations-TAcYNh0b.js → integrations-CH0zy1Mw2.js} +0 -0
  436. /package/client/dist/assets/{integrations-C1ThkTC52.js → integrations-CIAjhd7L.js} +0 -0
  437. /package/client/dist/assets/{integrations-2YcNyAl3.js → integrations-CZdwOe8A.js} +0 -0
  438. /package/client/dist/assets/{integrations-BXCvXUlG2.js → integrations-Ct7omMld.js} +0 -0
  439. /package/client/dist/assets/{integrations-Crk2rFhL2.js → integrations-D6MSxy3-.js} +0 -0
  440. /package/client/dist/assets/{integrations-Dn4JiGtW.js → integrations-DbZa-Zyc.js} +0 -0
  441. /package/client/dist/assets/{integrations-D9YrJ_Ep.js → integrations-b8pH-vfl2.js} +0 -0
  442. /package/client/dist/assets/{java-Dee-CNAM.js → java-BEtHBSE6.js} +0 -0
  443. /package/client/dist/assets/{jira-D5efgwp_.js → jira-BySfUuQZ.js} +0 -0
  444. /package/client/dist/assets/{jira-bDs7Im2C.js → jira-Byw2C0F8.js} +0 -0
  445. /package/client/dist/assets/{jira-BHAKtc4B.js → jira-CNeYi06w2.js} +0 -0
  446. /package/client/dist/assets/{jira-Be369RIR2.js → jira-D11gPhxz.js} +0 -0
  447. /package/client/dist/assets/{jira-CPpgA-5z2.js → jira-D38Gj7AT.js} +0 -0
  448. /package/client/dist/assets/{jira-uNH1gETL2.js → jira-gAvOyije.js} +0 -0
  449. /package/client/dist/assets/{jira-DS7ownDy.js → jira-pRaL7JQo2.js} +0 -0
  450. /package/client/dist/assets/{julia-CGNlLYuU.js → julia-Bri6UV-V.js} +0 -0
  451. /package/client/dist/assets/{kotlin-CY7lhknJ.js → kotlin-BOotOW0E.js} +0 -0
  452. /package/client/dist/assets/{less-D0KN7hfJ.js → less-B9JPFI3C.js} +0 -0
  453. /package/client/dist/assets/{lexon-CuRJufPs.js → lexon-CfSJPG6W.js} +0 -0
  454. /package/client/dist/assets/{loops-LIgO_-eM.js → loops-CPpgMapU.js} +0 -0
  455. /package/client/dist/assets/{loops-DJNhp17p2.js → loops-CWWbKuVL.js} +0 -0
  456. /package/client/dist/assets/{loops-D6D3XHjj.js → loops-Crc9It8o2.js} +0 -0
  457. /package/client/dist/assets/{loops-CvOWsi5r.js → loops-DH2hZcbK2.js} +0 -0
  458. /package/client/dist/assets/{loops-vhJdbcz92.js → loops-DS5TmvIU2.js} +0 -0
  459. /package/client/dist/assets/{loops-K5_HXfJa.js → loops-P2f0cN8-.js} +0 -0
  460. /package/client/dist/assets/{loops-BvqQ-bHR2.js → loops-fJV-9hH7.js} +0 -0
  461. /package/client/dist/assets/{lua-DHvH62Zf.js → lua-CsQS60Ue.js} +0 -0
  462. /package/client/dist/assets/{m3-tlthQ8Fo.js → m3-D-oSqn_W.js} +0 -0
  463. /package/client/dist/assets/{markdown-Dbx1LgUZ.js → markdown-Cimd5fb3.js} +0 -0
  464. /package/client/dist/assets/{mcp-Dcl-XxkR.js → mcp-Czd_iyp02.js} +0 -0
  465. /package/client/dist/assets/{mcp-XJNRfxVp2.js → mcp-D2pc5GGx.js} +0 -0
  466. /package/client/dist/assets/{mcp-sRmWE8WJ.js → mcp-D4ZjtIx32.js} +0 -0
  467. /package/client/dist/assets/{mcp-DtbxuH3V2.js → mcp-DIfBUv2D2.js} +0 -0
  468. /package/client/dist/assets/{mcp-H-gnQUdN.js → mcp-DsTwmjST.js} +0 -0
  469. /package/client/dist/assets/{mcp-N1nzp8oL.js → mcp-SMkn8boG.js} +0 -0
  470. /package/client/dist/assets/{mcp-CjVKXAhD2.js → mcp-fSfiVYAc.js} +0 -0
  471. /package/client/dist/assets/{mips-D_O3IUz_.js → mips-CIPQ_RoX.js} +0 -0
  472. /package/client/dist/assets/{msdax-C_ZmCnRt.js → msdax-DauUninz.js} +0 -0
  473. /package/client/dist/assets/{mysql-C-dCEYSX.js → mysql-SOo6toE5.js} +0 -0
  474. /package/client/dist/assets/{nav-BElxljIF2.js → nav-BRJBxaGt2.js} +0 -0
  475. /package/client/dist/assets/{nav-COu2E_-L.js → nav-CAGKv3_F.js} +0 -0
  476. /package/client/dist/assets/{nav-BXO7PJJG2.js → nav-CNskRJKQ2.js} +0 -0
  477. /package/client/dist/assets/{nav-CW9pxWfC2.js → nav-DY93irBL.js} +0 -0
  478. /package/client/dist/assets/{nav-CXHUA4uf.js → nav-DhjvYgqt.js} +0 -0
  479. /package/client/dist/assets/{nav-DdHF5VWJ.js → nav-Gb-r4wop.js} +0 -0
  480. /package/client/dist/assets/{nav-C8kOuGh8.js → nav-TvumAFD22.js} +0 -0
  481. /package/client/dist/assets/{objective-c-DZY9Aqxd.js → objective-c-FvmIjYaQ.js} +0 -0
  482. /package/client/dist/assets/{pascal-BVHLXoor.js → pascal-DrH0SRf2.js} +0 -0
  483. /package/client/dist/assets/{pascaligo-XdG_6u3k.js → pascaligo-D-ptJ9y-.js} +0 -0
  484. /package/client/dist/assets/{perl-b9QV0BcD.js → perl-oz_6vUea.js} +0 -0
  485. /package/client/dist/assets/{pgsql-Du5FDOgT.js → pgsql-DTj74zXo.js} +0 -0
  486. /package/client/dist/assets/{php-WJD0z2Z_.js → php-nr791fC2.js} +0 -0
  487. /package/client/dist/assets/{pla-CbciTf2w.js → pla-CopQ2nXW.js} +0 -0
  488. /package/client/dist/assets/{postiats-C1QxmvVq.js → postiats-43DmfD33.js} +0 -0
  489. /package/client/dist/assets/{powerquery-BEYDhPLZ.js → powerquery-D3hlyOfw.js} +0 -0
  490. /package/client/dist/assets/{powershell-CfaYw4XL.js → powershell-DmHpPYUd.js} +0 -0
  491. /package/client/dist/assets/{protobuf-pwZEDOYa.js → protobuf-C531GsRP.js} +0 -0
  492. /package/client/dist/assets/{pug-5ftqSPVe.js → pug-Z5eAx3Zn.js} +0 -0
  493. /package/client/dist/assets/{qsharp-uI6qZMq4.js → qsharp-DkqhCAOL.js} +0 -0
  494. /package/client/dist/assets/{r-BK54P1EG.js → r-BwWrilGY.js} +0 -0
  495. /package/client/dist/assets/{redis-DI0yd7kN.js → redis-ClamHrr6.js} +0 -0
  496. /package/client/dist/assets/{redshift-Dov26eyI.js → redshift-DT7zqm-g.js} +0 -0
  497. /package/client/dist/assets/{restructuredtext-QqJ_1dC-.js → restructuredtext-BYgofb2h.js} +0 -0
  498. /package/client/dist/assets/{ruby-5WjveeVb.js → ruby-DezsRK8O.js} +0 -0
  499. /package/client/dist/assets/{rust-BzVSfqFH.js → rust-DdL9SqIa.js} +0 -0
  500. /package/client/dist/assets/{sb-DMIUfT-3.js → sb-CcwsVR0C.js} +0 -0
  501. /package/client/dist/assets/{scala-CSKA7lqt.js → scala-DHpiXF5c.js} +0 -0
  502. /package/client/dist/assets/{scheme-CojOP2Qc.js → scheme-BeGwcela.js} +0 -0
  503. /package/client/dist/assets/{scss-DoBZGJG6.js → scss-gp-XZpBa.js} +0 -0
  504. /package/client/dist/assets/{settings-1w1O5Og82.js → settings-B9ilOSYO.js} +0 -0
  505. /package/client/dist/assets/{settings-Jc46vUhd.js → settings-BqlWHRHA.js} +0 -0
  506. /package/client/dist/assets/{settings-BSihkr0e.js → settings-D8dlqt4d.js} +0 -0
  507. /package/client/dist/assets/{settings-BiDOB3hV2.js → settings-DAZqjX2a2.js} +0 -0
  508. /package/client/dist/assets/{settings-DNL68KTE.js → settings-DRh39v1S2.js} +0 -0
  509. /package/client/dist/assets/{settings-BoubJhDP2.js → settings-Mj1OJbfd2.js} +0 -0
  510. /package/client/dist/assets/{settings-CB-EZ_W8.js → settings-lRReaSYm.js} +0 -0
  511. /package/client/dist/assets/{setup-D84y3wx-.js → setup-CGZjy2IF2.js} +0 -0
  512. /package/client/dist/assets/{setup-BuQNKztt2.js → setup-CXa0Sx16.js} +0 -0
  513. /package/client/dist/assets/{setup-BwMovM9z2.js → setup-D2ktKKMF2.js} +0 -0
  514. /package/client/dist/assets/{setup-DU0ULX8q2.js → setup-DG2H7ha5.js} +0 -0
  515. /package/client/dist/assets/{setup-BXpw07r-.js → setup-DK5bHDUq2.js} +0 -0
  516. /package/client/dist/assets/{setup-7J8TjG4O.js → setup-DrWlIQTZ.js} +0 -0
  517. /package/client/dist/assets/{setup-BiduSX9d.js → setup-rzHUVgAu.js} +0 -0
  518. /package/client/dist/assets/{shell-YOaE4udl.js → shell-CC2rA5mh.js} +0 -0
  519. /package/client/dist/assets/{solidity-l3jSyVLu.js → solidity-BEEn4gHE.js} +0 -0
  520. /package/client/dist/assets/{sophia-BhSGTaQJ.js → sophia-CRfGWb83.js} +0 -0
  521. /package/client/dist/assets/{sparql-qhk6QD9w.js → sparql-D_Lu-MrJ.js} +0 -0
  522. /package/client/dist/assets/{specs-CWtKcVDC.js → specs-BcyxaCwf2.js} +0 -0
  523. /package/client/dist/assets/{specs-DTplt2XM.js → specs-C1q9geAi.js} +0 -0
  524. /package/client/dist/assets/{specs-PozUNhqb.js → specs-CLZKysq-2.js} +0 -0
  525. /package/client/dist/assets/{specs-C9Sd7BhH2.js → specs-DZMS9o54.js} +0 -0
  526. /package/client/dist/assets/{specs-D2clrt2c2.js → specs-Dm6zUI-R.js} +0 -0
  527. /package/client/dist/assets/{specs-CBDvEYKv.js → specs-V3TvhKLX.js} +0 -0
  528. /package/client/dist/assets/{specs-CIPr9reA2.js → specs-dV3Mq5Mu2.js} +0 -0
  529. /package/client/dist/assets/{sql-DoO3y21-.js → sql-NEE52Syq.js} +0 -0
  530. /package/client/dist/assets/{st-ChZAY8oG.js → st-DbInun42.js} +0 -0
  531. /package/client/dist/assets/{swift-D290Tr7_.js → swift-Bxkupp3x.js} +0 -0
  532. /package/client/dist/assets/{systemverilog-DsxzgJ-o.js → systemverilog-Bz4Y3fRF.js} +0 -0
  533. /package/client/dist/assets/{tcl-BKDUHAAR.js → tcl-DISqw1ZD.js} +0 -0
  534. /package/client/dist/assets/{terminal-De2Brd23.js → terminal-BZiIu3rf.js} +0 -0
  535. /package/client/dist/assets/{terminal-cqfd3EDV.js → terminal-C2NAd7BW2.js} +0 -0
  536. /package/client/dist/assets/{terminal-WSAvHXoU.js → terminal-IahE4LW92.js} +0 -0
  537. /package/client/dist/assets/{terminal-Z7KsDM_n.js → terminal-QpXZU2Bn2.js} +0 -0
  538. /package/client/dist/assets/{terminal-xGbCZJHg.js → terminal-ScjyXNxQ.js} +0 -0
  539. /package/client/dist/assets/{terminal-BRm03iFA2.js → terminal-d7GadXbN.js} +0 -0
  540. /package/client/dist/assets/{terminal-DxQWCqSD2.js → terminal-tITa81iz.js} +0 -0
  541. /package/client/dist/assets/{twig-UpZQaM8G.js → twig-De2hgUGE.js} +0 -0
  542. /package/client/dist/assets/{typespec-DzaKvN8i.js → typespec-B8J7ngcE.js} +0 -0
  543. /package/client/dist/assets/{vb-CZpbb7tk.js → vb-DV3o63ZY.js} +0 -0
  544. /package/client/dist/assets/{wgsl-CNt3OIvl.js → wgsl-DpFanUEy.js} +0 -0
@@ -3,17 +3,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.HeadroomManager = exports.HEADROOM_MANAGED_PYTHON_VERSION = void 0;
6
+ exports.HeadroomManager = exports.HEADROOM_ACTIVATION_SOURCE = exports.HEADROOM_ACTIVATION_SCHEMA_VERSION = exports.HEADROOM_MANAGED_PYTHON_VERSION = exports.HEADROOM_RELAY_PATH = void 0;
7
7
  exports.getHeadroomManagedInstallPlan = getHeadroomManagedInstallPlan;
8
+ exports.parseWindowsOwnershipSnapshot = parseWindowsOwnershipSnapshot;
9
+ exports.windowsSnapshotHasOwnedListener = windowsSnapshotHasOwnedListener;
10
+ exports.parseHeadroomDoctorRoutes = parseHeadroomDoctorRoutes;
8
11
  const child_process_1 = require("child_process");
12
+ const crypto_1 = require("crypto");
9
13
  const fs_1 = __importDefault(require("fs"));
14
+ const http_1 = __importDefault(require("http"));
15
+ const net_1 = __importDefault(require("net"));
10
16
  const os_1 = __importDefault(require("os"));
11
17
  const path_1 = __importDefault(require("path"));
18
+ const auth_1 = require("./auth");
12
19
  const desktop_db_1 = require("./desktop-db");
13
20
  const headroom_routing_1 = require("./headroom-routing");
21
+ const win_spawn_1 = require("./util/win-spawn");
14
22
  const STATE_KEY = 'plugins.headroom.state';
15
23
  const DEFAULT_PORT = 8787;
24
+ const PROXY_HEALTH_REQUEST_TIMEOUT_MS = 750;
25
+ const RELAY_CONNECT_TIMEOUT_MS = 3_000;
26
+ exports.HEADROOM_RELAY_PATH = '/_specrails/headroom';
16
27
  exports.HEADROOM_MANAGED_PYTHON_VERSION = '3.12';
28
+ exports.HEADROOM_ACTIVATION_SCHEMA_VERSION = 1;
29
+ exports.HEADROOM_ACTIVATION_SOURCE = 'explicit-user-action';
17
30
  function nowIso() {
18
31
  return new Date().toISOString();
19
32
  }
@@ -209,6 +222,290 @@ function classifyProxyFailure(output, command) {
209
222
  }
210
223
  return issue('proxy_unhealthy', output, command);
211
224
  }
225
+ function positiveInteger(value) {
226
+ const parsed = typeof value === 'number' ? value : Number(value);
227
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
228
+ }
229
+ /** Parse the language-independent JSON emitted by the Windows ownership probe. */
230
+ function parseWindowsOwnershipSnapshot(raw) {
231
+ try {
232
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, '').trim());
233
+ const listeningValues = Array.isArray(parsed.listeningPids)
234
+ ? parsed.listeningPids
235
+ : parsed.listeningPids == null
236
+ ? []
237
+ : [parsed.listeningPids];
238
+ const processValues = Array.isArray(parsed.processes)
239
+ ? parsed.processes
240
+ : parsed.processes && typeof parsed.processes === 'object'
241
+ ? [parsed.processes]
242
+ : [];
243
+ const listeningPids = new Set();
244
+ for (const value of listeningValues) {
245
+ const pid = positiveInteger(value);
246
+ if (pid)
247
+ listeningPids.add(pid);
248
+ }
249
+ const parentByPid = new Map();
250
+ for (const value of processValues) {
251
+ if (!value || typeof value !== 'object')
252
+ continue;
253
+ const row = value;
254
+ const pid = positiveInteger(row.ProcessId);
255
+ const parentPid = positiveInteger(row.ParentProcessId);
256
+ if (pid && parentPid)
257
+ parentByPid.set(pid, parentPid);
258
+ }
259
+ return { listeningPids, parentByPid };
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ }
265
+ /** True only when a listening PID is the spawned process or one of its descendants. */
266
+ function windowsSnapshotHasOwnedListener(snapshot, rootPid) {
267
+ if (!Number.isInteger(rootPid) || rootPid <= 0)
268
+ return false;
269
+ for (const listenerPid of snapshot.listeningPids) {
270
+ let current = listenerPid;
271
+ const visited = new Set();
272
+ while (current > 0 && !visited.has(current)) {
273
+ if (current === rootPid)
274
+ return true;
275
+ visited.add(current);
276
+ current = snapshot.parentByPid.get(current) ?? 0;
277
+ }
278
+ }
279
+ return false;
280
+ }
281
+ function descendantPids(rootPid, parentByPid) {
282
+ const descendants = new Set([rootPid]);
283
+ let changed = true;
284
+ while (changed) {
285
+ changed = false;
286
+ for (const [pid, parentPid] of parentByPid) {
287
+ if (descendants.has(pid) || !descendants.has(parentPid))
288
+ continue;
289
+ descendants.add(pid);
290
+ changed = true;
291
+ }
292
+ }
293
+ return descendants;
294
+ }
295
+ async function captureCommandOutput(command, args, options) {
296
+ return await new Promise((resolve) => {
297
+ let child;
298
+ try {
299
+ child = (0, child_process_1.spawn)(command, args, {
300
+ env: options.env ?? process.env,
301
+ stdio: ['ignore', 'pipe', 'ignore'],
302
+ windowsHide: options.windowsHide,
303
+ });
304
+ }
305
+ catch {
306
+ resolve(null);
307
+ return;
308
+ }
309
+ let output = '';
310
+ let size = 0;
311
+ let settled = false;
312
+ const maxBuffer = options.maxBuffer ?? 2 * 1024 * 1024;
313
+ const settle = (value) => {
314
+ if (settled)
315
+ return;
316
+ settled = true;
317
+ clearTimeout(timer);
318
+ resolve(value);
319
+ };
320
+ const timer = setTimeout(() => {
321
+ try {
322
+ child.kill('SIGKILL');
323
+ }
324
+ catch { /* already gone */ }
325
+ settle(null);
326
+ }, options.timeoutMs);
327
+ timer.unref?.();
328
+ child.stdout?.on('data', (chunk) => {
329
+ if (settled)
330
+ return;
331
+ const value = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
332
+ size += Buffer.byteLength(value);
333
+ if (size > maxBuffer) {
334
+ try {
335
+ child.kill('SIGKILL');
336
+ }
337
+ catch { /* already gone */ }
338
+ settle(null);
339
+ return;
340
+ }
341
+ output += value;
342
+ });
343
+ child.once('error', () => settle(null));
344
+ child.once('close', (code) => settle(code === 0 ? output : null));
345
+ });
346
+ }
347
+ async function readLinuxDescendantPids(rootPid) {
348
+ const descendants = new Set([rootPid]);
349
+ const pending = [rootPid];
350
+ while (pending.length > 0) {
351
+ const current = pending.shift();
352
+ try {
353
+ const taskIds = await fs_1.default.promises.readdir(`/proc/${current}/task`);
354
+ for (const taskId of taskIds) {
355
+ let raw;
356
+ try {
357
+ raw = await fs_1.default.promises.readFile(`/proc/${current}/task/${taskId}/children`, 'utf8');
358
+ }
359
+ catch {
360
+ continue;
361
+ }
362
+ for (const value of raw.trim().split(/\s+/)) {
363
+ const childPid = positiveInteger(value);
364
+ if (!childPid || descendants.has(childPid))
365
+ continue;
366
+ descendants.add(childPid);
367
+ pending.push(childPid);
368
+ }
369
+ }
370
+ }
371
+ catch {
372
+ // A process can exit while its descendants are enumerated. Omitting that
373
+ // branch is fail-closed for listener ownership.
374
+ }
375
+ }
376
+ return descendants;
377
+ }
378
+ async function readPosixParentMap() {
379
+ const args = ['-axo', 'pid=,ppid='];
380
+ const raw = await captureCommandOutput('/bin/ps', args, { timeoutMs: 2_000 })
381
+ ?? await captureCommandOutput('/usr/bin/ps', args, { timeoutMs: 2_000 });
382
+ const parentByPid = new Map();
383
+ if (raw === null)
384
+ return parentByPid;
385
+ for (const line of raw.split(/\r?\n/)) {
386
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s*$/);
387
+ const pid = positiveInteger(match?.[1]);
388
+ const parentPid = positiveInteger(match?.[2]);
389
+ if (pid && parentPid)
390
+ parentByPid.set(pid, parentPid);
391
+ }
392
+ return parentByPid;
393
+ }
394
+ async function windowsProcessOwnsListeningPort(pid, port) {
395
+ const env = (0, win_spawn_1.windowsSpawnEnv)();
396
+ const systemRoot = (env.SystemRoot || env.windir || 'C:\\Windows').replace(/[\\/]$/, '');
397
+ const powershell = path_1.default.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
398
+ // uv-managed console entry points are native trampoline processes on
399
+ // Windows. The Python process that owns the socket is their child, so an
400
+ // exact-PID netstat comparison rejects every legitimate managed install.
401
+ // Query both the listener owner and the process tree in one fail-closed
402
+ // snapshot, then accept only a transitive descendant of our spawned PID.
403
+ const command = [
404
+ "$ErrorActionPreference = 'Stop'",
405
+ `$listeningPids = @(Get-NetTCPConnection -State Listen -LocalPort ${port} | ForEach-Object { [int]$_.OwningProcess })`,
406
+ '$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId)',
407
+ '[pscustomobject]@{ listeningPids = $listeningPids; processes = $processes } | ConvertTo-Json -Compress -Depth 4',
408
+ ].join('; ');
409
+ const raw = await captureCommandOutput(powershell, [
410
+ '-NoLogo',
411
+ '-NoProfile',
412
+ '-NonInteractive',
413
+ '-Command',
414
+ command,
415
+ ], {
416
+ // Keep this below the relay's connection deadline. A slow/ambiguous OS
417
+ // probe rejects the connection without ever releasing its queued headers.
418
+ timeoutMs: 2_500,
419
+ windowsHide: true,
420
+ env,
421
+ maxBuffer: 2 * 1024 * 1024,
422
+ });
423
+ if (raw === null)
424
+ return false;
425
+ const snapshot = parseWindowsOwnershipSnapshot(raw);
426
+ return !!snapshot && windowsSnapshotHasOwnedListener(snapshot, pid);
427
+ }
428
+ /**
429
+ * A successful HTTP probe is not proof that the process we spawned owns the
430
+ * endpoint: another local process can bind between the preflight probe and the
431
+ * child calling bind(2). Verify the listening socket belongs to the owned PID
432
+ * before provider credentials are routed to it.
433
+ */
434
+ async function processOwnsListeningPort(pid, port) {
435
+ if (!Number.isInteger(pid) || pid <= 0)
436
+ return false;
437
+ if (process.platform === 'linux') {
438
+ try {
439
+ const listeningInodes = new Set();
440
+ const tables = await Promise.all(['/proc/net/tcp', '/proc/net/tcp6'].map(async (table) => {
441
+ try {
442
+ return await fs_1.default.promises.readFile(table, 'utf8');
443
+ }
444
+ catch {
445
+ return '';
446
+ }
447
+ }));
448
+ for (const raw of tables) {
449
+ for (const line of raw.split(/\r?\n/).slice(1)) {
450
+ const columns = line.trim().split(/\s+/);
451
+ if (columns.length < 10 || columns[3] !== '0A')
452
+ continue;
453
+ const portHex = columns[1]?.split(':').pop();
454
+ if (!portHex || Number.parseInt(portHex, 16) !== port)
455
+ continue;
456
+ if (columns[9])
457
+ listeningInodes.add(columns[9]);
458
+ }
459
+ }
460
+ if (listeningInodes.size === 0)
461
+ return false;
462
+ const ownedPids = await readLinuxDescendantPids(pid);
463
+ for (const ownedPid of ownedPids) {
464
+ let fds;
465
+ try {
466
+ fds = await fs_1.default.promises.readdir(`/proc/${ownedPid}/fd`);
467
+ }
468
+ catch {
469
+ continue;
470
+ }
471
+ for (const fd of fds) {
472
+ let target;
473
+ try {
474
+ target = await fs_1.default.promises.readlink(`/proc/${ownedPid}/fd/${fd}`);
475
+ }
476
+ catch {
477
+ continue;
478
+ }
479
+ const match = target.match(/^socket:\[(\d+)\]$/);
480
+ if (match?.[1] && listeningInodes.has(match[1]))
481
+ return true;
482
+ }
483
+ }
484
+ return false;
485
+ }
486
+ catch {
487
+ return false;
488
+ }
489
+ }
490
+ if (process.platform === 'win32') {
491
+ return await windowsProcessOwnsListeningPort(pid, port);
492
+ }
493
+ const args = [
494
+ '-nP',
495
+ `-iTCP:${port}`,
496
+ '-sTCP:LISTEN',
497
+ '-Fp',
498
+ ];
499
+ const output = await captureCommandOutput('/usr/sbin/lsof', args, { timeoutMs: 2_000 })
500
+ ?? await captureCommandOutput('lsof', args, { timeoutMs: 2_000 });
501
+ if (output === null)
502
+ return false;
503
+ const ownedPids = descendantPids(pid, await readPosixParentMap());
504
+ return output.split(/\r?\n/).some((line) => {
505
+ const listenerPid = positiveInteger(line.match(/^p(\d+)$/)?.[1]);
506
+ return !!listenerPid && ownedPids.has(listenerPid);
507
+ });
508
+ }
212
509
  function providerLabel(provider) {
213
510
  return provider === 'codex' ? 'Codex' : 'Claude';
214
511
  }
@@ -260,6 +557,33 @@ function normalizeProviderKey(value) {
260
557
  return 'claude';
261
558
  return null;
262
559
  }
560
+ /** Parse only the documented pass/fail signal; human prose is never authority. */
561
+ function parseHeadroomDoctorRoutes(raw) {
562
+ const routes = { codex: false, claude: false };
563
+ try {
564
+ const parsed = JSON.parse(raw);
565
+ if (!Array.isArray(parsed.checks))
566
+ return routes;
567
+ for (const value of parsed.checks) {
568
+ if (!value || typeof value !== 'object')
569
+ continue;
570
+ const check = value;
571
+ const provider = normalizeProviderKey(check.name);
572
+ if (!provider || check.status !== 'pass')
573
+ continue;
574
+ const summary = typeof check.summary === 'string' ? check.summary.trim().toLowerCase() : '';
575
+ // Some Headroom versions have emitted a successful diagnostic check with
576
+ // text such as "not routed". A contradictory result must fail closed.
577
+ if (/\bnot(?:\s+\w+){0,3}\s+routed\b|\bunrouted\b|\brouting\s+(?:is\s+)?(?:disabled|inactive|off)\b/.test(summary))
578
+ continue;
579
+ routes[provider] = true;
580
+ }
581
+ }
582
+ catch {
583
+ // Malformed/unknown doctor schemas are not evidence of a configured route.
584
+ }
585
+ return routes;
586
+ }
263
587
  function num(value) {
264
588
  const parsed = Number(value);
265
589
  return Number.isFinite(parsed) ? parsed : 0;
@@ -295,18 +619,44 @@ class HeadroomManager {
295
619
  db;
296
620
  broadcast;
297
621
  availableProvidersSupplier;
622
+ processControl;
298
623
  proxy = null;
624
+ proxyTrustedPort = null;
625
+ proxyStart = null;
626
+ proxyStop = null;
627
+ proxyGeneration = 0;
628
+ shuttingDown = false;
629
+ shutdownPromise = null;
630
+ shutdownState = null;
631
+ lifecycleTail = Promise.resolve();
632
+ lifecycleCommands = new Map();
299
633
  proxyTail = '';
634
+ relayToken = (0, crypto_1.randomBytes)(32).toString('base64url');
635
+ relayAgent;
636
+ relayUpgradeTeardowns = new Set();
300
637
  routeDetectionCache = null;
301
638
  metricsCache = emptyMetricsState();
302
639
  metricsRefresh = null;
303
- constructor(db, broadcast, availableProvidersSupplier = () => ['claude', 'codex']) {
640
+ constructor(db, broadcast, availableProvidersSupplier = () => ['claude', 'codex'], processControl = {}) {
304
641
  this.db = db;
305
642
  this.broadcast = broadcast;
306
643
  this.availableProvidersSupplier = availableProvidersSupplier;
644
+ this.processControl = processControl;
645
+ this.relayAgent = new http_1.default.Agent({ keepAlive: true, maxSockets: 16, maxFreeSockets: 4 });
646
+ // Agent#createConnection supports an asynchronous callback. Returning no
647
+ // socket prevents ClientRequest from writing queued headers/body on TCP
648
+ // connect; the socket is handed over only after the post-connect PID check.
649
+ this.relayAgent.createConnection = ((options, callback) => {
650
+ const expectedPort = Number(options.port);
651
+ void this.connectVerifiedBackend(expectedPort).then(({ socket }) => callback?.(null, socket), (err) => callback?.(err, undefined));
652
+ return undefined;
653
+ });
307
654
  this.syncRouting();
308
655
  }
309
656
  getState() {
657
+ return this.buildState(true);
658
+ }
659
+ buildState(refreshMetrics) {
310
660
  const persisted = this.readPersisted();
311
661
  const uvPath = this.resolveUvPath();
312
662
  const install = this.resolveHeadroomInstall(persisted.executablePath ?? null, !!persisted.ignoreSystemInstall);
@@ -315,10 +665,7 @@ class HeadroomManager {
315
665
  const installed = !!exe && !!version;
316
666
  const availableProviders = this.availableProviderRecord();
317
667
  const detectedRoutes = installed && exe ? this.detectProviderRoutes(exe) : providerRecord(persisted.detectedRoutes);
318
- const activeProviders = {
319
- codex: persisted.activeProviders?.codex ?? (availableProviders.codex && detectedRoutes.codex),
320
- claude: persisted.activeProviders?.claude ?? (availableProviders.claude && detectedRoutes.claude),
321
- };
668
+ const activeProviders = this.explicitActiveProviders(persisted);
322
669
  if (installed) {
323
670
  const shouldPersistInstall = persisted.installed !== true ||
324
671
  persisted.executablePath !== exe ||
@@ -326,9 +673,7 @@ class HeadroomManager {
326
673
  persisted.installSource !== install?.source ||
327
674
  persisted.ignoreSystemInstall === true;
328
675
  const shouldPersistRoutes = persisted.detectedRoutes?.codex !== detectedRoutes.codex ||
329
- persisted.detectedRoutes?.claude !== detectedRoutes.claude ||
330
- persisted.activeProviders?.codex !== activeProviders.codex ||
331
- persisted.activeProviders?.claude !== activeProviders.claude;
676
+ persisted.detectedRoutes?.claude !== detectedRoutes.claude;
332
677
  if (shouldPersistInstall || shouldPersistRoutes) {
333
678
  this.updatePersisted({
334
679
  installed: true,
@@ -337,11 +682,11 @@ class HeadroomManager {
337
682
  installSource: install?.source ?? null,
338
683
  ignoreSystemInstall: false,
339
684
  detectedRoutes,
340
- activeProviders,
341
685
  });
342
686
  }
343
687
  }
344
- this.refreshMetricsInBackground();
688
+ if (refreshMetrics)
689
+ this.refreshMetricsInBackground();
345
690
  const learning = {
346
691
  enabled: persisted.learningEnabled ?? installed,
347
692
  ...this.readLearningBaseline(),
@@ -369,10 +714,357 @@ class HeadroomManager {
369
714
  };
370
715
  }
371
716
  async getFreshState() {
717
+ if (this.shuttingDown)
718
+ return this.shutdownState ?? this.buildState(false);
372
719
  await this.refreshMetrics();
373
720
  return this.getState();
374
721
  }
722
+ /**
723
+ * Stream a provider request through the stable desktop listener. A TCP
724
+ * connection to the backend is established first, then ownership is verified,
725
+ * and only then are headers/body (including provider credentials) written.
726
+ * A process that rebinds the backend port can therefore receive a TCP handshake
727
+ * but never a secret-bearing byte.
728
+ */
729
+ async handleRelayRequest(req, res) {
730
+ const upstreamPath = this.relayUpstreamPath(req.url);
731
+ if (!upstreamPath) {
732
+ this.writeRelayError(res, 404, 'Not Found');
733
+ return;
734
+ }
735
+ const backendPort = this.trustedBackendPort();
736
+ if (!backendPort) {
737
+ this.writeRelayError(res, 503, 'Headroom proxy is unavailable');
738
+ return;
739
+ }
740
+ const headers = this.filterRelayHttpHeaders(req.headers);
741
+ headers.host = `127.0.0.1:${backendPort}`;
742
+ let upstreamResponse = null;
743
+ let relaySettled = false;
744
+ const upstream = http_1.default.request({
745
+ host: '127.0.0.1',
746
+ port: backendPort,
747
+ method: req.method,
748
+ path: upstreamPath,
749
+ headers,
750
+ agent: this.relayAgent,
751
+ }, (response) => {
752
+ upstreamResponse = response;
753
+ if (res.destroyed) {
754
+ abortRelay(false);
755
+ return;
756
+ }
757
+ response.once('aborted', () => abortRelay(true));
758
+ response.once('error', () => abortRelay(true));
759
+ response.once('close', () => {
760
+ // IncomingMessage#close is also emitted after a fully-consumed response.
761
+ // Only an incomplete HTTP message is a broken backend stream.
762
+ if (!response.complete)
763
+ abortRelay(true);
764
+ });
765
+ response.once('end', settleCleanRelay);
766
+ res.writeHead(response.statusCode ?? 502, this.filterRelayHttpHeaders(response.headers));
767
+ response.pipe(res);
768
+ });
769
+ const abortRelay = (notifyClient) => {
770
+ if (relaySettled)
771
+ return;
772
+ relaySettled = true;
773
+ req.unpipe(upstream);
774
+ upstreamResponse?.unpipe(res);
775
+ upstreamResponse?.destroy();
776
+ upstream.destroy();
777
+ if (!notifyClient || res.destroyed || res.writableFinished)
778
+ return;
779
+ if (!res.headersSent)
780
+ this.writeRelayError(res, 503, 'Headroom proxy request failed');
781
+ else
782
+ res.destroy();
783
+ };
784
+ const settleCleanRelay = () => {
785
+ if (req.complete &&
786
+ upstreamResponse?.complete &&
787
+ res.writableFinished)
788
+ relaySettled = true;
789
+ };
790
+ upstream.once('error', () => abortRelay(true));
791
+ upstream.once('close', () => {
792
+ if (!upstreamResponse)
793
+ abortRelay(true);
794
+ });
795
+ req.once('aborted', () => abortRelay(false));
796
+ req.once('error', () => abortRelay(false));
797
+ req.once('close', () => {
798
+ // A normal IncomingMessage closes after the complete request body. Do not
799
+ // confuse that with a client disconnect while a valid response streams.
800
+ if (!req.complete)
801
+ abortRelay(false);
802
+ });
803
+ req.once('end', settleCleanRelay);
804
+ res.once('error', () => abortRelay(false));
805
+ res.once('close', () => {
806
+ if (!res.writableFinished)
807
+ abortRelay(false);
808
+ });
809
+ res.once('finish', settleCleanRelay);
810
+ req.pipe(upstream);
811
+ }
812
+ /** WebSocket equivalent of handleRelayRequest (used by Codex streaming). */
813
+ async handleRelayUpgrade(req, clientSocket, head) {
814
+ const upstreamPath = this.relayUpstreamPath(req.url);
815
+ if (!upstreamPath) {
816
+ this.rejectRelayUpgrade(clientSocket, 404, 'Not Found');
817
+ return;
818
+ }
819
+ let backend;
820
+ try {
821
+ backend = await this.connectVerifiedBackend();
822
+ }
823
+ catch {
824
+ this.rejectRelayUpgrade(clientSocket, 503, 'Headroom proxy unavailable');
825
+ return;
826
+ }
827
+ if (clientSocket.destroyed) {
828
+ backend.socket.destroy();
829
+ return;
830
+ }
831
+ const raw = [`${req.method ?? 'GET'} ${upstreamPath} HTTP/${req.httpVersion}`];
832
+ const connectionTokens = this.connectionHeaderTokens(req.headers);
833
+ let wroteHost = false;
834
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
835
+ const name = req.rawHeaders[i];
836
+ const value = req.rawHeaders[i + 1];
837
+ if (!name || value === undefined)
838
+ continue;
839
+ const lower = name.toLowerCase();
840
+ if (lower === 'host') {
841
+ if (!wroteHost)
842
+ raw.push(`Host: 127.0.0.1:${backend.port}`);
843
+ wroteHost = true;
844
+ continue;
845
+ }
846
+ if (lower === 'connection' ||
847
+ lower === 'proxy-connection' ||
848
+ lower === 'keep-alive' ||
849
+ lower === 'transfer-encoding' ||
850
+ lower === 'te' ||
851
+ lower === 'trailer' ||
852
+ lower === 'proxy-authenticate' ||
853
+ lower === 'proxy-authorization' ||
854
+ (connectionTokens.has(lower) && lower !== 'upgrade'))
855
+ continue;
856
+ raw.push(`${name}: ${value}`);
857
+ }
858
+ if (!wroteHost)
859
+ raw.push(`Host: 127.0.0.1:${backend.port}`);
860
+ raw.push('Connection: Upgrade');
861
+ raw.push('', '');
862
+ const backendSocket = backend.socket;
863
+ let clientEnded = false;
864
+ let backendEnded = false;
865
+ let clientClosed = false;
866
+ let backendClosed = false;
867
+ let tearingDown = false;
868
+ const cleanup = () => {
869
+ this.relayUpgradeTeardowns.delete(destroyBoth);
870
+ clientSocket.unpipe(backendSocket);
871
+ backendSocket.unpipe(clientSocket);
872
+ clientSocket.removeListener('error', destroyBoth);
873
+ backendSocket.removeListener('error', destroyBoth);
874
+ clientSocket.removeListener('end', onClientEnd);
875
+ backendSocket.removeListener('end', onBackendEnd);
876
+ clientSocket.removeListener('close', onClientClose);
877
+ backendSocket.removeListener('close', onBackendClose);
878
+ };
879
+ const destroyBoth = () => {
880
+ if (tearingDown)
881
+ return;
882
+ tearingDown = true;
883
+ cleanup();
884
+ if (!clientSocket.destroyed)
885
+ clientSocket.destroy();
886
+ if (!backendSocket.destroyed)
887
+ backendSocket.destroy();
888
+ };
889
+ const finishIfClosed = () => {
890
+ if (clientClosed && backendClosed)
891
+ cleanup();
892
+ };
893
+ const onClientEnd = () => {
894
+ clientEnded = true;
895
+ if (!backendSocket.destroyed && !backendSocket.writableEnded)
896
+ backendSocket.end();
897
+ };
898
+ const onBackendEnd = () => {
899
+ backendEnded = true;
900
+ if (!clientSocket.destroyed && !clientSocket.writableEnded)
901
+ clientSocket.end();
902
+ };
903
+ const onClientClose = () => {
904
+ clientClosed = true;
905
+ if (!clientEnded)
906
+ destroyBoth();
907
+ else
908
+ finishIfClosed();
909
+ };
910
+ const onBackendClose = () => {
911
+ backendClosed = true;
912
+ if (!backendEnded)
913
+ destroyBoth();
914
+ else
915
+ finishIfClosed();
916
+ };
917
+ this.relayUpgradeTeardowns.add(destroyBoth);
918
+ clientSocket.once('error', destroyBoth);
919
+ backendSocket.once('error', destroyBoth);
920
+ clientSocket.once('end', onClientEnd);
921
+ backendSocket.once('end', onBackendEnd);
922
+ clientSocket.once('close', onClientClose);
923
+ backendSocket.once('close', onBackendClose);
924
+ if (clientSocket.destroyed || backendSocket.destroyed) {
925
+ destroyBoth();
926
+ return;
927
+ }
928
+ try {
929
+ backendSocket.write(raw.join('\r\n'));
930
+ if (head.length > 0)
931
+ backendSocket.write(head);
932
+ // Preserve a clean half-close so a peer that has finished sending can
933
+ // still receive the other direction's final frames. Premature close or
934
+ // error takes the idempotent destroyBoth path above.
935
+ clientSocket.pipe(backendSocket, { end: false });
936
+ backendSocket.pipe(clientSocket, { end: false });
937
+ }
938
+ catch {
939
+ destroyBoth();
940
+ }
941
+ }
942
+ relayUpstreamPath(rawUrl) {
943
+ try {
944
+ const parsed = new URL(rawUrl || '/', 'http://127.0.0.1');
945
+ const fullPrefix = `${exports.HEADROOM_RELAY_PATH}/`;
946
+ let relative = parsed.pathname.startsWith(fullPrefix)
947
+ ? parsed.pathname.slice(exports.HEADROOM_RELAY_PATH.length)
948
+ : parsed.pathname;
949
+ const separator = relative.indexOf('/', 1);
950
+ const providedToken = separator === -1 ? relative.slice(1) : relative.slice(1, separator);
951
+ if (!providedToken || !(0, auth_1.safeEqual)(providedToken, this.relayToken))
952
+ return null;
953
+ relative = separator === -1 ? '/' : relative.slice(separator);
954
+ return `${relative}${parsed.search}`;
955
+ }
956
+ catch {
957
+ return null;
958
+ }
959
+ }
960
+ trustedBackendPort(expectedPort) {
961
+ const child = this.proxy;
962
+ const port = this.proxyTrustedPort;
963
+ if (!child || !port || !child.pid ||
964
+ (expectedPort !== undefined && port !== expectedPort) ||
965
+ child.exitCode != null || child.signalCode != null)
966
+ return null;
967
+ return port;
968
+ }
969
+ connectVerifiedBackend(expectedPort) {
970
+ const child = this.proxy;
971
+ const port = this.trustedBackendPort(expectedPort);
972
+ if (!child || !port || !child.pid)
973
+ return Promise.reject(new Error('Headroom proxy is not trusted'));
974
+ return new Promise((resolve, reject) => {
975
+ const socket = net_1.default.connect({ host: '127.0.0.1', port });
976
+ let settled = false;
977
+ const timer = setTimeout(() => fail(new Error('Headroom relay connect timed out')), RELAY_CONNECT_TIMEOUT_MS);
978
+ timer.unref?.();
979
+ const fail = (err) => {
980
+ if (settled)
981
+ return;
982
+ settled = true;
983
+ clearTimeout(timer);
984
+ socket.destroy();
985
+ reject(err);
986
+ };
987
+ socket.once('error', fail);
988
+ socket.once('connect', () => {
989
+ void (async () => {
990
+ const ownsProxyPort = this.processControl.ownsProxyPort ?? processOwnsListeningPort;
991
+ let listenerOwned = false;
992
+ try {
993
+ listenerOwned = await ownsProxyPort(child.pid, port);
994
+ }
995
+ catch { /* fail closed */ }
996
+ if (settled)
997
+ return;
998
+ const owned = this.proxy === child &&
999
+ this.proxyTrustedPort === port &&
1000
+ child.exitCode == null &&
1001
+ child.signalCode == null &&
1002
+ !!child.pid &&
1003
+ listenerOwned &&
1004
+ this.proxy === child &&
1005
+ this.proxyTrustedPort === port &&
1006
+ child.exitCode == null &&
1007
+ child.signalCode == null;
1008
+ if (!owned) {
1009
+ fail(new Error('Headroom backend ownership changed'));
1010
+ return;
1011
+ }
1012
+ settled = true;
1013
+ clearTimeout(timer);
1014
+ socket.removeListener('error', fail);
1015
+ resolve({ socket, port });
1016
+ })().catch((err) => fail(err instanceof Error ? err : new Error(String(err))));
1017
+ });
1018
+ });
1019
+ }
1020
+ writeRelayError(res, status, message) {
1021
+ if (res.headersSent || res.destroyed)
1022
+ return;
1023
+ res.writeHead(status, { 'content-type': 'application/json', connection: 'close' });
1024
+ res.end(JSON.stringify({ error: message }));
1025
+ }
1026
+ connectionHeaderTokens(headers) {
1027
+ const raw = headers.connection;
1028
+ const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
1029
+ return new Set(values.flatMap((value) => value.split(',')).map((value) => value.trim().toLowerCase()).filter(Boolean));
1030
+ }
1031
+ filterRelayHttpHeaders(headers) {
1032
+ const connectionTokens = this.connectionHeaderTokens(headers);
1033
+ const filtered = {};
1034
+ for (const [name, value] of Object.entries(headers)) {
1035
+ const lower = name.toLowerCase();
1036
+ if (value === undefined ||
1037
+ lower === 'connection' ||
1038
+ lower === 'proxy-connection' ||
1039
+ lower === 'keep-alive' ||
1040
+ lower === 'transfer-encoding' ||
1041
+ lower === 'upgrade' ||
1042
+ lower === 'te' ||
1043
+ lower === 'trailer' ||
1044
+ lower === 'proxy-authenticate' ||
1045
+ lower === 'proxy-authorization' ||
1046
+ connectionTokens.has(lower))
1047
+ continue;
1048
+ filtered[name] = value;
1049
+ }
1050
+ return filtered;
1051
+ }
1052
+ rejectRelayUpgrade(socket, status, message) {
1053
+ if (socket.destroyed)
1054
+ return;
1055
+ socket.end(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
1056
+ }
1057
+ destroyRelayTransports() {
1058
+ this.relayAgent.destroy();
1059
+ for (const teardown of [...this.relayUpgradeTeardowns])
1060
+ teardown();
1061
+ }
375
1062
  async install() {
1063
+ return this.serializeLifecycle(() => this.installLocked());
1064
+ }
1065
+ async installLocked() {
1066
+ if (this.shuttingDown)
1067
+ return this.cancelledProxyStart();
376
1068
  const uv = this.resolveUvPath();
377
1069
  if (!uv) {
378
1070
  const failure = issue('uv_missing_or_corrupt');
@@ -395,6 +1087,8 @@ class HeadroomManager {
395
1087
  this.emit('installing', logs[0]);
396
1088
  this.emit('installing', logs[1]);
397
1089
  const pythonResult = await this.runCommand(uv, plan.pythonInstallArgs, plan.env, logs);
1090
+ if (this.shuttingDown)
1091
+ return this.cancelledProxyStart();
398
1092
  if (pythonResult.code !== 0) {
399
1093
  const failure = classifyInstallFailure(logs.join('\n'), pythonCommand);
400
1094
  this.updatePersisted({ lastIssue: failure });
@@ -406,6 +1100,8 @@ class HeadroomManager {
406
1100
  this.emit('installing', logs[logs.length - 2]);
407
1101
  this.emit('installing', logs[logs.length - 1]);
408
1102
  const result = await this.runCommand(uv, plan.toolInstallArgs, plan.env, logs);
1103
+ if (this.shuttingDown)
1104
+ return this.cancelledProxyStart();
409
1105
  if (result.code !== 0) {
410
1106
  const failure = classifyInstallFailure(logs.join('\n'), installCommand);
411
1107
  this.updatePersisted({ lastIssue: failure });
@@ -429,11 +1125,20 @@ class HeadroomManager {
429
1125
  lastIssue: null,
430
1126
  });
431
1127
  await this.enableOutputLearning(exe, logs);
1128
+ if (this.shuttingDown)
1129
+ return this.cancelledProxyStart();
432
1130
  this.emit('installed', `Headroom ${version} installed`);
433
1131
  await this.refreshMetrics();
1132
+ if (this.shuttingDown)
1133
+ return this.cancelledProxyStart();
434
1134
  return { ok: true, state: this.getState(), logs };
435
1135
  }
436
1136
  async activate(provider) {
1137
+ return this.serializeLifecycle(() => this.activateLocked(provider));
1138
+ }
1139
+ async activateLocked(provider) {
1140
+ if (this.shuttingDown)
1141
+ return this.cancelledProxyStart();
437
1142
  const state = this.getState();
438
1143
  if (!state.installed || !state.executablePath) {
439
1144
  const failure = issue('not_installed');
@@ -443,13 +1148,18 @@ class HeadroomManager {
443
1148
  const proxy = await this.ensureProxy();
444
1149
  if (!proxy.ok)
445
1150
  return proxy;
1151
+ // The start promise can settle just before shutdown/deactivation invalidates
1152
+ // its generation. Re-check the owned runtime synchronously before persisting
1153
+ // a provider route so a resolved promise cannot resurrect activation.
1154
+ if (this.shuttingDown || !this.isProxyAvailable())
1155
+ return this.cancelledProxyStart();
446
1156
  const current = this.readPersisted();
447
- const activeProviders = { ...current.activeProviders, [provider]: true };
448
- this.updatePersisted({ activeProviders, lastIssue: null });
1157
+ const activeProviders = { ...this.explicitActiveProviders(current), [provider]: true };
1158
+ this.updatePersisted({ ...this.activationPatch(activeProviders), lastIssue: null });
449
1159
  this.syncRouting();
450
1160
  if (!this.verifyProviderRoute(provider)) {
451
1161
  const rolledBack = { ...activeProviders, [provider]: false };
452
- this.updatePersisted({ activeProviders: rolledBack, lastIssue: issue('provider_route_failed') });
1162
+ this.updatePersisted({ ...this.activationPatch(rolledBack), lastIssue: issue('provider_route_failed') });
453
1163
  this.syncRouting();
454
1164
  return { ok: false, state: this.getState(), issue: issue('provider_route_failed') };
455
1165
  }
@@ -457,22 +1167,34 @@ class HeadroomManager {
457
1167
  return { ok: true, state: this.getState() };
458
1168
  }
459
1169
  async deactivate(provider) {
1170
+ return this.serializeLifecycle(() => this.deactivateLocked(provider));
1171
+ }
1172
+ async deactivateLocked(provider) {
1173
+ if (this.shuttingDown)
1174
+ return this.cancelledProxyStart();
460
1175
  const current = this.readPersisted();
461
- const activeProviders = { ...current.activeProviders, [provider]: false };
462
- this.updatePersisted({ activeProviders, lastIssue: null });
1176
+ const activeProviders = { ...this.explicitActiveProviders(current), [provider]: false };
1177
+ this.updatePersisted({ ...this.activationPatch(activeProviders), lastIssue: null });
463
1178
  this.syncRouting();
464
1179
  if (!activeProviders.codex && !activeProviders.claude)
465
- this.stopProxy();
1180
+ await this.stopProxy();
466
1181
  this.emit('installed', `${provider} Headroom route disabled`);
467
1182
  return { ok: true, state: this.getState() };
468
1183
  }
469
1184
  async uninstall() {
1185
+ return this.serializeLifecycle(() => this.uninstallLocked());
1186
+ }
1187
+ async uninstallLocked() {
1188
+ if (this.shuttingDown)
1189
+ return this.cancelledProxyStart();
470
1190
  const before = this.getState();
471
1191
  this.updatePersisted({
472
- activeProviders: { codex: false, claude: false },
1192
+ ...this.activationPatch({ codex: false, claude: false }),
473
1193
  lastIssue: null,
474
1194
  });
475
- this.stopProxy();
1195
+ await this.stopProxy();
1196
+ if (this.shuttingDown)
1197
+ return this.cancelledProxyStart();
476
1198
  this.syncRouting();
477
1199
  if (!before.installed && !before.executablePath) {
478
1200
  this.updatePersisted({
@@ -480,7 +1202,7 @@ class HeadroomManager {
480
1202
  version: null,
481
1203
  executablePath: null,
482
1204
  installSource: null,
483
- activeProviders: { codex: false, claude: false },
1205
+ ...this.activationPatch({ codex: false, claude: false }),
484
1206
  detectedRoutes: { codex: false, claude: false },
485
1207
  lastIssue: null,
486
1208
  });
@@ -493,7 +1215,7 @@ class HeadroomManager {
493
1215
  executablePath: null,
494
1216
  installSource: null,
495
1217
  ignoreSystemInstall: true,
496
- activeProviders: { codex: false, claude: false },
1218
+ ...this.activationPatch({ codex: false, claude: false }),
497
1219
  detectedRoutes: { codex: false, claude: false },
498
1220
  lastIssue: null,
499
1221
  });
@@ -516,6 +1238,8 @@ class HeadroomManager {
516
1238
  UV_CACHE_DIR: uvCacheDir(),
517
1239
  UV_NO_PROGRESS: '1',
518
1240
  }, logs);
1241
+ if (this.shuttingDown)
1242
+ return this.cancelledProxyStart();
519
1243
  const stillInstalled = fileExists(path_1.default.join(uvBinDir(), headroomExeName()));
520
1244
  if (result.code !== 0 || stillInstalled) {
521
1245
  const failure = issue('uninstall_failed', logs.join('\n'), command);
@@ -528,7 +1252,7 @@ class HeadroomManager {
528
1252
  version: null,
529
1253
  executablePath: null,
530
1254
  installSource: null,
531
- activeProviders: { codex: false, claude: false },
1255
+ ...this.activationPatch({ codex: false, claude: false }),
532
1256
  detectedRoutes: { codex: false, claude: false },
533
1257
  lastIssue: null,
534
1258
  });
@@ -536,18 +1260,65 @@ class HeadroomManager {
536
1260
  return { ok: true, state: this.getState(), logs };
537
1261
  }
538
1262
  async setPort(port) {
1263
+ return this.serializeLifecycle(() => this.setPortLocked(port));
1264
+ }
1265
+ async setPortLocked(port) {
1266
+ if (this.shuttingDown)
1267
+ return this.cancelledProxyStart();
539
1268
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
540
1269
  const failure = issue('proxy_port_busy', 'Port must be an integer between 1 and 65535.');
541
1270
  return { ok: false, state: this.getState(), issue: failure };
542
1271
  }
1272
+ const previous = this.readPersisted();
1273
+ const previousPort = this.validPort(previous.port);
543
1274
  const active = this.getState().activeProviders;
544
- this.stopProxy();
545
- this.updatePersisted({ port, lastIssue: null });
546
- this.syncRouting();
547
- if (active.codex || active.claude) {
548
- return this.ensureProxy();
1275
+ await this.stopProxy();
1276
+ if (this.shuttingDown)
1277
+ return this.cancelledProxyStart();
1278
+ if (!active.codex && !active.claude) {
1279
+ this.updatePersisted({ port, lastIssue: null });
1280
+ return { ok: true, state: this.getState() };
549
1281
  }
550
- return { ok: true, state: this.getState() };
1282
+ // Keep routing disabled while the replacement is only a candidate. No job
1283
+ // may send provider credentials to it until our owned child is healthy.
1284
+ this.updatePersisted({
1285
+ port,
1286
+ ...this.activationPatch({ codex: false, claude: false }),
1287
+ lastIssue: null,
1288
+ });
1289
+ const candidate = await this.ensureProxy();
1290
+ if (this.shuttingDown)
1291
+ return this.cancelledProxyStart();
1292
+ if (candidate.ok) {
1293
+ this.updatePersisted({ ...this.activationPatch(active), lastIssue: null });
1294
+ return { ok: true, state: this.getState() };
1295
+ }
1296
+ // Roll back the entire transition, including the old owned proxy. If even
1297
+ // that cannot be restored, remain fail-closed instead of persisting an
1298
+ // active route with no healthy service behind it.
1299
+ this.updatePersisted({
1300
+ port: previousPort,
1301
+ ...this.activationPatch({ codex: false, claude: false }),
1302
+ lastIssue: candidate.issue ?? issue('proxy_unhealthy'),
1303
+ });
1304
+ const restored = await this.ensureProxy();
1305
+ if (restored.ok) {
1306
+ this.updatePersisted({
1307
+ ...this.activationPatch(active),
1308
+ lastIssue: candidate.issue ?? issue('proxy_unhealthy'),
1309
+ });
1310
+ }
1311
+ else {
1312
+ this.updatePersisted({
1313
+ ...this.activationPatch({ codex: false, claude: false }),
1314
+ lastIssue: restored.issue ?? candidate.issue ?? issue('proxy_unhealthy'),
1315
+ });
1316
+ }
1317
+ return {
1318
+ ok: false,
1319
+ state: this.getState(),
1320
+ issue: candidate.issue ?? issue('proxy_unhealthy'),
1321
+ };
551
1322
  }
552
1323
  diagnostics() {
553
1324
  const state = this.getState();
@@ -566,66 +1337,214 @@ class HeadroomManager {
566
1337
  learning: state.learning,
567
1338
  metrics: state.metrics,
568
1339
  routing: {
569
- codex: state.activeProviders.codex ? `OPENAI_BASE_URL=http://127.0.0.1:${state.port}/v1` : null,
570
- claude: state.activeProviders.claude ? `ANTHROPIC_BASE_URL=http://127.0.0.1:${state.port}` : null,
1340
+ codex: state.activeProviders.codex
1341
+ ? `OPENAI_BASE_URL=${this.relayDiagnosticBaseUrl() ?? `http://127.0.0.1:${state.port}`}/v1`
1342
+ : null,
1343
+ claude: state.activeProviders.claude
1344
+ ? `ANTHROPIC_BASE_URL=${this.relayDiagnosticBaseUrl() ?? `http://127.0.0.1:${state.port}`}`
1345
+ : null,
571
1346
  },
572
1347
  };
573
1348
  }
574
1349
  async startActiveProxyOnBoot() {
1350
+ return this.serializeLifecycle(() => this.startActiveProxyOnBootLocked());
1351
+ }
1352
+ async startActiveProxyOnBootLocked() {
1353
+ if (this.shuttingDown)
1354
+ return;
575
1355
  const active = this.getState().activeProviders;
576
1356
  this.syncRouting();
577
1357
  if (active.codex || active.claude) {
578
1358
  const result = await this.ensureProxy();
1359
+ if (this.shuttingDown)
1360
+ return;
579
1361
  if (!result.ok) {
1362
+ // Never keep provider credentials/prompts routed to a process we did not
1363
+ // spawn and therefore cannot authenticate. The user can retry activation
1364
+ // after resolving the port conflict.
1365
+ this.updatePersisted({
1366
+ ...this.activationPatch({ codex: false, claude: false }),
1367
+ lastIssue: result.issue ?? issue('proxy_unhealthy'),
1368
+ });
580
1369
  console.warn('[headroom] boot proxy start failed:', result.issue);
581
1370
  }
582
1371
  }
583
1372
  }
1373
+ serializeLifecycle(operation) {
1374
+ const run = this.lifecycleTail.then(operation, operation);
1375
+ this.lifecycleTail = run.then(() => undefined, () => undefined);
1376
+ return run;
1377
+ }
584
1378
  async ensureProxy() {
1379
+ if (this.proxyStop)
1380
+ await this.proxyStop;
585
1381
  const state = this.getState();
586
1382
  if (!state.installed || !state.executablePath) {
587
1383
  const failure = issue('not_installed');
588
1384
  return { ok: false, state, issue: failure };
589
1385
  }
590
- if (this.isProxyRunning())
1386
+ if (this.shuttingDown) {
1387
+ const failure = issue('proxy_unhealthy', 'Headroom proxy startup was cancelled during shutdown.');
1388
+ return { ok: false, state, issue: failure };
1389
+ }
1390
+ if (this.proxyStart) {
1391
+ if (this.proxyStart.port === state.port)
1392
+ return this.proxyStart.promise;
1393
+ await this.stopProxy();
1394
+ if (this.shuttingDown)
1395
+ return this.cancelledProxyStart();
1396
+ }
1397
+ if (this.isProxyAvailable(state.port))
591
1398
  return { ok: true, state };
592
- if (await this.adoptHealthyProxy(state.port)) {
593
- return { ok: true, state: this.getState() };
1399
+ if (this.isProxyRunning())
1400
+ await this.stopProxy();
1401
+ const generation = ++this.proxyGeneration;
1402
+ const promise = this.startProxy(state, state.executablePath, generation);
1403
+ this.proxyStart = { generation, port: state.port, promise };
1404
+ const clear = () => {
1405
+ if (this.proxyStart?.promise === promise)
1406
+ this.proxyStart = null;
1407
+ };
1408
+ void promise.then(clear, clear);
1409
+ return promise;
1410
+ }
1411
+ async startProxy(state, executablePath, generation) {
1412
+ // A healthy endpoint is still not proof that the process belongs to this
1413
+ // Specrails instance. Adopting it would route provider auth and prompts to an
1414
+ // unauthenticated port occupant. Treat every pre-existing listener as a hard
1415
+ // conflict and only trust the child created below.
1416
+ if (await this.probeProxyHealthy(state.port)) {
1417
+ if (!this.isProxyGenerationCurrent(generation))
1418
+ return this.cancelledProxyStart();
1419
+ const failure = issue('proxy_port_busy');
1420
+ this.updatePersisted({ lastIssue: failure });
1421
+ this.emit('failed', failure.title);
1422
+ return { ok: false, state: this.getState(), issue: failure };
594
1423
  }
1424
+ if (!this.isProxyGenerationCurrent(generation))
1425
+ return this.cancelledProxyStart();
595
1426
  const args = ['proxy', '--host', '127.0.0.1', '--port', String(state.port)];
596
- const command = `${state.executablePath} ${args.join(' ')}`;
1427
+ const command = `${executablePath} ${args.join(' ')}`;
597
1428
  this.emit('starting-proxy', `Starting proxy on ${state.port}`);
598
1429
  this.proxyTail = '';
599
- const child = (0, child_process_1.spawn)(state.executablePath, args, {
600
- env: {
601
- ...process.env,
602
- HEADROOM_PORT: String(state.port),
603
- HEADROOM_OUTPUT_SHAPER: '1',
604
- HEADROOM_LEARN: state.learning.enabled ? '1' : '0',
605
- HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
606
- HEADROOM_MODE: process.env.HEADROOM_MODE ?? 'token',
607
- },
608
- stdio: ['ignore', 'pipe', 'pipe'],
609
- });
1430
+ let child;
1431
+ try {
1432
+ child = (this.processControl.spawnProxy ?? child_process_1.spawn)(executablePath, args, {
1433
+ env: {
1434
+ ...process.env,
1435
+ HEADROOM_PORT: String(state.port),
1436
+ HEADROOM_OUTPUT_SHAPER: '1',
1437
+ HEADROOM_LEARN: state.learning.enabled ? '1' : '0',
1438
+ HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
1439
+ HEADROOM_MODE: process.env.HEADROOM_MODE ?? 'token',
1440
+ },
1441
+ stdio: ['ignore', 'pipe', 'pipe'],
1442
+ });
1443
+ }
1444
+ catch (err) {
1445
+ const failure = classifyProxyFailure(err instanceof Error ? err.message : String(err), command);
1446
+ this.updatePersisted({ lastIssue: failure });
1447
+ this.emit('failed', failure.title);
1448
+ return { ok: false, state: this.getState(), issue: failure };
1449
+ }
610
1450
  this.proxy = child;
1451
+ this.proxyTrustedPort = null;
1452
+ this.syncRouting();
611
1453
  child.stdout?.on('data', (chunk) => { this.proxyTail += chunk.toString(); this.trimProxyTail(); });
612
1454
  child.stderr?.on('data', (chunk) => { this.proxyTail += chunk.toString(); this.trimProxyTail(); });
613
- child.on('close', () => {
614
- if (this.proxy === child)
615
- this.proxy = null;
1455
+ const invalidateOwnedProxy = () => {
1456
+ if (this.proxy !== child)
1457
+ return;
1458
+ this.destroyRelayTransports();
1459
+ this.proxy = null;
1460
+ this.proxyTrustedPort = null;
1461
+ this.syncRouting();
1462
+ };
1463
+ // `spawn()` reports ENOENT/EACCES asynchronously. Without this listener the
1464
+ // EventEmitter contract turns a recoverable activation failure into an
1465
+ // uncaught exception that terminates the desktop sidecar.
1466
+ child.on('error', (err) => {
1467
+ this.proxyTail += `${err.message}\n`;
1468
+ this.trimProxyTail();
1469
+ invalidateOwnedProxy();
616
1470
  });
617
- const healthy = await this.waitForProxyHealthy(state.port, 6000);
1471
+ // `exit` invalidates routing immediately; `close` is retained as the final
1472
+ // fallback for unusual ChildProcess implementations and stdio teardown.
1473
+ child.on('exit', invalidateOwnedProxy);
1474
+ child.on('close', invalidateOwnedProxy);
1475
+ const healthy = await this.waitForProxyHealthy(state.port, 6000, () => this.isProxyGenerationCurrent(generation));
1476
+ if (!this.isProxyGenerationCurrent(generation)) {
1477
+ await this.discardProxyChild(child);
1478
+ return this.cancelledProxyStart();
1479
+ }
618
1480
  if (!healthy) {
619
1481
  const failure = classifyProxyFailure(this.proxyTail, command);
620
- this.stopProxy();
1482
+ await this.discardProxyChild(child);
1483
+ this.updatePersisted({ lastIssue: failure });
1484
+ this.emit('failed', failure.title);
1485
+ return { ok: false, state: this.getState(), issue: failure };
1486
+ }
1487
+ const ownsProxyPort = this.processControl.ownsProxyPort ?? processOwnsListeningPort;
1488
+ const ownedEndpointIsCurrent = async () => {
1489
+ if (!this.isOwnedProxyCurrent(child, generation) || !child.pid)
1490
+ return false;
1491
+ let listenerOwned = false;
1492
+ try {
1493
+ listenerOwned = await ownsProxyPort(child.pid, state.port);
1494
+ }
1495
+ catch { /* fail closed */ }
1496
+ if (!listenerOwned)
1497
+ return false;
1498
+ return this.isOwnedProxyCurrent(child, generation);
1499
+ };
1500
+ if (!(await ownedEndpointIsCurrent())) {
1501
+ const failure = issue('proxy_port_busy', 'A healthy endpoint answered, but the Specrails-owned Headroom process does not own the listening socket.', command);
1502
+ await this.discardProxyChild(child);
621
1503
  this.updatePersisted({ lastIssue: failure });
622
1504
  this.emit('failed', failure.title);
623
1505
  return { ok: false, state: this.getState(), issue: failure };
624
1506
  }
625
- this.updatePersisted({ lastIssue: null });
626
1507
  await this.refreshMetrics();
1508
+ if (!this.isProxyGenerationCurrent(generation)) {
1509
+ await this.discardProxyChild(child);
1510
+ return this.cancelledProxyStart();
1511
+ }
1512
+ if (!(await ownedEndpointIsCurrent())) {
1513
+ const failure = issue('proxy_unhealthy', 'The owned Headroom proxy exited during startup.', command);
1514
+ await this.discardProxyChild(child);
1515
+ this.updatePersisted({ lastIssue: failure });
1516
+ this.emit('failed', failure.title);
1517
+ return { ok: false, state: this.getState(), issue: failure };
1518
+ }
1519
+ this.proxyTrustedPort = state.port;
1520
+ this.updatePersisted({ lastIssue: null });
627
1521
  return { ok: true, state: this.getState() };
628
1522
  }
1523
+ cancelledProxyStart() {
1524
+ const failure = issue('proxy_unhealthy', 'Headroom proxy startup was superseded by a newer lifecycle operation.');
1525
+ const state = this.shuttingDown && this.shutdownState
1526
+ ? this.shutdownState
1527
+ : this.getState();
1528
+ return { ok: false, state, issue: failure };
1529
+ }
1530
+ isProxyGenerationCurrent(generation) {
1531
+ return !this.shuttingDown && this.proxyGeneration === generation;
1532
+ }
1533
+ isOwnedProxyCurrent(child, generation) {
1534
+ return this.isProxyGenerationCurrent(generation) &&
1535
+ this.proxy === child &&
1536
+ child.exitCode == null &&
1537
+ child.signalCode == null;
1538
+ }
1539
+ async discardProxyChild(child) {
1540
+ if (this.proxy === child) {
1541
+ this.destroyRelayTransports();
1542
+ this.proxy = null;
1543
+ this.proxyTrustedPort = null;
1544
+ this.syncRouting();
1545
+ }
1546
+ await this.terminateProxyChild(child);
1547
+ }
629
1548
  resolveUvPath() {
630
1549
  for (const p of bundledUvCandidates()) {
631
1550
  if (fileExists(p))
@@ -657,7 +1576,7 @@ class HeadroomManager {
657
1576
  if (this.routeDetectionCache?.executablePath === exe && this.routeDetectionCache.expiresAt > now) {
658
1577
  return this.routeDetectionCache.routes;
659
1578
  }
660
- const routes = { codex: false, claude: false };
1579
+ let routes = { codex: false, claude: false };
661
1580
  try {
662
1581
  const state = this.readPersisted();
663
1582
  const probe = (0, child_process_1.spawnSync)(exe, ['doctor', '--json', '--port', String(this.validPort(state.port))], {
@@ -666,16 +1585,8 @@ class HeadroomManager {
666
1585
  env: process.env,
667
1586
  });
668
1587
  const raw = `${probe.stdout ?? ''}`.trim();
669
- const parsed = raw ? JSON.parse(raw) : {};
670
- for (const check of parsed.checks ?? []) {
671
- const provider = normalizeProviderKey(check.name);
672
- if (!provider)
673
- continue;
674
- const routed = check.status === 'pass' || String(check.summary ?? '').toLowerCase().includes('routed');
675
- if (routed)
676
- routes[provider] = true;
677
- }
678
- this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw: parsed };
1588
+ routes = probe.status === 0 && raw ? parseHeadroomDoctorRoutes(raw) : routes;
1589
+ this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw };
679
1590
  }
680
1591
  catch {
681
1592
  this.routeDetectionCache = { executablePath: exe, expiresAt: now + 10_000, routes, raw: null };
@@ -718,6 +1629,8 @@ class HeadroomManager {
718
1629
  HEADROOM_OUTPUT_SHAPER: '1',
719
1630
  HEADROOM_SAVINGS_PROFILE: process.env.HEADROOM_SAVINGS_PROFILE ?? 'agent-90',
720
1631
  }, logs);
1632
+ if (this.shuttingDown)
1633
+ return;
721
1634
  if (result.code === 0) {
722
1635
  this.updatePersisted({
723
1636
  learningEnabled: true,
@@ -734,6 +1647,8 @@ class HeadroomManager {
734
1647
  });
735
1648
  }
736
1649
  refreshMetricsInBackground() {
1650
+ if (this.shuttingDown)
1651
+ return;
737
1652
  const state = this.getMetricsPrereqState();
738
1653
  if (!state.installed || !state.executablePath)
739
1654
  return;
@@ -757,10 +1672,7 @@ class HeadroomManager {
757
1672
  port: this.validPort(persisted.port),
758
1673
  availableProviders,
759
1674
  detectedRoutes,
760
- activeProviders: {
761
- codex: persisted.activeProviders?.codex ?? (availableProviders.codex && detectedRoutes.codex),
762
- claude: persisted.activeProviders?.claude ?? (availableProviders.claude && detectedRoutes.claude),
763
- },
1675
+ activeProviders: this.explicitActiveProviders(persisted),
764
1676
  };
765
1677
  }
766
1678
  async refreshMetrics() {
@@ -845,17 +1757,60 @@ class HeadroomManager {
845
1757
  return JSON.parse(result.output);
846
1758
  }
847
1759
  async readProxyStats(port) {
848
- const controller = new AbortController();
849
- const timer = setTimeout(() => controller.abort(), 2500);
850
- try {
851
- const res = await fetch(`http://127.0.0.1:${port}/stats?cached=1`, { signal: controller.signal });
852
- if (!res.ok)
853
- return null;
854
- return await res.json();
855
- }
856
- finally {
857
- clearTimeout(timer);
858
- }
1760
+ if (!this.isProxyAvailable(port))
1761
+ return null;
1762
+ return await new Promise((resolve, reject) => {
1763
+ let settled = false;
1764
+ const finish = (value, err) => {
1765
+ if (settled)
1766
+ return;
1767
+ settled = true;
1768
+ clearTimeout(timer);
1769
+ if (err)
1770
+ reject(err);
1771
+ else
1772
+ resolve(value);
1773
+ };
1774
+ const request = http_1.default.request({
1775
+ host: '127.0.0.1',
1776
+ port,
1777
+ method: 'GET',
1778
+ path: '/stats?cached=1',
1779
+ headers: { host: `127.0.0.1:${port}` },
1780
+ agent: this.relayAgent,
1781
+ }, (response) => {
1782
+ if ((response.statusCode ?? 500) < 200 || (response.statusCode ?? 500) >= 300) {
1783
+ response.resume();
1784
+ finish(null);
1785
+ return;
1786
+ }
1787
+ const chunks = [];
1788
+ let size = 0;
1789
+ response.on('data', (chunk) => {
1790
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1791
+ size += value.length;
1792
+ if (size > 2 * 1024 * 1024) {
1793
+ request.destroy(new Error('Headroom stats response exceeded 2 MB'));
1794
+ return;
1795
+ }
1796
+ chunks.push(value);
1797
+ });
1798
+ response.on('end', () => {
1799
+ try {
1800
+ finish(JSON.parse(Buffer.concat(chunks).toString('utf8')));
1801
+ }
1802
+ catch (err) {
1803
+ finish(null, err instanceof Error ? err : new Error(String(err)));
1804
+ }
1805
+ });
1806
+ });
1807
+ const timer = setTimeout(() => {
1808
+ request.destroy(new Error('Headroom stats request timed out'));
1809
+ }, 2_500);
1810
+ timer.unref?.();
1811
+ request.once('error', (err) => finish(null, err));
1812
+ request.end();
1813
+ });
859
1814
  }
860
1815
  decorateMetrics(metrics, activeProviders, availableProviders, detectedRoutes) {
861
1816
  return {
@@ -882,11 +1837,29 @@ class HeadroomManager {
882
1837
  };
883
1838
  }
884
1839
  async runCommand(command, args, extraEnv, logs) {
1840
+ if (this.shuttingDown)
1841
+ return { code: null };
885
1842
  return new Promise((resolve) => {
886
- const child = (0, child_process_1.spawn)(command, args, {
887
- env: { ...process.env, ...extraEnv },
888
- stdio: ['ignore', 'pipe', 'pipe'],
889
- });
1843
+ let child;
1844
+ try {
1845
+ child = (0, child_process_1.spawn)(command, args, {
1846
+ env: { ...process.env, ...extraEnv },
1847
+ stdio: ['ignore', 'pipe', 'pipe'],
1848
+ });
1849
+ }
1850
+ catch (err) {
1851
+ logs.push(err instanceof Error ? err.message : String(err));
1852
+ resolve({ code: null });
1853
+ return;
1854
+ }
1855
+ let settled = false;
1856
+ const settle = (code) => {
1857
+ if (settled)
1858
+ return;
1859
+ settled = true;
1860
+ resolve({ code });
1861
+ };
1862
+ this.trackLifecycleCommand(child, () => settle(null));
890
1863
  child.stdout?.on('data', (chunk) => {
891
1864
  for (const line of chunk.toString().split(/\r?\n/)) {
892
1865
  if (!line)
@@ -905,32 +1878,45 @@ class HeadroomManager {
905
1878
  });
906
1879
  child.on('error', (err) => {
907
1880
  logs.push(err.message);
908
- resolve({ code: null });
1881
+ settle(null);
909
1882
  });
910
- child.on('close', (code) => resolve({ code }));
1883
+ child.on('close', (code) => settle(code));
1884
+ if (this.shuttingDown) {
1885
+ settle(null);
1886
+ void this.terminateProxyChild(child);
1887
+ }
911
1888
  });
912
1889
  }
913
1890
  async runCommandCapture(command, args, extraEnv, timeoutMs) {
1891
+ if (this.shuttingDown)
1892
+ return { code: null, output: '' };
914
1893
  return new Promise((resolve) => {
915
1894
  let output = '';
916
1895
  let settled = false;
917
- const child = (0, child_process_1.spawn)(command, args, {
918
- env: { ...process.env, ...extraEnv },
919
- stdio: ['ignore', 'pipe', 'pipe'],
920
- });
1896
+ let child;
1897
+ try {
1898
+ child = (0, child_process_1.spawn)(command, args, {
1899
+ env: { ...process.env, ...extraEnv },
1900
+ stdio: ['ignore', 'pipe', 'pipe'],
1901
+ });
1902
+ }
1903
+ catch (err) {
1904
+ resolve({ code: null, output: err instanceof Error ? err.message : String(err) });
1905
+ return;
1906
+ }
1907
+ let timer = null;
921
1908
  const settle = (code) => {
922
1909
  if (settled)
923
1910
  return;
924
1911
  settled = true;
925
- clearTimeout(timer);
1912
+ if (timer)
1913
+ clearTimeout(timer);
926
1914
  resolve({ code, output });
927
1915
  };
928
- const timer = setTimeout(() => {
929
- try {
930
- child.kill('SIGTERM');
931
- }
932
- catch { /* ignore */ }
1916
+ this.trackLifecycleCommand(child, () => settle(null));
1917
+ timer = setTimeout(() => {
933
1918
  settle(null);
1919
+ void this.terminateProxyChild(child);
934
1920
  }, timeoutMs);
935
1921
  child.stdout?.on('data', (chunk) => { output += chunk.toString(); });
936
1922
  child.stderr?.on('data', (chunk) => { output += chunk.toString(); });
@@ -939,23 +1925,68 @@ class HeadroomManager {
939
1925
  settle(null);
940
1926
  });
941
1927
  child.on('close', (code) => settle(code));
1928
+ if (this.shuttingDown) {
1929
+ settle(null);
1930
+ void this.terminateProxyChild(child);
1931
+ }
1932
+ });
1933
+ }
1934
+ trackLifecycleCommand(child, cancel) {
1935
+ this.lifecycleCommands.set(child, { cancel });
1936
+ child.once('close', () => {
1937
+ this.lifecycleCommands.delete(child);
942
1938
  });
943
1939
  }
944
- async waitForProxyHealthy(port, timeoutMs) {
1940
+ /** Cancel/terminate every command, including children added while draining. */
1941
+ async drainLifecycleCommands() {
1942
+ const attempted = new Set();
1943
+ while (true) {
1944
+ const pending = [...this.lifecycleCommands.entries()]
1945
+ .filter(([child]) => !attempted.has(child));
1946
+ if (pending.length === 0)
1947
+ return;
1948
+ for (const [child, control] of pending) {
1949
+ attempted.add(child);
1950
+ control.cancel();
1951
+ }
1952
+ await Promise.all(pending.map(([child]) => this.terminateProxyChild(child)));
1953
+ }
1954
+ }
1955
+ async waitForProxyHealthy(port, timeoutMs, isCurrent = () => true) {
945
1956
  const deadline = Date.now() + timeoutMs;
946
1957
  while (Date.now() < deadline) {
947
- try {
948
- const res = await fetch(`http://127.0.0.1:${port}/livez`);
949
- if (res.ok)
950
- return true;
951
- }
952
- catch {
953
- // retry until timeout
954
- }
1958
+ if (!isCurrent())
1959
+ return false;
1960
+ const requestTimeoutMs = Math.max(1, Math.min(PROXY_HEALTH_REQUEST_TIMEOUT_MS, deadline - Date.now()));
1961
+ const healthy = await this.fetchProxyHealth(port, requestTimeoutMs);
1962
+ if (!isCurrent())
1963
+ return false;
1964
+ if (healthy)
1965
+ return true;
1966
+ if (!isCurrent())
1967
+ return false;
955
1968
  await new Promise((r) => setTimeout(r, 250));
956
1969
  }
957
1970
  return false;
958
1971
  }
1972
+ async probeProxyHealthy(port) {
1973
+ return this.fetchProxyHealth(port, PROXY_HEALTH_REQUEST_TIMEOUT_MS);
1974
+ }
1975
+ async fetchProxyHealth(port, timeoutMs) {
1976
+ const controller = new AbortController();
1977
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1978
+ timeout.unref?.();
1979
+ try {
1980
+ const res = await fetch(`http://127.0.0.1:${port}/livez`, { signal: controller.signal });
1981
+ return res.ok;
1982
+ }
1983
+ catch {
1984
+ return false;
1985
+ }
1986
+ finally {
1987
+ clearTimeout(timeout);
1988
+ }
1989
+ }
959
1990
  verifyProviderRoute(provider) {
960
1991
  const state = this.getState();
961
1992
  return provider === 'codex'
@@ -963,27 +1994,124 @@ class HeadroomManager {
963
1994
  : !!state.activeProviders.claude && state.port > 0;
964
1995
  }
965
1996
  isProxyRunning() {
966
- return !!this.proxy && !this.proxy.killed && this.proxy.exitCode == null;
967
- }
968
- async adoptHealthyProxy(port) {
969
- if (!await this.waitForProxyHealthy(port, 750))
970
- return false;
971
- await this.refreshMetrics();
972
- this.updatePersisted({ lastIssue: null });
973
- return true;
1997
+ return !!this.proxy && this.proxy.exitCode == null && this.proxy.signalCode == null;
974
1998
  }
975
- isProxyAvailable() {
976
- return this.isProxyRunning() || this.metricsCache.proxyStatsAvailable;
1999
+ isProxyAvailable(port = this.validPort(this.readPersisted().port)) {
2000
+ return this.proxyTrustedPort === port && this.isProxyRunning();
977
2001
  }
978
- stopProxy() {
2002
+ async stopProxy() {
2003
+ this.proxyGeneration += 1;
2004
+ this.proxyStart = null;
2005
+ this.destroyRelayTransports();
979
2006
  const child = this.proxy;
980
2007
  this.proxy = null;
981
- if (child && !child.killed) {
2008
+ this.proxyTrustedPort = null;
2009
+ this.syncRouting();
2010
+ const previousStop = this.proxyStop;
2011
+ const stop = (async () => {
2012
+ if (previousStop)
2013
+ await previousStop;
2014
+ if (child)
2015
+ await this.terminateProxyChild(child);
2016
+ })();
2017
+ this.proxyStop = stop;
2018
+ try {
2019
+ await stop;
2020
+ }
2021
+ finally {
2022
+ if (this.proxyStop === stop)
2023
+ this.proxyStop = null;
2024
+ }
2025
+ }
2026
+ /**
2027
+ * Synchronously close lifecycle admission while other process owners drain.
2028
+ * The proxy remains alive until shutdown() so already-routed children can be
2029
+ * terminated before their transport disappears.
2030
+ */
2031
+ beginShutdown() {
2032
+ if (this.shuttingDown)
2033
+ return;
2034
+ this.shuttingDown = true;
2035
+ this.syncRouting();
2036
+ const state = this.buildState(false);
2037
+ this.shutdownState = { ...state, proxyRunning: false, proxyPid: null };
2038
+ }
2039
+ /** Stop the owned proxy before the app tears down its DB and HTTP server. */
2040
+ shutdown() {
2041
+ if (this.shutdownPromise)
2042
+ return this.shutdownPromise;
2043
+ this.beginShutdown();
2044
+ const shutdown = this.finishShutdown();
2045
+ this.shutdownPromise = shutdown;
2046
+ return shutdown;
2047
+ }
2048
+ async finishShutdown() {
2049
+ // Runtime-only fail closed; persisted activation remains so a clean restart
2050
+ // can start a fresh owned proxy.
2051
+ const persistedPort = this.validPort(this.readPersisted().port);
2052
+ (0, headroom_routing_1.setHeadroomRoutingState)({
2053
+ port: this.routingClientPort(persistedPort),
2054
+ relayBaseUrl: this.relayBaseUrl(),
2055
+ activeProviders: { codex: false, claude: false },
2056
+ });
2057
+ // Routed provider children inherited the stable desktop endpoint. Stop and
2058
+ // await them before the app can release that listener; until then the relay
2059
+ // remains bound and rejects any untrusted backend instead of exposing a
2060
+ // reusable Headroom port.
2061
+ const stoppingProxy = this.stopProxy();
2062
+ await Promise.all([
2063
+ (0, headroom_routing_1.terminateHeadroomRoutedChildren)(),
2064
+ stoppingProxy,
2065
+ this.drainLifecycleCommands(),
2066
+ ]);
2067
+ // A lifecycle action may still be unwinding an aborted health/metrics
2068
+ // request. Keep the desktop DB alive until it has observed `shuttingDown`
2069
+ // and completed; queued actions fail at their locked-method guard.
2070
+ await this.lifecycleTail;
2071
+ // Defense in depth for future lifecycle operations: no child may survive a
2072
+ // completed shutdown even if a new await is added after an initial guard.
2073
+ await this.drainLifecycleCommands();
2074
+ await this.stopProxy();
2075
+ }
2076
+ terminateProxyChild(child) {
2077
+ if (!child.pid || child.exitCode != null || child.signalCode != null)
2078
+ return Promise.resolve();
2079
+ const pid = child.pid;
2080
+ const killTree = this.processControl.killTree ?? win_spawn_1.treeKillSafe;
2081
+ return new Promise((resolve) => {
2082
+ let closed = false;
2083
+ let killTimer = null;
2084
+ let deadlineTimer = null;
2085
+ const finish = () => {
2086
+ if (closed)
2087
+ return;
2088
+ closed = true;
2089
+ if (killTimer)
2090
+ clearTimeout(killTimer);
2091
+ if (deadlineTimer)
2092
+ clearTimeout(deadlineTimer);
2093
+ child.removeListener('close', finish);
2094
+ resolve();
2095
+ };
2096
+ child.once('close', finish);
982
2097
  try {
983
- child.kill('SIGTERM');
2098
+ killTree(pid, 'SIGTERM', () => { });
984
2099
  }
985
- catch { /* ignore */ }
986
- }
2100
+ catch { /* gone */ }
2101
+ killTimer = setTimeout(() => {
2102
+ if (closed)
2103
+ return;
2104
+ try {
2105
+ killTree(pid, 'SIGKILL', () => { });
2106
+ }
2107
+ catch { /* gone */ }
2108
+ }, 2_000);
2109
+ killTimer.unref?.();
2110
+ // A D-state process may never emit close. Do not wedge app shutdown after
2111
+ // escalation; the OS will reclaim it when possible.
2112
+ deadlineTimer = setTimeout(finish, 2_500);
2113
+ deadlineTimer.unref?.();
2114
+ });
987
2115
  }
988
2116
  trimProxyTail() {
989
2117
  if (this.proxyTail.length > 32_000)
@@ -992,6 +2120,40 @@ class HeadroomManager {
992
2120
  validPort(value) {
993
2121
  return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535 ? Number(value) : DEFAULT_PORT;
994
2122
  }
2123
+ relayBaseUrl() {
2124
+ const origin = this.processControl.relayOrigin;
2125
+ if (!origin)
2126
+ return undefined;
2127
+ try {
2128
+ const parsed = new URL(origin);
2129
+ if (parsed.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname)) {
2130
+ return undefined;
2131
+ }
2132
+ return `${parsed.origin}${exports.HEADROOM_RELAY_PATH}/${this.relayToken}`;
2133
+ }
2134
+ catch {
2135
+ return undefined;
2136
+ }
2137
+ }
2138
+ relayDiagnosticBaseUrl() {
2139
+ const relay = this.relayBaseUrl();
2140
+ if (!relay)
2141
+ return undefined;
2142
+ return `${new URL(relay).origin}${exports.HEADROOM_RELAY_PATH}/<redacted>`;
2143
+ }
2144
+ routingClientPort(backendPort) {
2145
+ const relay = this.relayBaseUrl();
2146
+ if (!relay)
2147
+ return backendPort;
2148
+ try {
2149
+ const parsed = new URL(relay);
2150
+ const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
2151
+ return this.validPort(port);
2152
+ }
2153
+ catch {
2154
+ return backendPort;
2155
+ }
2156
+ }
995
2157
  readPersisted() {
996
2158
  const raw = (0, desktop_db_1.getDesktopSetting)(this.db, STATE_KEY);
997
2159
  if (!raw)
@@ -1013,13 +2175,40 @@ class HeadroomManager {
1013
2175
  (0, desktop_db_1.setDesktopSetting)(this.db, STATE_KEY, JSON.stringify(next));
1014
2176
  this.syncRouting();
1015
2177
  }
2178
+ explicitActiveProviders(persisted) {
2179
+ const activation = persisted.activation;
2180
+ if (activation?.version !== exports.HEADROOM_ACTIVATION_SCHEMA_VERSION ||
2181
+ activation.source !== exports.HEADROOM_ACTIVATION_SOURCE)
2182
+ return { codex: false, claude: false };
2183
+ return {
2184
+ codex: activation.activeProviders?.codex === true,
2185
+ claude: activation.activeProviders?.claude === true,
2186
+ };
2187
+ }
2188
+ activationPatch(activeProviders) {
2189
+ const normalized = providerRecord(activeProviders);
2190
+ return {
2191
+ // Retain the old field for diagnostics/downgrades, but the versioned
2192
+ // envelope below is the sole routing authority.
2193
+ activeProviders: normalized,
2194
+ activation: {
2195
+ version: exports.HEADROOM_ACTIVATION_SCHEMA_VERSION,
2196
+ source: exports.HEADROOM_ACTIVATION_SOURCE,
2197
+ activeProviders: normalized,
2198
+ },
2199
+ };
2200
+ }
1016
2201
  syncRouting() {
1017
2202
  const persisted = this.readPersisted();
2203
+ const port = this.validPort(persisted.port);
2204
+ const trustedRuntimeAvailable = this.proxyTrustedPort === port && this.isProxyRunning();
2205
+ const explicitActiveProviders = this.explicitActiveProviders(persisted);
1018
2206
  (0, headroom_routing_1.setHeadroomRoutingState)({
1019
- port: this.validPort(persisted.port),
1020
- activeProviders: {
1021
- codex: !!persisted.activeProviders?.codex,
1022
- claude: !!persisted.activeProviders?.claude,
2207
+ port: this.routingClientPort(port),
2208
+ relayBaseUrl: this.relayBaseUrl(),
2209
+ activeProviders: this.shuttingDown || !trustedRuntimeAvailable ? { codex: false, claude: false } : {
2210
+ codex: explicitActiveProviders.codex,
2211
+ claude: explicitActiveProviders.claude,
1023
2212
  },
1024
2213
  });
1025
2214
  }