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
@@ -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.AgentChatManager = void 0;
6
+ exports.AgentChatManager = exports.SessionRebuildBusyError = void 0;
7
7
  exports.sanitizeAgentTitle = sanitizeAgentTitle;
8
8
  const crypto_1 = require("crypto");
9
9
  const readline_1 = require("readline");
@@ -35,6 +35,8 @@ const agent_store_1 = require("../../agents/runtime/agent-store");
35
35
  const explore_draft_title_1 = require("../../conversations/runtime/explore-draft-title");
36
36
  const agent_input_store_1 = require("./agent-input-store");
37
37
  const agent_steering_1 = require("./agent-steering");
38
+ const core_session_runner_1 = require("./core-session-runner");
39
+ const agent_session_store_1 = require("./agent-session-store");
38
40
  /**
39
41
  * Providers report an expired or foreign resume token as a normalized error.
40
42
  * Those errors are safe to recover from by clearing the token and retrying the
@@ -47,12 +49,18 @@ function isStaleResumeError(message) {
47
49
  /unknown (?:session|thread)(?: id)?\b/i.test(message) ||
48
50
  /(?:session|thread)(?: id)?\b[^\n]{0,160}\b(?:not found|does not exist)\b/i.test(message));
49
51
  }
52
+ /** A projection rebuild was requested while a turn is running. */
53
+ class SessionRebuildBusyError extends Error {
54
+ }
55
+ exports.SessionRebuildBusyError = SessionRebuildBusyError;
50
56
  class AgentChatManager {
51
57
  _broadcast;
52
58
  _db;
53
59
  _port;
54
60
  _registry;
55
61
  _active = new Map();
62
+ /** Missions' Core agent sessions (null = legacy transports only). */
63
+ _coreSessions = null;
56
64
  _nativeInputNotifiers = new Map();
57
65
  /** Conversations with a turn in-flight but not yet spawned. Closes the TOCTOU
58
66
  * window the attachment-extraction await opens between the busy guard and
@@ -89,6 +97,21 @@ class AgentChatManager {
89
97
  // A sidecar restart must retain unsent input without replaying operations.
90
98
  (0, agent_input_store_1.recoverPendingAgentInputs)(db);
91
99
  }
100
+ /** Composition-time wiring of Core agent sessions (see agent-sessions module). */
101
+ setCoreSessions(coreSessions) {
102
+ this._coreSessions = coreSessions;
103
+ }
104
+ /** Conversation deleted: close its Core session (reads the row before it is dropped). */
105
+ closeCoreSession(conversationId) {
106
+ const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
107
+ if (!conversation || !this._coreSessions)
108
+ return Promise.resolve();
109
+ return this._coreSessions.close(conversation);
110
+ }
111
+ /** A turn is live while its provider child or its Core session turn is running. */
112
+ _isTurnLive(conversationId) {
113
+ return this._active.has(conversationId) || !!this._coreSessions?.handle(conversationId);
114
+ }
92
115
  async _awaitWhileLive(work) {
93
116
  if (this._disposed)
94
117
  return { disposed: true };
@@ -160,11 +183,11 @@ class AgentChatManager {
160
183
  }
161
184
  /** True while a turn is streaming for this conversation. */
162
185
  isStreaming(conversationId) {
163
- return !this._disposed && this._active.has(conversationId);
186
+ return !this._disposed && this._isTurnLive(conversationId);
164
187
  }
165
188
  /** True while a turn is in flight (spawned or reserved). */
166
189
  isBusy(conversationId) {
167
- return !this._disposed && (this._active.has(conversationId) || this._reserved.has(conversationId));
190
+ return !this._disposed && (this._isTurnLive(conversationId) || this._reserved.has(conversationId));
168
191
  }
169
192
  pendingMessages(conversationId) {
170
193
  return (this._queue.get(conversationId) ?? []).map((item) => ({
@@ -192,8 +215,62 @@ class AgentChatManager {
192
215
  pendingMessages: this.pendingMessages(conversationId),
193
216
  streamingText: this._streamingText.get(conversationId) ?? '',
194
217
  })),
218
+ // Core agent sessions with background work: the client restores the
219
+ // background indicator and live sub-agent rows from this after reconnecting.
220
+ sessions: (0, agent_session_store_1.listActiveSessionCursors)(this._db).map((cursor) => ({
221
+ conversationId: cursor.conversationId,
222
+ residentPhase: cursor.residentPhase,
223
+ processAlive: cursor.processAlive,
224
+ liveSubagents: cursor.liveSubagents,
225
+ subagents: (0, agent_session_store_1.listSubagents)(this._db, cursor.conversationId).filter((node) => node.phase === 'running'),
226
+ })),
195
227
  };
196
228
  }
229
+ /** Projected Core session state of one mission (null when it never ran in Core). */
230
+ sessionState(conversationId) {
231
+ const cursor = (0, agent_session_store_1.getSessionCursor)(this._db, conversationId);
232
+ if (!cursor)
233
+ return null;
234
+ return { residentPhase: cursor.residentPhase, processAlive: cursor.processAlive, liveSubagents: cursor.liveSubagents, subagents: (0, agent_session_store_1.listSubagents)(this._db, conversationId) };
235
+ }
236
+ /** Stop a mission's background sub-agents through its Core session. */
237
+ async stopSubagents(conversationId, subagentIds) {
238
+ const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
239
+ if (!conversation || !this._coreSessions)
240
+ return [];
241
+ return this._coreSessions.stopSubagents(conversation, subagentIds);
242
+ }
243
+ /** Delegated runtime: launch a sub-agent on the mission's configured provider. */
244
+ async delegateSubagent(conversationId, params) {
245
+ const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
246
+ if (!conversation || !this._coreSessions)
247
+ throw new Error('This mission does not run in a Core session.');
248
+ return this._coreSessions.delegate(conversation, params);
249
+ }
250
+ /** Delegated runtime: wait for sub-agents (results are delivered to the agent once). */
251
+ async waitSubagents(conversationId, subagentIds, timeoutMs) {
252
+ const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
253
+ if (!conversation || !this._coreSessions)
254
+ throw new Error('This mission does not run in a Core session.');
255
+ return this._coreSessions.waitSubagents(conversation, subagentIds, timeoutMs);
256
+ }
257
+ /** Plugin MCP servers for a project-pinned mission (empty for providers without plugin MCP entries). */
258
+ _pluginMcpServers(providerId, projectId) {
259
+ const project = projectId ? this._registry?.getProjectRow?.(projectId) : null;
260
+ if (!project)
261
+ return [];
262
+ const execution = (0, workspace_resolution_1.resolveProjectExecution)(project);
263
+ return (0, codex_spawn_1.resolveCodexPluginMcpServers)({ providerId, stateRoot: execution.cwd, repositoryPath: project.path, legacyProviderId: project.provider });
264
+ }
265
+ /** Rebuild a mission's session projection from Core's journal (operator repair). */
266
+ async rebuildSessionProjection(conversationId) {
267
+ const conversation = (0, agent_store_1.getAgentConversation)(this._db, conversationId);
268
+ if (!conversation || !this._coreSessions)
269
+ return null;
270
+ if (this._isTurnLive(conversationId))
271
+ throw new SessionRebuildBusyError('Wait for the current turn to finish before rebuilding');
272
+ return this._coreSessions.rebuildProjection(conversation);
273
+ }
197
274
  notifyConversationCreated(conversationId) {
198
275
  this._broadcast({ type: 'agent_conversation_created', conversationId, timestamp: new Date().toISOString() });
199
276
  }
@@ -478,25 +555,31 @@ class AgentChatManager {
478
555
  const invocationId = (0, crypto_1.randomUUID)();
479
556
  currentCapability = agentCapability;
480
557
  currentInvocationId = invocationId;
481
- let mcpArgs;
482
- let mcpEnv;
483
- try {
484
- const wiring = (0, agent_mcp_config_1.prepareAgentMcp)({
485
- adapterId: adapter.id, conversationId, invocationId, cwd,
486
- port: this._port, capability: agentCapability,
487
- external: (0, external_mcp_1.resolveExternalEntries)(adapter.id, this._db),
488
- });
489
- mcpArgs = wiring.extraArgs;
490
- mcpEnv = wiring.env;
491
- const project = conversation.pinned_project_id ? this._registry?.getProjectRow?.(conversation.pinned_project_id) : null;
492
- if (adapter.id === 'codex' && project) {
493
- const execution = (0, workspace_resolution_1.resolveProjectExecution)(project);
494
- mcpArgs.push(...(0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: execution.cwd, repositoryPath: project.path, legacyProviderId: project.provider }));
558
+ let mcpArgs = [];
559
+ let mcpEnv = {};
560
+ // Core agent sessions: a resident session presents this turn's capability
561
+ // through structured MCP specs (no provider-specific argv).
562
+ const coreContext = this._coreSessions
563
+ ? await this._coreSessions.prepareTurn(conversation, adapter, { capability: agentCapability, external: (0, external_mcp_1.resolveExternalEntries)(adapter.id, this._db), plugins: this._pluginMcpServers(adapter.id, conversation.pinned_project_id) })
564
+ : null;
565
+ if (!coreContext)
566
+ try {
567
+ const wiring = (0, agent_mcp_config_1.prepareAgentMcp)({
568
+ adapterId: adapter.id, conversationId, invocationId, cwd,
569
+ port: this._port, capability: agentCapability,
570
+ external: (0, external_mcp_1.resolveExternalEntries)(adapter.id, this._db),
571
+ });
572
+ mcpArgs = wiring.extraArgs;
573
+ mcpEnv = wiring.env;
574
+ const project = conversation.pinned_project_id ? this._registry?.getProjectRow?.(conversation.pinned_project_id) : null;
575
+ if (adapter.id === 'codex' && project) {
576
+ const execution = (0, workspace_resolution_1.resolveProjectExecution)(project);
577
+ mcpArgs.push(...(0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: execution.cwd, repositoryPath: project.path, legacyProviderId: project.provider }));
578
+ }
579
+ }
580
+ catch (err) {
581
+ throw new Error('Specrails could not prepare its MCP tools. The agent was not launched. Check the app installation and retry. ' + (err instanceof Error ? err.message : String(err)));
495
582
  }
496
- }
497
- catch (err) {
498
- throw new Error('Specrails could not prepare its MCP tools. The agent was not launched. Check the app installation and retry. ' + (err instanceof Error ? err.message : String(err)));
499
- }
500
583
  const action = useResume ? 'chat-resume' : 'chat-turn';
501
584
  const startedAt = new Date().toISOString();
502
585
  let streamed = '';
@@ -505,7 +588,7 @@ class AgentChatManager {
505
588
  let capturedSessionId = useResume ? conversation.session_id ?? null : null;
506
589
  let capturedError = null;
507
590
  console.log(`[agent-chat] turn start conv=${conversationId} provider=${adapter.id} binary=${adapter.binary} ` +
508
- `action=${action} model=${model} cwd=${cwd} mcp=${mcpArgs.length ? 'mcp-config' : Object.keys(mcpEnv).join(',') || 'none'}`);
591
+ `action=${action} model=${model} cwd=${cwd} mcp=${coreContext ? `core-session:${coreContext.mcpServers.map((server) => server.name).join(',') || 'none'}` : mcpArgs.length ? 'mcp-config' : Object.keys(mcpEnv).join(',') || 'none'}`);
509
592
  const buildOpts = {
510
593
  prompt: effectivePrompt(useResume),
511
594
  systemPrompt: adapter.capabilities.systemPromptArg ? systemPrompt : undefined,
@@ -515,7 +598,7 @@ class AgentChatManager {
515
598
  imagePaths: spawnImagePaths,
516
599
  reasoning_effort: reasoningEffort,
517
600
  };
518
- const nativeRunner = (0, live_session_1.nativeLiveSessionRunner)(adapter);
601
+ const nativeRunner = coreContext ? (0, core_session_runner_1.createCoreSessionRunner)(coreContext) : (0, live_session_1.nativeLiveSessionRunner)(adapter);
519
602
  let invocationLive = true;
520
603
  let nativeSink;
521
604
  let nativeDrain = Promise.resolve();
@@ -782,13 +865,13 @@ class AgentChatManager {
782
865
  // A killed child (abort / conversation DELETE) keeps flushing its
783
866
  // buffered stdout — suppress the broadcasts once it left _active
784
867
  // so stragglers can't resurrect client-side streaming state.
785
- if (this._active.has(conversationId)) {
868
+ if (this._isTurnLive(conversationId)) {
786
869
  this._streamingText.set(conversationId, streamed.slice(segmentStart));
787
870
  this._broadcast({ type: 'agent_stream', conversationId, delta: ev.text, timestamp: timestamp() });
788
871
  }
789
872
  break;
790
873
  case 'tool-use':
791
- if (this._active.has(conversationId)) {
874
+ if (this._isTurnLive(conversationId)) {
792
875
  this._broadcast({
793
876
  type: 'agent_tool',
794
877
  conversationId,
@@ -802,7 +885,7 @@ class AgentChatManager {
802
885
  case 'tool-result':
803
886
  // Feeds the activity-log modal's output column. Claude-only
804
887
  // today; other adapters never emit this kind.
805
- if (this._active.has(conversationId)) {
888
+ if (this._isTurnLive(conversationId)) {
806
889
  this._broadcast({
807
890
  type: 'agent_tool_result',
808
891
  conversationId,
@@ -845,7 +928,10 @@ class AgentChatManager {
845
928
  this._nativeInputNotifiers.delete(conversationId);
846
929
  stopSteering();
847
930
  this._steeringCapabilities.delete(conversationId);
848
- (0, agent_capability_1.revokeAgentCapability)(agentCapability);
931
+ // A resident Core session keeps presenting this capability for its
932
+ // background turns until the next turn rotates it or Core retires it.
933
+ if (!this._coreSessions?.retains(conversationId, agentCapability))
934
+ (0, agent_capability_1.revokeAgentCapability)(agentCapability);
849
935
  (0, agent_mcp_config_1.removeAgentCapabilityFile)(conversationId, invocationId);
850
936
  currentCapability = null;
851
937
  currentInvocationId = undefined;
@@ -874,7 +960,8 @@ class AgentChatManager {
874
960
  if (result.stderrTail && (result.spawnFailed || (result.code ?? 0) !== 0 || !text)) {
875
961
  console.error(`[agent-chat] ${adapter.id} stderr:\n${result.stderrTail}`);
876
962
  }
877
- return { disposed: false, text, fullText: streamed.trim(), checkpointed, sessionId: capturedSessionId, error: capturedError, code: result.code, spawnFailed: result.spawnFailed, stderrTail: result.stderrTail, events: result.events, startedAt };
963
+ const coreResult = result;
964
+ return { disposed: false, text, fullText: streamed.trim(), checkpointed, sessionId: capturedSessionId, error: capturedError, code: result.code, spawnFailed: result.spawnFailed, stderrTail: result.stderrTail, events: result.events, startedAt, ...(coreResult.usage ? { usage: coreResult.usage } : {}), ...(coreResult.coreTurnId ? { coreTurnId: coreResult.coreTurnId } : {}), ...(coreResult.errorCode ? { errorCode: coreResult.errorCode } : {}) };
878
965
  };
879
966
  // Conversation CONFIG (provider/model/tier) is owned by the PATCH route —
880
967
  // never write turn-START snapshots back at settle, or a mid-turn provider
@@ -896,9 +983,9 @@ class AgentChatManager {
896
983
  // Deliberate user Stop: keep any partial text (it was already streamed to
897
984
  // the client), never auto-heal, never surface an error.
898
985
  if (r.text && (0, agent_store_1.getAgentConversation)(this._db, conversationId)) {
899
- const message = (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text });
986
+ const message = (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text, ...(r.coreTurnId ? { coreTurnId: r.coreTurnId, turnOrigin: 'user' } : {}) });
900
987
  persistSession(r.sessionId);
901
- this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, messageId: message.id, timestamp: timestamp() });
988
+ this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, messageId: message.id, ...(r.coreTurnId ? { coreTurnId: r.coreTurnId } : {}), timestamp: timestamp() });
902
989
  }
903
990
  };
904
991
  // Cost accounting (HIGH-3): exactly one row per settled turn, whichever
@@ -964,14 +1051,15 @@ class AgentChatManager {
964
1051
  const message = (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text });
965
1052
  this._broadcast({ type: 'agent_partial', conversationId, fullText: r.text, messageId: message.id, error: reason, timestamp: timestamp() });
966
1053
  }
967
- this._emitError(conversationId, reason);
1054
+ this._emitError(conversationId, reason, r.errorCode ? { code: r.errorCode, provider: adapter.id } : undefined);
968
1055
  record(r, 'failed');
969
1056
  return;
970
1057
  }
971
1058
  if (r.text || r.checkpointed) {
972
- const message = r.text ? (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text }) : undefined;
1059
+ const message = r.text ? (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'assistant', content: r.text, ...(r.coreTurnId ? { coreTurnId: r.coreTurnId, turnOrigin: 'user' } : {}) }) : undefined;
973
1060
  persistSession(r.sessionId);
974
- this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, ...(message ? { messageId: message.id } : {}), timestamp: timestamp() });
1061
+ // The Core turn id anchors this turn's sub-agents under the reply in every client.
1062
+ this._broadcast({ type: 'agent_done', conversationId, fullText: r.text, ...(message ? { messageId: message.id } : {}), ...(r.coreTurnId ? { coreTurnId: r.coreTurnId } : {}), timestamp: timestamp() });
975
1063
  record(r, 'success');
976
1064
  // AI title after the FIRST completed turn (industry standard — ChatGPT /
977
1065
  // Claude.ai), on the conversation's own provider. The deterministic title
@@ -989,7 +1077,7 @@ class AgentChatManager {
989
1077
  }
990
1078
  finally {
991
1079
  this._steeringCapabilities.delete(conversationId);
992
- if (currentCapability)
1080
+ if (currentCapability && !this._coreSessions?.retains(conversationId, currentCapability))
993
1081
  (0, agent_capability_1.revokeAgentCapability)(currentCapability);
994
1082
  if (currentInvocationId)
995
1083
  (0, agent_mcp_config_1.removeAgentCapabilityFile)(conversationId, currentInvocationId);
@@ -1009,10 +1097,11 @@ class AgentChatManager {
1009
1097
  return;
1010
1098
  try {
1011
1099
  const finishedAt = new Date().toISOString();
1012
- const { result, estimated } = (0, result_event_1.finaliseInvocationResult)(adapter, outcome.events, {
1013
- fallbackModel: model,
1014
- durationMs: Math.max(0, Date.parse(finishedAt) - Date.parse(outcome.startedAt)),
1015
- });
1100
+ const finaliseOptions = { fallbackModel: model, durationMs: Math.max(0, Date.parse(finishedAt) - Date.parse(outcome.startedAt)) };
1101
+ // Core sessions report a normalized per-turn delta; never reparse provider frames for them.
1102
+ const { result, estimated } = outcome.usage
1103
+ ? (0, result_event_1.finaliseNormalisedResult)(adapter, outcome.usage, finaliseOptions)
1104
+ : (0, result_event_1.finaliseInvocationResult)(adapter, outcome.events, finaliseOptions);
1016
1105
  (0, desktop_db_1.recordAgentInvocation)(this._db, {
1017
1106
  id: (0, crypto_1.randomUUID)(),
1018
1107
  conversation_id: conversation.id,
@@ -1437,6 +1526,12 @@ class AgentChatManager {
1437
1526
  (0, agent_capability_1.revokeAgentCapability)(capability);
1438
1527
  this._steeringCapabilities.delete(conversationId);
1439
1528
  this._nativeInputNotifiers.delete(conversationId);
1529
+ const coreTurn = this._coreSessions?.handle(conversationId);
1530
+ if (coreTurn) {
1531
+ // Stop the turn inside the resident session; never kill a process by PID.
1532
+ void coreTurn.interrupt();
1533
+ return true;
1534
+ }
1440
1535
  const child = this._active.get(conversationId);
1441
1536
  if (!child)
1442
1537
  return false;
@@ -1478,6 +1573,7 @@ class AgentChatManager {
1478
1573
  this._closeChildIo(child);
1479
1574
  }
1480
1575
  this._active.clear();
1576
+ this._coreSessions?.shutdown();
1481
1577
  for (const child of this._auxProcesses) {
1482
1578
  const reader = this._auxReaders.get(child);
1483
1579
  try {
@@ -1490,10 +1586,11 @@ class AgentChatManager {
1490
1586
  this._auxReaders.clear();
1491
1587
  this._auxProcesses.clear();
1492
1588
  }
1493
- _emitError(conversationId, error) {
1589
+ /** `detail.code` lets clients explain known failures (e.g. `policy_unenforceable`) in the user's language. */
1590
+ _emitError(conversationId, error, detail) {
1494
1591
  if (this._disposed)
1495
1592
  return;
1496
- this._broadcast({ type: 'agent_error', conversationId, error, timestamp: new Date().toISOString() });
1593
+ this._broadcast({ type: 'agent_error', conversationId, error, ...(detail ?? {}), timestamp: new Date().toISOString() });
1497
1594
  }
1498
1595
  }
1499
1596
  exports.AgentChatManager = AgentChatManager;
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isAgentChatEnabled = isAgentChatEnabled;
7
7
  exports.createAgentChatRouter = createAgentChatRouter;
8
8
  const fs_1 = __importDefault(require("fs"));
9
+ const agent_session_store_1 = require("./agent-session-store");
9
10
  const multer_1 = __importDefault(require("multer"));
10
11
  const express_1 = require("express");
12
+ const agent_chat_manager_1 = require("./agent-chat-manager");
11
13
  const providers_1 = require("../../../providers");
12
14
  const provider_selection_1 = require("../../../provider-selection");
13
15
  const agent_tier_1 = require("./agent-tier");
@@ -213,8 +215,72 @@ function createAgentChatRouter(deps) {
213
215
  messages: (0, agent_input_store_1.decorateAgentInputMessages)(desktopDb, (0, agent_store_1.listAgentMessages)(desktopDb, conversation.id)),
214
216
  pendingMessages: manager.pendingMessages(conversation.id),
215
217
  live: manager.conversationLive(conversation.id),
218
+ session: manager.sessionState(conversation.id),
216
219
  });
217
220
  });
221
+ // ── Sub-agents of a mission's Core agent session (core-agent-sessions-host) ──
222
+ router.get('/conversations/:id/subagents', (req, res) => {
223
+ const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
224
+ if (!conversation) {
225
+ res.status(404).json({ error: 'Unknown conversation' });
226
+ return;
227
+ }
228
+ res.json({ subagents: (0, agent_session_store_1.listSubagents)(desktopDb, conversation.id), session: manager.sessionState(conversation.id) });
229
+ });
230
+ router.get('/conversations/:id/subagents/:subagentId/events', (req, res) => {
231
+ const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
232
+ if (!conversation) {
233
+ res.status(404).json({ error: 'Unknown conversation' });
234
+ return;
235
+ }
236
+ const after = Number.parseInt(String(req.query.after ?? '0'), 10);
237
+ const limit = Number.parseInt(String(req.query.limit ?? '200'), 10);
238
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1000) {
239
+ res.status(400).json({ error: 'after must be ≥ 0 and limit between 1 and 1000' });
240
+ return;
241
+ }
242
+ res.json((0, agent_session_store_1.pageSubagentEvents)(desktopDb, conversation.id, String(req.params.subagentId), after, limit));
243
+ });
244
+ router.post('/conversations/:id/subagents/stop', async (req, res) => {
245
+ const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
246
+ if (!conversation) {
247
+ res.status(404).json({ error: 'Unknown conversation' });
248
+ return;
249
+ }
250
+ const ids = req.body?.subagentIds;
251
+ if (ids !== undefined && (!Array.isArray(ids) || ids.length > 500 || !ids.every((id) => typeof id === 'string' && id.length > 0 && id.length <= 200))) {
252
+ res.status(400).json({ error: 'subagentIds must be an array of ids' });
253
+ return;
254
+ }
255
+ try {
256
+ res.json({ stopped: await manager.stopSubagents(conversation.id, ids) });
257
+ }
258
+ catch (err) {
259
+ res.status(502).json({ error: err instanceof Error ? err.message : 'Could not stop the sub-agents' });
260
+ }
261
+ });
262
+ router.post('/conversations/:id/session/rebuild', async (req, res) => {
263
+ const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
264
+ if (!conversation) {
265
+ res.status(404).json({ error: 'Unknown conversation' });
266
+ return;
267
+ }
268
+ try {
269
+ const result = await manager.rebuildSessionProjection(conversation.id);
270
+ if (!result) {
271
+ res.status(409).json({ error: 'This mission does not run in a Core session' });
272
+ return;
273
+ }
274
+ res.json(result);
275
+ }
276
+ catch (err) {
277
+ if (err instanceof agent_chat_manager_1.SessionRebuildBusyError) {
278
+ res.status(409).json({ error: err.message });
279
+ return;
280
+ }
281
+ res.status(502).json({ error: err instanceof Error ? err.message : 'Could not rebuild the session projection' });
282
+ }
283
+ });
218
284
  router.patch('/conversations/:id', (req, res) => {
219
285
  const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
220
286
  if (!conversation) {
@@ -289,6 +355,8 @@ function createAgentChatRouter(deps) {
289
355
  router.delete('/conversations/:id', (req, res) => {
290
356
  const id = String(req.params.id);
291
357
  manager.abort(id);
358
+ // Must run before the row is deleted: it reads the conversation's scope.
359
+ void manager.closeCoreSession(id).catch((e) => console.error('[agent-chat] Core session close failed:', e));
292
360
  (0, transient_children_1.killBackgroundProcessesForChat)(id);
293
361
  (0, transient_children_1.purgeBackgroundProcessHistory)({ chatId: id });
294
362
  (0, agent_store_1.deleteAgentConversation)(desktopDb, id);
@@ -546,13 +546,12 @@ the user's language.
546
546
  a description following the template, at least one acceptance criterion, and
547
547
  you have no outstanding question. Then — and only then — emit the complete
548
548
  final \`spec-draft\` block one last time, state the one-line short summary you
549
- will attach, and ask exactly one question: whether to create it. Say what
550
- creation does: one normal write for the spec itself, plus by default one
551
- short background AI pass that appends a Contract Layer (exact identifiers,
552
- data shapes, invariants for the implementing agents) — small cost, seconds.
553
- If the user declines the enrichment ("no contract layer"), you will pass
554
- \`contractRefine: false\`. Do not call any persisting tool until the user
555
- answers yes to that render. If they edit, update and re-render.
549
+ will attach, and ask exactly one question: whether to create it. The
550
+ Contract Layer is always added: never ask about it, never offer to skip it and
551
+ never present it as an option (the user does not want that question). Pass
552
+ \`contractRefine: false\` only when the user asked for no contract layer on
553
+ their own initiative. Do not call any persisting tool until the user answers
554
+ yes to that render. If they edit, update and re-render.
556
555
 
557
556
  **Pausing.** There is no draft-ticket path for you (\`save_draft\` needs a real
558
557
  app Explore conversation). If the user pauses, summarize the full draft state
@@ -569,8 +568,8 @@ summary later.
569
568
  author — the same post-persist enrichment the app's Add Spec runs. After the
570
569
  spec lands, one short background AI pass appends a \`## Contract Layer\`
571
570
  section (naming contract, data shapes, state machine, invariants, file touch
572
- list). Pass \`contractRefine: false\` to skip it when the user declined it or
573
- wants zero AI spend. It requires a structured-action provider (currently
571
+ list). Never ask whether to add it. Pass \`contractRefine: false\` only when
572
+ the user asked, unprompted, for no contract layer or zero AI spend. It requires a structured-action provider (currently
574
573
  Claude), respects the app-wide kill switch, and leaves the spec unenriched when
575
574
  it cannot run.
576
575
  - The enrichment is asynchronous: the commit returns immediately and the
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSessionCursor = getSessionCursor;
4
+ exports.listActiveSessionCursors = listActiveSessionCursors;
5
+ exports.ensureSessionCursor = ensureSessionCursor;
6
+ exports.advanceSessionCursor = advanceSessionCursor;
7
+ exports.setResidentState = setResidentState;
8
+ exports.resetSessionProjection = resetSessionProjection;
9
+ exports.coreTurnInvocationId = coreTurnInvocationId;
10
+ exports.isCoreTurnRecorded = isCoreTurnRecorded;
11
+ exports.upsertSubagent = upsertSubagent;
12
+ exports.appendSubagentEvent = appendSubagentEvent;
13
+ exports.listSubagents = listSubagents;
14
+ exports.pageSubagentEvents = pageSubagentEvents;
15
+ function getSessionCursor(db, conversationId) {
16
+ const row = db.prepare('SELECT * FROM agent_session_cursors WHERE conversation_id = ?').get(conversationId);
17
+ if (!row)
18
+ return null;
19
+ return {
20
+ conversationId: String(row.conversation_id),
21
+ coreSessionId: String(row.core_session_id),
22
+ scope: String(row.scope),
23
+ lastSeq: Number(row.last_seq),
24
+ residentPhase: row.resident_phase,
25
+ processAlive: Number(row.process_alive) === 1,
26
+ liveSubagents: Number(row.live_subagents),
27
+ };
28
+ }
29
+ /** Missions whose Core session still has work in flight (non-idle or live sub-agents). */
30
+ function listActiveSessionCursors(db) {
31
+ const rows = db.prepare("SELECT conversation_id FROM agent_session_cursors WHERE resident_phase != 'idle' OR live_subagents > 0").all();
32
+ return rows.map((row) => getSessionCursor(db, row.conversation_id)).filter((row) => row !== null);
33
+ }
34
+ function ensureSessionCursor(db, conversationId, coreSessionId, scope) {
35
+ db.prepare(`INSERT INTO agent_session_cursors (conversation_id, core_session_id, scope) VALUES (?, ?, ?)
36
+ ON CONFLICT(conversation_id) DO UPDATE SET core_session_id = excluded.core_session_id, scope = excluded.scope`).run(conversationId, coreSessionId, scope);
37
+ }
38
+ function advanceSessionCursor(db, conversationId, seq) {
39
+ db.prepare("UPDATE agent_session_cursors SET last_seq = ?, updated_at = datetime('now') WHERE conversation_id = ?").run(seq, conversationId);
40
+ }
41
+ function setResidentState(db, conversationId, phase, processAlive, liveSubagents) {
42
+ db.prepare("UPDATE agent_session_cursors SET resident_phase = ?, process_alive = ?, live_subagents = ?, updated_at = datetime('now') WHERE conversation_id = ?")
43
+ .run(phase, processAlive ? 1 : 0, liveSubagents, conversationId);
44
+ }
45
+ /**
46
+ * Drop a mission's derived session rows so the journal can be replayed into
47
+ * them. Messages and invocations stay: replay recognizes turns it already
48
+ * recorded (see `coreTurnInvocationId`).
49
+ */
50
+ function resetSessionProjection(db, conversationId) {
51
+ db.transaction(() => {
52
+ db.prepare('DELETE FROM agent_subagent_events WHERE conversation_id = ?').run(conversationId);
53
+ db.prepare('DELETE FROM agent_subagents WHERE conversation_id = ?').run(conversationId);
54
+ db.prepare("UPDATE agent_session_cursors SET last_seq = 0, resident_phase = 'idle', process_alive = 0, live_subagents = 0, updated_at = datetime('now') WHERE conversation_id = ?").run(conversationId);
55
+ })();
56
+ }
57
+ /** Deterministic invocation id of a Core turn: recording it twice is a no-op. */
58
+ function coreTurnInvocationId(conversationId, turnId) {
59
+ return `core-turn:${conversationId}:${turnId}`;
60
+ }
61
+ function isCoreTurnRecorded(db, conversationId, turnId) {
62
+ return !!db.prepare('SELECT 1 FROM agent_invocations WHERE id = ?').get(coreTurnInvocationId(conversationId, turnId))
63
+ || !!db.prepare('SELECT 1 FROM agent_messages WHERE conversation_id = ? AND core_turn_id = ?').get(conversationId, turnId);
64
+ }
65
+ function upsertSubagent(db, conversationId, view) {
66
+ db.prepare(`INSERT INTO agent_subagents (conversation_id, subagent_id, parent_id, kind, agent_type, description, phase, reason, restarts, launched_turn_id, started_at, ended_at, usage_json, tool_uses, duration_ms, result_summary, delegated_driver, delegated_model, updated_at)
67
+ VALUES (@conversationId, @subagentId, @parentId, @kind, @agentType, @description, @phase, @reason, @restarts, @launchedInTurnId, @startedAt, @endedAt, @usage, @toolUses, @durationMs, @resultSummary, @delegatedDriver, @delegatedModel, datetime('now'))
68
+ ON CONFLICT(conversation_id, subagent_id) DO UPDATE SET
69
+ phase = excluded.phase, reason = excluded.reason, restarts = excluded.restarts, ended_at = excluded.ended_at,
70
+ usage_json = excluded.usage_json, tool_uses = excluded.tool_uses, duration_ms = excluded.duration_ms,
71
+ result_summary = excluded.result_summary, updated_at = excluded.updated_at`).run({
72
+ conversationId,
73
+ subagentId: view.subagentId,
74
+ parentId: view.parentId,
75
+ kind: view.kind,
76
+ agentType: view.agentType,
77
+ description: view.description,
78
+ phase: view.phase,
79
+ reason: view.reason,
80
+ restarts: view.restarts,
81
+ launchedInTurnId: view.launchedInTurnId,
82
+ startedAt: view.startedAt,
83
+ endedAt: view.endedAt,
84
+ usage: view.usage ? JSON.stringify(view.usage) : null,
85
+ toolUses: view.toolUses,
86
+ durationMs: view.durationMs,
87
+ resultSummary: view.resultSummary,
88
+ delegatedDriver: view.delegated?.driver ?? null,
89
+ delegatedModel: view.delegated?.model ?? null,
90
+ });
91
+ }
92
+ function appendSubagentEvent(db, conversationId, subagentId, seq, channel, delta, tool) {
93
+ db.prepare('INSERT OR IGNORE INTO agent_subagent_events (conversation_id, subagent_id, seq, channel, delta, tool_json) VALUES (?, ?, ?, ?, ?, ?)')
94
+ .run(conversationId, subagentId, seq, channel, delta, tool ? JSON.stringify(tool) : null);
95
+ }
96
+ function mapSubagent(row) {
97
+ return {
98
+ conversationId: String(row.conversation_id),
99
+ subagentId: String(row.subagent_id),
100
+ parentId: row.parent_id ?? null,
101
+ kind: row.kind,
102
+ agentType: row.agent_type ?? null,
103
+ description: String(row.description),
104
+ phase: row.phase,
105
+ reason: row.reason ?? null,
106
+ restarts: Number(row.restarts),
107
+ startedAt: String(row.started_at),
108
+ endedAt: row.ended_at ?? null,
109
+ usage: row.usage_json ? JSON.parse(String(row.usage_json)) : null,
110
+ toolUses: row.tool_uses === null ? null : Number(row.tool_uses),
111
+ durationMs: row.duration_ms === null ? null : Number(row.duration_ms),
112
+ resultSummary: row.result_summary ?? null,
113
+ launchedInTurnId: row.launched_turn_id ?? null,
114
+ delegated: row.delegated_driver ? { driver: String(row.delegated_driver), model: String(row.delegated_model ?? '') } : null,
115
+ };
116
+ }
117
+ function listSubagents(db, conversationId) {
118
+ return db.prepare('SELECT * FROM agent_subagents WHERE conversation_id = ? ORDER BY started_at, rowid').all(conversationId).map(mapSubagent);
119
+ }
120
+ function pageSubagentEvents(db, conversationId, subagentId, afterSeq, limit) {
121
+ const rows = db.prepare('SELECT seq, channel, delta, tool_json, created_at FROM agent_subagent_events WHERE conversation_id = ? AND subagent_id = ? AND seq > ? ORDER BY seq LIMIT ?')
122
+ .all(conversationId, subagentId, afterSeq, limit + 1);
123
+ return {
124
+ events: rows.slice(0, limit).map((row) => ({
125
+ seq: Number(row.seq),
126
+ channel: row.channel,
127
+ delta: row.delta ?? null,
128
+ tool: row.tool_json ? JSON.parse(String(row.tool_json)) : null,
129
+ createdAt: String(row.created_at),
130
+ })),
131
+ hasMore: rows.length > limit,
132
+ };
133
+ }