specrails-desktop 2.60.0 → 2.62.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 (190) hide show
  1. package/client/dist/assets/ActivityFeedPage-C_pqc281.js +1 -0
  2. package/client/dist/assets/{AgentBrowserCapture-D4a3wYM1.js → AgentBrowserCapture-vz6rbDtu.js} +1 -1
  3. package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +2 -0
  4. package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +2 -0
  5. package/client/dist/assets/{AgentModeJobsPane-25tQlLpF.js → AgentModeJobsPane-99fBBLri.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-CUDp_sfw.js → AgentsPage-CTwm_6T8.js} +3 -3
  7. package/client/dist/assets/{AnalyticsPage-DXWMGn8b.js → AnalyticsPage-YD8MLoPa.js} +1 -1
  8. package/client/dist/assets/CodePage-CLJn2F2e.js +3 -0
  9. package/client/dist/assets/{DesktopAnalyticsPage-BkFBq0E6.js → DesktopAnalyticsPage-MWGQVbwp.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BL7uSbIJ.js → DocsDialog-XUCYKoq0.js} +1 -1
  11. package/client/dist/assets/{DocsPage-CsDLbmZX.js → DocsPage-dOLC8bXA.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-B4Olg1Fk.js → ExportDropdown-BvKQgwM9.js} +1 -1
  13. package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +19 -0
  14. package/client/dist/assets/{JobDetailModal-DS_Owxs6.js → JobDetailModal-CkiubrkZ.js} +1 -1
  15. package/client/dist/assets/JobDetailPage-CNe4Wn7l.js +1 -0
  16. package/client/dist/assets/JobsPage-DSts_L_d.js +1 -0
  17. package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +1 -0
  18. package/client/dist/assets/{LoopPreviewModal-CH1r-KhU.js → LoopPreviewModal-Dv3dxHr_.js} +1 -1
  19. package/client/dist/assets/{LoopWindowSurface-CiZdI6aw.js → LoopWindowSurface-DN2lbSnL.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-BwFujdGi.js → LoopsPage-DHgIreEW.js} +2 -2
  21. package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +1 -0
  22. package/client/dist/assets/PluginsPage-D1EJ9oja.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +1 -0
  24. package/client/dist/assets/{RepositoryDeliveries-NNorcGzT.js → RepositoryDeliveries-CW4eKG3x.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-D-jb4bI_.js → RepositoryScopeSelector-DrtPqvc_.js} +1 -1
  26. package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +4 -0
  27. package/client/dist/assets/{TemplatePreviewModal-5CZdAZ0G.js → TemplatePreviewModal-DcFlhUxy.js} +1 -1
  28. package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +16 -0
  29. package/client/dist/assets/{Trans-wmTUDVyC.js → Trans-CAY2Rd2f.js} +1 -1
  30. package/client/dist/assets/agent-5bSixVzz.js +1 -0
  31. package/client/dist/assets/agent-BR4EHfhq.js +1 -0
  32. package/client/dist/assets/agent-C0IKkJP8.js +1 -0
  33. package/client/dist/assets/agent-C7li9ayC.js +1 -0
  34. package/client/dist/assets/agent-D0dRmEen.js +1 -0
  35. package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
  36. package/client/dist/assets/agent-Dip4h2YE.js +1 -0
  37. package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
  38. package/client/dist/assets/agentRuntime-B-myNBUP.js +1 -0
  39. package/client/dist/assets/agentRuntime-Bjt7Ij6_.js +1 -0
  40. package/client/dist/assets/agentRuntime-C_yLPKDy.js +1 -0
  41. package/client/dist/assets/agentRuntime-D2kYQqNO.js +1 -0
  42. package/client/dist/assets/agentRuntime-DvhEe1um.js +1 -0
  43. package/client/dist/assets/agentRuntime-DxC6-9rN.js +1 -0
  44. package/client/dist/assets/agentRuntime-TKQyg137.js +1 -0
  45. package/client/dist/assets/agentRuntime-WSv8G0pW.js +1 -0
  46. package/client/dist/assets/ban-E4EaZe3a.js +1 -0
  47. package/client/dist/assets/chevron-up-CwGUHd6D.js +1 -0
  48. package/client/dist/assets/dist-Bq10nS2m.js +1 -0
  49. package/client/dist/assets/{formatDistanceToNow-Dax4zqPF.js → formatDistanceToNow-DlQY1Lm_.js} +1 -1
  50. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-dpIo42JZ.js → getTimezoneOffsetInMilliseconds-COUeq65G.js} +1 -1
  51. package/client/dist/assets/index-BacrO_ub.css +2 -0
  52. package/client/dist/assets/index-CONHvEJw.js +83 -0
  53. package/client/dist/assets/{jira-api-CLrIvXFm.js → jira-api-C94VNHNY.js} +1 -1
  54. package/client/dist/assets/layers-eDMjG41I.js +1 -0
  55. package/client/dist/assets/{loops-Cm55CXJ4.js → loops-B2PhQLKR.js} +1 -1
  56. package/client/dist/assets/{loops-3IutojWt.js → loops-BqbF46st.js} +1 -1
  57. package/client/dist/assets/{loops-BAosWtXE.js → loops-CINnHbfI.js} +1 -1
  58. package/client/dist/assets/{loops-kIJJkaxf.js → loops-D-3XhH04.js} +1 -1
  59. package/client/dist/assets/{loops-DHQtnfze.js → loops-DKkEEjZ_.js} +1 -1
  60. package/client/dist/assets/{loops-D7tL_Saw.js → loops-DzWR5-v6.js} +1 -1
  61. package/client/dist/assets/{loops--KWr0X-w.js → loops-WyWDkhok.js} +1 -1
  62. package/client/dist/assets/{loops-BISDssL5.js → loops-scD5fIBf.js} +1 -1
  63. package/client/dist/assets/{narration-DvLQBW9U.js → narration-BEKMVxGm.js} +1 -1
  64. package/client/dist/assets/{narration-CHI06czm.js → narration-CjLwGKap.js} +1 -1
  65. package/client/dist/assets/{narration-DjKB3pQZ.js → narration-DANWuGsW.js} +1 -1
  66. package/client/dist/assets/{narration-eSx3gR0W.js → narration-DGeEDWiT.js} +1 -1
  67. package/client/dist/assets/{narration-DtCYvRLM.js → narration-DJgpGwIP.js} +1 -1
  68. package/client/dist/assets/{narration-n3hOA7LY.js → narration-DbHnz0qV.js} +1 -1
  69. package/client/dist/assets/{narration-thJOeT2-.js → narration-bZSsTZG8.js} +1 -1
  70. package/client/dist/assets/{narration--hUhjM5v.js → narration-i6aKJKEc.js} +1 -1
  71. package/client/dist/assets/{project-repositories-BkyT8AjU.js → project-repositories-Cc3oNv8X.js} +1 -1
  72. package/client/dist/assets/save-B6wGWwBR.js +1 -0
  73. package/client/dist/assets/{settings-Dn-pXukr.js → settings-BKJZ2KEM.js} +1 -1
  74. package/client/dist/assets/{settings-BBj_kZaD.js → settings-BV3jocIx.js} +1 -1
  75. package/client/dist/assets/{settings-COlH_P33.js → settings-BjM2Dw8_.js} +1 -1
  76. package/client/dist/assets/settings-CKu0LTib.js +1 -0
  77. package/client/dist/assets/{settings-D77mC_Lb.js → settings-DA77M48m.js} +1 -1
  78. package/client/dist/assets/{settings-ByVE7jdZ.js → settings-LfFnl1UH.js} +1 -1
  79. package/client/dist/assets/{settings-DZKy7MqN.js → settings-P1IQvhnL.js} +1 -1
  80. package/client/dist/assets/{settings-B5Hy_Kol.js → settings-eIMJjyK_.js} +1 -1
  81. package/client/dist/assets/{spending-Oov7p52A.js → spending-DemeFfrH.js} +1 -1
  82. package/client/dist/assets/{useDesktop-DpiS8Uov.js → useDesktop-9Lwoel_8.js} +1 -1
  83. package/client/dist/assets/{useRuntimeRuns-DBm1mul0.js → useRuntimeRuns-DL0xYoZ2.js} +1 -1
  84. package/client/dist/assets/{useSharedWebSocket-BZxKaVNT.js → useSharedWebSocket-BEHPeU1Y.js} +2 -2
  85. package/client/dist/assets/zap-CHo3aCD1.js +1 -0
  86. package/client/dist/index.html +26 -22
  87. package/docs/guide/de/integrations/6-agent-chat.md +11 -0
  88. package/docs/guide/en/integrations/6-agent-chat.md +11 -0
  89. package/docs/guide/es/integrations/6-agent-chat.md +11 -0
  90. package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
  91. package/docs/guide/it/integrations/6-agent-chat.md +11 -0
  92. package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
  93. package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
  94. package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
  95. package/docs/internals/README.md +1 -0
  96. package/docs/internals/agent-sessions.md +157 -0
  97. package/docs/internals/api-reference.md +33 -4
  98. package/docs/internals/configuration.md +36 -0
  99. package/docs/internals/core-runtime-updates.md +10 -3
  100. package/docs/internals/operations-runbook.md +11 -0
  101. package/docs/internals/programmatic-agent-runtime.md +2 -0
  102. package/docs/internals/source-map.md +40 -1
  103. package/mcp-bridge/dist/specrails-mcp.js +82 -15
  104. package/package.json +1 -1
  105. package/server/dist/agent-mcp-config.js +41 -0
  106. package/server/dist/core-compat.js +2 -0
  107. package/server/dist/desktop-db.js +101 -4
  108. package/server/dist/desktop-router.js +31 -2
  109. package/server/dist/index.js +58 -0
  110. package/server/dist/mcp/tools/app-settings.js +11 -0
  111. package/server/dist/mcp/tools/mission.js +45 -2
  112. package/server/dist/mcp/tools/specs.js +2 -2
  113. package/server/dist/modules/accounting/runtime/result-event.js +9 -1
  114. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +12 -0
  115. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
  116. package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +2 -0
  117. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +6 -2
  118. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +18 -6
  119. package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
  120. package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
  121. package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
  122. package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
  123. package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
  124. package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
  125. package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
  126. package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
  127. package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
  128. package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
  129. package/server/dist/modules/agent-sessions/index.js +31 -0
  130. package/server/dist/modules/agent-sessions/ports.js +2 -0
  131. package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
  132. package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
  133. package/server/dist/modules/agents/runtime/agent-store.js +3 -1
  134. package/server/dist/modules/loops/runtime/loop-core-factory.js +2 -2
  135. package/server/dist/modules/loops/runtime/loop-definition.js +1 -0
  136. package/server/dist/modules/loops/runtime/loop-factory.js +1 -1
  137. package/server/dist/modules/loops/runtime/loop-graph.js +8 -1
  138. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +41 -11
  139. package/server/dist/modules/loops/runtime/loop-run-manager.js +4 -0
  140. package/server/dist/modules/loops/runtime/loop-templates.js +3 -3
  141. package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
  142. package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
  143. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
  144. package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
  145. package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
  146. package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
  147. package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
  148. package/server/dist/modules/project-settings/adapters/http.js +14 -2
  149. package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
  150. package/server/dist/modules/project-settings/domain.js +24 -0
  151. package/server/dist/modules/project-settings/index.js +2 -1
  152. package/server/dist/modules/specs/runtime/spec-models.js +1 -1
  153. package/server/dist/plugins/codex-spawn.js +15 -7
  154. package/server/dist/project-registry.js +32 -0
  155. package/server/dist/project-router-settings.js +7 -1
  156. package/server/dist/providers/claude-adapter.js +6 -1
  157. package/server/dist/schemas/agent-runtime.schema.json +9 -7
  158. package/client/dist/assets/ActivityFeedPage-3J7fR8yB.js +0 -1
  159. package/client/dist/assets/AgentModeAnalyticsPane-Dps30tk9.js +0 -2
  160. package/client/dist/assets/AgentModeCodePane-CvLimey8.js +0 -2
  161. package/client/dist/assets/CodePage-D981zRHy.js +0 -3
  162. package/client/dist/assets/InteractiveJobComposer-Z6fl4791.js +0 -19
  163. package/client/dist/assets/JobDetailPage-1TUQ9vlE.js +0 -1
  164. package/client/dist/assets/JobsPage-BSrl8P7u.js +0 -1
  165. package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +0 -1
  166. package/client/dist/assets/MinimizedChatsContext-BD2bsRBe.js +0 -1
  167. package/client/dist/assets/PluginsPage-B8f49Mvn.js +0 -2
  168. package/client/dist/assets/ProjectSettingsDialog-D7q-O6h3.js +0 -1
  169. package/client/dist/assets/ReviewPacketPage-DpZSzF-Q.js +0 -4
  170. package/client/dist/assets/TicketDetailModalContext-BHmtj1As.js +0 -16
  171. package/client/dist/assets/agent--8QAfoXL.js +0 -1
  172. package/client/dist/assets/agent-3HxXkIWX.js +0 -1
  173. package/client/dist/assets/agent-CpFCz32T.js +0 -1
  174. package/client/dist/assets/agent-DVnw6ybO.js +0 -1
  175. package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
  176. package/client/dist/assets/agent-hv57-zyG.js +0 -1
  177. package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
  178. package/client/dist/assets/agentRuntime-7Ydcvr6P.js +0 -1
  179. package/client/dist/assets/agentRuntime-BHVw9orA.js +0 -1
  180. package/client/dist/assets/agentRuntime-DCPDaQcV.js +0 -1
  181. package/client/dist/assets/agentRuntime-DKTw4Ajg.js +0 -1
  182. package/client/dist/assets/agentRuntime-DVhij_i_.js +0 -1
  183. package/client/dist/assets/agentRuntime-Doss-T8o.js +0 -1
  184. package/client/dist/assets/agentRuntime-eZSMUlkM.js +0 -1
  185. package/client/dist/assets/agentRuntime-qSSU7fQk.js +0 -1
  186. package/client/dist/assets/index-Bfunvi6w.css +0 -2
  187. package/client/dist/assets/index-g8S62tpS.js +0 -83
  188. package/client/dist/assets/save-OsUqVn99.js +0 -1
  189. package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
  190. package/client/dist/assets/zap-CUOlvtT1.js +0 -1
@@ -140,6 +140,17 @@ If a single provider or feature is breaking the app, you can turn it off with an
140
140
 
141
141
  These are the most common ops escape hatches; see [Configuration → Environment variables](configuration.md#environment-variables) for the complete list of gates and flags.
142
142
 
143
+ ### Missions on Core agent sessions
144
+
145
+ With `SPECRAILS_CORE_SESSIONS` at `auto` (the default) or `on`, missions run in a Core host
146
+ process per project scope (`specrails-core runtime host --stdio --scope …`).
147
+ The server log prefix is `[agent-sessions]`.
148
+
149
+ - **Status.** The server logs each scope's transitions: `starting`, `ready`, `restarting`, `degraded`. `GET /api/agent/session-hosts` lists them. A degraded scope sends new mission turns through the legacy transport, and the mission shows why, with a **Retry** button (`POST /api/agent/session-hosts/:scope/retry`). Crashes degrade a scope after repeated failures. Some causes degrade it at once, without restart attempts: another Desktop instance holding the journal (`journal_locked`), an incompatible Core (`protocol_mismatch`), or a Core without the host (`driver_unavailable`). Close the other instance or fix Core, then retry.
150
+ - **Session data.** Core owns the journals in `~/.specrails/sessions/<project>/sessions.sqlite`. Desktop's tables (`agent_session_cursors`, `agent_subagents`, `agent_subagent_events`) are a projection. If they look wrong, rebuild them for one mission with `POST /api/agent/conversations/:id/session/rebuild`. This never duplicates messages or spend.
151
+ - **Turn it off.** `SPECRAILS_CORE_SESSIONS=off` returns every mission to the legacy transports. Missions keep their history; sub-agent cards remain for past turns.
152
+ - **Live check (paid, opt-in).** `SPECRAILS_LIVE_PROVIDER_SMOKE=1 npx vitest run server/modules/missions/runtime/mission-core-sessions.live.test.ts` runs real Claude missions against the local `../specrails-core` build. Add `SPECRAILS_LIVE_PROVIDERS=claude,codex` to include Codex, and `SPECRAILS_LIVE_<PROVIDER>_MODEL` to pick a model the account can use.
153
+
143
154
  ### Per-project reset (keeps the registration)
144
155
 
145
156
  To clear one project's job history, invocations, and telemetry pointers while keeping it registered, delete just that project's `jobs.sqlite` with the app stopped:
@@ -119,6 +119,8 @@ All routes are under `/api/projects/:projectId`:
119
119
  | `POST /agent-runtime/runs/:runId/resume` | Accept `{}`, `{ "approve": ["archive"] }`, `{ "recover": ["developer"] }`, explicit `invalidate` phase IDs, or `{ "answer": "…" }` for a pending question (required while one is open) |
120
120
  | `POST /agent-runtime/runs/:runId/cancel` | Cancel a continuation owned by this controller |
121
121
 
122
+ Host blockers (Core `hostBlockers: 1` / `setupCommands: 1`): the Implement recipe opts its `verify` nodes into host-owned environment repair and the `blocked` outcome. Core emits `[environment] <command> (<repository>)` and `Environment: installed …` lines on the `verification-output` channel, so they reach the readable job log through the same projection as checks. A precondition Core cannot satisfy (no network for a browser download, registry credentials, a failing `setup` command, a missing toolchain) ends the run at `host-blocked` with `completion.blocker = { kind, reason, command, args, cwd, requiredAction, evidenceId? }`; a fixer-declared `blocker` ends it at `fixer-blocked`. A developer-declared `blocker` ends the run at `developer-blocked` only for kinds the person must provide (`credential`, `network`, `environment-variable`); any other kind goes to host verification first: a host precondition ends at `host-blocked`, a reproduced failure goes to the fixer as a correction, and passing checks reach the reviewer, whose rejection of unfinished tasks is corrected within the same budget. The developer loop is bounded like corrections: when `tasks` finds approved tasks still unchecked, the developer runs again only while its last turn changed the candidate (`implementation-progress`, otherwise `implementation-stalled`) and within `implementationLimit` = 3 attempts (`implementation-exhausted`); both ends report the developer's `incomplete` list and summary. The bridge maps that completion (`ok: false`) to `runtimeStatus: 'blocked'`, the run row shows the blocked badge and the loop log renders "Blocked by the host environment" with the localized kind, the failing command and cwd, the required action and a copy button. Older Core packages receive the pre-blocker definition and no `setup` list.
123
+
122
124
  Resume responds `202` after admission and continues asynchronously. Legacy phase IDs are `architect`, `developer`, `fixer`, `verify`, `reviewer` and `archive`. For engine v2, use `POST /loop-runs/:runId/resume` instead. `recover` contains exact attempt IDs from `GET /loop-runs/:runId/recovery`; approval and answer controls use pending interrupt IDs. Node paths are display context, not recovery identities. The saved-execution panel and job header expose explicit per-attempt selection. Core's lease and Desktop's execution/mount claim jointly prevent competing writers. Resident human pauses keep their original settlement callback; restart recovery reconnects to the frozen delivery allocation and terminal outbox.
123
125
 
124
126
  Desktop launches `node <Core>/dist/agent-runtime/cli.js` with structured argv (`--approve`, `--recover`, `--invalidate`, `--answer <text>` on resume) and consumes JSON lines for phase events (`workflow-event`), agent output (`agent-event`), verification output, trace spans (`span`: `{ traceId, spanId, name, stepId, attempt, visit, startedAt, endedAt, status, usage?, error? }`) and the terminal `runtime-result`. Spans are stored verbatim as job events for diagnostics and are not narrated in the log. `runtime status --compact` returns `state.traceId`, `pendingApproval`, `pendingQuestion` and per-step `{ status, visits }`. Desktop probes `runtime api` once per executable package content digest; `runtime validate --stdin` runs on every save. The job log shows phase transitions (`[runtime] step_started: developer`), live tool activity per role (`[developer] Read src/app.ts`, `[developer] Bash npm test`) and Core's own phase notes (architecture written, verification passed, review approved or corrections requested); the final JSON of architect and reviewer is not echoed. The narrated view (Relato) derives its milestones from the same events: each runtime phase, the tools used, correction loops and a stopped workflow with Core's structural reason. Accounting uses the invocation's new attempts, so resuming a completed phase does not bill its cumulative history twice. Status queries use `--compact`, are read-only and do not invoke providers; accumulated logs stay in Core's checkpoint instead of overflowing the process status response.
@@ -5,7 +5,7 @@ architectural dependency declaration. Search a heading or filename and read only
5
5
  the relevant source and tests. Feature prefixes group the legacy server files;
6
6
  new bounded modules live under `server/modules/`.
7
7
 
8
- Includes 1018 source/build files. Nearby tests are linked where names
8
+ Includes 1042 source/build files. Nearby tests are linked where names
9
9
  match; integration suites may live elsewhere. Use `rg` to find other consumers.
10
10
 
11
11
  ## cli
@@ -457,7 +457,9 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
457
457
  - [AgentRefChip.tsx](../../client/src/features/missions/components/AgentRefChip.tsx)
458
458
  - [AgentRunFailureMarker.tsx](../../client/src/features/missions/components/AgentRunFailureMarker.tsx)
459
459
  - [AgentRuntimeSelector.tsx](../../client/src/features/missions/components/AgentRuntimeSelector.tsx)
460
+ - [AgentSessionIndicators.tsx](../../client/src/features/missions/components/AgentSessionIndicators.tsx)
460
461
  - [AgentSpecDraftCard.tsx](../../client/src/features/missions/components/AgentSpecDraftCard.tsx)
462
+ - [AgentSubagentsCard.tsx](../../client/src/features/missions/components/AgentSubagentsCard.tsx)
461
463
  - [AgentThinkingHalo.tsx](../../client/src/features/missions/components/AgentThinkingHalo.tsx) · [test](../../client/src/features/missions/components/__tests__/AgentThinkingHalo.test.tsx)
462
464
  - [AgentTierChip.tsx](../../client/src/features/missions/components/AgentTierChip.tsx)
463
465
  - [AgentToolbarSelector.tsx](../../client/src/features/missions/components/AgentToolbarSelector.tsx)
@@ -486,6 +488,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
486
488
  - [AgentChatContext.tsx](../../client/src/features/missions/context/AgentChatContext.tsx) · [test](../../client/src/features/missions/context/AgentChatContext.test.tsx)
487
489
  - [AgentWorkspaceContext.tsx](../../client/src/features/missions/context/AgentWorkspaceContext.tsx) · [test](../../client/src/features/missions/context/__tests__/AgentWorkspaceContext.test.tsx)
488
490
  - [MinimizedChatsContext.tsx](../../client/src/features/missions/context/MinimizedChatsContext.tsx) · [test](../../client/src/features/missions/context/__tests__/MinimizedChatsContext.test.tsx)
491
+ - [MissionSessionsContext.tsx](../../client/src/features/missions/context/MissionSessionsContext.tsx) · [test](../../client/src/features/missions/context/__tests__/MissionSessionsContext.test.tsx)
489
492
  - [MissionSplitViewsContext.tsx](../../client/src/features/missions/context/MissionSplitViewsContext.tsx)
490
493
  - [MissionWindowsContext.tsx](../../client/src/features/missions/context/MissionWindowsContext.tsx) · [test](../../client/src/features/missions/context/__tests__/MissionWindowsContext.test.tsx)
491
494
 
@@ -500,10 +503,12 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
500
503
  - [agent-context-palette.ts](../../client/src/features/missions/lib/agent-context-palette.ts) · [test](../../client/src/features/missions/lib/__tests__/agent-context-palette.test.ts)
501
504
  - [agent-refs.ts](../../client/src/features/missions/lib/agent-refs.ts) · [test](../../client/src/features/missions/lib/__tests__/agent-refs.test.ts)
502
505
  - [mission-search.ts](../../client/src/features/missions/lib/mission-search.ts) · [test](../../client/src/features/missions/lib/__tests__/mission-search.test.ts)
506
+ - [mission-sessions.ts](../../client/src/features/missions/lib/mission-sessions.ts) · [test](../../client/src/features/missions/lib/__tests__/mission-sessions.test.ts)
503
507
  - [mission-split-layout.ts](../../client/src/features/missions/lib/mission-split-layout.ts) · [test](../../client/src/features/missions/lib/__tests__/mission-split-layout.test.ts)
504
508
  - [mission-view-state.ts](../../client/src/features/missions/lib/mission-view-state.ts)
505
509
  - [mission-window-controller.ts](../../client/src/features/missions/lib/mission-window-controller.ts) · [test](../../client/src/features/missions/lib/__tests__/mission-window-controller.test.ts)
506
510
  - [mission-windows.ts](../../client/src/features/missions/lib/mission-windows.ts) · [test](../../client/src/features/missions/lib/__tests__/mission-windows.test.ts)
511
+ - [subagent-result.ts](../../client/src/features/missions/lib/subagent-result.ts) · [test](../../client/src/features/missions/lib/__tests__/subagent-result.test.ts)
507
512
 
508
513
  ## client/src/features/plugins/lib
509
514
 
@@ -600,6 +605,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
600
605
  - [RuntimeRetention.tsx](../../client/src/features/settings/components/RuntimeRetention.tsx) · [test](../../client/src/features/settings/components/__tests__/RuntimeRetention.test.tsx)
601
606
  - [RuntimeRolePrompts.tsx](../../client/src/features/settings/components/RuntimeRolePrompts.tsx) · [test](../../client/src/features/settings/components/__tests__/RuntimeRolePrompts.test.tsx)
602
607
  - [RuntimeSteering.tsx](../../client/src/features/settings/components/RuntimeSteering.tsx) · [test](../../client/src/features/settings/components/__tests__/RuntimeSteering.test.tsx)
608
+ - [SubagentsSettingsSection.tsx](../../client/src/features/settings/components/SubagentsSettingsSection.tsx) · [test](../../client/src/features/settings/components/__tests__/SubagentsSettingsSection.test.tsx)
603
609
  - [TerminalSettingsSection.tsx](../../client/src/features/settings/components/TerminalSettingsSection.tsx)
604
610
 
605
611
  ## client/src/features/settings/components/pickers
@@ -1098,6 +1104,35 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
1098
1104
  - [agent-runtime-verification-log.ts](../../server/modules/agent-runtime/runtime/agent-runtime-verification-log.ts)
1099
1105
  - [agent-runtime-verification-suggestions.ts](../../server/modules/agent-runtime/runtime/agent-runtime-verification-suggestions.ts) · [test](../../server/modules/agent-runtime/runtime/agent-runtime-verification-suggestions.test.ts)
1100
1106
 
1107
+ ## server/modules/agent-sessions
1108
+
1109
+ - [index.ts](../../server/modules/agent-sessions/index.ts)
1110
+ - [ports.ts](../../server/modules/agent-sessions/ports.ts)
1111
+
1112
+ ## server/modules/agent-sessions/adapters
1113
+
1114
+ - [host-process.ts](../../server/modules/agent-sessions/adapters/host-process.ts)
1115
+ - [http.ts](../../server/modules/agent-sessions/adapters/http.ts)
1116
+ - [rpc-client.ts](../../server/modules/agent-sessions/adapters/rpc-client.ts)
1117
+
1118
+ ## server/modules/agent-sessions/application
1119
+
1120
+ - [session-event-pump.ts](../../server/modules/agent-sessions/application/session-event-pump.ts)
1121
+
1122
+ ## server/modules/agent-sessions/domain
1123
+
1124
+ - [availability.ts](../../server/modules/agent-sessions/domain/availability.ts)
1125
+ - [errors.ts](../../server/modules/agent-sessions/domain/errors.ts)
1126
+ - [host-state.ts](../../server/modules/agent-sessions/domain/host-state.ts)
1127
+ - [projection.ts](../../server/modules/agent-sessions/domain/projection.ts)
1128
+ - [protocol.ts](../../server/modules/agent-sessions/domain/protocol.ts)
1129
+ - [subagent-policy.ts](../../server/modules/agent-sessions/domain/subagent-policy.ts)
1130
+
1131
+ ## server/modules/agent-sessions/runtime
1132
+
1133
+ - [core-sessions-availability.ts](../../server/modules/agent-sessions/runtime/core-sessions-availability.ts)
1134
+ - [session-host-registry.ts](../../server/modules/agent-sessions/runtime/session-host-registry.ts)
1135
+
1101
1136
  ## server/modules/agents/runtime
1102
1137
 
1103
1138
  - [agent-catalog.ts](../../server/modules/agents/runtime/agent-catalog.ts)
@@ -1309,10 +1344,14 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
1309
1344
  - [agent-fence-promotion.ts](../../server/modules/missions/runtime/agent-fence-promotion.ts)
1310
1345
  - [agent-input-store.ts](../../server/modules/missions/runtime/agent-input-store.ts) · [test](../../server/modules/missions/runtime/agent-input-store.test.ts)
1311
1346
  - [agent-operator-prompt.ts](../../server/modules/missions/runtime/agent-operator-prompt.ts) · [test](../../server/modules/missions/runtime/agent-operator-prompt.test.ts)
1347
+ - [agent-session-store.ts](../../server/modules/missions/runtime/agent-session-store.ts)
1312
1348
  - [agent-spec-framing.ts](../../server/modules/missions/runtime/agent-spec-framing.ts) · [test](../../server/modules/missions/runtime/agent-spec-framing.test.ts)
1313
1349
  - [agent-steering.ts](../../server/modules/missions/runtime/agent-steering.ts) · [test](../../server/modules/missions/runtime/agent-steering.test.ts)
1314
1350
  - [agent-tier.ts](../../server/modules/missions/runtime/agent-tier.ts)
1351
+ - [core-session-runner.ts](../../server/modules/missions/runtime/core-session-runner.ts) · [test](../../server/modules/missions/runtime/core-session-runner.test.ts)
1352
+ - [mission-core-sessions.ts](../../server/modules/missions/runtime/mission-core-sessions.ts) · [test](../../server/modules/missions/runtime/mission-core-sessions.test.ts)
1315
1353
  - [mission-run-notify.ts](../../server/modules/missions/runtime/mission-run-notify.ts) · [test](../../server/modules/missions/runtime/mission-run-notify.test.ts)
1354
+ - [mission-session-projector.ts](../../server/modules/missions/runtime/mission-session-projector.ts) · [test](../../server/modules/missions/runtime/mission-session-projector.test.ts)
1316
1355
 
1317
1356
  ## server/modules/project-settings
1318
1357
 
@@ -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.60.0",
3
+ "version": "2.62.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 ────────────────────────────────────────────────────────────