specrails-desktop 2.61.0 → 2.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/cli/dist/specrails-desktop.js +39 -5
  2. package/client/dist/assets/ActivityFeedPage-Bn2MpG5M.js +1 -0
  3. package/client/dist/assets/{AgentBrowserCapture-CkNWDZT3.js → AgentBrowserCapture-CHlXWdXq.js} +1 -1
  4. package/client/dist/assets/AgentModeAnalyticsPane-yj-YCgrK.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-Xczcyghb.js +2 -0
  6. package/client/dist/assets/{AgentModeJobsPane-U1J6PaBo.js → AgentModeJobsPane-DtOW01ln.js} +1 -1
  7. package/client/dist/assets/AgentsPage-BMfJ4mzU.js +118 -0
  8. package/client/dist/assets/AnalyticsPage-CGn5cgtc.js +1 -0
  9. package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
  10. package/client/dist/assets/CodePage-C8NIginr.js +3 -0
  11. package/client/dist/assets/DesktopAnalyticsPage-C87r4y26.js +1 -0
  12. package/client/dist/assets/{DocsDialog-CSlMPemn.js → DocsDialog-Z50a_KPv.js} +1 -1
  13. package/client/dist/assets/{DocsPage-BKlYAvWq.js → DocsPage-DIArbEPl.js} +1 -1
  14. package/client/dist/assets/{ExportDropdown-CweP_1GP.js → ExportDropdown-CJdCqYd7.js} +1 -1
  15. package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
  16. package/client/dist/assets/InteractiveJobComposer-BxLh4me-.js +19 -0
  17. package/client/dist/assets/{JobDetailModal-DYyGq1Vp.js → JobDetailModal-C7dWoRu6.js} +1 -1
  18. package/client/dist/assets/JobDetailPage-Ilu294WS.js +1 -0
  19. package/client/dist/assets/JobsPage-fPq1hefb.js +1 -0
  20. package/client/dist/assets/LoopBuilderPage-5M7IypEP.js +1 -0
  21. package/client/dist/assets/{LoopPreviewModal-DkLRfsYX.js → LoopPreviewModal-ChI3NQC7.js} +1 -1
  22. package/client/dist/assets/{LoopWindowSurface-CuACkXFV.js → LoopWindowSurface-D1WB9i-9.js} +1 -1
  23. package/client/dist/assets/LoopsPage-CCwc0Biu.js +2 -0
  24. package/client/dist/assets/MinimizedChatsContext-eH_KaAZ1.js +1 -0
  25. package/client/dist/assets/PluginsPage-DfDfEfbW.js +2 -0
  26. package/client/dist/assets/ProjectSettingsDialog-9NHnG8JQ.js +1 -0
  27. package/client/dist/assets/RepositoryDeliveries-DgzNnJmn.js +5 -0
  28. package/client/dist/assets/{RepositoryScopeSelector-CeCr6Jz-.js → RepositoryScopeSelector-BSgUqk_u.js} +1 -1
  29. package/client/dist/assets/ReviewPacketPage-D34bGpSA.js +4 -0
  30. package/client/dist/assets/{TemplatePreviewModal-B8yd1AGU.js → TemplatePreviewModal-CcfSxWQ7.js} +1 -1
  31. package/client/dist/assets/TicketDetailModalContext-BoafEcm4.js +16 -0
  32. package/client/dist/assets/Trans-CVSryAL1.js +1 -0
  33. package/client/dist/assets/agent-5bSixVzz.js +1 -0
  34. package/client/dist/assets/agent-BR4EHfhq.js +1 -0
  35. package/client/dist/assets/agent-C0IKkJP8.js +1 -0
  36. package/client/dist/assets/agent-C7li9ayC.js +1 -0
  37. package/client/dist/assets/agent-D0dRmEen.js +1 -0
  38. package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
  39. package/client/dist/assets/agent-Dip4h2YE.js +1 -0
  40. package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
  41. package/client/dist/assets/ban-wqfdzoXt.js +1 -0
  42. package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
  43. package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
  44. package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
  45. package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
  46. package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
  47. package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
  48. package/client/dist/assets/download-C9wP_42h.js +1 -0
  49. package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
  50. package/client/dist/assets/file-text-C0blZHLh.js +1 -0
  51. package/client/dist/assets/{formatDistanceToNow-DZ3VWsHq.js → formatDistanceToNow-NVM6rK3i.js} +1 -1
  52. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-9JiGlFrx.js → getTimezoneOffsetInMilliseconds-ZHg7WpdH.js} +1 -1
  53. package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
  54. package/client/dist/assets/index-Culan6y7.js +83 -0
  55. package/client/dist/assets/index-D1v33giW.css +2 -0
  56. package/client/dist/assets/jira-api-Bl8xEIpk.js +1 -0
  57. package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
  58. package/client/dist/assets/layers-BCLTgHXO.js +1 -0
  59. package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
  60. package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
  61. package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
  62. package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
  63. package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
  64. package/client/dist/assets/plus-fHDdjBk1.js +1 -0
  65. package/client/dist/assets/{project-repositories-CadLZOnr.js → project-repositories-CvJf1a6H.js} +1 -1
  66. package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
  67. package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
  68. package/client/dist/assets/save-LmPSfsvZ.js +1 -0
  69. package/client/dist/assets/search-BDVp3fkH.js +1 -0
  70. package/client/dist/assets/{settings-Dn-pXukr.js → settings-BO2N-oPf.js} +1 -1
  71. package/client/dist/assets/{settings-ByVE7jdZ.js → settings-BQChAs_w.js} +1 -1
  72. package/client/dist/assets/{settings-BBj_kZaD.js → settings-BzmYlcQB.js} +1 -1
  73. package/client/dist/assets/{settings-COlH_P33.js → settings-CGjXb6TX.js} +1 -1
  74. package/client/dist/assets/{settings-D77mC_Lb.js → settings-CMxEWY46.js} +1 -1
  75. package/client/dist/assets/{settings-DZKy7MqN.js → settings-C_dRSgI2.js} +1 -1
  76. package/client/dist/assets/{settings-B5Hy_Kol.js → settings-Hw7av-B1.js} +1 -1
  77. package/client/dist/assets/settings-nryN6wuS.js +1 -0
  78. package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
  79. package/client/dist/assets/spending-BfjSJjBd.js +1 -0
  80. package/client/dist/assets/square-D0JTbRSR.js +1 -0
  81. package/client/dist/assets/trash-D8zYUmTg.js +1 -0
  82. package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
  83. package/client/dist/assets/upload-CCHvUkRs.js +1 -0
  84. package/client/dist/assets/{useDesktop-DKNbOF0u.js → useDesktop-C5JVBeur.js} +1 -1
  85. package/client/dist/assets/{useRuntimeRuns-D3YMw9qx.js → useRuntimeRuns-DohBmeg8.js} +1 -1
  86. package/client/dist/assets/useSharedWebSocket-CNOvmgTr.js +2 -0
  87. package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
  88. package/client/dist/assets/x-Bu7X_eR2.js +1 -0
  89. package/client/dist/assets/zap-bugHyWzQ.js +1 -0
  90. package/client/dist/index.html +44 -41
  91. package/docs/ci-cd.md +8 -1
  92. package/docs/customizing.md +1 -0
  93. package/docs/getting-started.md +1 -1
  94. package/docs/guide/de/integrations/6-agent-chat.md +11 -0
  95. package/docs/guide/de/settings/5-project-environment.md +27 -0
  96. package/docs/guide/en/integrations/6-agent-chat.md +11 -0
  97. package/docs/guide/en/settings/5-project-environment.md +27 -0
  98. package/docs/guide/es/integrations/6-agent-chat.md +11 -0
  99. package/docs/guide/es/settings/5-project-environment.md +27 -0
  100. package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
  101. package/docs/guide/fr/settings/5-project-environment.md +27 -0
  102. package/docs/guide/it/integrations/6-agent-chat.md +11 -0
  103. package/docs/guide/it/settings/5-project-environment.md +27 -0
  104. package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
  105. package/docs/guide/ja/settings/5-project-environment.md +27 -0
  106. package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
  107. package/docs/guide/pt/settings/5-project-environment.md +27 -0
  108. package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
  109. package/docs/guide/zh/settings/5-project-environment.md +27 -0
  110. package/docs/internals/README.md +1 -0
  111. package/docs/internals/agent-sessions.md +157 -0
  112. package/docs/internals/api-reference.md +35 -4
  113. package/docs/internals/configuration.md +80 -5
  114. package/docs/internals/core-runtime-updates.md +10 -3
  115. package/docs/internals/operations-runbook.md +11 -0
  116. package/docs/internals/programmatic-agent-runtime.md +1 -1
  117. package/docs/internals/safe-pr-review-flow.md +15 -0
  118. package/docs/internals/source-map.md +42 -1
  119. package/docs/mcp.md +1 -1
  120. package/docs/platforms/macos.md +2 -0
  121. package/docs/platforms/windows-parity.md +4 -4
  122. package/docs/platforms/windows.md +9 -9
  123. package/local-runner/dist/specrails-local-runner.js +1 -1
  124. package/mcp-bridge/dist/specrails-mcp.js +82 -15
  125. package/package.json +8 -9
  126. package/server/dist/agent-mcp-config.js +41 -0
  127. package/server/dist/core-compat.js +2 -0
  128. package/server/dist/desktop-db.js +101 -4
  129. package/server/dist/desktop-router.js +31 -2
  130. package/server/dist/index.js +58 -0
  131. package/server/dist/mcp/tools/app-settings.js +11 -0
  132. package/server/dist/mcp/tools/env.js +38 -3
  133. package/server/dist/mcp/tools/mission.js +45 -2
  134. package/server/dist/mcp/tools/specs.js +2 -2
  135. package/server/dist/modules/accounting/runtime/result-event.js +9 -1
  136. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
  137. package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
  138. package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
  139. package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
  140. package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
  141. package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
  142. package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
  143. package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
  144. package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
  145. package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
  146. package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
  147. package/server/dist/modules/agent-sessions/index.js +31 -0
  148. package/server/dist/modules/agent-sessions/ports.js +2 -0
  149. package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
  150. package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
  151. package/server/dist/modules/agents/runtime/agent-store.js +3 -1
  152. package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
  153. package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
  154. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +15 -4
  155. package/server/dist/modules/loops/runtime/loop-run-manager.js +11 -0
  156. package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
  157. package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
  158. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
  159. package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
  160. package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
  161. package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
  162. package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
  163. package/server/dist/modules/project-settings/adapters/http.js +38 -2
  164. package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
  165. package/server/dist/modules/project-settings/domain.js +24 -0
  166. package/server/dist/modules/project-settings/index.js +2 -1
  167. package/server/dist/modules/specs/runtime/spec-models.js +1 -1
  168. package/server/dist/path-resolver.js +135 -20
  169. package/server/dist/plugins/codex-spawn.js +15 -7
  170. package/server/dist/project-env.js +265 -17
  171. package/server/dist/project-registry.js +46 -0
  172. package/server/dist/project-router-settings.js +24 -1
  173. package/server/dist/providers/claude-adapter.js +6 -1
  174. package/server/dist/worktree-node-modules.js +95 -1
  175. package/client/dist/assets/ActivityFeedPage-Wi5XNgoG.js +0 -1
  176. package/client/dist/assets/AgentModeAnalyticsPane-CXz2j-Bt.js +0 -2
  177. package/client/dist/assets/AgentModeCodePane-DnsBPtpS.js +0 -2
  178. package/client/dist/assets/AgentsPage-CXrqu5vo.js +0 -118
  179. package/client/dist/assets/AnalyticsPage-Cvtt-LOO.js +0 -1
  180. package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
  181. package/client/dist/assets/CodePage-D35o-MFO.js +0 -3
  182. package/client/dist/assets/DesktopAnalyticsPage-CsDFy5rp.js +0 -1
  183. package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
  184. package/client/dist/assets/InteractiveJobComposer-CFKvbQeJ.js +0 -19
  185. package/client/dist/assets/JobDetailPage-C6gpdYvj.js +0 -1
  186. package/client/dist/assets/JobsPage-CgS0pnKd.js +0 -1
  187. package/client/dist/assets/LoopBuilderPage-jUp20DSZ.js +0 -1
  188. package/client/dist/assets/LoopsPage-ClSrWzDd.js +0 -2
  189. package/client/dist/assets/MinimizedChatsContext-CroAs6Kh.js +0 -1
  190. package/client/dist/assets/PluginsPage-nCKQOwRL.js +0 -2
  191. package/client/dist/assets/ProjectSettingsDialog-BVDYwllU.js +0 -1
  192. package/client/dist/assets/RepositoryDeliveries-DJCm1pZr.js +0 -5
  193. package/client/dist/assets/ReviewPacketPage-kTErrEjx.js +0 -4
  194. package/client/dist/assets/TicketDetailModalContext-YBJX3RmP.js +0 -16
  195. package/client/dist/assets/Trans-D5eOjfvG.js +0 -1
  196. package/client/dist/assets/agent--8QAfoXL.js +0 -1
  197. package/client/dist/assets/agent-3HxXkIWX.js +0 -1
  198. package/client/dist/assets/agent-CpFCz32T.js +0 -1
  199. package/client/dist/assets/agent-DVnw6ybO.js +0 -1
  200. package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
  201. package/client/dist/assets/agent-hv57-zyG.js +0 -1
  202. package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
  203. package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
  204. package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
  205. package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
  206. package/client/dist/assets/download-CFHyyHcw.js +0 -1
  207. package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
  208. package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
  209. package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
  210. package/client/dist/assets/index-Bfunvi6w.css +0 -2
  211. package/client/dist/assets/index-lTGJJTuk.js +0 -83
  212. package/client/dist/assets/jira-api-Bp0zXwjg.js +0 -1
  213. package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
  214. package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
  215. package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
  216. package/client/dist/assets/plus-CsnjfkmX.js +0 -1
  217. package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
  218. package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
  219. package/client/dist/assets/save-OsUqVn99.js +0 -1
  220. package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
  221. package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
  222. package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
  223. package/client/dist/assets/spending-DQDNtM8q.js +0 -1
  224. package/client/dist/assets/square-Ci-PFcFe.js +0 -1
  225. package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
  226. package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
  227. package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
  228. package/client/dist/assets/useSharedWebSocket-DaWuiO0S.js +0 -2
  229. package/client/dist/assets/x-CBMI9ejn.js +0 -1
  230. package/client/dist/assets/zap-CUOlvtT1.js +0 -1
@@ -15,14 +15,14 @@ Installers are published on every release under:
15
15
 
16
16
  > 📥 `https://specrails.dev/downloads/specrails-desktop/latest/`
17
17
 
18
- Pick the pair that matches your CPU architecture:
18
+ Pick the installer that matches your CPU architecture:
19
19
 
20
- | Architecture | NSIS installer (recommended) | MSI installer (enterprise/group-policy) |
21
- | --- | --- | --- |
22
- | x64 | `specrails-desktop-<version>-x64-setup.exe` | `specrails-desktop-<version>-x64.msi` |
23
- | ARM64 | `specrails-desktop-<version>-arm64-setup.exe` | `specrails-desktop-<version>-arm64.msi` |
20
+ | Architecture | Installer |
21
+ | --- | --- |
22
+ | x64 | `specrails-desktop-<version>-x64-setup.exe` |
23
+ | ARM64 | `specrails-desktop-<version>-arm64-setup.exe` |
24
24
 
25
- If you are unsure which to use, the NSIS `-setup.exe` is the right choice for individual installs; the MSI exists for enterprise deployment. Versioned copies live at `downloads/specrails-desktop/v<version>/` for archival and deep-linking.
25
+ Windows ships only the NSIS installer. For unattended or managed deployment, run it silently with `/S` (optionally `/D=<install dir>` as the last argument). Releases up to 2.62 also published an `.msi`; see [Updates](#updates) for how those installations migrate. The installer provisions Microsoft Edge WebView2 when it is missing, downloading it through a small embedded bootstrapper (Windows 11 and current Windows 10 already include it). Versioned copies live at `downloads/specrails-desktop/v<version>/` for archival and deep-linking.
26
26
 
27
27
  A machine-readable `manifest.json` in `latest/` describes the current release (version, sha256, size) per platform, including `windows-x64` and `windows-arm64` entries — consumers can read it to build download links without hardcoding a version.
28
28
 
@@ -59,7 +59,7 @@ When the bundle is present, the Tauri host sets `SPECRAILS_IS_DESKTOP=1` and `SP
59
59
 
60
60
  The desktop app self-updates via the Tauri updater plugin. It checks a GitHub Releases `latest.json` endpoint and, on Windows, applies updates with `installMode: "passive"` — the update runs with a minimal progress UI and the app relaunches into the new version.
61
61
 
62
- Updates preserve the installation format: NSIS installs receive a signed `-setup.exe` updater artifact, and MSI installs receive a signed `.msi` artifact for the same architecture. The signatures are Tauri/minisign integrity signatures, separate from Authenticode signing. An incomplete installer/signature pair blocks publication; the release no longer silently substitutes MSI for NSIS.
62
+ Updates use the signed NSIS `-setup.exe` for the same architecture. Installations made from an earlier `.msi` receive that NSIS update too: the installer detects the MSI installation and removes it before installing, so a single installation remains (Windows may ask for administrator approval to remove the machine-wide MSI). The signatures are Tauri/minisign integrity signatures, separate from Authenticode signing. An incomplete installer/signature pair blocks publication.
63
63
 
64
64
  Normal quit and updates request an authenticated graceful shutdown of the owned sidecar first, giving processes and persistent logs time to close before a bounded force-stop fallback.
65
65
 
@@ -73,8 +73,8 @@ Reserved paths (`.specrails/profiles/**`, `.claude/agents/custom-*.md`) are pres
73
73
 
74
74
  ## Uninstall
75
75
 
76
- - NSIS: use the **Start Menu → Specrails → Uninstall** entry, or *Settings → Apps*.
77
- - MSI: use *Settings → Apps* or `msiexec /x <msi-path>`.
76
+ - Use the **Start Menu → Specrails → Uninstall** entry, or *Settings → Apps*.
77
+ - An installation from an earlier `.msi` release that has not updated yet: *Settings → Apps* or `msiexec /x <msi-path>`.
78
78
 
79
79
  ## Known limitations
80
80
 
@@ -4036,7 +4036,7 @@ var require_core = __commonJS({
4036
4036
  constructor(opts = {}) {
4037
4037
  this.schemas = {};
4038
4038
  this.refs = {};
4039
- this.formats = {};
4039
+ this.formats = /* @__PURE__ */ Object.create(null);
4040
4040
  this._compilations = /* @__PURE__ */ new Set();
4041
4041
  this._loading = {};
4042
4042
  this._cache = /* @__PURE__ */ new Map();
@@ -7051,7 +7051,7 @@ function connectBridge(clientFacing, appFacing) {
7051
7051
  });
7052
7052
  }
7053
7053
  pending.clear();
7054
- closeBoth();
7054
+ if (!appFacing.recoverable) closeBoth();
7055
7055
  };
7056
7056
  }
7057
7057
 
@@ -7062,7 +7062,7 @@ function authenticatedFetch(agentHeaders, fetchImpl = fetch) {
7062
7062
  const token = readMcpToken();
7063
7063
  const headers = new Headers(init?.headers);
7064
7064
  if (token) headers.set("Authorization", `Bearer ${token}`);
7065
- for (const [key, value] of Object.entries(agentHeaders)) headers.set(key, value);
7065
+ for (const [key, value] of Object.entries(typeof agentHeaders === "function" ? agentHeaders() : agentHeaders)) headers.set(key, value);
7066
7066
  const response = await fetchImpl(input, { ...init, headers });
7067
7067
  const refreshed = response.status === 401 ? readMcpToken() : null;
7068
7068
  if (refreshed && refreshed !== token) {
@@ -7073,12 +7073,25 @@ function authenticatedFetch(agentHeaders, fetchImpl = fetch) {
7073
7073
  return response;
7074
7074
  };
7075
7075
  }
7076
+ function rejectsStaleSession(err) {
7077
+ const failure = err;
7078
+ if (failure?.code === 404) return true;
7079
+ return failure?.code === 403 && typeof failure.message === "string" && /reinitiali[sz]e/i.test(failure.message);
7080
+ }
7081
+ function isLostStream(error2) {
7082
+ return /SSE stream disconnected|Maximum reconnection attempts|Failed to reconnect/i.test(error2.message);
7083
+ }
7076
7084
  var RecoveringHttpTransport = class {
7077
- constructor(create) {
7085
+ constructor(create, log = () => {
7086
+ }) {
7078
7087
  this.create = create;
7088
+ this.log = log;
7079
7089
  this.current = this.attach(create());
7080
7090
  }
7081
7091
  create;
7092
+ log;
7093
+ /** connectBridge keeps the client side open when the app side can recover. */
7094
+ recoverable = true;
7082
7095
  onmessage;
7083
7096
  onerror;
7084
7097
  onclose;
@@ -7086,12 +7099,19 @@ var RecoveringHttpTransport = class {
7086
7099
  initializeMessage;
7087
7100
  recovery;
7088
7101
  closed = false;
7102
+ /** The last recovery failed; the next request must reinitialize first. */
7103
+ stale = false;
7089
7104
  attach(transport) {
7090
7105
  transport.onmessage = (message, extra) => {
7091
7106
  if (transport === this.current && !this.closed) this.onmessage?.(message, extra);
7092
7107
  };
7093
7108
  transport.onerror = (error2) => {
7094
- if (transport === this.current && !this.closed) this.onerror?.(error2);
7109
+ if (transport !== this.current || this.closed) return;
7110
+ if (isLostStream(error2)) {
7111
+ this.stale = true;
7112
+ this.log("stream-lost", error2.message);
7113
+ }
7114
+ this.onerror?.(error2);
7095
7115
  };
7096
7116
  transport.onclose = () => {
7097
7117
  if (transport === this.current && !this.closed) this.onclose?.();
@@ -7104,21 +7124,31 @@ var RecoveringHttpTransport = class {
7104
7124
  async send(message) {
7105
7125
  if (this.closed) throw new Error("MCP bridge is closed");
7106
7126
  if ("method" in message && message.method === "initialize") this.initializeMessage = message;
7127
+ if (this.stale && this.initializeMessage && message !== this.initializeMessage) this.recovery ??= this.recover("retry after a failed recovery");
7107
7128
  if (this.recovery) await this.recovery;
7108
7129
  const attempted = this.current;
7109
7130
  try {
7110
7131
  await attempted.send(message);
7111
7132
  } catch (err) {
7112
- if (err?.code !== 404 || !this.initializeMessage || message === this.initializeMessage) throw err;
7113
- if (attempted === this.current) {
7114
- this.recovery ??= this.reinitialize().finally(() => {
7115
- this.recovery = void 0;
7116
- });
7117
- }
7133
+ if (!rejectsStaleSession(err) || !this.initializeMessage || message === this.initializeMessage) throw err;
7134
+ if (attempted === this.current) this.recovery ??= this.recover(`session rejected (${err.code})`);
7118
7135
  if (this.recovery) await this.recovery;
7119
7136
  await this.current.send(message);
7120
7137
  }
7121
7138
  }
7139
+ recover(reason) {
7140
+ this.log("recover", reason);
7141
+ return this.reinitialize().then(() => {
7142
+ this.stale = false;
7143
+ this.log("recovered");
7144
+ }).catch((error2) => {
7145
+ this.stale = true;
7146
+ this.log("recover-failed", error2 instanceof Error ? error2.message : String(error2));
7147
+ throw error2;
7148
+ }).finally(() => {
7149
+ this.recovery = void 0;
7150
+ });
7151
+ }
7122
7152
  async reinitialize() {
7123
7153
  const previous = this.current;
7124
7154
  const next = this.attach(this.create());
@@ -7141,9 +7171,6 @@ var RecoveringHttpTransport = class {
7141
7171
  void next.send({ ...this.initializeMessage, id }).catch(reject);
7142
7172
  });
7143
7173
  await next.send({ jsonrpc: "2.0", method: "notifications/initialized" });
7144
- } catch (err) {
7145
- await this.close();
7146
- throw err;
7147
7174
  } finally {
7148
7175
  if (timer) clearTimeout(timer);
7149
7176
  next.onmessage = relay;
@@ -7171,15 +7198,55 @@ var RecoveringHttpTransport = class {
7171
7198
  };
7172
7199
 
7173
7200
  // mcp-bridge/src/index.ts
7201
+ var import_node_fs = require("node:fs");
7202
+ var import_node_path = require("node:path");
7203
+ var LOG_LIMIT_BYTES = 256 * 1024;
7204
+ function lifecycleLog() {
7205
+ const capabilityFile = process.env.SPECRAILS_AGENT_CAPABILITY_FILE?.trim();
7206
+ if (!capabilityFile) return () => {
7207
+ };
7208
+ const file = (0, import_node_path.join)((0, import_node_path.dirname)(capabilityFile), "bridge.log");
7209
+ return (event, detail) => {
7210
+ try {
7211
+ try {
7212
+ if ((0, import_node_fs.statSync)(file).size > LOG_LIMIT_BYTES) (0, import_node_fs.truncateSync)(file, 0);
7213
+ } catch {
7214
+ }
7215
+ (0, import_node_fs.appendFileSync)(file, `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${event}${detail ? ` ${detail.replace(/\s+/g, " ").slice(0, 500)}` : ""}
7216
+ `, { mode: 384 });
7217
+ } catch {
7218
+ }
7219
+ };
7220
+ }
7174
7221
  async function main() {
7175
- const fetchWithCredentials = authenticatedFetch(agentForwardHeaders());
7176
- const appFacing = new RecoveringHttpTransport(() => new StreamableHTTPClientTransport(appUrl(), { fetch: fetchWithCredentials }));
7222
+ const log = lifecycleLog();
7223
+ log("start", `ppid=${process.ppid}`);
7224
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) process.once(signal, () => {
7225
+ log("signal", signal);
7226
+ process.exit(0);
7227
+ });
7228
+ process.on("exit", (code) => log("exit", `code=${code}`));
7229
+ process.stdin.once("end", () => log("stdin-end", "the client closed its side"));
7230
+ agentForwardHeaders();
7231
+ const fetchWithCredentials = authenticatedFetch(() => agentForwardHeaders());
7232
+ const appFacing = new RecoveringHttpTransport(() => new StreamableHTTPClientTransport(appUrl(), { fetch: fetchWithCredentials }), log);
7177
7233
  const clientFacing = new StdioServerTransport();
7178
7234
  connectBridge(clientFacing, appFacing);
7235
+ const relayError = appFacing.onerror;
7236
+ appFacing.onerror = (error2) => {
7237
+ log("app-error", error2.message);
7238
+ relayError?.(error2);
7239
+ };
7240
+ const relayClose = appFacing.onclose;
7241
+ appFacing.onclose = () => {
7242
+ log("close", "app side closed");
7243
+ relayClose?.();
7244
+ };
7179
7245
  await appFacing.start();
7180
7246
  await clientFacing.start();
7181
7247
  }
7182
7248
  main().catch((err) => {
7249
+ lifecycleLog()("fatal", err instanceof Error ? err.message : String(err));
7183
7250
  process.stderr.write(`[specrails-mcp] fatal: ${err instanceof Error ? err.message : String(err)}
7184
7251
  `);
7185
7252
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.61.0",
3
+ "version": "2.63.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,7 +56,7 @@
56
56
  "dependencies": {
57
57
  "@modelcontextprotocol/sdk": "^1.29.0",
58
58
  "@tailwindcss/typography": "^0.5.19",
59
- "ajv": "^8.17.1",
59
+ "ajv": "^8.20.0",
60
60
  "better-sqlite3": "^12.8.0",
61
61
  "chokidar": "^5.0.0",
62
62
  "cross-spawn": "^7.0.6",
@@ -71,29 +71,28 @@
71
71
  "selfsigned": "^5.5.0",
72
72
  "tree-kill": "^1.2.2",
73
73
  "werift": "^0.23.0",
74
- "ws": "^8.21.0",
74
+ "ws": "^8.22.0",
75
75
  "zod": "^3.25.76"
76
76
  },
77
77
  "devDependencies": {
78
+ "@babel/parser": "^7.29.9",
78
79
  "@tauri-apps/cli": "^2.0.0",
79
80
  "@types/better-sqlite3": "^7.6.0",
80
- "@types/chokidar": "^1.7.5",
81
81
  "@types/cross-spawn": "^6.0.6",
82
- "@types/diff": "^7.0.2",
83
82
  "@types/express": "^5.0.6",
84
83
  "@types/js-yaml": "^4.0.9",
85
84
  "@types/multer": "^2.1.0",
86
- "@types/node": "^25.5.0",
85
+ "@types/node": "^26.6.5",
87
86
  "@types/pdf-parse": "^1.1.5",
88
87
  "@types/supertest": "^7.2.0",
89
88
  "@types/ws": "^8.5.0",
90
- "@vitest/coverage-v8": "^4.1.10",
89
+ "@vitest/coverage-v8": "^5.0.3",
91
90
  "concurrently": "^9.2.1",
92
91
  "esbuild": "^0.28.1",
93
92
  "supertest": "^7.2.2",
94
93
  "tsx": "^4.23.0",
95
- "typescript": "^5.4.0",
96
- "vitest": "^4.1.10"
94
+ "typescript": "^7.0.2",
95
+ "vitest": "^5.0.3"
97
96
  },
98
97
  "overrides": {
99
98
  "ip": "npm:@eggjs/ip@2.1.0",
@@ -8,6 +8,8 @@ exports.resolveNodeCommand = resolveNodeCommand;
8
8
  exports.buildSpecrailsMcpEntry = buildSpecrailsMcpEntry;
9
9
  exports.mergeSpecrailsIntoWorkspaceMcp = mergeSpecrailsIntoWorkspaceMcp;
10
10
  exports.buildAgentMcpArgs = buildAgentMcpArgs;
11
+ exports.prepareAgentMcpSpec = prepareAgentMcpSpec;
12
+ exports.rotateAgentSessionCapability = rotateAgentSessionCapability;
11
13
  exports.removeAgentCapabilityFile = removeAgentCapabilityFile;
12
14
  exports.prepareAgentMcp = prepareAgentMcp;
13
15
  exports.syncExternalServersIntoJsonFile = syncExternalServersIntoJsonFile;
@@ -235,6 +237,45 @@ function buildAgentTurnEntry(opts) {
235
237
  }
236
238
  return entry;
237
239
  }
240
+ function stringRecord(value) {
241
+ if (!value || typeof value !== 'object' || Array.isArray(value))
242
+ return undefined;
243
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === 'string');
244
+ return entries.length ? Object.fromEntries(entries) : undefined;
245
+ }
246
+ /**
247
+ * MCP servers for a resident Core agent session. The `specrails` bridge reads
248
+ * the conversation's capability file on every request, and this call
249
+ * atomically (re)writes that file with the CURRENT turn's capability — so a
250
+ * long-lived provider process always acts under the latest turn's tier.
251
+ * External servers are passed through as structured specs.
252
+ */
253
+ function prepareAgentMcpSpec(opts) {
254
+ const entry = buildAgentTurnEntry({ conversationId: opts.conversationId, port: opts.port, capability: opts.capability });
255
+ if (!entry)
256
+ throw new Error('The bundled Specrails MCP bridge is unavailable.');
257
+ // Specrails authorizes every action against the mission tier; never auto-approve external servers.
258
+ const specs = [{ name: 'specrails', command: entry.command, args: [...entry.args], env: { ...entry.env }, autoApprove: true }];
259
+ for (const server of opts.external ?? []) {
260
+ if (server.name === 'specrails')
261
+ continue;
262
+ const config = server.config;
263
+ if (typeof config.url === 'string') {
264
+ const headers = stringRecord(config.headers);
265
+ specs.push({ name: server.name, url: config.url, ...(headers ? { headers } : {}) });
266
+ }
267
+ else if (typeof config.command === 'string') {
268
+ const env = stringRecord(config.env);
269
+ specs.push({ name: server.name, command: config.command, args: Array.isArray(config.args) ? config.args.filter((arg) => typeof arg === 'string') : [], ...(env ? { env } : {}) });
270
+ }
271
+ }
272
+ return specs;
273
+ }
274
+ /** Rotate the resident session's capability for a new turn (atomic rewrite). */
275
+ function rotateAgentSessionCapability(conversationId, port, capability) {
276
+ if (!buildAgentTurnEntry({ conversationId, port, capability }))
277
+ throw new Error('The bundled Specrails MCP bridge is unavailable.');
278
+ }
238
279
  /** Delete the on-disk bearer as soon as its owning agent turn settles. */
239
280
  function removeAgentCapabilityFile(conversationId, invocationId) {
240
281
  if (!AGENT_CONVERSATION_ID_RE.test(conversationId) ||
@@ -12,6 +12,7 @@ exports.getCLIStatus = getCLIStatus;
12
12
  exports.checkCoreCompat = checkCoreCompat;
13
13
  exports.coreCompatSupportsProvider = coreCompatSupportsProvider;
14
14
  const child_process_1 = require("child_process");
15
+ const agent_sessions_1 = require("./modules/agent-sessions");
15
16
  const fs_1 = __importDefault(require("fs"));
16
17
  const path_1 = __importDefault(require("path"));
17
18
  const setup_manager_1 = require("./setup-manager");
@@ -264,6 +265,7 @@ async function checkCoreCompat() {
264
265
  contractFound: true,
265
266
  contractSchemaVersion: contract.schemaVersion,
266
267
  supportedProviders,
268
+ sessions: contract.agentRuntime?.sessions ? (0, agent_sessions_1.checkSessionContract)(contract.agentRuntime.sessions) : null,
267
269
  };
268
270
  }
269
271
  /** True only when the located Core contract explicitly renders this provider. */
@@ -3,7 +3,7 @@ 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.AGENT_SEARCH_INDEX_DDL = void 0;
6
+ exports.GLOBAL_SUBAGENT_RUNTIME_KEY = exports.GLOBAL_ALLOW_SUBAGENTS_KEY = exports.AGENT_SEARCH_INDEX_DDL = void 0;
7
7
  exports.setProjectProvidersMirror = setProjectProvidersMirror;
8
8
  exports.getDesktopDbPath = getDesktopDbPath;
9
9
  exports.rebuildAgentSearchIndex = rebuildAgentSearchIndex;
@@ -19,6 +19,9 @@ exports.updateProjectRepository = updateProjectRepository;
19
19
  exports.removeProjectRepository = removeProjectRepository;
20
20
  exports.removeProject = removeProject;
21
21
  exports.touchProject = touchProject;
22
+ exports.getGlobalAllowSubagents = getGlobalAllowSubagents;
23
+ exports.getGlobalSubagentRuntime = getGlobalSubagentRuntime;
24
+ exports.setGlobalSubagentRuntime = setGlobalSubagentRuntime;
22
25
  exports.getDesktopSetting = getDesktopSetting;
23
26
  exports.setDesktopSetting = setDesktopSetting;
24
27
  exports.setProjectSetupSession = setProjectSetupSession;
@@ -39,6 +42,7 @@ exports.getWebhook = getWebhook;
39
42
  exports.addWebhook = addWebhook;
40
43
  exports.updateWebhook = updateWebhook;
41
44
  exports.removeWebhook = removeWebhook;
45
+ const project_settings_1 = require("./modules/project-settings");
42
46
  const fs_1 = __importDefault(require("fs"));
43
47
  const path_1 = __importDefault(require("path"));
44
48
  const os_1 = __importDefault(require("os"));
@@ -683,6 +687,75 @@ function applyDesktopMigrations(db) {
683
687
  if (!columns.includes('workspace_paths'))
684
688
  db.exec('ALTER TABLE project_repositories ADD COLUMN workspace_paths TEXT');
685
689
  },
690
+ // 33: core-agent-sessions-host — Desktop's rebuildable projection of Core
691
+ // agent sessions. Core's journal (~/.specrails/sessions/<scope>/) is the
692
+ // source of truth; these rows are re-derivable by replaying it. Cursor and
693
+ // projected rows are always written in one transaction. Column-guarded for
694
+ // migration replay fixtures.
695
+ () => {
696
+ db.exec(`
697
+ CREATE TABLE IF NOT EXISTS agent_session_cursors (
698
+ conversation_id TEXT PRIMARY KEY REFERENCES agent_conversations(id) ON DELETE CASCADE,
699
+ core_session_id TEXT NOT NULL,
700
+ scope TEXT NOT NULL,
701
+ last_seq INTEGER NOT NULL DEFAULT 0,
702
+ resident_phase TEXT NOT NULL DEFAULT 'idle' CHECK (resident_phase IN ('idle', 'turn', 'background')),
703
+ process_alive INTEGER NOT NULL DEFAULT 0,
704
+ live_subagents INTEGER NOT NULL DEFAULT 0,
705
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
706
+ );
707
+ CREATE TABLE IF NOT EXISTS agent_subagents (
708
+ conversation_id TEXT NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE,
709
+ subagent_id TEXT NOT NULL,
710
+ parent_id TEXT,
711
+ kind TEXT NOT NULL CHECK (kind IN ('foreground', 'background')),
712
+ agent_type TEXT,
713
+ description TEXT NOT NULL,
714
+ phase TEXT NOT NULL CHECK (phase IN ('running', 'idle', 'failed', 'stopped', 'killed', 'interrupted')),
715
+ reason TEXT,
716
+ restarts INTEGER NOT NULL DEFAULT 0,
717
+ launched_turn_id TEXT,
718
+ started_at TEXT NOT NULL,
719
+ ended_at TEXT,
720
+ usage_json TEXT,
721
+ tool_uses INTEGER,
722
+ duration_ms INTEGER,
723
+ result_summary TEXT,
724
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
725
+ PRIMARY KEY (conversation_id, subagent_id)
726
+ );
727
+ CREATE INDEX IF NOT EXISTS idx_agent_subagents_turn ON agent_subagents(conversation_id, launched_turn_id);
728
+ CREATE TABLE IF NOT EXISTS agent_subagent_events (
729
+ conversation_id TEXT NOT NULL,
730
+ subagent_id TEXT NOT NULL,
731
+ seq INTEGER NOT NULL,
732
+ channel TEXT NOT NULL CHECK (channel IN ('text', 'tool')),
733
+ delta TEXT,
734
+ tool_json TEXT,
735
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
736
+ PRIMARY KEY (conversation_id, subagent_id, seq),
737
+ FOREIGN KEY (conversation_id, subagent_id) REFERENCES agent_subagents(conversation_id, subagent_id) ON DELETE CASCADE
738
+ );
739
+ `);
740
+ const invocationColumns = db.prepare('PRAGMA table_info(agent_invocations)').all().map((c) => c.name);
741
+ if (!invocationColumns.includes('origin'))
742
+ db.exec(`ALTER TABLE agent_invocations ADD COLUMN origin TEXT NOT NULL DEFAULT 'user' CHECK (origin IN ('user', 'subagent', 'system'));`);
743
+ const messageColumns = db.prepare('PRAGMA table_info(agent_messages)').all().map((c) => c.name);
744
+ if (!messageColumns.includes('core_turn_id'))
745
+ db.exec(`ALTER TABLE agent_messages ADD COLUMN core_turn_id TEXT;`);
746
+ if (!messageColumns.includes('turn_origin'))
747
+ db.exec(`ALTER TABLE agent_messages ADD COLUMN turn_origin TEXT CHECK (turn_origin IS NULL OR turn_origin IN ('user', 'subagent', 'system'));`);
748
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_messages_core_turn ON agent_messages(conversation_id, core_turn_id) WHERE core_turn_id IS NOT NULL;`);
749
+ },
750
+ // 34: hybrid sub-agent runtime — sub-agents Core launched on another
751
+ // provider record which driver and model ran them. Column-guarded.
752
+ () => {
753
+ const columns = db.prepare('PRAGMA table_info(agent_subagents)').all().map((c) => c.name);
754
+ if (!columns.includes('delegated_driver'))
755
+ db.exec('ALTER TABLE agent_subagents ADD COLUMN delegated_driver TEXT');
756
+ if (!columns.includes('delegated_model'))
757
+ db.exec('ALTER TABLE agent_subagents ADD COLUMN delegated_model TEXT');
758
+ },
686
759
  ];
687
760
  (0, sqlite_migrations_1.applyNumberedMigrations)(db, migrations);
688
761
  }
@@ -805,6 +878,30 @@ function removeProject(db, id) {
805
878
  function touchProject(db, id) {
806
879
  db.prepare("UPDATE projects SET last_seen_at = datetime('now') WHERE id = ?").run(id);
807
880
  }
881
+ /** App-global "Allow sub-agents" for missions without a project (default off). */
882
+ exports.GLOBAL_ALLOW_SUBAGENTS_KEY = 'agent_allow_subagents';
883
+ function getGlobalAllowSubagents(db) {
884
+ return getDesktopSetting(db, exports.GLOBAL_ALLOW_SUBAGENTS_KEY) === 'true';
885
+ }
886
+ /** App-wide "Run sub-agents with" for missions without a project; null = the mission agent's provider. */
887
+ exports.GLOBAL_SUBAGENT_RUNTIME_KEY = 'agent_subagent_runtime';
888
+ function getGlobalSubagentRuntime(db) {
889
+ const raw = getDesktopSetting(db, exports.GLOBAL_SUBAGENT_RUNTIME_KEY);
890
+ if (!raw)
891
+ return null;
892
+ try {
893
+ return (0, project_settings_1.parseSubagentRuntimeSetting)(JSON.parse(raw));
894
+ }
895
+ catch {
896
+ return null;
897
+ }
898
+ }
899
+ function setGlobalSubagentRuntime(db, value) {
900
+ if (value === null)
901
+ db.prepare('DELETE FROM desktop_settings WHERE key = ?').run(exports.GLOBAL_SUBAGENT_RUNTIME_KEY);
902
+ else
903
+ setDesktopSetting(db, exports.GLOBAL_SUBAGENT_RUNTIME_KEY, JSON.stringify(value));
904
+ }
808
905
  function getDesktopSetting(db, key) {
809
906
  const row = db.prepare('SELECT value FROM desktop_settings WHERE key = ?').get(key);
810
907
  return row?.value;
@@ -828,9 +925,9 @@ function recordAgentInvocation(db, input) {
828
925
  id, conversation_id, project_id, provider, surface, model, status,
829
926
  started_at, finished_at, duration_ms, duration_api_ms,
830
927
  tokens_in, tokens_out, tokens_cache_read, tokens_cache_create,
831
- total_cost_usd, total_cost_usd_estimated, num_turns, session_id
832
- ) VALUES (?, ?, ?, ?, 'agent-chat', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
833
- `).run(input.id, input.conversation_id, input.project_id ?? null, input.provider, input.model ?? null, input.status, input.started_at, input.finished_at ?? null, input.duration_ms ?? null, input.duration_api_ms ?? null, input.tokens_in ?? null, input.tokens_out ?? null, input.tokens_cache_read ?? null, input.tokens_cache_create ?? null, input.total_cost_usd ?? null, input.total_cost_usd_estimated ? 1 : 0, input.num_turns ?? null, input.session_id ?? null);
928
+ total_cost_usd, total_cost_usd_estimated, num_turns, session_id, origin
929
+ ) VALUES (?, ?, ?, ?, 'agent-chat', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
930
+ `).run(input.id, input.conversation_id, input.project_id ?? null, input.provider, input.model ?? null, input.status, input.started_at, input.finished_at ?? null, input.duration_ms ?? null, input.duration_api_ms ?? null, input.tokens_in ?? null, input.tokens_out ?? null, input.tokens_cache_read ?? null, input.tokens_cache_create ?? null, input.total_cost_usd ?? null, input.total_cost_usd_estimated ? 1 : 0, input.num_turns ?? null, input.session_id ?? null, input.origin ?? 'user');
834
931
  }
835
932
  /**
836
933
  * Sum of all agent-chat turn costs, optionally since an ISO instant (inclusive).
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createDesktopRouter = createDesktopRouter;
7
+ const project_settings_1 = require("./modules/project-settings");
7
8
  const http_1 = require("./modules/subscription-usage/adapters/http");
8
9
  const runtime_role_prompts_router_1 = require("./runtime-role-prompts-router");
9
10
  const express_1 = require("express");
@@ -816,11 +817,26 @@ function createDesktopRouter(registry, broadcast, usageService) {
816
817
  'http://localhost:3000';
817
818
  const costAlertThresholdRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd');
818
819
  const costAlertThresholdUsd = costAlertThresholdRaw != null ? parseFloat(costAlertThresholdRaw) : null;
819
- res.json({ port: parseInt(port, 10), specrailsTechUrl, costAlertThresholdUsd });
820
+ const allowSubagents = (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb);
821
+ res.json({ port: parseInt(port, 10), specrailsTechUrl, costAlertThresholdUsd, allowSubagents, subagentRuntime: (0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb) });
820
822
  });
821
823
  // PUT /api/settings — update app-level settings
822
824
  router.put('/settings', (req, res) => {
823
- const { port, specrailsTechUrl, costAlertThresholdUsd } = req.body ?? {};
825
+ const { port, specrailsTechUrl, costAlertThresholdUsd, allowSubagents } = req.body ?? {};
826
+ if (allowSubagents !== undefined && typeof allowSubagents !== 'boolean') {
827
+ res.status(400).json({ error: 'allowSubagents must be a boolean' });
828
+ return;
829
+ }
830
+ let subagentRuntime;
831
+ if (req.body?.subagentRuntime !== undefined) {
832
+ try {
833
+ subagentRuntime = (0, project_settings_1.parseSubagentRuntimeSetting)(req.body.subagentRuntime);
834
+ }
835
+ catch (error) {
836
+ res.status(400).json({ error: error.message });
837
+ return;
838
+ }
839
+ }
824
840
  if (port !== undefined) {
825
841
  const n = Number(port);
826
842
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
@@ -848,6 +864,19 @@ function createDesktopRouter(registry, broadcast, usageService) {
848
864
  (0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd', String(costAlertThresholdUsd));
849
865
  }
850
866
  }
867
+ if (allowSubagents !== undefined) {
868
+ const previous = (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb);
869
+ (0, desktop_db_1.setDesktopSetting)(registry.desktopDb, desktop_db_1.GLOBAL_ALLOW_SUBAGENTS_KEY, allowSubagents ? 'true' : 'false');
870
+ // Project-less missions re-read their sub-agent policy.
871
+ if (previous !== allowSubagents)
872
+ registry.notifySettingsChanged({ projectId: null });
873
+ }
874
+ if (subagentRuntime !== undefined) {
875
+ const previous = JSON.stringify((0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb));
876
+ (0, desktop_db_1.setGlobalSubagentRuntime)(registry.desktopDb, subagentRuntime);
877
+ if (previous !== JSON.stringify(subagentRuntime))
878
+ registry.notifySettingsChanged({ projectId: null });
879
+ }
851
880
  res.json({ ok: true });
852
881
  });
853
882
  // ─── Budget routes ────────────────────────────────────────────────────────────
@@ -19,6 +19,8 @@ const desktop_router_1 = require("./desktop-router");
19
19
  const provider_detection_1 = require("./provider-detection");
20
20
  const provider_selection_1 = require("./provider-selection");
21
21
  const desktop_db_1 = require("./desktop-db");
22
+ const sqlite_1 = require("./modules/project-settings/adapters/sqlite");
23
+ const agent_sessions_1 = require("./modules/agent-sessions");
22
24
  const legacy_migration_1 = require("./legacy-migration");
23
25
  const framework_reseed_1 = require("./framework-reseed");
24
26
  const project_router_1 = require("./project-router");
@@ -31,6 +33,13 @@ const feature_flags_1 = require("./feature-flags");
31
33
  const mobile_1 = require("./mobile");
32
34
  const mcp_1 = require("./mcp");
33
35
  const agent_chat_manager_1 = require("./modules/missions/runtime/agent-chat-manager");
36
+ const mission_core_sessions_1 = require("./modules/missions/runtime/mission-core-sessions");
37
+ const session_host_registry_1 = require("./modules/agent-sessions/runtime/session-host-registry");
38
+ const http_2 = require("./modules/agent-sessions/adapters/http");
39
+ const host_process_1 = require("./modules/agent-sessions/adapters/host-process");
40
+ const core_sessions_availability_1 = require("./modules/agent-sessions/runtime/core-sessions-availability");
41
+ const registry_1 = require("./providers/registry");
42
+ const agent_capability_1 = require("./mcp/agent-capability");
34
43
  const agent_chat_router_1 = require("./modules/missions/runtime/agent-chat-router");
35
44
  const blueprint_chat_manager_1 = require("./modules/builder/runtime/blueprint-chat-manager");
36
45
  const blueprint_router_1 = require("./modules/builder/runtime/blueprint-router");
@@ -282,6 +291,7 @@ let _registry = null;
282
291
  let _mobileGateway = null;
283
292
  let _mcpManager = null;
284
293
  let _agentChatManager = null;
294
+ let _sessionHosts = null;
285
295
  let _blueprintChatManager = null;
286
296
  let _headroomManager = null;
287
297
  server.on('upgrade', (request, socket, head) => {
@@ -654,10 +664,56 @@ function applyPtyWsRateLimiting(ws) {
654
664
  });
655
665
  const agentChatManager = new agent_chat_manager_1.AgentChatManager(broadcast, registry.desktopDb, port, registry);
656
666
  _agentChatManager = agentChatManager;
667
+ // Core agent sessions (core-agent-sessions-host): one supervised Core host per
668
+ // project scope; missions use it when SPECRAILS_CORE_SESSIONS allows and the
669
+ // selected Core advertises `sessions`. Otherwise missions keep legacy runners.
670
+ const sessionHosts = new session_host_registry_1.SessionHostRegistry({
671
+ launcher: new host_process_1.CoreHostLauncher({ host: { name: 'specrails-desktop', version: process.env.npm_package_version ?? 'unknown' } }),
672
+ clock: { now: () => Date.now(), after: (ms, callback) => { const timer = setTimeout(callback, ms); timer.unref?.(); return { cancel: () => clearTimeout(timer) }; } },
673
+ onStatus: (scope, status, detail, code) => {
674
+ console.log(`[agent-sessions] ${scope}: ${status}${detail ? ` (${detail})` : ''}`);
675
+ // App-level event (no projectId): open missions show a notice when their scope degrades.
676
+ broadcast({ type: 'agent_sessions_host', scope, status, detail: detail ?? null, code: code ?? null, timestamp: new Date().toISOString() });
677
+ },
678
+ });
679
+ _sessionHosts = sessionHosts;
680
+ const projectAllowsSubagents = (projectId) => {
681
+ // A pinned project that is not loaded cannot vouch for sub-agents.
682
+ const context = registry.getContext(projectId);
683
+ return context ? (0, sqlite_1.getProjectSettings)(context.db).allowSubagents : false;
684
+ };
685
+ const missionCoreSessions = new mission_core_sessions_1.MissionCoreSessions({
686
+ db: registry.desktopDb,
687
+ registry: sessionHosts,
688
+ port,
689
+ broadcast: (message) => broadcast(message),
690
+ adapterFor: registry_1.getAdapter,
691
+ availability: () => (0, core_sessions_availability_1.coreSessionsAvailability)(),
692
+ projectKey: (projectId) => registry.getProjectRow(projectId)?.slug ?? null,
693
+ revokeCapability: agent_capability_1.revokeAgentCapability,
694
+ // "Allow sub-agents": the project setting decides; missions without a project use the app setting.
695
+ subagentPolicy: (conversation) => (0, agent_sessions_1.resolveSubagentPolicy)({
696
+ surface: 'mission',
697
+ projectAllows: conversation.pinned_project_id ? projectAllowsSubagents(conversation.pinned_project_id) : null,
698
+ globalAllows: (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb),
699
+ }),
700
+ // "Run sub-agents with": the project's choice, or the app-wide one without a project.
701
+ subagentRuntime: (conversation) => {
702
+ if (!conversation.pinned_project_id)
703
+ return (0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb);
704
+ const context = registry.getContext(conversation.pinned_project_id);
705
+ return context ? (0, sqlite_1.getProjectSettings)(context.db).subagentRuntime : null;
706
+ },
707
+ });
708
+ agentChatManager.setCoreSessions(missionCoreSessions);
709
+ registry.onSettingsChanged((scope) => { void missionCoreSessions.refreshSubagentPolicy(scope); });
710
+ registry.onProjectRemoved(({ slug }) => { void sessionHosts.stop(slug); });
657
711
  // Publish the instance to the process-wide registry so the rails layer can
658
712
  // post PR-decision cards (safe-pr-review-flow). Left null when agent chat is
659
713
  // disabled — the rails callers are null-safe and simply skip the card.
660
714
  (0, agent_chat_registry_1.setAgentChatManager)((0, agent_chat_router_1.isAgentChatEnabled)() ? agentChatManager : null);
715
+ // Before the agent router: `/api/agent/session-hosts` is not a conversation route.
716
+ app.use('/api/agent/session-hosts', (0, http_2.createSessionHostsRouter)(sessionHosts));
661
717
  app.use('/api/agent', (0, agent_chat_router_1.createAgentChatRouter)({ manager: agentChatManager, desktopDb: registry.desktopDb }));
662
718
  // Project Builder day-0 chat + orchestrated commit (add-project-builder).
663
719
  // App-level like agent chat — a Builder conversation runs before any project
@@ -790,6 +846,8 @@ async function shutdown() {
790
846
  Promise.resolve().then(() => _mobileGateway?.stop()),
791
847
  Promise.resolve().then(() => _mcpManager?.stop()),
792
848
  Promise.resolve().then(() => _agentChatManager?.shutdown()),
849
+ // Core session hosts record running work as interrupted and release their journals.
850
+ Promise.resolve().then(() => _sessionHosts?.stopAll()),
793
851
  Promise.resolve().then(() => _blueprintChatManager?.shutdown()),
794
852
  ]);
795
853
  // The shell can exit before its application descendants. Keep the event