specrails-desktop 2.36.0 → 2.42.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 (510) hide show
  1. package/README.md +94 -247
  2. package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
  3. package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
  6. package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
  7. package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
  8. package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
  9. package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
  10. package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
  11. package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
  12. package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
  13. package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
  16. package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
  17. package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
  20. package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
  22. package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
  24. package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
  25. package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
  26. package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
  27. package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
  28. package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
  29. package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
  30. package/client/dist/assets/agent-BdT0SB1D.js +1 -0
  31. package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
  32. package/client/dist/assets/agent-C4ibdFk9.js +1 -0
  33. package/client/dist/assets/agent-CSFY22Zo.js +1 -0
  34. package/client/dist/assets/agent-DAnqxUn8.js +1 -0
  35. package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
  36. package/client/dist/assets/agent-T54WuR_Y.js +1 -0
  37. package/client/dist/assets/agent-i75WDZOA.js +1 -0
  38. package/client/dist/assets/browser-05vRDg_y.js +1 -0
  39. package/client/dist/assets/browser-Bulb-cB1.js +1 -0
  40. package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
  41. package/client/dist/assets/browser-CG0PE1j_.js +1 -0
  42. package/client/dist/assets/browser-CMzaqAjo.js +1 -0
  43. package/client/dist/assets/browser-DMajYdNX.js +1 -0
  44. package/client/dist/assets/browser-DxcOHrdP.js +1 -0
  45. package/client/dist/assets/browser-ObE7xVo3.js +1 -0
  46. package/client/dist/assets/builder-765NeYTp.js +1 -0
  47. package/client/dist/assets/builder-8MG3V6ea.js +1 -0
  48. package/client/dist/assets/builder-B-1UBs5K.js +1 -0
  49. package/client/dist/assets/builder-BTiGnPT0.js +1 -0
  50. package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
  51. package/client/dist/assets/builder-DKFMELBl.js +1 -0
  52. package/client/dist/assets/builder-OcmKrC0h.js +1 -0
  53. package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
  54. package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
  55. package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
  56. package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
  57. package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
  58. package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
  59. package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
  60. package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
  61. package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
  62. package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
  63. package/client/dist/assets/commands-0XjAftla.js +1 -0
  64. package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
  65. package/client/dist/assets/commands-BBwowgiE.js +1 -0
  66. package/client/dist/assets/commands-BacjIZLN.js +1 -0
  67. package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
  68. package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
  69. package/client/dist/assets/commands-DlbFzgrp.js +1 -0
  70. package/client/dist/assets/commands-LjPSsi5F.js +1 -0
  71. package/client/dist/assets/common-B5AZNYMH.js +1 -0
  72. package/client/dist/assets/common-BFZ0c_vN.js +1 -0
  73. package/client/dist/assets/common-BrlwEnL7.js +1 -0
  74. package/client/dist/assets/common-BttQx3z4.js +1 -0
  75. package/client/dist/assets/common-CM65k_-e.js +1 -0
  76. package/client/dist/assets/common-D4mvFFlu.js +1 -0
  77. package/client/dist/assets/common-TZ9Ny96S.js +1 -0
  78. package/client/dist/assets/common-nFpSF4GC.js +1 -0
  79. package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
  80. package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
  81. package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
  82. package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
  83. package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
  84. package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
  85. package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
  86. package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
  87. package/client/dist/assets/dialog-SRw01_QU.js +45 -0
  88. package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
  89. package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
  91. package/client/dist/assets/index-DwAmL7m5.css +2 -0
  92. package/client/dist/assets/index-EzrkUb8B.js +76 -0
  93. package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
  94. package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
  95. package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
  96. package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
  97. package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
  98. package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
  99. package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
  100. package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
  101. package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
  102. package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
  103. package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
  104. package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
  105. package/client/dist/assets/mcp-CCpSelOh.js +1 -0
  106. package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
  107. package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
  108. package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
  109. package/client/dist/assets/mcp-DUVQamDz.js +1 -0
  110. package/client/dist/assets/narration-B-ynWTpn.js +1 -0
  111. package/client/dist/assets/narration-Bj_laZwg.js +1 -0
  112. package/client/dist/assets/narration-BxHpmMv_.js +1 -0
  113. package/client/dist/assets/narration-C1Px0-0j.js +1 -0
  114. package/client/dist/assets/narration-CAXhGbzM.js +1 -0
  115. package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
  116. package/client/dist/assets/narration-XISXwqZd.js +1 -0
  117. package/client/dist/assets/narration-kwzfglQo.js +1 -0
  118. package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
  119. package/client/dist/assets/nav-B34QkESm.js +1 -0
  120. package/client/dist/assets/nav-Bap_UXAq.js +1 -0
  121. package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
  122. package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
  123. package/client/dist/assets/nav-CygC_WN-.js +1 -0
  124. package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
  125. package/client/dist/assets/nav-m00uxYUG.js +1 -0
  126. package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
  127. package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
  128. package/client/dist/assets/packet-C3Ievb9R.js +1 -0
  129. package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
  130. package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
  131. package/client/dist/assets/packet-DUTGX52M.js +1 -0
  132. package/client/dist/assets/packet-DhsKtUPB.js +1 -0
  133. package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
  134. package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
  135. package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
  136. package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
  137. package/client/dist/assets/settings-BJB9RKJj.js +1 -0
  138. package/client/dist/assets/settings-CHwQR2rB.js +1 -0
  139. package/client/dist/assets/settings-CrMi3kSi.js +1 -0
  140. package/client/dist/assets/settings-DKyhDGD1.js +1 -0
  141. package/client/dist/assets/settings-Deupji9c.js +1 -0
  142. package/client/dist/assets/settings-DqRMmwAM.js +1 -0
  143. package/client/dist/assets/settings-J4rj6vo-.js +1 -0
  144. package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
  145. package/client/dist/assets/setup-Ba11l2JA.js +1 -0
  146. package/client/dist/assets/setup-BzW4kMok.js +1 -0
  147. package/client/dist/assets/setup-COzwYhkJ.js +1 -0
  148. package/client/dist/assets/setup-CRVULhtF.js +1 -0
  149. package/client/dist/assets/setup-DgvlxwpE.js +1 -0
  150. package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
  151. package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
  152. package/client/dist/assets/spending-BqbdPm_4.js +1 -0
  153. package/client/dist/assets/square-Ci-PFcFe.js +1 -0
  154. package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
  155. package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
  156. package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
  157. package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
  158. package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
  159. package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
  160. package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
  161. package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
  162. package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
  163. package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
  164. package/client/dist/index.html +34 -35
  165. package/docs/README.md +3 -0
  166. package/docs/agent-live-steering.md +57 -0
  167. package/docs/ci-cd.md +57 -0
  168. package/docs/code-explorer.md +48 -0
  169. package/docs/codex.md +12 -1
  170. package/docs/features/detachable-mission-windows.md +59 -0
  171. package/docs/guide/de/integrations/5-mcp-server.md +13 -9
  172. package/docs/guide/de/integrations/6-agent-chat.md +1 -0
  173. package/docs/guide/en/integrations/5-mcp-server.md +13 -9
  174. package/docs/guide/en/integrations/6-agent-chat.md +1 -0
  175. package/docs/guide/es/integrations/5-mcp-server.md +13 -9
  176. package/docs/guide/es/integrations/6-agent-chat.md +1 -0
  177. package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
  178. package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
  179. package/docs/guide/it/integrations/5-mcp-server.md +13 -9
  180. package/docs/guide/it/integrations/6-agent-chat.md +1 -0
  181. package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
  182. package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
  183. package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
  184. package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
  185. package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
  186. package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
  187. package/docs/internals/README.md +1 -0
  188. package/docs/internals/browser-capture-performance.md +55 -3
  189. package/docs/internals/browser-login-popups.md +45 -0
  190. package/docs/internals/browser-native-retina-audit.md +39 -0
  191. package/docs/internals/ci-cd-audit.md +39 -0
  192. package/docs/internals/core-runtime-updates.md +68 -0
  193. package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
  194. package/docs/internals/implementation-reliability-audit.md +93 -0
  195. package/docs/internals/interactive-jobs.md +110 -6
  196. package/docs/internals/mcp-mission-audit.md +76 -0
  197. package/docs/internals/project-builder.md +404 -42
  198. package/docs/internals/safe-pr-review-flow.md +52 -10
  199. package/docs/internals/startup-recovery-audit.md +58 -0
  200. package/docs/mcp.md +74 -41
  201. package/docs/mission-processes.md +59 -0
  202. package/docs/multi-repo-projects.md +51 -0
  203. package/docs/platforms/windows-parity.md +70 -0
  204. package/docs/platforms/windows.md +13 -11
  205. package/docs/running-pipelines.md +14 -0
  206. package/mcp-bridge/dist/specrails-mcp.js +7186 -0
  207. package/package.json +9 -4
  208. package/server/dist/agent-chat-manager.js +537 -68
  209. package/server/dist/agent-chat-router.js +87 -6
  210. package/server/dist/agent-context-resolver.js +129 -25
  211. package/server/dist/agent-defaults.js +19 -11
  212. package/server/dist/agent-input-store.js +206 -0
  213. package/server/dist/agent-mcp-config.js +54 -15
  214. package/server/dist/agent-operator-prompt.js +255 -65
  215. package/server/dist/agent-spec-framing.js +202 -0
  216. package/server/dist/agent-steering.js +218 -0
  217. package/server/dist/agent-store.js +153 -0
  218. package/server/dist/api-not-found.js +8 -0
  219. package/server/dist/background-process-control.js +164 -0
  220. package/server/dist/background-process-service.js +61 -0
  221. package/server/dist/background-process-store.js +242 -0
  222. package/server/dist/background-windows-bootstrap.js +238 -0
  223. package/server/dist/blueprint-chat-manager.js +476 -62
  224. package/server/dist/blueprint-commit.js +21 -1
  225. package/server/dist/blueprint-draft-parser.js +122 -17
  226. package/server/dist/blueprint-generation.js +238 -0
  227. package/server/dist/blueprint-operator-prompt.js +91 -63
  228. package/server/dist/blueprint-router.js +55 -3
  229. package/server/dist/blueprint-spec-fixtures.js +85 -0
  230. package/server/dist/blueprint-spec-quality.js +57 -16
  231. package/server/dist/blueprint-store.js +92 -1
  232. package/server/dist/browser-capture-manager.js +86 -30
  233. package/server/dist/browser-playwright.js +168 -45
  234. package/server/dist/browser-viewport.js +26 -0
  235. package/server/dist/chat-manager.js +23 -15
  236. package/server/dist/chromium-archive.cjs +83 -0
  237. package/server/dist/chromium-resolver.js +72 -64
  238. package/server/dist/code-activity.js +145 -0
  239. package/server/dist/code-explorer-router.js +714 -114
  240. package/server/dist/core-compat.js +28 -1
  241. package/server/dist/core-execution.js +111 -0
  242. package/server/dist/core-package.js +4 -16
  243. package/server/dist/core-runtime.js +202 -0
  244. package/server/dist/core-update-manager.js +161 -60
  245. package/server/dist/core-update-state.js +29 -0
  246. package/server/dist/db.js +66 -35
  247. package/server/dist/delivery-evidence.js +142 -39
  248. package/server/dist/desktop-db.js +239 -43
  249. package/server/dist/desktop-router.js +109 -9
  250. package/server/dist/dev-ports.js +8 -3
  251. package/server/dist/file-provenance.js +34 -22
  252. package/server/dist/file-story-manager.js +62 -64
  253. package/server/dist/file-story.js +118 -35
  254. package/server/dist/file-summary-generator.js +46 -5
  255. package/server/dist/file-summary-manager.js +355 -106
  256. package/server/dist/framework-manager.js +98 -110
  257. package/server/dist/framework-reseed.js +76 -10
  258. package/server/dist/git-diagnostics.js +2 -2
  259. package/server/dist/headroom-manager.js +38 -3
  260. package/server/dist/host-control.js +52 -0
  261. package/server/dist/index.js +43 -2
  262. package/server/dist/interactive-job-session.js +149 -16
  263. package/server/dist/internal-api.js +48 -0
  264. package/server/dist/jira/jira-materializer.js +1 -0
  265. package/server/dist/json-tolerant.js +174 -0
  266. package/server/dist/legacy-migration.js +4 -2
  267. package/server/dist/loop-command-catalog.js +83 -21
  268. package/server/dist/loop-constants.js +6 -1
  269. package/server/dist/loop-decider.js +11 -1
  270. package/server/dist/loop-executors.js +194 -45
  271. package/server/dist/loop-factory.js +11 -4
  272. package/server/dist/loop-graph.js +90 -4
  273. package/server/dist/loop-run-manager.js +301 -69
  274. package/server/dist/loop-shell-invocation.js +21 -0
  275. package/server/dist/loop-step-idle.js +55 -0
  276. package/server/dist/loop-templates.js +31 -7
  277. package/server/dist/mcp/agent-capability.js +37 -4
  278. package/server/dist/mcp/guide.js +149 -36
  279. package/server/dist/mcp/mcp-admin-router.js +20 -4
  280. package/server/dist/mcp/mcp-server.js +89 -43
  281. package/server/dist/mcp/mcp-tiers.js +6 -5
  282. package/server/dist/mcp/mcp-token.js +3 -1
  283. package/server/dist/mcp/resources.js +5 -4
  284. package/server/dist/mcp/tools/catalog.js +4 -0
  285. package/server/dist/mcp/tools/code.js +125 -14
  286. package/server/dist/mcp/tools/context.js +153 -0
  287. package/server/dist/mcp/tools/git.js +15 -5
  288. package/server/dist/mcp/tools/jobs.js +84 -71
  289. package/server/dist/mcp/tools/loops.js +17 -6
  290. package/server/dist/mcp/tools/meta.js +74 -27
  291. package/server/dist/mcp/tools/mission.js +29 -0
  292. package/server/dist/mcp/tools/projects.js +22 -6
  293. package/server/dist/mcp/tools/rails.js +48 -11
  294. package/server/dist/mcp/tools/specs.js +86 -14
  295. package/server/dist/mcp/tools/support.js +1 -1
  296. package/server/dist/mcp/tools/types.js +117 -43
  297. package/server/dist/mcp/tools/watch.js +144 -51
  298. package/server/dist/milestone-chain-store.js +164 -0
  299. package/server/dist/milestone-chain.js +487 -0
  300. package/server/dist/milestone-progress.js +348 -0
  301. package/server/dist/mobile/mobile-devices.js +10 -7
  302. package/server/dist/mobile/mobile-gateway.js +2 -0
  303. package/server/dist/mobile/mobile-missions.js +214 -0
  304. package/server/dist/mobile/mobile-redact.js +17 -3
  305. package/server/dist/mobile/mobile-router.js +46 -4
  306. package/server/dist/mobile/mobile-ws.js +49 -4
  307. package/server/dist/multi-repo-bases.js +56 -0
  308. package/server/dist/multi-repo-checkout.js +101 -0
  309. package/server/dist/multi-repo-delivery.js +100 -0
  310. package/server/dist/multi-repo-execution-store.js +65 -0
  311. package/server/dist/multi-repo-execution.js +453 -0
  312. package/server/dist/offline-assemble.js +10 -8
  313. package/server/dist/openspec-runtime-plugin-commands.json +18 -0
  314. package/server/dist/openspec-runtime-plugin.js +120 -0
  315. package/server/dist/plugins/codex-spawn.js +49 -0
  316. package/server/dist/plugins/serena/manifest.js +2 -2
  317. package/server/dist/plugins/serena/templates/instructions.md +16 -0
  318. package/server/dist/pr-publisher.js +1 -1
  319. package/server/dist/profiles-router.js +3 -18
  320. package/server/dist/project-code-discovery.js +142 -0
  321. package/server/dist/project-git.js +19 -21
  322. package/server/dist/project-registry.js +272 -20
  323. package/server/dist/project-repositories.js +157 -0
  324. package/server/dist/project-repository-provenance.js +31 -0
  325. package/server/dist/project-router-background-processes.js +105 -0
  326. package/server/dist/project-router-git.js +75 -16
  327. package/server/dist/project-router-jobs.js +2 -126
  328. package/server/dist/project-router-loop-runs.js +72 -2
  329. package/server/dist/project-router-tickets.js +53 -8
  330. package/server/dist/project-router.js +239 -38
  331. package/server/dist/provider-detection.js +18 -6
  332. package/server/dist/provider-limit.js +64 -0
  333. package/server/dist/providers/claude-adapter.js +26 -1
  334. package/server/dist/providers/claude-background-tasks.js +18 -0
  335. package/server/dist/providers/claude-live-session.js +383 -0
  336. package/server/dist/providers/codex-adapter.js +19 -0
  337. package/server/dist/providers/codex-live-session.js +545 -0
  338. package/server/dist/providers/gemini-adapter.js +1 -0
  339. package/server/dist/providers/live-session-types.js +12 -0
  340. package/server/dist/providers/live-session.js +13 -0
  341. package/server/dist/providers/terminal-result.js +24 -0
  342. package/server/dist/queue-manager.js +2 -0
  343. package/server/dist/rail-isolated-launch.js +141 -16
  344. package/server/dist/rail-pr-decision.js +364 -99
  345. package/server/dist/rail-pr-store.js +24 -10
  346. package/server/dist/rail-worktree-release.js +34 -2
  347. package/server/dist/rail-worktrees-store.js +4 -4
  348. package/server/dist/rails-router.js +249 -55
  349. package/server/dist/repo-lock.js +92 -17
  350. package/server/dist/review-packet.js +23 -13
  351. package/server/dist/schemas/file-summary.v1.json +59 -59
  352. package/server/dist/schemas/profile.v1.json +137 -137
  353. package/server/dist/setup-manager.js +52 -8
  354. package/server/dist/shell-integration/bash-shim.bash +45 -0
  355. package/server/dist/shell-integration/fish-shim.fish +36 -0
  356. package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
  357. package/server/dist/shell-integration/zsh-shim.zsh +47 -0
  358. package/server/dist/smash-runner.js +1 -0
  359. package/server/dist/spawn-lifecycle.js +44 -27
  360. package/server/dist/spec-contract-prompt.js +143 -0
  361. package/server/dist/spec-draft-parser.js +6 -1
  362. package/server/dist/spec-models.js +1 -0
  363. package/server/dist/stuck-run-detector.js +1 -0
  364. package/server/dist/terminal-manager.js +21 -1
  365. package/server/dist/terminal-shell-integration.js +4 -1
  366. package/server/dist/transient-children.js +619 -230
  367. package/server/dist/util/cli-prompt.js +78 -5
  368. package/server/dist/util/sqlite-migrations.js +20 -0
  369. package/server/dist/util/win-spawn.js +18 -3
  370. package/server/dist/util/windows-provider-shell.js +29 -0
  371. package/server/dist/verification-sentinel.js +14 -0
  372. package/server/dist/vitest-setup.js +2 -0
  373. package/server/dist/windows-job-supervisor.js +133 -0
  374. package/server/dist/workspace-manager.js +9 -30
  375. package/server/dist/worktree-manager.js +17 -2
  376. package/server/dist/worktree-overlay.js +3 -1
  377. package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
  378. package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
  379. package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
  380. package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
  381. package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
  382. package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
  383. package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
  384. package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
  385. package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
  386. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
  387. package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
  388. package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
  389. package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
  390. package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
  391. package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
  392. package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
  393. package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
  394. package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
  395. package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
  396. package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
  397. package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
  398. package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
  399. package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
  400. package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
  401. package/client/dist/assets/agent-BQgKv_2F.js +0 -1
  402. package/client/dist/assets/agent-C1NRDJqL.js +0 -1
  403. package/client/dist/assets/agent-CLNleiy6.js +0 -1
  404. package/client/dist/assets/agent-CmceArkQ.js +0 -1
  405. package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
  406. package/client/dist/assets/agent-DB1JeE64.js +0 -1
  407. package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
  408. package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
  409. package/client/dist/assets/auth-D_5bQv5W.js +0 -1
  410. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  411. package/client/dist/assets/bot-DzuuuC5R.js +0 -1
  412. package/client/dist/assets/browser-BgqHDMXP.js +0 -1
  413. package/client/dist/assets/browser-CsKJtrpv.js +0 -1
  414. package/client/dist/assets/browser-Cu58EpZU.js +0 -1
  415. package/client/dist/assets/browser-CvXOJm-j.js +0 -1
  416. package/client/dist/assets/browser-DOrFh1c7.js +0 -1
  417. package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
  418. package/client/dist/assets/browser-LPgxvJu2.js +0 -1
  419. package/client/dist/assets/browser-eL-3MIEI.js +0 -1
  420. package/client/dist/assets/builder-0IWtosUm.js +0 -1
  421. package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
  422. package/client/dist/assets/builder-BnqaaBkT.js +0 -1
  423. package/client/dist/assets/builder-BxxfGgAO.js +0 -1
  424. package/client/dist/assets/builder-C5IIgWsq.js +0 -1
  425. package/client/dist/assets/builder-CHuahItu.js +0 -1
  426. package/client/dist/assets/builder-CjWosXL_.js +0 -1
  427. package/client/dist/assets/builder-CpCIOoKj.js +0 -1
  428. package/client/dist/assets/code-CHPkOkDS.js +0 -1
  429. package/client/dist/assets/commands-6tYQih5m.js +0 -1
  430. package/client/dist/assets/commands-BK2QinXN.js +0 -1
  431. package/client/dist/assets/commands-C2Nca76h.js +0 -1
  432. package/client/dist/assets/commands-CKdLihNX.js +0 -1
  433. package/client/dist/assets/commands-CYIBTAlc.js +0 -1
  434. package/client/dist/assets/commands-CjUtYJIM.js +0 -1
  435. package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
  436. package/client/dist/assets/commands-D_ZHYQug.js +0 -1
  437. package/client/dist/assets/common-6MLbD-jM.js +0 -1
  438. package/client/dist/assets/common-BKGEu4rm.js +0 -1
  439. package/client/dist/assets/common-BNbnv8cE.js +0 -1
  440. package/client/dist/assets/common-BqGWtDbt.js +0 -1
  441. package/client/dist/assets/common-DBtTzeYm.js +0 -1
  442. package/client/dist/assets/common-DEW3Nr8n.js +0 -1
  443. package/client/dist/assets/common-DHsv9N5d.js +0 -1
  444. package/client/dist/assets/common-mkrEcW-Y.js +0 -1
  445. package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
  446. package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
  447. package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
  448. package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
  449. package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
  450. package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
  451. package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
  452. package/client/dist/assets/dialog-CdWCb02j.js +0 -45
  453. package/client/dist/assets/dist-bVqfYmby.js +0 -1
  454. package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
  455. package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
  456. package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
  457. package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
  458. package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
  459. package/client/dist/assets/index-DIhMEAVw.js +0 -66
  460. package/client/dist/assets/index-DwygcU7s.css +0 -2
  461. package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
  462. package/client/dist/assets/mcp-C2AojZZN.js +0 -1
  463. package/client/dist/assets/mcp-D8BT78_L.js +0 -1
  464. package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
  465. package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
  466. package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
  467. package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
  468. package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
  469. package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
  470. package/client/dist/assets/narration-BGuiEDj3.js +0 -1
  471. package/client/dist/assets/narration-BuSoqRmv.js +0 -1
  472. package/client/dist/assets/narration-CB9t29fy.js +0 -1
  473. package/client/dist/assets/narration-CU3x8FHD.js +0 -1
  474. package/client/dist/assets/narration-CWeXhfP5.js +0 -1
  475. package/client/dist/assets/narration-DRaDnhT5.js +0 -1
  476. package/client/dist/assets/narration-DtEbQgCw.js +0 -1
  477. package/client/dist/assets/nav-ApJVg3xu.js +0 -1
  478. package/client/dist/assets/nav-BKNky1Bg.js +0 -1
  479. package/client/dist/assets/nav-C6vWWKYx.js +0 -1
  480. package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
  481. package/client/dist/assets/nav-CsmFBIDf.js +0 -1
  482. package/client/dist/assets/nav-DjkmQJjd.js +0 -1
  483. package/client/dist/assets/packet-B1IxaL5f.js +0 -1
  484. package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
  485. package/client/dist/assets/packet-By8fqYxW.js +0 -1
  486. package/client/dist/assets/packet-CZdvA44E.js +0 -1
  487. package/client/dist/assets/packet-CrjHRQCP.js +0 -1
  488. package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
  489. package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
  490. package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
  491. package/client/dist/assets/play-DNlrawbS.js +0 -1
  492. package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
  493. package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
  494. package/client/dist/assets/settings-C1-7cXzC.js +0 -1
  495. package/client/dist/assets/settings-CAFaT38b.js +0 -1
  496. package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
  497. package/client/dist/assets/settings-DHh7_IF5.js +0 -1
  498. package/client/dist/assets/settings-DQXAmdcv.js +0 -1
  499. package/client/dist/assets/settings-guzlhpup.js +0 -1
  500. package/client/dist/assets/settings-sZfTPMEH.js +0 -1
  501. package/client/dist/assets/setup-3jWaoVUO.js +0 -1
  502. package/client/dist/assets/setup-BjgRneb2.js +0 -1
  503. package/client/dist/assets/setup-BzFIjv-f.js +0 -1
  504. package/client/dist/assets/setup-C4VfQsxE.js +0 -1
  505. package/client/dist/assets/setup-DfUkpDbN.js +0 -1
  506. package/client/dist/assets/setup-ECe90Pmu.js +0 -1
  507. package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
  508. package/client/dist/assets/setup-jwfLMiM2.js +0 -1
  509. package/client/dist/assets/spending-EC9xqio-.js +0 -1
  510. package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.BACKGROUND_LOG_RETENTION_MS = void 0;
3
+ exports.BACKGROUND_PERSISTENCE_FLUSH_MS = exports.BACKGROUND_LOG_RETENTION_MS = void 0;
4
+ exports.initializeBackgroundProcessPersistence = initializeBackgroundProcessPersistence;
5
+ exports.flushBackgroundProcessPersistence = flushBackgroundProcessPersistence;
6
+ exports.closeBackgroundProcessPersistence = closeBackgroundProcessPersistence;
7
+ exports.purgeBackgroundProcessHistory = purgeBackgroundProcessHistory;
7
8
  exports.trackTransientChild = trackTransientChild;
8
9
  exports.killTransientChildren = killTransientChildren;
9
10
  exports.startBackgroundProcess = startBackgroundProcess;
@@ -13,62 +14,238 @@ exports.listBackgroundProcesses = listBackgroundProcesses;
13
14
  exports.killBackgroundProcess = killBackgroundProcess;
14
15
  exports.killOwnedBackgroundProcess = killOwnedBackgroundProcess;
15
16
  exports.killBackgroundProcessesForChat = killBackgroundProcessesForChat;
16
- const tree_kill_1 = __importDefault(require("tree-kill"));
17
+ exports.awaitBackgroundProcessesStopped = awaitBackgroundProcessesStopped;
18
+ const win_spawn_1 = require("./util/win-spawn");
17
19
  const child_process_1 = require("child_process");
18
- const readline_1 = __importDefault(require("readline"));
20
+ const crypto_1 = require("crypto");
21
+ const string_decoder_1 = require("string_decoder");
19
22
  const process_admission_1 = require("./process-admission");
20
- /**
21
- * Registry of fire-and-forget child processes that are NOT owned by a long-lived
22
- * manager (QueueManager / ChatManager / SetupManager track their own). The Quick
23
- * spec-generation spawn in particular keeps only a closure handle + a watchdog,
24
- * so `ProjectRegistry.removeProject()` / `shutdown()` could not terminate it —
25
- * it survived project removal (burning AI-CLI spend) until the watchdog fired.
26
- *
27
- * Children registered here are tree-killed when their project is removed or the
28
- * app shuts down. Self-cleaning: each child is auto-unregistered on `close`.
29
- */
23
+ const background_process_control_1 = require("./background-process-control");
24
+ const win_spawn_2 = require("./util/win-spawn");
25
+ const background_process_store_1 = require("./background-process-store");
26
+ const background_windows_bootstrap_1 = require("./background-windows-bootstrap");
27
+ /** Fire-and-forget children not already owned by a long-lived provider manager. */
30
28
  const byProject = new Map();
31
29
  const backgroundByPid = new Map();
32
- const finishedBackgroundByPid = new Map();
33
- const backgroundTerminal = new Set(['exited', 'killed', 'failed']);
30
+ const finishedBackgroundById = new Map();
31
+ const backgroundTerminal = new Set(['exited', 'killed', 'failed', 'interrupted']);
34
32
  const BACKGROUND_KILL_GRACE_MS = 2500;
35
- const BACKGROUND_OUTPUT_MAX_LINE_CHARS = 1000;
33
+ const BACKGROUND_STOP_CONFIRM_MS = 2500;
34
+ const BACKGROUND_OUTPUT_MAX_LINE_CHARS = 4000;
36
35
  const BACKGROUND_OUTPUT_MAX_LINES_PER_SECOND = 20;
37
- const BACKGROUND_LOG_MAX_LINES = 500;
36
+ const BACKGROUND_LOG_MAX_LINES = 2000;
37
+ const BACKGROUND_FINISHED_MAX = 32;
38
38
  exports.BACKGROUND_LOG_RETENTION_MS = 10 * 60 * 1000;
39
+ exports.BACKGROUND_PERSISTENCE_FLUSH_MS = 250;
40
+ const pendingPersistence = new Map();
41
+ let backgroundStore;
42
+ let persistenceAttempted = false;
43
+ let persistenceError;
44
+ let persistenceTimer;
45
+ let lastPersistenceAttempt = 0;
46
+ let persistenceRetryDelay = exports.BACKGROUND_PERSISTENCE_FLUSH_MS;
47
+ let persistenceFile;
48
+ let persistenceClosed = false;
49
+ let lastHistoryOpenAttempt = 0;
50
+ function ensureBackgroundProcessStore() {
51
+ if (backgroundStore || !persistenceAttempted)
52
+ return backgroundStore;
53
+ if (!persistenceClosed && persistenceFile && Date.now() - lastHistoryOpenAttempt >= 1000) {
54
+ try {
55
+ initializeBackgroundProcessPersistence(persistenceFile);
56
+ }
57
+ catch { /* report the retained open failure below */ }
58
+ }
59
+ if (!backgroundStore)
60
+ throw new Error(`Background process history is unavailable: ${persistenceClosed ? 'the store is closed' : persistenceError ?? 'the store could not be opened'}. Retry after the storage problem is resolved.`);
61
+ return backgroundStore;
62
+ }
63
+ function schedulePersistenceFlush(delay) {
64
+ if (persistenceTimer || !pendingPersistence.size || !backgroundStore)
65
+ return;
66
+ persistenceTimer = setTimeout(() => {
67
+ persistenceTimer = undefined;
68
+ try {
69
+ flushBackgroundProcessPersistence();
70
+ }
71
+ catch { /* failure schedules a bounded retry even for a quiet process */ }
72
+ }, delay);
73
+ persistenceTimer.unref?.();
74
+ }
75
+ function initializeBackgroundProcessPersistence(file) {
76
+ if (backgroundStore) {
77
+ flushBackgroundProcessPersistence();
78
+ return;
79
+ }
80
+ persistenceFile = file;
81
+ persistenceClosed = false;
82
+ lastHistoryOpenAttempt = Date.now();
83
+ persistenceAttempted = true;
84
+ try {
85
+ backgroundStore = new background_process_store_1.BackgroundProcessStore(file);
86
+ persistenceError = undefined;
87
+ for (const record of backgroundByPid.values()) {
88
+ for (const line of record.outputLines)
89
+ queuePersistence(record, line);
90
+ queuePersistence(record);
91
+ }
92
+ flushBackgroundProcessPersistence();
93
+ }
94
+ catch (error) {
95
+ persistenceError = error instanceof Error ? error.message : String(error);
96
+ throw error;
97
+ }
98
+ }
99
+ function queuePersistence(record, line) {
100
+ if (!backgroundStore || record.skipPersistence)
101
+ return;
102
+ let pending = pendingPersistence.get(record.process.processId);
103
+ if (!pending) {
104
+ pending = { record, lines: new Map() };
105
+ pendingPersistence.set(record.process.processId, pending);
106
+ }
107
+ if (line)
108
+ pending.lines.set(line.sequence, cloneLogLine(line));
109
+ if (pending.lines.size >= BACKGROUND_LOG_MAX_LINES) {
110
+ if (!persistenceError || Date.now() - lastPersistenceAttempt >= exports.BACKGROUND_PERSISTENCE_FLUSH_MS) {
111
+ try {
112
+ flushBackgroundProcessPersistence();
113
+ }
114
+ catch { /* failure stays visible and the buffer remains bounded */ }
115
+ }
116
+ while (pending.lines.size > BACKGROUND_LOG_MAX_LINES)
117
+ pending.lines.delete(pending.lines.keys().next().value);
118
+ }
119
+ schedulePersistenceFlush(persistenceError ? persistenceRetryDelay : exports.BACKGROUND_PERSISTENCE_FLUSH_MS);
120
+ }
121
+ /** One transaction and one bulk line statement per batch, including upserts
122
+ * for partial lines. A crash can lose at most the last 250ms of normal output. */
123
+ function flushBackgroundProcessPersistence() {
124
+ if (persistenceTimer) {
125
+ clearTimeout(persistenceTimer);
126
+ persistenceTimer = undefined;
127
+ }
128
+ if (!backgroundStore) {
129
+ if (persistenceError)
130
+ throw new Error(persistenceError);
131
+ return;
132
+ }
133
+ lastPersistenceAttempt = Date.now();
134
+ let failure;
135
+ for (const [processId, pending] of pendingPersistence) {
136
+ if (pending.record.skipPersistence) {
137
+ pendingPersistence.delete(processId);
138
+ continue;
139
+ }
140
+ const record = pending.record;
141
+ try {
142
+ const { persistenceError: previousError, ...process } = record.process;
143
+ backgroundStore.write({ process, lines: [...pending.lines.values()], nextSequence: record.nextSequence, clipped: record.clippedOutput });
144
+ pendingPersistence.delete(processId);
145
+ delete record.process.persistenceError;
146
+ const retained = finishedBackgroundById.get(processId);
147
+ if (retained)
148
+ delete retained.process.persistenceError;
149
+ if (previousError)
150
+ safelyNotify(() => record.hooks.onUpdated?.(cloneProcess(record.process)));
151
+ }
152
+ catch (error) {
153
+ failure = error;
154
+ const message = `Background history could not be saved: ${error instanceof Error ? error.message : String(error)}`;
155
+ if (record.process.persistenceError !== message) {
156
+ record.process.persistenceError = message;
157
+ const retained = finishedBackgroundById.get(processId);
158
+ if (retained)
159
+ retained.process.persistenceError = message;
160
+ safelyNotify(() => record.hooks.onUpdated?.(cloneProcess(record.process)));
161
+ }
162
+ }
163
+ }
164
+ persistenceError = failure ? (failure instanceof Error ? failure.message : String(failure)) : undefined;
165
+ if (failure) {
166
+ schedulePersistenceFlush(persistenceRetryDelay);
167
+ persistenceRetryDelay = Math.min(5000, persistenceRetryDelay * 2);
168
+ throw failure;
169
+ }
170
+ persistenceRetryDelay = exports.BACKGROUND_PERSISTENCE_FLUSH_MS;
171
+ }
172
+ function closeBackgroundProcessPersistence() {
173
+ persistenceClosed = true;
174
+ try {
175
+ flushBackgroundProcessPersistence();
176
+ }
177
+ finally {
178
+ if (persistenceTimer) {
179
+ clearTimeout(persistenceTimer);
180
+ persistenceTimer = undefined;
181
+ }
182
+ const store = backgroundStore;
183
+ backgroundStore = undefined;
184
+ store?.close();
185
+ }
186
+ }
187
+ function purgeBackgroundProcessHistory(filter) {
188
+ if (!filter.chatId && !filter.projectId)
189
+ throw new Error('A conversation or project is required to purge background history.');
190
+ // Do not discard pending data or acknowledge deletion while its durable
191
+ // store is inaccessible. A subsequent explicit retry can reopen it.
192
+ ensureBackgroundProcessStore()?.purge(filter);
193
+ const matches = (process) => (!filter.chatId || process.chatId === filter.chatId) && (!filter.projectId || process.projectId === filter.projectId);
194
+ for (const record of backgroundByPid.values())
195
+ if (matches(record.process)) {
196
+ record.skipPersistence = true;
197
+ pendingPersistence.delete(record.process.processId);
198
+ }
199
+ for (const [id, record] of finishedBackgroundById)
200
+ if (matches(record.process)) {
201
+ clearTimeout(record.cleanupTimer);
202
+ finishedBackgroundById.delete(id);
203
+ pendingPersistence.delete(id);
204
+ }
205
+ }
39
206
  function treeKillSafe(pid, signal) {
40
207
  try {
41
- (0, tree_kill_1.default)(pid, signal, () => undefined);
208
+ (0, win_spawn_1.treeKillSafe)(pid, signal, () => undefined);
42
209
  }
43
- catch { /* best-effort */ }
210
+ catch { /* generic provider owners handle their own terminal state */ }
44
211
  }
45
- function treeKillWithEscalation(pid, isStillAlive = () => backgroundByPid.has(pid)) {
46
- treeKillSafe(pid, 'SIGTERM');
47
- const timer = setTimeout(() => {
48
- if (isStillAlive())
49
- treeKillSafe(pid, 'SIGKILL');
50
- }, BACKGROUND_KILL_GRACE_MS);
51
- timer.unref?.();
52
- return timer;
212
+ function isCurrent(record) { return backgroundByPid.get(record.process.pid) === record; }
213
+ function cloneProcess(value) { return { ...value }; }
214
+ function cloneLogLine(value) { return { ...value }; }
215
+ function safelyNotify(callback) {
216
+ try {
217
+ callback?.();
218
+ }
219
+ catch (error) {
220
+ console.warn('[background-process] notification failed:', error instanceof Error ? error.message : String(error));
221
+ }
53
222
  }
54
- function cloneProcess(process) {
55
- return { ...process };
223
+ function notifyUpdated(record) {
224
+ queuePersistence(record);
225
+ try {
226
+ flushBackgroundProcessPersistence();
227
+ }
228
+ catch { /* process control must continue when history persistence fails */ }
229
+ safelyNotify(() => record.hooks.onUpdated?.(cloneProcess(record.process)));
56
230
  }
57
- function cloneLogLine(line) {
58
- return { ...line };
231
+ function reportError(record, error) {
232
+ if (!isCurrent(record))
233
+ return;
234
+ const message = error instanceof Error ? error.message : String(error);
235
+ if (record.process.error === message)
236
+ return;
237
+ record.process.error = message;
238
+ notifyUpdated(record);
59
239
  }
60
- /** Track a child under a projectId; auto-removes itself on close. */
240
+ /** Track a provider child under a projectId; its own manager retains lifecycle ownership. */
61
241
  function trackTransientChild(projectId, child) {
62
242
  try {
63
243
  (0, process_admission_1.assertProcessAdmission)(projectId);
64
244
  }
65
- catch (err) {
66
- // A continuation may have spawned immediately before observing the closed
67
- // project epoch. Never leave that child unowned simply because registration
68
- // lost the teardown race.
245
+ catch (error) {
69
246
  if (child.pid)
70
247
  treeKillSafe(child.pid, 'SIGTERM');
71
- throw err;
248
+ throw error;
72
249
  }
73
250
  let set = byProject.get(projectId);
74
251
  if (!set) {
@@ -77,247 +254,459 @@ function trackTransientChild(projectId, child) {
77
254
  }
78
255
  set.add(child);
79
256
  const drop = () => {
80
- const s = byProject.get(projectId);
81
- if (s) {
82
- s.delete(child);
83
- if (s.size === 0)
84
- byProject.delete(projectId);
85
- }
257
+ const current = byProject.get(projectId);
258
+ current?.delete(child);
259
+ if (current?.size === 0)
260
+ byProject.delete(projectId);
86
261
  };
87
262
  child.once('close', drop);
88
263
  child.once('error', drop);
89
264
  }
90
- /** SIGTERM the whole subtree of every tracked child for a project, then escalate. */
265
+ /** Background apps retain their group owner until exit is confirmed. */
91
266
  function killTransientChildren(projectId) {
92
- const set = byProject.get(projectId);
93
- if (set)
94
- for (const child of set) {
95
- if (child.pid) {
96
- if (backgroundByPid.has(child.pid))
97
- continue;
98
- let alive = true;
99
- const timer = treeKillWithEscalation(child.pid, () => alive);
100
- const clear = () => { alive = false; clearTimeout(timer); };
101
- child.once('close', clear);
102
- child.once('error', clear);
103
- }
267
+ const children = byProject.get(projectId);
268
+ if (children)
269
+ for (const child of children) {
270
+ if (!child.pid || backgroundByPid.get(child.pid)?.child === child)
271
+ continue;
272
+ let alive = true;
273
+ treeKillSafe(child.pid, 'SIGTERM');
274
+ const timer = setTimeout(() => { if (alive)
275
+ treeKillSafe(child.pid, 'SIGKILL'); }, BACKGROUND_KILL_GRACE_MS);
276
+ timer.unref?.();
277
+ const clear = () => { alive = false; clearTimeout(timer); };
278
+ child.once('close', clear);
279
+ child.once('error', clear);
104
280
  }
105
281
  byProject.delete(projectId);
106
- for (const [pid, record] of [...backgroundByPid]) {
107
- if (record.process.projectId !== projectId)
108
- continue;
109
- markTerminal(record, 'killed', null, 'SIGTERM');
110
- terminateBackgroundRecord(record);
282
+ for (const record of backgroundByPid.values()) {
283
+ if (record.process.projectId === projectId)
284
+ terminateBackgroundRecord(record);
111
285
  }
112
286
  }
113
- function startBackgroundProcess(command, cwd, chatId, projectId, hooks) {
287
+ function startBackgroundProcess(command, cwd, chatId, projectId, hooks = {}, metadata = {}) {
114
288
  (0, process_admission_1.assertProcessAdmission)(projectId);
115
- const child = (0, child_process_1.spawn)(command, {
116
- cwd,
117
- shell: true,
118
- detached: true,
119
- stdio: ['ignore', 'pipe', 'pipe'],
289
+ ensureBackgroundProcessStore();
290
+ if (persistenceError)
291
+ flushBackgroundProcessPersistence();
292
+ const startedAt = Date.now();
293
+ const windowsBootstrap = process.platform === 'win32' ? (0, background_windows_bootstrap_1.spawnWindowsBackgroundBootstrap)(command, cwd) : undefined;
294
+ const child = windowsBootstrap?.child ?? (0, child_process_1.spawn)(command, {
295
+ cwd, shell: true, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
296
+ ...(process.platform === 'win32' ? { env: (0, win_spawn_2.windowsSpawnEnv)(), windowsHide: true } : {}),
120
297
  });
121
298
  if (!child.pid) {
122
- throw new Error('background process failed to start: missing pid');
123
- }
124
- const process = {
125
- pid: child.pid,
126
- command,
127
- cwd,
128
- startedAt: Date.now(),
129
- status: 'running',
130
- chatId,
131
- projectId,
132
- };
133
- const staleFinished = finishedBackgroundByPid.get(child.pid);
134
- if (staleFinished) {
135
- clearTimeout(staleFinished.cleanupTimer);
136
- finishedBackgroundByPid.delete(child.pid);
299
+ // spawn emits error asynchronously even when it returns no pid.
300
+ child.once('error', () => { });
301
+ child.stdout?.destroy();
302
+ child.stderr?.destroy();
303
+ throw new Error('Background process failed to start; check its command and working directory.');
304
+ }
305
+ const previous = backgroundByPid.get(child.pid);
306
+ if (previous) {
307
+ // Defensive against a reused pid in stale registry state: never let old
308
+ // timers, child events or UI requests operate on the replacement.
309
+ finishRecord(previous, 'failed', 'The operating system reused this process ID.');
137
310
  }
138
311
  const record = {
139
- child,
140
- process,
141
- hooks: hooks ?? {},
142
- outputLines: [],
143
- droppedOutputLines: 0,
312
+ child, control: windowsBootstrap?.control ?? (0, background_process_control_1.createBackgroundProcessControl)(child, startedAt), hooks, windowsBootstrap,
313
+ process: { processId: (0, crypto_1.randomUUID)(), pid: child.pid, command, cwd, startedAt, status: windowsBootstrap ? 'starting' : 'running', chatId, projectId, ...metadata },
314
+ stoppingAttempt: 0, stopAttemptActive: false, rootClosed: false, rootExited: false, userStopRequested: false, groupGone: false, exitCode: null, exitSignal: null,
315
+ outputLines: [], outputBySequence: new Map(), nextSequence: 0, droppedOutputLines: 0, clippedOutput: false,
316
+ flushOutput: [], outputWindowStart: Date.now(), outputEventsInWindow: 0,
144
317
  };
145
318
  backgroundByPid.set(child.pid, record);
146
- trackTransientChild(projectId, child);
147
- attachOutput(child, 'stdout', record);
148
- attachOutput(child, 'stderr', record);
149
- child.once('error', () => {
150
- markTerminal(record, process.status === 'killed' ? 'killed' : 'failed', null, null);
151
- backgroundByPid.delete(process.pid);
152
- retainFinishedRecord(record);
319
+ attachOutput(child.stdout, 'stdout', record);
320
+ attachOutput(child.stderr, 'stderr', record);
321
+ child.once('error', error => { reportError(record, error); void probeRecord(record); });
322
+ child.once('exit', (code, signal) => {
323
+ record.rootExited = true;
324
+ record.exitCode = code;
325
+ record.exitSignal = signal;
326
+ void probeRecord(record);
153
327
  });
154
328
  child.once('close', (code, signal) => {
155
- const status = process.status === 'killed' ? 'killed' : code === 0 ? 'exited' : 'failed';
156
- markTerminal(record, status, code, signal);
157
- backgroundByPid.delete(process.pid);
158
- retainFinishedRecord(record);
329
+ record.rootExited = true;
330
+ record.rootClosed = true;
331
+ record.exitCode = code;
332
+ record.exitSignal = signal;
333
+ record.flushOutput.forEach(flush => flush());
334
+ void probeRecord(record);
159
335
  });
160
- record.hooks.onStarted?.(cloneProcess(process));
161
- return cloneProcess(process);
336
+ queuePersistence(record);
337
+ try {
338
+ flushBackgroundProcessPersistence();
339
+ }
340
+ catch (error) {
341
+ terminateBackgroundRecord(record);
342
+ throw error;
343
+ }
344
+ safelyNotify(() => hooks.onStarted?.(cloneProcess(record.process)));
345
+ if (windowsBootstrap) {
346
+ void record.control.ready?.then(() => {
347
+ if (!isCurrent(record) || record.process.status !== 'starting' || record.rootExited) {
348
+ windowsBootstrap.cancel();
349
+ return;
350
+ }
351
+ windowsBootstrap.start();
352
+ record.process.status = 'running';
353
+ queuePersistence(record);
354
+ safelyNotify(() => hooks.onUpdated?.(cloneProcess(record.process)));
355
+ }).catch(error => {
356
+ if (!isCurrent(record))
357
+ return;
358
+ reportError(record, error);
359
+ if (windowsBootstrap.hasLaunched())
360
+ terminateBackgroundRecord(record, true);
361
+ else
362
+ windowsBootstrap.cancel();
363
+ });
364
+ }
365
+ scheduleProbe(record);
366
+ return cloneProcess(record.process);
367
+ }
368
+ function scheduleProbe(record) {
369
+ if (!isCurrent(record) || record.probeTimer)
370
+ return;
371
+ const delay = record.process.status === 'stopping' && process.platform !== 'win32' ? 100 : 1000;
372
+ record.probeTimer = setTimeout(() => { record.probeTimer = undefined; void probeRecord(record); }, delay);
373
+ record.probeTimer.unref?.();
374
+ }
375
+ function probeRecord(record) {
376
+ if (!isCurrent(record))
377
+ return Promise.resolve();
378
+ if (record.probing)
379
+ return record.probing;
380
+ const pending = Promise.resolve().then(async () => {
381
+ try {
382
+ // Before admission the bootstrap has no descendants. Its own child handle
383
+ // can confirm exit even if Windows identity discovery failed or was cancelled.
384
+ if (record.windowsBootstrap && !record.windowsBootstrap.hasLaunched() && record.rootClosed)
385
+ record.groupGone = true;
386
+ if (!record.groupGone)
387
+ record.groupGone = !(await record.control.isAlive());
388
+ if (!isCurrent(record))
389
+ return;
390
+ if (record.groupGone && record.rootClosed) {
391
+ const containmentFailure = record.control.terminalFailure?.();
392
+ finishRecord(record, containmentFailure ? 'failed' : record.userStopRequested ? 'killed' : record.exitCode === 0 ? 'exited' : 'failed', containmentFailure);
393
+ }
394
+ else if (record.rootExited && !record.groupGone && record.process.status !== 'stopping') {
395
+ // A wrapper that exits must not leave an unowned app behind. Clean up
396
+ // its group, retaining the wrapper's original success/failure outcome.
397
+ terminateBackgroundRecord(record, true);
398
+ }
399
+ else if (record.forceKillAt !== undefined && Date.now() - record.forceKillAt >= BACKGROUND_STOP_CONFIRM_MS && !record.groupGone) {
400
+ record.stopAttemptActive = false;
401
+ reportError(record, new Error('The process group has not stopped after the force-stop request. Retry stopping it.'));
402
+ }
403
+ }
404
+ catch (error) {
405
+ record.stopAttemptActive = false;
406
+ reportError(record, error);
407
+ }
408
+ }).finally(() => { if (record.probing === pending)
409
+ record.probing = undefined; scheduleProbe(record); });
410
+ record.probing = pending;
411
+ return pending;
412
+ }
413
+ /** Stop is idempotent while an attempt is progressing. Failed attempts remain
414
+ * visible and may be retried; terminal state requires both OS and child close. */
415
+ function terminateBackgroundRecord(record, automatic = false) {
416
+ record.windowsBootstrap?.cancel();
417
+ if (!automatic)
418
+ record.userStopRequested = true;
419
+ if (!isCurrent(record) || record.stopAttemptActive)
420
+ return;
421
+ record.stopAttemptActive = true;
422
+ const attempt = ++record.stoppingAttempt;
423
+ record.process.status = 'stopping';
424
+ record.process.stopRequestedAt ??= Date.now();
425
+ delete record.process.error;
426
+ record.forceKillAt = undefined;
427
+ if (record.killTimer)
428
+ clearTimeout(record.killTimer);
429
+ notifyUpdated(record);
430
+ const signal = async (value) => {
431
+ if (!isCurrent(record) || record.stoppingAttempt !== attempt || record.groupGone)
432
+ return;
433
+ try {
434
+ await record.control.terminate(value);
435
+ }
436
+ catch (error) {
437
+ if (record.stoppingAttempt === attempt) {
438
+ record.stopAttemptActive = false;
439
+ reportError(record, error);
440
+ }
441
+ }
442
+ await probeRecord(record);
443
+ };
444
+ void signal('SIGTERM');
445
+ record.killTimer = setTimeout(() => {
446
+ record.killTimer = undefined;
447
+ if (!isCurrent(record) || record.stoppingAttempt !== attempt || record.groupGone)
448
+ return;
449
+ record.forceKillAt = Date.now();
450
+ void signal('SIGKILL');
451
+ }, BACKGROUND_KILL_GRACE_MS);
452
+ // Keep the escalation armed even after the shell's close event. Shutdown
453
+ // additionally awaits the drain below before exiting the server process.
454
+ record.killTimer.unref?.();
162
455
  }
163
- function attachOutput(child, source, record) {
164
- const stream = source === 'stdout' ? child.stdout : child.stderr;
165
- const rl = readline_1.default.createInterface({ input: stream });
166
- let windowStart = Date.now();
167
- let emittedInWindow = 0;
168
- rl.on('line', (line) => {
169
- const clipped = line.length > BACKGROUND_OUTPUT_MAX_LINE_CHARS ? `${line.slice(0, BACKGROUND_OUTPUT_MAX_LINE_CHARS)}...` : line;
170
- appendOutputLine(record, source, clipped);
171
- if (!record.hooks.onOutput)
456
+ /** Incremental bounded terminal-text capture. StringDecoder keeps split UTF-8
457
+ * intact; escape state is finite even for unterminated OSC/CSI sequences. */
458
+ function attachOutput(stream, source, record) {
459
+ if (!stream)
460
+ return;
461
+ const decoder = new string_decoder_1.StringDecoder('utf8');
462
+ let text = '', clipped = false, pendingCR = false, ended = false;
463
+ let escape = 'plain';
464
+ let row;
465
+ const publish = (partial) => {
466
+ if (!text && !row && partial)
172
467
  return;
468
+ if (!row || !record.outputBySequence.has(row.sequence)) {
469
+ row = { sequence: ++record.nextSequence, at: Date.now(), source, line: '', partial };
470
+ record.outputLines.push(row);
471
+ record.outputBySequence.set(row.sequence, row);
472
+ if (record.outputLines.length > BACKGROUND_LOG_MAX_LINES) {
473
+ const removed = record.outputLines.shift();
474
+ record.outputBySequence.delete(removed.sequence);
475
+ record.droppedOutputLines += 1;
476
+ }
477
+ }
478
+ row.line = clipped ? `${text.slice(0, BACKGROUND_OUTPUT_MAX_LINE_CHARS - 1).replace(/[\uD800-\uDBFF]$/, '')}…` : text;
479
+ row.partial = partial;
480
+ if (clipped)
481
+ record.clippedOutput = true;
482
+ queuePersistence(record, row);
173
483
  const now = Date.now();
174
- if (now - windowStart >= 1000) {
175
- windowStart = now;
176
- emittedInWindow = 0;
484
+ if (now - record.outputWindowStart >= 1000) {
485
+ record.outputWindowStart = now;
486
+ record.outputEventsInWindow = 0;
487
+ }
488
+ // Live notifications are hints; sequence gaps are recoverable from the
489
+ // bounded authoritative snapshot. Never drop lines from that snapshot.
490
+ if (record.outputEventsInWindow < BACKGROUND_OUTPUT_MAX_LINES_PER_SECOND) {
491
+ record.outputEventsInWindow += 1;
492
+ safelyNotify(() => record.hooks.onOutput?.({ ...cloneLogLine(row), processId: record.process.processId, pid: record.process.pid, chatId: record.process.chatId, projectId: record.process.projectId }));
493
+ }
494
+ };
495
+ const consume = (chunk) => {
496
+ for (const character of chunk) {
497
+ if (escape === 'escape') {
498
+ escape = character === '[' ? 'csi' : [']', 'P', '^', '_'].includes(character) ? 'osc' : 'plain';
499
+ continue;
500
+ }
501
+ if (escape === 'csi') {
502
+ if (character >= '@' && character <= '~')
503
+ escape = 'plain';
504
+ continue;
505
+ }
506
+ if (escape === 'osc') {
507
+ if (character === '\u0007')
508
+ escape = 'plain';
509
+ else if (character === '\u001b')
510
+ escape = 'oscEscape';
511
+ continue;
512
+ }
513
+ if (escape === 'oscEscape') {
514
+ escape = character === '\\' ? 'plain' : 'osc';
515
+ continue;
516
+ }
517
+ if (character === '\u001b') {
518
+ escape = 'escape';
519
+ continue;
520
+ }
521
+ if (character === '\u009b') {
522
+ escape = 'csi';
523
+ continue;
524
+ }
525
+ if (pendingCR) {
526
+ pendingCR = false;
527
+ if (character !== '\n') {
528
+ text = '';
529
+ clipped = false;
530
+ }
531
+ }
532
+ if (character === '\r') {
533
+ pendingCR = true;
534
+ continue;
535
+ }
536
+ if (character === '\n') {
537
+ publish(false);
538
+ text = '';
539
+ clipped = false;
540
+ row = undefined;
541
+ continue;
542
+ }
543
+ if (character === '\b') {
544
+ text = Array.from(text).slice(0, -1).join('');
545
+ continue;
546
+ }
547
+ if ((character < ' ' && character !== '\t') || character === '\u007f' || (character >= '\u0080' && character <= '\u009f'))
548
+ continue;
549
+ if (text.length + character.length <= BACKGROUND_OUTPUT_MAX_LINE_CHARS)
550
+ text += character;
551
+ else
552
+ clipped = true;
177
553
  }
178
- if (emittedInWindow >= BACKGROUND_OUTPUT_MAX_LINES_PER_SECOND)
554
+ publish(true);
555
+ };
556
+ stream.on('data', (chunk) => { if (!ended)
557
+ consume(typeof chunk === 'string' ? chunk : decoder.write(chunk)); });
558
+ const finish = () => {
559
+ if (ended)
179
560
  return;
180
- emittedInWindow += 1;
181
- record.hooks.onOutput?.({
182
- pid: record.process.pid,
183
- chatId: record.process.chatId,
184
- projectId: record.process.projectId,
185
- source,
186
- line: clipped,
187
- });
188
- });
561
+ consume(decoder.end());
562
+ ended = true;
563
+ if (row || text)
564
+ publish(false);
565
+ };
566
+ stream.once('end', finish);
567
+ stream.once('close', finish);
568
+ stream.once('error', error => { reportError(record, error); finish(); });
569
+ record.flushOutput.push(finish);
189
570
  }
190
- function appendOutputLine(record, source, line) {
191
- record.outputLines.push({ at: Date.now(), source, line });
192
- if (record.outputLines.length > BACKGROUND_LOG_MAX_LINES) {
193
- const overflow = record.outputLines.length - BACKGROUND_LOG_MAX_LINES;
194
- record.outputLines.splice(0, overflow);
195
- record.droppedOutputLines += overflow;
571
+ function finishRecord(record, status, error) {
572
+ if (!isCurrent(record))
573
+ return;
574
+ if (record.killTimer)
575
+ clearTimeout(record.killTimer);
576
+ if (record.probeTimer)
577
+ clearTimeout(record.probeTimer);
578
+ record.process.status = status;
579
+ record.process.endedAt = Date.now();
580
+ record.process.exitCode = record.exitCode;
581
+ record.process.signal = record.exitSignal;
582
+ if (error)
583
+ record.process.error = error;
584
+ record.flushOutput.forEach(flush => flush());
585
+ queuePersistence(record);
586
+ try {
587
+ flushBackgroundProcessPersistence();
196
588
  }
197
- }
198
- function retainFinishedRecord(record) {
199
- if (record.retained) {
200
- const existing = finishedBackgroundByPid.get(record.process.pid);
201
- if (existing) {
202
- existing.process = cloneProcess(record.process);
203
- existing.outputLines = record.outputLines.map(cloneLogLine);
204
- existing.droppedOutputLines = record.droppedOutputLines;
205
- }
589
+ catch { /* retain the pending batch and visible persistenceError for a later retry */ }
590
+ backgroundByPid.delete(record.process.pid);
591
+ if (record.skipPersistence) {
592
+ safelyNotify(() => record.hooks.onExited?.(cloneProcess(record.process)));
206
593
  return;
207
594
  }
208
- record.retained = true;
209
- const previous = finishedBackgroundByPid.get(record.process.pid);
210
- if (previous)
211
- clearTimeout(previous.cleanupTimer);
212
- const cleanupTimer = setTimeout(() => {
213
- finishedBackgroundByPid.delete(record.process.pid);
214
- }, exports.BACKGROUND_LOG_RETENTION_MS);
595
+ const processId = record.process.processId;
596
+ const cleanupTimer = setTimeout(() => { finishedBackgroundById.delete(processId); }, exports.BACKGROUND_LOG_RETENTION_MS);
215
597
  cleanupTimer.unref?.();
216
- finishedBackgroundByPid.set(record.process.pid, {
217
- process: cloneProcess(record.process),
218
- outputLines: record.outputLines.map(cloneLogLine),
219
- droppedOutputLines: record.droppedOutputLines,
220
- cleanupTimer,
598
+ finishedBackgroundById.set(processId, {
599
+ process: cloneProcess(record.process), outputLines: record.outputLines.map(cloneLogLine),
600
+ nextSequence: record.nextSequence, droppedOutputLines: record.droppedOutputLines, clippedOutput: record.clippedOutput, cleanupTimer,
221
601
  });
602
+ while (finishedBackgroundById.size > BACKGROUND_FINISHED_MAX) {
603
+ const oldest = finishedBackgroundById.entries().next().value;
604
+ clearTimeout(oldest[1].cleanupTimer);
605
+ finishedBackgroundById.delete(oldest[0]);
606
+ }
607
+ safelyNotify(() => record.hooks.onExited?.(cloneProcess(record.process)));
222
608
  }
223
- function markTerminal(record, status, exitCode, signal) {
224
- if (record.killTimer)
225
- clearTimeout(record.killTimer);
226
- record.process.status = status;
227
- record.process.exitCode = exitCode;
228
- record.process.signal = signal;
229
- if (record.terminalNotified)
230
- return;
231
- record.terminalNotified = true;
232
- record.hooks.onExited?.(cloneProcess(record.process));
609
+ function findRecord(pid, processId) {
610
+ const live = backgroundByPid.get(pid);
611
+ if (live && (!processId || live.process.processId === processId))
612
+ return live;
613
+ if (processId) {
614
+ const finished = finishedBackgroundById.get(processId);
615
+ return finished?.process.pid === pid ? finished : undefined;
616
+ }
617
+ return [...finishedBackgroundById.values()].reverse().find(record => record.process.pid === pid);
233
618
  }
234
- function getBackgroundProcess(pid) {
235
- const record = backgroundByPid.get(pid);
236
- if (record)
237
- return cloneProcess(record.process);
238
- const finished = finishedBackgroundByPid.get(pid);
239
- return finished ? cloneProcess(finished.process) : null;
619
+ function getBackgroundProcess(pid, processId) {
620
+ const record = findRecord(pid, processId);
621
+ return record ? cloneProcess(record.process) : ensureBackgroundProcessStore()?.get(pid, processId) ?? null;
240
622
  }
241
623
  function getBackgroundProcessLogs(pid, filter = {}) {
242
- const live = backgroundByPid.get(pid);
243
- const record = live
244
- ? {
245
- process: live.process,
246
- outputLines: live.outputLines,
247
- droppedOutputLines: live.droppedOutputLines,
248
- }
249
- : finishedBackgroundByPid.get(pid);
624
+ const record = findRecord(pid, filter.processId);
250
625
  if (!record)
626
+ return ensureBackgroundProcessStore()?.logs(pid, filter) ?? null;
627
+ if ((filter.projectId && record.process.projectId !== filter.projectId) || (filter.chatId && record.process.chatId !== filter.chatId))
251
628
  return null;
252
- if (filter.projectId && record.process.projectId !== filter.projectId)
253
- return null;
254
- if (filter.chatId && record.process.chatId !== filter.chatId)
255
- return null;
256
- const limit = typeof filter.limit === 'number' && Number.isFinite(filter.limit)
257
- ? Math.max(1, Math.min(BACKGROUND_LOG_MAX_LINES, Math.floor(filter.limit)))
258
- : BACKGROUND_LOG_MAX_LINES;
259
- const start = Math.max(0, record.outputLines.length - limit);
260
- const lines = record.outputLines.slice(start).map(cloneLogLine);
261
- const droppedByLimit = record.outputLines.length - lines.length;
262
- const droppedLines = record.droppedOutputLines + droppedByLimit;
629
+ const maxLines = backgroundStore ? background_process_store_1.BACKGROUND_HISTORY_MAX_LINES : BACKGROUND_LOG_MAX_LINES;
630
+ const limit = typeof filter.limit === 'number' && Number.isFinite(filter.limit) ? Math.max(1, Math.min(maxLines, Math.floor(filter.limit))) : BACKGROUND_LOG_MAX_LINES;
631
+ let durable = null;
632
+ try {
633
+ durable = ensureBackgroundProcessStore()?.logs(pid, { ...filter, processId: record.process.processId, limit }) ?? null;
634
+ }
635
+ catch (error) {
636
+ record.process.persistenceError = `Background history could not be read: ${error instanceof Error ? error.message : String(error)}`;
637
+ }
638
+ const combined = new Map((durable?.lines ?? []).map(line => [line.sequence, line]));
639
+ for (const line of record.outputLines)
640
+ combined.set(line.sequence, line);
641
+ for (const line of pendingPersistence.get(record.process.processId)?.lines.values() ?? [])
642
+ combined.set(line.sequence, line);
643
+ const lines = [...combined.values()].sort((a, b) => a.sequence - b.sequence).slice(-limit).map(cloneLogLine);
644
+ const nextSequence = Math.max(record.nextSequence, durable?.nextSequence ?? 0);
645
+ const droppedLines = Math.max(0, nextSequence - lines.length);
263
646
  return {
264
- process: cloneProcess(record.process),
265
- lines,
266
- truncated: droppedLines > 0,
267
- droppedLines,
268
- maxLines: BACKGROUND_LOG_MAX_LINES,
269
- maxLineChars: BACKGROUND_OUTPUT_MAX_LINE_CHARS,
270
- retentionMs: exports.BACKGROUND_LOG_RETENTION_MS,
647
+ process: cloneProcess(record.process), lines, nextSequence,
648
+ truncated: droppedLines > 0 || record.clippedOutput || (durable?.truncated ?? false), droppedLines,
649
+ maxLines, maxLineChars: BACKGROUND_OUTPUT_MAX_LINE_CHARS, retentionMs: backgroundStore ? background_process_store_1.BACKGROUND_HISTORY_RETENTION_MS : exports.BACKGROUND_LOG_RETENTION_MS,
271
650
  };
272
651
  }
273
652
  function listBackgroundProcesses(filter = {}) {
274
- const processes = [];
275
- for (const record of backgroundByPid.values()) {
276
- const process = record.process;
277
- if (filter.projectId && process.projectId !== filter.projectId)
278
- continue;
279
- if (filter.chatId && process.chatId !== filter.chatId)
280
- continue;
281
- if (backgroundTerminal.has(process.status))
282
- continue;
283
- processes.push(cloneProcess(process));
284
- }
285
- return processes;
653
+ const records = [...backgroundByPid.values(), ...(filter.includeFinished ? finishedBackgroundById.values() : [])];
654
+ const combined = new Map((ensureBackgroundProcessStore()?.list(filter) ?? []).map(process => [process.processId, process]));
655
+ for (const record of records)
656
+ if ((!filter.projectId || record.process.projectId === filter.projectId) && (!filter.chatId || record.process.chatId === filter.chatId))
657
+ combined.set(record.process.processId, cloneProcess(record.process));
658
+ return [...combined.values()].sort((a, b) => a.startedAt - b.startedAt);
286
659
  }
287
- function killBackgroundProcess(pid) {
660
+ function killBackgroundProcess(pid, processId) {
288
661
  const record = backgroundByPid.get(pid);
289
- if (!record)
290
- return;
291
- record.process.status = 'killed';
292
- terminateBackgroundRecord(record);
662
+ if (record && (!processId || processId === record.process.processId))
663
+ terminateBackgroundRecord(record);
293
664
  }
294
665
  function killOwnedBackgroundProcess(pid, owner) {
295
- const record = backgroundByPid.get(pid);
666
+ const record = findRecord(pid, owner.processId);
296
667
  if (!record)
668
+ return false; // historical rows never regain operating-system control
669
+ if (!record || record.process.projectId !== owner.projectId || record.process.chatId !== owner.chatId)
297
670
  return false;
298
- if (record.process.projectId !== owner.projectId)
299
- return false;
300
- if (record.process.chatId !== owner.chatId)
301
- return false;
302
- killBackgroundProcess(pid);
671
+ if (!backgroundTerminal.has(record.process.status))
672
+ killBackgroundProcess(pid, record.process.processId);
303
673
  return true;
304
674
  }
305
- function terminateBackgroundRecord(record) {
306
- if (record.killTimer)
307
- clearTimeout(record.killTimer);
308
- record.killTimer = treeKillWithEscalation(record.process.pid);
309
- }
310
675
  function killBackgroundProcessesForChat(chatId, projectId) {
311
- let killed = 0;
676
+ let count = 0;
312
677
  for (const record of backgroundByPid.values()) {
313
- if (record.process.chatId !== chatId)
314
- continue;
315
- if (projectId && record.process.projectId !== projectId)
678
+ if (record.process.chatId !== chatId || (projectId && record.process.projectId !== projectId))
316
679
  continue;
317
- record.process.status = 'killed';
318
- markTerminal(record, 'killed', null, 'SIGTERM');
319
680
  terminateBackgroundRecord(record);
320
- killed += 1;
681
+ count += 1;
682
+ }
683
+ return count;
684
+ }
685
+ /** Shutdown requests stop for all owned apps and keeps the event loop alive
686
+ * through escalation. Returns unresolved processes rather than claiming exit. */
687
+ async function awaitBackgroundProcessesStopped(timeoutMs = 6000) {
688
+ for (const record of backgroundByPid.values())
689
+ terminateBackgroundRecord(record);
690
+ const deadline = Date.now() + Math.max(0, timeoutMs);
691
+ while (backgroundByPid.size > 0 && Date.now() < deadline) {
692
+ let deadlineTimer;
693
+ try {
694
+ await Promise.race([
695
+ Promise.all([...backgroundByPid.values()].map(probeRecord)),
696
+ new Promise(resolve => { deadlineTimer = setTimeout(resolve, Math.max(0, deadline - Date.now())); }),
697
+ ]);
698
+ }
699
+ finally {
700
+ if (deadlineTimer)
701
+ clearTimeout(deadlineTimer);
702
+ }
703
+ if (backgroundByPid.size === 0 || Date.now() >= deadline)
704
+ break;
705
+ await new Promise(resolve => setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))));
706
+ }
707
+ try {
708
+ flushBackgroundProcessPersistence();
321
709
  }
322
- return killed;
710
+ catch { /* storage failure must not prevent control/shutdown; close reports it to the app */ }
711
+ return [...backgroundByPid.values()].map(record => cloneProcess(record.process));
323
712
  }