yepanywhere 0.5.2 → 0.6.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 (858) hide show
  1. package/README.md +21 -5
  2. package/bundled/@yep-anywhere/shared/dist/agent-context.d.ts +9 -0
  3. package/bundled/@yep-anywhere/shared/dist/agent-context.d.ts.map +1 -0
  4. package/bundled/@yep-anywhere/shared/dist/agent-context.js +18 -0
  5. package/bundled/@yep-anywhere/shared/dist/agent-context.js.map +1 -0
  6. package/bundled/@yep-anywhere/shared/dist/app-types.d.ts +104 -1
  7. package/bundled/@yep-anywhere/shared/dist/app-types.d.ts.map +1 -1
  8. package/bundled/@yep-anywhere/shared/dist/app-types.js +4 -0
  9. package/bundled/@yep-anywhere/shared/dist/app-types.js.map +1 -1
  10. package/bundled/@yep-anywhere/shared/dist/binary-framing.d.ts +4 -1
  11. package/bundled/@yep-anywhere/shared/dist/binary-framing.d.ts.map +1 -1
  12. package/bundled/@yep-anywhere/shared/dist/binary-framing.js +8 -3
  13. package/bundled/@yep-anywhere/shared/dist/binary-framing.js.map +1 -1
  14. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/AssistantEntrySchema.d.ts +4 -0
  15. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/AssistantEntrySchema.d.ts.map +1 -1
  16. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/BaseEntrySchema.d.ts +4 -0
  17. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/BaseEntrySchema.d.ts.map +1 -1
  18. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/BaseEntrySchema.js +7 -0
  19. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/BaseEntrySchema.js.map +1 -1
  20. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/ProgressEntrySchema.d.ts +4 -0
  21. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/ProgressEntrySchema.d.ts.map +1 -1
  22. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/SystemEntrySchema.d.ts +64 -1
  23. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/SystemEntrySchema.d.ts.map +1 -1
  24. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/SystemEntrySchema.js +9 -0
  25. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/SystemEntrySchema.js.map +1 -1
  26. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/UserEntrySchema.d.ts +4 -0
  27. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/entry/UserEntrySchema.d.ts.map +1 -1
  28. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/guards.d.ts.map +1 -1
  29. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/guards.js +45 -1
  30. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/guards.js.map +1 -1
  31. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/index.d.ts +144 -2
  32. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/index.d.ts.map +1 -1
  33. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/tool/ToolResultSchemas.d.ts +6 -4
  34. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/tool/ToolResultSchemas.d.ts.map +1 -1
  35. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/tool/ToolResultSchemas.js +8 -1
  36. package/bundled/@yep-anywhere/shared/dist/claude-sdk-schema/tool/ToolResultSchemas.js.map +1 -1
  37. package/bundled/@yep-anywhere/shared/dist/codex-schema/index.d.ts +3 -2
  38. package/bundled/@yep-anywhere/shared/dist/codex-schema/index.d.ts.map +1 -1
  39. package/bundled/@yep-anywhere/shared/dist/codex-schema/index.js +3 -2
  40. package/bundled/@yep-anywhere/shared/dist/codex-schema/index.js.map +1 -1
  41. package/bundled/@yep-anywhere/shared/dist/codex-schema/session.d.ts +87 -3
  42. package/bundled/@yep-anywhere/shared/dist/codex-schema/session.d.ts.map +1 -1
  43. package/bundled/@yep-anywhere/shared/dist/codex-schema/session.js +39 -1
  44. package/bundled/@yep-anywhere/shared/dist/codex-schema/session.js.map +1 -1
  45. package/bundled/@yep-anywhere/shared/dist/dag.d.ts +31 -11
  46. package/bundled/@yep-anywhere/shared/dist/dag.d.ts.map +1 -1
  47. package/bundled/@yep-anywhere/shared/dist/dag.js +77 -33
  48. package/bundled/@yep-anywhere/shared/dist/dag.js.map +1 -1
  49. package/bundled/@yep-anywhere/shared/dist/git-status.d.ts +70 -1
  50. package/bundled/@yep-anywhere/shared/dist/git-status.d.ts.map +1 -1
  51. package/bundled/@yep-anywhere/shared/dist/git-status.js +6 -1
  52. package/bundled/@yep-anywhere/shared/dist/git-status.js.map +1 -1
  53. package/bundled/@yep-anywhere/shared/dist/ideMetadata.js +1 -1
  54. package/bundled/@yep-anywhere/shared/dist/ideMetadata.js.map +1 -1
  55. package/bundled/@yep-anywhere/shared/dist/index.d.ts +20 -11
  56. package/bundled/@yep-anywhere/shared/dist/index.d.ts.map +1 -1
  57. package/bundled/@yep-anywhere/shared/dist/index.js +8 -3
  58. package/bundled/@yep-anywhere/shared/dist/index.js.map +1 -1
  59. package/bundled/@yep-anywhere/shared/dist/local-resource.d.ts +1 -0
  60. package/bundled/@yep-anywhere/shared/dist/local-resource.d.ts.map +1 -1
  61. package/bundled/@yep-anywhere/shared/dist/local-resource.js +4 -0
  62. package/bundled/@yep-anywhere/shared/dist/local-resource.js.map +1 -1
  63. package/bundled/@yep-anywhere/shared/dist/opencode-schema/events.d.ts +136 -0
  64. package/bundled/@yep-anywhere/shared/dist/opencode-schema/events.d.ts.map +1 -1
  65. package/bundled/@yep-anywhere/shared/dist/opencode-schema/events.js +65 -3
  66. package/bundled/@yep-anywhere/shared/dist/opencode-schema/events.js.map +1 -1
  67. package/bundled/@yep-anywhere/shared/dist/opencode-schema/types.d.ts +1 -1
  68. package/bundled/@yep-anywhere/shared/dist/opencode-schema/types.d.ts.map +1 -1
  69. package/bundled/@yep-anywhere/shared/dist/project-queue.d.ts +153 -0
  70. package/bundled/@yep-anywhere/shared/dist/project-queue.d.ts.map +1 -0
  71. package/bundled/@yep-anywhere/shared/dist/project-queue.js +8 -0
  72. package/bundled/@yep-anywhere/shared/dist/project-queue.js.map +1 -0
  73. package/bundled/@yep-anywhere/shared/dist/relay-protocol.d.ts +26 -2
  74. package/bundled/@yep-anywhere/shared/dist/relay-protocol.d.ts.map +1 -1
  75. package/bundled/@yep-anywhere/shared/dist/relay-protocol.js +33 -0
  76. package/bundled/@yep-anywhere/shared/dist/relay-protocol.js.map +1 -1
  77. package/bundled/@yep-anywhere/shared/dist/relay.d.ts +45 -5
  78. package/bundled/@yep-anywhere/shared/dist/relay.d.ts.map +1 -1
  79. package/bundled/@yep-anywhere/shared/dist/relay.js.map +1 -1
  80. package/bundled/@yep-anywhere/shared/dist/safe-restart.d.ts +25 -0
  81. package/bundled/@yep-anywhere/shared/dist/safe-restart.d.ts.map +1 -0
  82. package/bundled/@yep-anywhere/shared/dist/safe-restart.js +2 -0
  83. package/bundled/@yep-anywhere/shared/dist/safe-restart.js.map +1 -0
  84. package/bundled/@yep-anywhere/shared/dist/session/SessionView.d.ts +2 -2
  85. package/bundled/@yep-anywhere/shared/dist/session/SessionView.js +3 -3
  86. package/bundled/@yep-anywhere/shared/dist/session/SessionView.js.map +1 -1
  87. package/bundled/@yep-anywhere/shared/dist/session/UnifiedSession.d.ts +10 -0
  88. package/bundled/@yep-anywhere/shared/dist/session/UnifiedSession.d.ts.map +1 -1
  89. package/bundled/@yep-anywhere/shared/dist/session-liveness.d.ts +18 -0
  90. package/bundled/@yep-anywhere/shared/dist/session-liveness.d.ts.map +1 -1
  91. package/bundled/@yep-anywhere/shared/dist/types.d.ts +253 -8
  92. package/bundled/@yep-anywhere/shared/dist/types.d.ts.map +1 -1
  93. package/bundled/@yep-anywhere/shared/dist/types.js +48 -5
  94. package/bundled/@yep-anywhere/shared/dist/types.js.map +1 -1
  95. package/bundled/@yep-anywhere/shared/dist/upload.d.ts +34 -2
  96. package/bundled/@yep-anywhere/shared/dist/upload.d.ts.map +1 -1
  97. package/bundled/@yep-anywhere/shared/dist/user-message-metadata.d.ts +47 -0
  98. package/bundled/@yep-anywhere/shared/dist/user-message-metadata.d.ts.map +1 -1
  99. package/bundled/@yep-anywhere/shared/dist/user-message-metadata.js +59 -1
  100. package/bundled/@yep-anywhere/shared/dist/user-message-metadata.js.map +1 -1
  101. package/bundled/@yep-anywhere/shared/dist/workstreams.d.ts +54 -0
  102. package/bundled/@yep-anywhere/shared/dist/workstreams.d.ts.map +1 -0
  103. package/bundled/@yep-anywhere/shared/dist/workstreams.js +8 -0
  104. package/bundled/@yep-anywhere/shared/dist/workstreams.js.map +1 -0
  105. package/bundled/@yep-anywhere/shared/package.json +1 -1
  106. package/client-dist/assets/alegreya-sans-latin-400-italic-BQIUBrX-.woff2 +0 -0
  107. package/client-dist/assets/alegreya-sans-latin-400-normal-BxeZdXas.woff2 +0 -0
  108. package/client-dist/assets/alegreya-sans-latin-500-normal-uodOEoAl.woff2 +0 -0
  109. package/client-dist/assets/alegreya-sans-latin-700-normal-DFYOGfC8.woff2 +0 -0
  110. package/client-dist/assets/de-CHMDbVD5.js +6 -0
  111. package/client-dist/assets/es-CK8MPF_C.js +6 -0
  112. package/client-dist/assets/fira-mono-latin-400-normal-DVTTRLHv.woff2 +0 -0
  113. package/client-dist/assets/fira-mono-latin-ext-400-normal-B2gPvaNr.woff2 +0 -0
  114. package/client-dist/assets/fira-mono-symbols2-400-normal-C6JptOil.woff2 +0 -0
  115. package/client-dist/assets/fr-49PXQiUL.js +6 -0
  116. package/client-dist/assets/index-B-09fVut.js +557 -0
  117. package/client-dist/assets/index-uJnfvD1f.css +1 -0
  118. package/client-dist/assets/inter-latin-400-italic-D1l0FflY.woff2 +0 -0
  119. package/client-dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  120. package/client-dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  121. package/client-dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  122. package/client-dist/assets/ja-CrCGPmoH.js +6 -0
  123. package/client-dist/assets/zh-CN-AFJxv5ke.js +6 -0
  124. package/client-dist/index.html +3 -3
  125. package/client-dist/sw.js +96 -9
  126. package/dist/app.d.ts +25 -0
  127. package/dist/app.d.ts.map +1 -1
  128. package/dist/app.js +350 -35
  129. package/dist/app.js.map +1 -1
  130. package/dist/augments/augment-generator.d.ts +1 -1
  131. package/dist/augments/augment-generator.d.ts.map +1 -1
  132. package/dist/augments/augment-generator.js +2 -2
  133. package/dist/augments/augment-generator.js.map +1 -1
  134. package/dist/augments/markdown-augments.d.ts +1 -1
  135. package/dist/augments/markdown-augments.d.ts.map +1 -1
  136. package/dist/augments/markdown-augments.js +5 -5
  137. package/dist/augments/markdown-augments.js.map +1 -1
  138. package/dist/augments/safe-markdown.d.ts +10 -0
  139. package/dist/augments/safe-markdown.d.ts.map +1 -1
  140. package/dist/augments/safe-markdown.js +146 -3
  141. package/dist/augments/safe-markdown.js.map +1 -1
  142. package/dist/augments/stream-augmenter.d.ts +3 -0
  143. package/dist/augments/stream-augmenter.d.ts.map +1 -1
  144. package/dist/augments/stream-augmenter.js +6 -3
  145. package/dist/augments/stream-augmenter.js.map +1 -1
  146. package/dist/augments/stream-coordinator.d.ts +5 -1
  147. package/dist/augments/stream-coordinator.d.ts.map +1 -1
  148. package/dist/augments/stream-coordinator.js +6 -4
  149. package/dist/augments/stream-coordinator.js.map +1 -1
  150. package/dist/augments/task-list-augments.d.ts +27 -0
  151. package/dist/augments/task-list-augments.d.ts.map +1 -0
  152. package/dist/augments/task-list-augments.js +360 -0
  153. package/dist/augments/task-list-augments.js.map +1 -0
  154. package/dist/cli.d.ts +1 -17
  155. package/dist/cli.d.ts.map +1 -1
  156. package/dist/cli.js +22 -8
  157. package/dist/cli.js.map +1 -1
  158. package/dist/codex/correlationDebugLogger.d.ts.map +1 -1
  159. package/dist/codex/correlationDebugLogger.js +36 -8
  160. package/dist/codex/correlationDebugLogger.js.map +1 -1
  161. package/dist/codex/normalization.d.ts +1 -0
  162. package/dist/codex/normalization.d.ts.map +1 -1
  163. package/dist/codex/normalization.js +262 -7
  164. package/dist/codex/normalization.js.map +1 -1
  165. package/dist/config.d.ts +49 -6
  166. package/dist/config.d.ts.map +1 -1
  167. package/dist/config.js +55 -14
  168. package/dist/config.js.map +1 -1
  169. package/dist/defaults.d.ts +3 -0
  170. package/dist/defaults.d.ts.map +1 -0
  171. package/dist/defaults.js +3 -0
  172. package/dist/defaults.js.map +1 -0
  173. package/dist/device/DeviceBridgeService.js +1 -1
  174. package/dist/device/DeviceBridgeService.js.map +1 -1
  175. package/dist/envSettings.d.ts +64 -0
  176. package/dist/envSettings.d.ts.map +1 -0
  177. package/dist/envSettings.js +553 -0
  178. package/dist/envSettings.js.map +1 -0
  179. package/dist/frontend/static.d.ts.map +1 -1
  180. package/dist/frontend/static.js +3 -1
  181. package/dist/frontend/static.js.map +1 -1
  182. package/dist/index.d.ts +1 -1
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +190 -14
  185. package/dist/index.js.map +1 -1
  186. package/dist/indexes/SessionDiscoveryIndex.d.ts +78 -0
  187. package/dist/indexes/SessionDiscoveryIndex.d.ts.map +1 -0
  188. package/dist/indexes/SessionDiscoveryIndex.js +223 -0
  189. package/dist/indexes/SessionDiscoveryIndex.js.map +1 -0
  190. package/dist/indexes/SessionIndexService.d.ts +114 -1
  191. package/dist/indexes/SessionIndexService.d.ts.map +1 -1
  192. package/dist/indexes/SessionIndexService.js +875 -105
  193. package/dist/indexes/SessionIndexService.js.map +1 -1
  194. package/dist/indexes/index.d.ts +2 -1
  195. package/dist/indexes/index.d.ts.map +1 -1
  196. package/dist/indexes/index.js +1 -0
  197. package/dist/indexes/index.js.map +1 -1
  198. package/dist/indexes/types.d.ts +26 -0
  199. package/dist/indexes/types.d.ts.map +1 -1
  200. package/dist/logging/logger.d.ts.map +1 -1
  201. package/dist/logging/logger.js +2 -3
  202. package/dist/logging/logger.js.map +1 -1
  203. package/dist/maintenance/server.d.ts.map +1 -1
  204. package/dist/maintenance/server.js +8 -11
  205. package/dist/maintenance/server.js.map +1 -1
  206. package/dist/metadata/ProjectMetadataService.d.ts +7 -0
  207. package/dist/metadata/ProjectMetadataService.d.ts.map +1 -1
  208. package/dist/metadata/ProjectMetadataService.js +63 -19
  209. package/dist/metadata/ProjectMetadataService.js.map +1 -1
  210. package/dist/metadata/SessionMetadataService.d.ts +82 -3
  211. package/dist/metadata/SessionMetadataService.d.ts.map +1 -1
  212. package/dist/metadata/SessionMetadataService.js +233 -17
  213. package/dist/metadata/SessionMetadataService.js.map +1 -1
  214. package/dist/middleware/file-access.d.ts +66 -0
  215. package/dist/middleware/file-access.d.ts.map +1 -0
  216. package/dist/middleware/file-access.js +112 -0
  217. package/dist/middleware/file-access.js.map +1 -0
  218. package/dist/projects/codex-scanner.d.ts +28 -1
  219. package/dist/projects/codex-scanner.d.ts.map +1 -1
  220. package/dist/projects/codex-scanner.js +149 -39
  221. package/dist/projects/codex-scanner.js.map +1 -1
  222. package/dist/projects/gemini-scanner.d.ts.map +1 -1
  223. package/dist/projects/gemini-scanner.js +59 -6
  224. package/dist/projects/gemini-scanner.js.map +1 -1
  225. package/dist/projects/paths.d.ts +15 -1
  226. package/dist/projects/paths.d.ts.map +1 -1
  227. package/dist/projects/paths.js +69 -23
  228. package/dist/projects/paths.js.map +1 -1
  229. package/dist/projects/scanner.d.ts +5 -0
  230. package/dist/projects/scanner.d.ts.map +1 -1
  231. package/dist/projects/scanner.js +136 -28
  232. package/dist/projects/scanner.js.map +1 -1
  233. package/dist/push/InactivityPushNotifier.d.ts +61 -0
  234. package/dist/push/InactivityPushNotifier.d.ts.map +1 -0
  235. package/dist/push/InactivityPushNotifier.js +305 -0
  236. package/dist/push/InactivityPushNotifier.js.map +1 -0
  237. package/dist/push/PushNotifier.d.ts.map +1 -1
  238. package/dist/push/PushNotifier.js.map +1 -1
  239. package/dist/push/PushService.d.ts +7 -4
  240. package/dist/push/PushService.d.ts.map +1 -1
  241. package/dist/push/PushService.js +27 -7
  242. package/dist/push/PushService.js.map +1 -1
  243. package/dist/push/index.d.ts +2 -1
  244. package/dist/push/index.d.ts.map +1 -1
  245. package/dist/push/index.js +1 -0
  246. package/dist/push/index.js.map +1 -1
  247. package/dist/push/routes.d.ts.map +1 -1
  248. package/dist/push/routes.js +28 -1
  249. package/dist/push/routes.js.map +1 -1
  250. package/dist/push/types.d.ts +21 -3
  251. package/dist/push/types.d.ts.map +1 -1
  252. package/dist/push/types.js +2 -0
  253. package/dist/push/types.js.map +1 -1
  254. package/dist/routes/dev.d.ts +2 -0
  255. package/dist/routes/dev.d.ts.map +1 -1
  256. package/dist/routes/dev.js +25 -0
  257. package/dist/routes/dev.js.map +1 -1
  258. package/dist/routes/env-settings.d.ts +20 -0
  259. package/dist/routes/env-settings.d.ts.map +1 -0
  260. package/dist/routes/env-settings.js +25 -0
  261. package/dist/routes/env-settings.js.map +1 -0
  262. package/dist/routes/files.d.ts +9 -0
  263. package/dist/routes/files.d.ts.map +1 -1
  264. package/dist/routes/files.js +98 -10
  265. package/dist/routes/files.js.map +1 -1
  266. package/dist/routes/git-status.d.ts.map +1 -1
  267. package/dist/routes/git-status.js +621 -23
  268. package/dist/routes/git-status.js.map +1 -1
  269. package/dist/routes/global-sessions.d.ts +11 -1
  270. package/dist/routes/global-sessions.d.ts.map +1 -1
  271. package/dist/routes/global-sessions.js +53 -33
  272. package/dist/routes/global-sessions.js.map +1 -1
  273. package/dist/routes/inbox.d.ts +10 -1
  274. package/dist/routes/inbox.d.ts.map +1 -1
  275. package/dist/routes/inbox.js +30 -3
  276. package/dist/routes/inbox.js.map +1 -1
  277. package/dist/routes/local-file.d.ts +6 -2
  278. package/dist/routes/local-file.d.ts.map +1 -1
  279. package/dist/routes/local-file.js +13 -4
  280. package/dist/routes/local-file.js.map +1 -1
  281. package/dist/routes/local-image.d.ts +2 -1
  282. package/dist/routes/local-image.d.ts.map +1 -1
  283. package/dist/routes/local-image.js.map +1 -1
  284. package/dist/routes/local-resource-policy.d.ts +7 -1
  285. package/dist/routes/local-resource-policy.d.ts.map +1 -1
  286. package/dist/routes/local-resource-policy.js +0 -0
  287. package/dist/routes/local-resource-policy.js.map +1 -1
  288. package/dist/routes/processes.d.ts.map +1 -1
  289. package/dist/routes/processes.js +49 -11
  290. package/dist/routes/processes.js.map +1 -1
  291. package/dist/routes/project-queue-response.d.ts +26 -0
  292. package/dist/routes/project-queue-response.d.ts.map +1 -0
  293. package/dist/routes/project-queue-response.js +188 -0
  294. package/dist/routes/project-queue-response.js.map +1 -0
  295. package/dist/routes/project-queue.d.ts +5 -0
  296. package/dist/routes/project-queue.d.ts.map +1 -0
  297. package/dist/routes/project-queue.js +219 -0
  298. package/dist/routes/project-queue.js.map +1 -0
  299. package/dist/routes/projects.d.ts +6 -0
  300. package/dist/routes/projects.d.ts.map +1 -1
  301. package/dist/routes/projects.js +116 -28
  302. package/dist/routes/projects.js.map +1 -1
  303. package/dist/routes/providers.d.ts +5 -0
  304. package/dist/routes/providers.d.ts.map +1 -1
  305. package/dist/routes/providers.js +52 -33
  306. package/dist/routes/providers.js.map +1 -1
  307. package/dist/routes/public-shares.d.ts.map +1 -1
  308. package/dist/routes/public-shares.js +46 -17
  309. package/dist/routes/public-shares.js.map +1 -1
  310. package/dist/routes/recents.d.ts +3 -0
  311. package/dist/routes/recents.d.ts.map +1 -1
  312. package/dist/routes/recents.js +3 -1
  313. package/dist/routes/recents.js.map +1 -1
  314. package/dist/routes/server-admin.d.ts +9 -0
  315. package/dist/routes/server-admin.d.ts.map +1 -1
  316. package/dist/routes/server-admin.js +45 -7
  317. package/dist/routes/server-admin.js.map +1 -1
  318. package/dist/routes/session-claude-resume-guard.d.ts +16 -0
  319. package/dist/routes/session-claude-resume-guard.d.ts.map +1 -0
  320. package/dist/routes/session-claude-resume-guard.js +50 -0
  321. package/dist/routes/session-claude-resume-guard.js.map +1 -0
  322. package/dist/routes/session-compact-thresholds.d.ts +22 -0
  323. package/dist/routes/session-compact-thresholds.d.ts.map +1 -0
  324. package/dist/routes/session-compact-thresholds.js +35 -0
  325. package/dist/routes/session-compact-thresholds.js.map +1 -0
  326. package/dist/routes/session-index.d.ts +7 -0
  327. package/dist/routes/session-index.d.ts.map +1 -0
  328. package/dist/routes/session-index.js +9 -0
  329. package/dist/routes/session-index.js.map +1 -0
  330. package/dist/routes/session-list-options.d.ts +1 -1
  331. package/dist/routes/session-list-options.d.ts.map +1 -1
  332. package/dist/routes/session-list-options.js +1 -1
  333. package/dist/routes/session-list-options.js.map +1 -1
  334. package/dist/routes/session-request-helpers.d.ts +31 -0
  335. package/dist/routes/session-request-helpers.d.ts.map +1 -0
  336. package/dist/routes/session-request-helpers.js +212 -0
  337. package/dist/routes/session-request-helpers.js.map +1 -0
  338. package/dist/routes/sessions.d.ts +14 -1
  339. package/dist/routes/sessions.d.ts.map +1 -1
  340. package/dist/routes/sessions.js +2141 -551
  341. package/dist/routes/sessions.js.map +1 -1
  342. package/dist/routes/settings.d.ts +7 -1
  343. package/dist/routes/settings.d.ts.map +1 -1
  344. package/dist/routes/settings.js +771 -25
  345. package/dist/routes/settings.js.map +1 -1
  346. package/dist/routes/speech.d.ts +24 -2
  347. package/dist/routes/speech.d.ts.map +1 -1
  348. package/dist/routes/speech.js +539 -10
  349. package/dist/routes/speech.js.map +1 -1
  350. package/dist/routes/supervisor-queue.d.ts +6 -0
  351. package/dist/routes/supervisor-queue.d.ts.map +1 -0
  352. package/dist/routes/supervisor-queue.js +35 -0
  353. package/dist/routes/supervisor-queue.js.map +1 -0
  354. package/dist/routes/upload.d.ts +2 -0
  355. package/dist/routes/upload.d.ts.map +1 -1
  356. package/dist/routes/upload.js +190 -38
  357. package/dist/routes/upload.js.map +1 -1
  358. package/dist/routes/version.d.ts +18 -1
  359. package/dist/routes/version.d.ts.map +1 -1
  360. package/dist/routes/version.js +22 -1
  361. package/dist/routes/version.js.map +1 -1
  362. package/dist/routes/workstreams.d.ts +11 -0
  363. package/dist/routes/workstreams.d.ts.map +1 -0
  364. package/dist/routes/workstreams.js +144 -0
  365. package/dist/routes/workstreams.js.map +1 -0
  366. package/dist/routes/ws-message-router.d.ts +10 -1
  367. package/dist/routes/ws-message-router.d.ts.map +1 -1
  368. package/dist/routes/ws-message-router.js +43 -4
  369. package/dist/routes/ws-message-router.js.map +1 -1
  370. package/dist/routes/ws-relay-handlers.d.ts +27 -5
  371. package/dist/routes/ws-relay-handlers.d.ts.map +1 -1
  372. package/dist/routes/ws-relay-handlers.js +177 -19
  373. package/dist/routes/ws-relay-handlers.js.map +1 -1
  374. package/dist/routes/ws-relay.d.ts +19 -0
  375. package/dist/routes/ws-relay.d.ts.map +1 -1
  376. package/dist/routes/ws-relay.js +21 -7
  377. package/dist/routes/ws-relay.js.map +1 -1
  378. package/dist/sdk/cli-detection.d.ts +1 -1
  379. package/dist/sdk/cli-detection.d.ts.map +1 -1
  380. package/dist/sdk/cli-detection.js +47 -7
  381. package/dist/sdk/cli-detection.js.map +1 -1
  382. package/dist/sdk/messageLogger.d.ts.map +1 -1
  383. package/dist/sdk/messageLogger.js +38 -10
  384. package/dist/sdk/messageLogger.js.map +1 -1
  385. package/dist/sdk/messageQueue.d.ts.map +1 -1
  386. package/dist/sdk/messageQueue.js +26 -0
  387. package/dist/sdk/messageQueue.js.map +1 -1
  388. package/dist/sdk/providers/__mocks__/grok.d.ts +1 -1
  389. package/dist/sdk/providers/__mocks__/grok.d.ts.map +1 -1
  390. package/dist/sdk/providers/__mocks__/grok.js +1 -1
  391. package/dist/sdk/providers/__mocks__/grok.js.map +1 -1
  392. package/dist/sdk/providers/agentctl-session-env.d.ts +8 -0
  393. package/dist/sdk/providers/agentctl-session-env.d.ts.map +1 -0
  394. package/dist/sdk/providers/agentctl-session-env.js +52 -0
  395. package/dist/sdk/providers/agentctl-session-env.js.map +1 -0
  396. package/dist/sdk/providers/claude-ollama.d.ts +2 -1
  397. package/dist/sdk/providers/claude-ollama.d.ts.map +1 -1
  398. package/dist/sdk/providers/claude-ollama.js +1 -0
  399. package/dist/sdk/providers/claude-ollama.js.map +1 -1
  400. package/dist/sdk/providers/claude-retention.d.ts +19 -0
  401. package/dist/sdk/providers/claude-retention.d.ts.map +1 -0
  402. package/dist/sdk/providers/claude-retention.js +169 -0
  403. package/dist/sdk/providers/claude-retention.js.map +1 -0
  404. package/dist/sdk/providers/claude.d.ts +52 -5
  405. package/dist/sdk/providers/claude.d.ts.map +1 -1
  406. package/dist/sdk/providers/claude.js +593 -71
  407. package/dist/sdk/providers/claude.js.map +1 -1
  408. package/dist/sdk/providers/codex-protocol/generated/AgentMessageInputContent.d.ts +8 -0
  409. package/dist/sdk/providers/codex-protocol/generated/AgentMessageInputContent.d.ts.map +1 -0
  410. package/dist/sdk/providers/codex-protocol/generated/AgentMessageInputContent.js +3 -0
  411. package/dist/sdk/providers/codex-protocol/generated/AgentMessageInputContent.js.map +1 -0
  412. package/dist/sdk/providers/codex-protocol/generated/InternalChatMessageMetadataPassthrough.d.ts +10 -0
  413. package/dist/sdk/providers/codex-protocol/generated/InternalChatMessageMetadataPassthrough.d.ts.map +1 -0
  414. package/dist/sdk/providers/codex-protocol/generated/InternalChatMessageMetadataPassthrough.js +3 -0
  415. package/dist/sdk/providers/codex-protocol/generated/InternalChatMessageMetadataPassthrough.js.map +1 -0
  416. package/dist/sdk/providers/codex-protocol/generated/LegacyAppPathString.d.ts +23 -0
  417. package/dist/sdk/providers/codex-protocol/generated/LegacyAppPathString.d.ts.map +1 -0
  418. package/dist/sdk/providers/codex-protocol/generated/LegacyAppPathString.js +3 -0
  419. package/dist/sdk/providers/codex-protocol/generated/LegacyAppPathString.js.map +1 -0
  420. package/dist/sdk/providers/codex-protocol/generated/MultiAgentMode.d.ts +8 -0
  421. package/dist/sdk/providers/codex-protocol/generated/MultiAgentMode.d.ts.map +1 -0
  422. package/dist/sdk/providers/codex-protocol/generated/MultiAgentMode.js +3 -0
  423. package/dist/sdk/providers/codex-protocol/generated/MultiAgentMode.js.map +1 -0
  424. package/dist/sdk/providers/codex-protocol/generated/ReasoningEffort.d.ts +1 -1
  425. package/dist/sdk/providers/codex-protocol/generated/ReasoningEffort.d.ts.map +1 -1
  426. package/dist/sdk/providers/codex-protocol/generated/ResponseItem.d.ts +38 -1
  427. package/dist/sdk/providers/codex-protocol/generated/ResponseItem.d.ts.map +1 -1
  428. package/dist/sdk/providers/codex-protocol/generated/v2/AdditionalFileSystemPermissions.d.ts +3 -3
  429. package/dist/sdk/providers/codex-protocol/generated/v2/AdditionalFileSystemPermissions.d.ts.map +1 -1
  430. package/dist/sdk/providers/codex-protocol/generated/v2/CapabilityRootLocation.d.ts +9 -0
  431. package/dist/sdk/providers/codex-protocol/generated/v2/CapabilityRootLocation.d.ts.map +1 -0
  432. package/dist/sdk/providers/codex-protocol/generated/v2/CapabilityRootLocation.js +3 -0
  433. package/dist/sdk/providers/codex-protocol/generated/v2/CapabilityRootLocation.js.map +1 -0
  434. package/dist/sdk/providers/codex-protocol/generated/v2/CommandExecutionRequestApprovalParams.d.ts +6 -2
  435. package/dist/sdk/providers/codex-protocol/generated/v2/CommandExecutionRequestApprovalParams.d.ts.map +1 -1
  436. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolFunctionSpec.d.ts +8 -0
  437. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolFunctionSpec.d.ts.map +1 -0
  438. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolFunctionSpec.js +3 -0
  439. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolFunctionSpec.js.map +1 -0
  440. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceSpec.d.ts +7 -0
  441. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceSpec.d.ts.map +1 -0
  442. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceSpec.js +3 -0
  443. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceSpec.js.map +1 -0
  444. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceTool.d.ts +5 -0
  445. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceTool.d.ts.map +1 -0
  446. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceTool.js +3 -0
  447. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolNamespaceTool.js.map +1 -0
  448. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolSpec.d.ts +6 -7
  449. package/dist/sdk/providers/codex-protocol/generated/v2/DynamicToolSpec.d.ts.map +1 -1
  450. package/dist/sdk/providers/codex-protocol/generated/v2/FileSystemPath.d.ts +2 -2
  451. package/dist/sdk/providers/codex-protocol/generated/v2/FileSystemPath.d.ts.map +1 -1
  452. package/dist/sdk/providers/codex-protocol/generated/v2/McpToolCallAppContext.d.ts +6 -0
  453. package/dist/sdk/providers/codex-protocol/generated/v2/McpToolCallAppContext.d.ts.map +1 -0
  454. package/dist/sdk/providers/codex-protocol/generated/v2/McpToolCallAppContext.js +3 -0
  455. package/dist/sdk/providers/codex-protocol/generated/v2/McpToolCallAppContext.js.map +1 -0
  456. package/dist/sdk/providers/codex-protocol/generated/v2/PermissionsRequestApprovalParams.d.ts +1 -0
  457. package/dist/sdk/providers/codex-protocol/generated/v2/PermissionsRequestApprovalParams.d.ts.map +1 -1
  458. package/dist/sdk/providers/codex-protocol/generated/v2/SelectedCapabilityRoot.d.ts +15 -0
  459. package/dist/sdk/providers/codex-protocol/generated/v2/SelectedCapabilityRoot.d.ts.map +1 -0
  460. package/dist/sdk/providers/codex-protocol/generated/v2/SelectedCapabilityRoot.js +3 -0
  461. package/dist/sdk/providers/codex-protocol/generated/v2/SelectedCapabilityRoot.js.map +1 -0
  462. package/dist/sdk/providers/codex-protocol/generated/v2/SortDirection.d.ts +2 -0
  463. package/dist/sdk/providers/codex-protocol/generated/v2/SortDirection.d.ts.map +1 -0
  464. package/dist/sdk/providers/codex-protocol/generated/v2/SortDirection.js +3 -0
  465. package/dist/sdk/providers/codex-protocol/generated/v2/SortDirection.js.map +1 -0
  466. package/dist/sdk/providers/codex-protocol/generated/v2/SubAgentActivityKind.d.ts +2 -0
  467. package/dist/sdk/providers/codex-protocol/generated/v2/SubAgentActivityKind.d.ts.map +1 -0
  468. package/dist/sdk/providers/codex-protocol/generated/v2/SubAgentActivityKind.js +3 -0
  469. package/dist/sdk/providers/codex-protocol/generated/v2/SubAgentActivityKind.js.map +1 -0
  470. package/dist/sdk/providers/codex-protocol/generated/v2/Thread.d.ts +8 -0
  471. package/dist/sdk/providers/codex-protocol/generated/v2/Thread.d.ts.map +1 -1
  472. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartParams.d.ts +4 -0
  473. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartParams.d.ts.map +1 -0
  474. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartParams.js +3 -0
  475. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartParams.js.map +1 -0
  476. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartResponse.d.ts +2 -0
  477. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartResponse.d.ts.map +1 -0
  478. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartResponse.js +3 -0
  479. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadCompactStartResponse.js.map +1 -0
  480. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkParams.d.ts +64 -0
  481. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkParams.d.ts.map +1 -0
  482. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkParams.js +3 -0
  483. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkParams.js.map +1 -0
  484. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkResponse.d.ts +46 -0
  485. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkResponse.d.ts.map +1 -0
  486. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkResponse.js +3 -0
  487. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadForkResponse.js.map +1 -0
  488. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadItem.d.ts +19 -1
  489. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadItem.d.ts.map +1 -1
  490. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadReadResponse.d.ts +5 -0
  491. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadReadResponse.d.ts.map +1 -0
  492. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadReadResponse.js +3 -0
  493. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadReadResponse.js.map +1 -0
  494. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeInitialTurnsPageParams.d.ts +17 -0
  495. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeInitialTurnsPageParams.d.ts.map +1 -0
  496. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeInitialTurnsPageParams.js +3 -0
  497. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeInitialTurnsPageParams.js.map +1 -0
  498. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeParams.d.ts +7 -7
  499. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeParams.d.ts.map +1 -1
  500. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeResponse.d.ts +13 -2
  501. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadResumeResponse.d.ts.map +1 -1
  502. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackParams.d.ts +11 -0
  503. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackParams.d.ts.map +1 -0
  504. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackParams.js +3 -0
  505. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackParams.js.map +1 -0
  506. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackResponse.d.ts +12 -0
  507. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackResponse.d.ts.map +1 -0
  508. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackResponse.js +3 -0
  509. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadRollbackResponse.js.map +1 -0
  510. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadSource.d.ts +1 -1
  511. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadSource.d.ts.map +1 -1
  512. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadStartParams.d.ts +13 -9
  513. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadStartParams.d.ts.map +1 -1
  514. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadStartResponse.d.ts +8 -2
  515. package/dist/sdk/providers/codex-protocol/generated/v2/ThreadStartResponse.d.ts.map +1 -1
  516. package/dist/sdk/providers/codex-protocol/generated/v2/ToolRequestUserInputParams.d.ts +1 -0
  517. package/dist/sdk/providers/codex-protocol/generated/v2/ToolRequestUserInputParams.d.ts.map +1 -1
  518. package/dist/sdk/providers/codex-protocol/generated/v2/TurnEnvironmentParams.d.ts +2 -2
  519. package/dist/sdk/providers/codex-protocol/generated/v2/TurnEnvironmentParams.d.ts.map +1 -1
  520. package/dist/sdk/providers/codex-protocol/generated/v2/TurnStartParams.d.ts +17 -5
  521. package/dist/sdk/providers/codex-protocol/generated/v2/TurnStartParams.d.ts.map +1 -1
  522. package/dist/sdk/providers/codex-protocol/generated/v2/TurnSteerParams.d.ts +8 -1
  523. package/dist/sdk/providers/codex-protocol/generated/v2/TurnSteerParams.d.ts.map +1 -1
  524. package/dist/sdk/providers/codex-protocol/generated/v2/TurnsPage.d.ts +7 -0
  525. package/dist/sdk/providers/codex-protocol/generated/v2/TurnsPage.d.ts.map +1 -0
  526. package/dist/sdk/providers/codex-protocol/generated/v2/TurnsPage.js +3 -0
  527. package/dist/sdk/providers/codex-protocol/generated/v2/TurnsPage.js.map +1 -0
  528. package/dist/sdk/providers/codex-protocol/index.d.ts +7 -0
  529. package/dist/sdk/providers/codex-protocol/index.d.ts.map +1 -1
  530. package/dist/sdk/providers/codex.d.ts +26 -4
  531. package/dist/sdk/providers/codex.d.ts.map +1 -1
  532. package/dist/sdk/providers/codex.js +457 -37
  533. package/dist/sdk/providers/codex.js.map +1 -1
  534. package/dist/sdk/providers/env-filter.d.ts.map +1 -1
  535. package/dist/sdk/providers/env-filter.js +19 -1
  536. package/dist/sdk/providers/env-filter.js.map +1 -1
  537. package/dist/sdk/providers/gemini-tools.d.ts +28 -0
  538. package/dist/sdk/providers/gemini-tools.d.ts.map +1 -0
  539. package/dist/sdk/providers/gemini-tools.js +57 -0
  540. package/dist/sdk/providers/gemini-tools.js.map +1 -0
  541. package/dist/sdk/providers/gemini.d.ts.map +1 -1
  542. package/dist/sdk/providers/gemini.js +4 -2
  543. package/dist/sdk/providers/gemini.js.map +1 -1
  544. package/dist/sdk/providers/grok-acp.d.ts +1 -1
  545. package/dist/sdk/providers/grok-acp.d.ts.map +1 -1
  546. package/dist/sdk/providers/grok-acp.js +1 -1
  547. package/dist/sdk/providers/grok-acp.js.map +1 -1
  548. package/dist/sdk/providers/index.d.ts +1 -0
  549. package/dist/sdk/providers/index.d.ts.map +1 -1
  550. package/dist/sdk/providers/index.js +6 -0
  551. package/dist/sdk/providers/index.js.map +1 -1
  552. package/dist/sdk/providers/opencode-tools.d.ts +36 -0
  553. package/dist/sdk/providers/opencode-tools.d.ts.map +1 -0
  554. package/dist/sdk/providers/opencode-tools.js +78 -0
  555. package/dist/sdk/providers/opencode-tools.js.map +1 -0
  556. package/dist/sdk/providers/opencode.d.ts +53 -5
  557. package/dist/sdk/providers/opencode.d.ts.map +1 -1
  558. package/dist/sdk/providers/opencode.js +379 -71
  559. package/dist/sdk/providers/opencode.js.map +1 -1
  560. package/dist/sdk/providers/pi-rpc-client.d.ts +90 -0
  561. package/dist/sdk/providers/pi-rpc-client.d.ts.map +1 -0
  562. package/dist/sdk/providers/pi-rpc-client.js +168 -0
  563. package/dist/sdk/providers/pi-rpc-client.js.map +1 -0
  564. package/dist/sdk/providers/pi-tools.d.ts +38 -0
  565. package/dist/sdk/providers/pi-tools.d.ts.map +1 -0
  566. package/dist/sdk/providers/pi-tools.js +214 -0
  567. package/dist/sdk/providers/pi-tools.js.map +1 -0
  568. package/dist/sdk/providers/pi.d.ts +84 -0
  569. package/dist/sdk/providers/pi.d.ts.map +1 -0
  570. package/dist/sdk/providers/pi.js +686 -0
  571. package/dist/sdk/providers/pi.js.map +1 -0
  572. package/dist/sdk/providers/staticSlashCommands.d.ts +4 -0
  573. package/dist/sdk/providers/staticSlashCommands.d.ts.map +1 -0
  574. package/dist/sdk/providers/staticSlashCommands.js +14 -0
  575. package/dist/sdk/providers/staticSlashCommands.js.map +1 -0
  576. package/dist/sdk/providers/types.d.ts +144 -17
  577. package/dist/sdk/providers/types.d.ts.map +1 -1
  578. package/dist/sdk/remote-spawn.d.ts.map +1 -1
  579. package/dist/sdk/remote-spawn.js.map +1 -1
  580. package/dist/sdk/slashCommandEmulation.d.ts +9 -0
  581. package/dist/sdk/slashCommandEmulation.d.ts.map +1 -1
  582. package/dist/sdk/slashCommandEmulation.js +15 -0
  583. package/dist/sdk/slashCommandEmulation.js.map +1 -1
  584. package/dist/sdk/types.d.ts +40 -0
  585. package/dist/sdk/types.d.ts.map +1 -1
  586. package/dist/security/approvalAuditLog.d.ts +3 -2
  587. package/dist/security/approvalAuditLog.d.ts.map +1 -1
  588. package/dist/security/approvalAuditLog.js +62 -2
  589. package/dist/security/approvalAuditLog.js.map +1 -1
  590. package/dist/services/BrowserProfileService.d.ts +21 -0
  591. package/dist/services/BrowserProfileService.d.ts.map +1 -1
  592. package/dist/services/BrowserProfileService.js +74 -1
  593. package/dist/services/BrowserProfileService.js.map +1 -1
  594. package/dist/services/CacheMissBillingMonitor.d.ts +25 -0
  595. package/dist/services/CacheMissBillingMonitor.d.ts.map +1 -0
  596. package/dist/services/CacheMissBillingMonitor.js +179 -0
  597. package/dist/services/CacheMissBillingMonitor.js.map +1 -0
  598. package/dist/services/CodexUpdateChecker.d.ts.map +1 -1
  599. package/dist/services/CodexUpdateChecker.js +3 -2
  600. package/dist/services/CodexUpdateChecker.js.map +1 -1
  601. package/dist/services/ModelInfoService.d.ts +58 -9
  602. package/dist/services/ModelInfoService.d.ts.map +1 -1
  603. package/dist/services/ModelInfoService.js +127 -14
  604. package/dist/services/ModelInfoService.js.map +1 -1
  605. package/dist/services/ProjectQueueScheduler.d.ts +91 -0
  606. package/dist/services/ProjectQueueScheduler.d.ts.map +1 -0
  607. package/dist/services/ProjectQueueScheduler.js +404 -0
  608. package/dist/services/ProjectQueueScheduler.js.map +1 -0
  609. package/dist/services/ProjectQueueService.d.ts +60 -0
  610. package/dist/services/ProjectQueueService.d.ts.map +1 -0
  611. package/dist/services/ProjectQueueService.js +804 -0
  612. package/dist/services/ProjectQueueService.js.map +1 -0
  613. package/dist/services/RelayClientService.d.ts +5 -0
  614. package/dist/services/RelayClientService.d.ts.map +1 -1
  615. package/dist/services/RelayClientService.js +26 -12
  616. package/dist/services/RelayClientService.js.map +1 -1
  617. package/dist/services/SafeRestartService.d.ts +48 -0
  618. package/dist/services/SafeRestartService.d.ts.map +1 -0
  619. package/dist/services/SafeRestartService.js +173 -0
  620. package/dist/services/SafeRestartService.js.map +1 -0
  621. package/dist/services/ServerSettingsService.d.ts +41 -1
  622. package/dist/services/ServerSettingsService.d.ts.map +1 -1
  623. package/dist/services/ServerSettingsService.js +110 -1
  624. package/dist/services/ServerSettingsService.js.map +1 -1
  625. package/dist/services/SessionQueuePersistenceService.d.ts +55 -0
  626. package/dist/services/SessionQueuePersistenceService.d.ts.map +1 -0
  627. package/dist/services/SessionQueuePersistenceService.js +264 -0
  628. package/dist/services/SessionQueuePersistenceService.js.map +1 -0
  629. package/dist/services/WorkstreamService.d.ts +92 -0
  630. package/dist/services/WorkstreamService.d.ts.map +1 -0
  631. package/dist/services/WorkstreamService.js +627 -0
  632. package/dist/services/WorkstreamService.js.map +1 -0
  633. package/dist/services/index.d.ts +4 -0
  634. package/dist/services/index.d.ts.map +1 -1
  635. package/dist/services/index.js +4 -0
  636. package/dist/services/index.js.map +1 -1
  637. package/dist/services/projectWorkIdle.d.ts +42 -0
  638. package/dist/services/projectWorkIdle.d.ts.map +1 -0
  639. package/dist/services/projectWorkIdle.js +50 -0
  640. package/dist/services/projectWorkIdle.js.map +1 -0
  641. package/dist/services/voice/SpeechBackend.d.ts +18 -1
  642. package/dist/services/voice/SpeechBackend.d.ts.map +1 -1
  643. package/dist/services/voice/SpeechBackend.js +3 -0
  644. package/dist/services/voice/SpeechBackend.js.map +1 -1
  645. package/dist/services/voice/audioRetention.d.ts +20 -0
  646. package/dist/services/voice/audioRetention.d.ts.map +1 -1
  647. package/dist/services/voice/audioRetention.js +2 -0
  648. package/dist/services/voice/audioRetention.js.map +1 -1
  649. package/dist/services/voice/deepgramBackend.d.ts.map +1 -1
  650. package/dist/services/voice/deepgramBackend.js +4 -1
  651. package/dist/services/voice/deepgramBackend.js.map +1 -1
  652. package/dist/services/voice/localNemoBackend.d.ts +31 -0
  653. package/dist/services/voice/localNemoBackend.d.ts.map +1 -0
  654. package/dist/services/voice/localNemoBackend.js +181 -0
  655. package/dist/services/voice/localNemoBackend.js.map +1 -0
  656. package/dist/services/voice/localParakeetBackend.d.ts +31 -0
  657. package/dist/services/voice/localParakeetBackend.d.ts.map +1 -0
  658. package/dist/services/voice/localParakeetBackend.js +180 -0
  659. package/dist/services/voice/localParakeetBackend.js.map +1 -0
  660. package/dist/services/voice/localSttRuntime.d.ts +21 -0
  661. package/dist/services/voice/localSttRuntime.d.ts.map +1 -0
  662. package/dist/services/voice/localSttRuntime.js +87 -0
  663. package/dist/services/voice/localSttRuntime.js.map +1 -0
  664. package/dist/services/voice/localWhisperBackend.d.ts +2 -1
  665. package/dist/services/voice/localWhisperBackend.d.ts.map +1 -1
  666. package/dist/services/voice/localWhisperBackend.js +47 -34
  667. package/dist/services/voice/localWhisperBackend.js.map +1 -1
  668. package/dist/services/voice/nemo_worker.py +232 -0
  669. package/dist/services/voice/parakeet_worker.py +151 -0
  670. package/dist/services/voice/registry.d.ts +19 -4
  671. package/dist/services/voice/registry.d.ts.map +1 -1
  672. package/dist/services/voice/registry.js +72 -22
  673. package/dist/services/voice/registry.js.map +1 -1
  674. package/dist/services/voice/serialQueue.d.ts +17 -0
  675. package/dist/services/voice/serialQueue.d.ts.map +1 -0
  676. package/dist/services/voice/serialQueue.js +24 -0
  677. package/dist/services/voice/serialQueue.js.map +1 -0
  678. package/dist/services/voice/whisper_worker.py +94 -0
  679. package/dist/services/voice/xaiSttBackend.d.ts +1 -1
  680. package/dist/services/voice/xaiSttBackend.d.ts.map +1 -1
  681. package/dist/services/voice/xaiSttBackend.js +15 -3
  682. package/dist/services/voice/xaiSttBackend.js.map +1 -1
  683. package/dist/sessions/agent-excerpt.d.ts +29 -0
  684. package/dist/sessions/agent-excerpt.d.ts.map +1 -0
  685. package/dist/sessions/agent-excerpt.js +128 -0
  686. package/dist/sessions/agent-excerpt.js.map +1 -0
  687. package/dist/sessions/claude-messages.d.ts.map +1 -1
  688. package/dist/sessions/claude-messages.js +75 -4
  689. package/dist/sessions/claude-messages.js.map +1 -1
  690. package/dist/sessions/claude-summary.d.ts +33 -0
  691. package/dist/sessions/claude-summary.d.ts.map +1 -0
  692. package/dist/sessions/claude-summary.js +461 -0
  693. package/dist/sessions/claude-summary.js.map +1 -0
  694. package/dist/sessions/codex-discovery.d.ts +46 -0
  695. package/dist/sessions/codex-discovery.d.ts.map +1 -0
  696. package/dist/sessions/codex-discovery.js +269 -0
  697. package/dist/sessions/codex-discovery.js.map +1 -0
  698. package/dist/sessions/codex-reader.d.ts +96 -13
  699. package/dist/sessions/codex-reader.d.ts.map +1 -1
  700. package/dist/sessions/codex-reader.js +1069 -141
  701. package/dist/sessions/codex-reader.js.map +1 -1
  702. package/dist/sessions/dag.d.ts +25 -4
  703. package/dist/sessions/dag.d.ts.map +1 -1
  704. package/dist/sessions/dag.js +37 -4
  705. package/dist/sessions/dag.js.map +1 -1
  706. package/dist/sessions/gemini-reader.d.ts +3 -1
  707. package/dist/sessions/gemini-reader.d.ts.map +1 -1
  708. package/dist/sessions/gemini-reader.js +17 -8
  709. package/dist/sessions/gemini-reader.js.map +1 -1
  710. package/dist/sessions/grok-reader.d.ts +1 -0
  711. package/dist/sessions/grok-reader.d.ts.map +1 -1
  712. package/dist/sessions/grok-reader.js +11 -5
  713. package/dist/sessions/grok-reader.js.map +1 -1
  714. package/dist/sessions/normalization.d.ts +3 -1
  715. package/dist/sessions/normalization.d.ts.map +1 -1
  716. package/dist/sessions/normalization.js +72 -16
  717. package/dist/sessions/normalization.js.map +1 -1
  718. package/dist/sessions/null-reader.d.ts +31 -0
  719. package/dist/sessions/null-reader.d.ts.map +1 -0
  720. package/dist/sessions/null-reader.js +30 -0
  721. package/dist/sessions/null-reader.js.map +1 -0
  722. package/dist/sessions/opencode-db-reader.d.ts +66 -0
  723. package/dist/sessions/opencode-db-reader.d.ts.map +1 -0
  724. package/dist/sessions/opencode-db-reader.js +333 -0
  725. package/dist/sessions/opencode-db-reader.js.map +1 -0
  726. package/dist/sessions/opencode-reader.d.ts +50 -0
  727. package/dist/sessions/opencode-reader.d.ts.map +1 -1
  728. package/dist/sessions/opencode-reader.js +375 -71
  729. package/dist/sessions/opencode-reader.js.map +1 -1
  730. package/dist/sessions/pagination.d.ts.map +1 -1
  731. package/dist/sessions/pagination.js +119 -3
  732. package/dist/sessions/pagination.js.map +1 -1
  733. package/dist/sessions/persisted-augments.d.ts +2 -1
  734. package/dist/sessions/persisted-augments.d.ts.map +1 -1
  735. package/dist/sessions/persisted-augments.js +2 -2
  736. package/dist/sessions/persisted-augments.js.map +1 -1
  737. package/dist/sessions/pi-fork.d.ts +12 -0
  738. package/dist/sessions/pi-fork.d.ts.map +1 -0
  739. package/dist/sessions/pi-fork.js +125 -0
  740. package/dist/sessions/pi-fork.js.map +1 -0
  741. package/dist/sessions/pi-reader.d.ts +81 -0
  742. package/dist/sessions/pi-reader.d.ts.map +1 -0
  743. package/dist/sessions/pi-reader.js +413 -0
  744. package/dist/sessions/pi-reader.js.map +1 -0
  745. package/dist/sessions/provider-resolution.d.ts +10 -3
  746. package/dist/sessions/provider-resolution.d.ts.map +1 -1
  747. package/dist/sessions/provider-resolution.js +62 -6
  748. package/dist/sessions/provider-resolution.js.map +1 -1
  749. package/dist/sessions/reader.d.ts +17 -24
  750. package/dist/sessions/reader.d.ts.map +1 -1
  751. package/dist/sessions/reader.js +86 -157
  752. package/dist/sessions/reader.js.map +1 -1
  753. package/dist/sessions/recap-overlays.d.ts +38 -0
  754. package/dist/sessions/recap-overlays.d.ts.map +1 -0
  755. package/dist/sessions/recap-overlays.js +176 -0
  756. package/dist/sessions/recap-overlays.js.map +1 -0
  757. package/dist/sessions/summary-parser-worker-client.d.ts +80 -0
  758. package/dist/sessions/summary-parser-worker-client.d.ts.map +1 -0
  759. package/dist/sessions/summary-parser-worker-client.js +539 -0
  760. package/dist/sessions/summary-parser-worker-client.js.map +1 -0
  761. package/dist/sessions/summary-parser-worker-entry.d.ts +2 -0
  762. package/dist/sessions/summary-parser-worker-entry.d.ts.map +1 -0
  763. package/dist/sessions/summary-parser-worker-entry.js +29 -0
  764. package/dist/sessions/summary-parser-worker-entry.js.map +1 -0
  765. package/dist/sessions/summary-parser-worker-protocol.d.ts +105 -0
  766. package/dist/sessions/summary-parser-worker-protocol.d.ts.map +1 -0
  767. package/dist/sessions/summary-parser-worker-protocol.js +35 -0
  768. package/dist/sessions/summary-parser-worker-protocol.js.map +1 -0
  769. package/dist/sessions/summary-parser-worker-runner.d.ts +3 -0
  770. package/dist/sessions/summary-parser-worker-runner.d.ts.map +1 -0
  771. package/dist/sessions/summary-parser-worker-runner.js +158 -0
  772. package/dist/sessions/summary-parser-worker-runner.js.map +1 -0
  773. package/dist/sessions/types.d.ts +30 -1
  774. package/dist/sessions/types.d.ts.map +1 -1
  775. package/dist/startupEnv.d.ts +2 -0
  776. package/dist/startupEnv.d.ts.map +1 -0
  777. package/dist/startupEnv.js +97 -0
  778. package/dist/startupEnv.js.map +1 -0
  779. package/dist/subscriptions.d.ts.map +1 -1
  780. package/dist/subscriptions.js +28 -18
  781. package/dist/subscriptions.js.map +1 -1
  782. package/dist/supervisor/ExternalSessionTracker.d.ts +5 -1
  783. package/dist/supervisor/ExternalSessionTracker.d.ts.map +1 -1
  784. package/dist/supervisor/ExternalSessionTracker.js +98 -6
  785. package/dist/supervisor/ExternalSessionTracker.js.map +1 -1
  786. package/dist/supervisor/Process.d.ts +252 -32
  787. package/dist/supervisor/Process.d.ts.map +1 -1
  788. package/dist/supervisor/Process.js +1233 -219
  789. package/dist/supervisor/Process.js.map +1 -1
  790. package/dist/supervisor/Supervisor.d.ts +201 -5
  791. package/dist/supervisor/Supervisor.d.ts.map +1 -1
  792. package/dist/supervisor/Supervisor.js +1074 -55
  793. package/dist/supervisor/Supervisor.js.map +1 -1
  794. package/dist/supervisor/WorkerQueue.d.ts +3 -1
  795. package/dist/supervisor/WorkerQueue.d.ts.map +1 -1
  796. package/dist/supervisor/WorkerQueue.js +1 -0
  797. package/dist/supervisor/WorkerQueue.js.map +1 -1
  798. package/dist/supervisor/composeTimeAnchor.d.ts +36 -0
  799. package/dist/supervisor/composeTimeAnchor.d.ts.map +1 -0
  800. package/dist/supervisor/composeTimeAnchor.js +51 -0
  801. package/dist/supervisor/composeTimeAnchor.js.map +1 -0
  802. package/dist/supervisor/deferredDeliverySettings.d.ts +23 -0
  803. package/dist/supervisor/deferredDeliverySettings.d.ts.map +1 -0
  804. package/dist/supervisor/deferredDeliverySettings.js +40 -0
  805. package/dist/supervisor/deferredDeliverySettings.js.map +1 -0
  806. package/dist/supervisor/liveness.d.ts +5 -2
  807. package/dist/supervisor/liveness.d.ts.map +1 -1
  808. package/dist/supervisor/liveness.js +40 -2
  809. package/dist/supervisor/liveness.js.map +1 -1
  810. package/dist/supervisor/types.d.ts +56 -5
  811. package/dist/supervisor/types.d.ts.map +1 -1
  812. package/dist/supervisor/types.js +0 -2
  813. package/dist/supervisor/types.js.map +1 -1
  814. package/dist/uploads/AttachmentStagingService.d.ts +94 -0
  815. package/dist/uploads/AttachmentStagingService.d.ts.map +1 -0
  816. package/dist/uploads/AttachmentStagingService.js +673 -0
  817. package/dist/uploads/AttachmentStagingService.js.map +1 -0
  818. package/dist/uploads/index.d.ts +2 -0
  819. package/dist/uploads/index.d.ts.map +1 -1
  820. package/dist/uploads/index.js +1 -0
  821. package/dist/uploads/index.js.map +1 -1
  822. package/dist/uploads/manager.d.ts +1 -0
  823. package/dist/uploads/manager.d.ts.map +1 -1
  824. package/dist/uploads/manager.js +72 -12
  825. package/dist/uploads/manager.js.map +1 -1
  826. package/dist/utils/codexRolloutFiles.d.ts +16 -0
  827. package/dist/utils/codexRolloutFiles.d.ts.map +1 -0
  828. package/dist/utils/codexRolloutFiles.js +45 -0
  829. package/dist/utils/codexRolloutFiles.js.map +1 -0
  830. package/dist/utils/jsonl.d.ts +8 -0
  831. package/dist/utils/jsonl.d.ts.map +1 -1
  832. package/dist/utils/jsonl.js +158 -5
  833. package/dist/utils/jsonl.js.map +1 -1
  834. package/dist/utils/publicShareViewerUrl.d.ts.map +1 -1
  835. package/dist/utils/publicShareViewerUrl.js +2 -15
  836. package/dist/utils/publicShareViewerUrl.js.map +1 -1
  837. package/dist/watcher/EventBus.d.ts +42 -4
  838. package/dist/watcher/EventBus.d.ts.map +1 -1
  839. package/dist/watcher/EventBus.js.map +1 -1
  840. package/dist/watcher/FileWatcher.d.ts +47 -0
  841. package/dist/watcher/FileWatcher.d.ts.map +1 -1
  842. package/dist/watcher/FileWatcher.js +185 -14
  843. package/dist/watcher/FileWatcher.js.map +1 -1
  844. package/dist/watcher/index.d.ts +2 -2
  845. package/dist/watcher/index.d.ts.map +1 -1
  846. package/dist/watcher/index.js.map +1 -1
  847. package/dist/yaModuleEnv.d.ts +6 -6
  848. package/dist/yaModuleEnv.d.ts.map +1 -1
  849. package/dist/yaModuleEnv.js +12 -12
  850. package/dist/yaModuleEnv.js.map +1 -1
  851. package/package.json +5 -6
  852. package/client-dist/assets/de-BuEb60Yc.js +0 -6
  853. package/client-dist/assets/es-CRlmPKnE.js +0 -6
  854. package/client-dist/assets/fr-Cglgjc2X.js +0 -6
  855. package/client-dist/assets/index-BNj7ibmq.js +0 -524
  856. package/client-dist/assets/index-CAmsUazi.css +0 -1
  857. package/client-dist/assets/ja-DMb9mFPv.js +0 -6
  858. package/client-dist/assets/zh-CN-D1tbfamz.js +0 -6
@@ -1,4 +1,5 @@
1
- import { PROMPT_SUGGESTION_MODES, HELPER_SIDE_MODEL_CHEAPEST, HELPER_SIDE_MODEL_SAME_AS_MAIN, RECAP_MODES, getModelContextWindow, isUrlProjectId, thinkingOptionToConfig, truncateSessionTitle, } from "../../bundled/@yep-anywhere/shared/dist/index.js";
1
+ import { ALL_PERMISSION_MODES, PROMPT_SUGGESTION_MODES, buildEffectiveAgentContext, clampRecapAfterSeconds, getModelContextWindow, isUrlProjectId, isWorkstreamId, mainWorkstreamId, thinkingOptionToConfig, truncateSessionTitle, } from "../../bundled/@yep-anywhere/shared/dist/index.js";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { mkdir } from "node:fs/promises";
3
4
  import { performance } from "node:perf_hooks";
4
5
  import { Hono } from "hono";
@@ -6,21 +7,36 @@ import { augmentTextBlocks } from "../augments/markdown-augments.js";
6
7
  import { getLogger } from "../logging/logger.js";
7
8
  import { DETACHED_PROJECT_PATH, encodeProjectId } from "../projects/paths.js";
8
9
  import { ensureRemoteDirectory } from "../sdk/remote-spawn.js";
10
+ import { parseSlashCommandSubmission } from "../sdk/slashCommandEmulation.js";
9
11
  import { getProjectDirFromCwd, syncSessions } from "../sdk/session-sync.js";
10
12
  import { appendApprovalAuditLog } from "../security/approvalAuditLog.js";
11
13
  import { CodexSessionReader } from "../sessions/codex-reader.js";
12
14
  import { cloneClaudeSession, cloneCodexSession } from "../sessions/fork.js";
13
- import { GeminiSessionReader } from "../sessions/gemini-reader.js";
14
- import { buildDag } from "../sessions/dag.js";
15
15
  import { GrokSessionReader } from "../sessions/grok-reader.js";
16
+ import { extractLastAgentExcerpt } from "../sessions/agent-excerpt.js";
16
17
  import { normalizeSession } from "../sessions/normalization.js";
18
+ import { applyRecapOverlayToSession, applyRecapOverlayToSummary, hasEquivalentRecapMessage, hasUnreadProviderContent, latestRecapMessage, mergeRecapMessages, } from "../sessions/recap-overlays.js";
17
19
  import { sliceAfterMessageIdWithMatch, sliceAtCompactBoundaries, sliceAtUserTurnBoundary, } from "../sessions/pagination.js";
20
+ import { augmentTaskListSnapshots, pruneTaskListSnapshotsToLatest, } from "../augments/task-list-augments.js";
18
21
  import { augmentEditToolUses, augmentPersistedSessionMessages, } from "../sessions/persisted-augments.js";
19
- import { findSessionSummaryAcrossProviders } from "../sessions/provider-resolution.js";
20
- import { isValidSshHostAlias, normalizeSshHostAlias, } from "../utils/sshHostAlias.js";
22
+ import { findSessionSummaryAcrossProviders, getSessionSources, } from "../sessions/provider-resolution.js";
23
+ import { getProvider } from "../sdk/providers/index.js";
24
+ import { getStaticSlashCommandsForProvider } from "../sdk/providers/staticSlashCommands.js";
25
+ import { ResumeCompactionError } from "../supervisor/Supervisor.js";
26
+ import { buildUserMessageMetadata, normalizeOptionalServiceTier, parseHelperSettings, parseOptionalExecutor, parseOptionalResumeMode, } from "./session-request-helpers.js";
27
+ import { resolveCompactPercent, resolveCompactWindow, } from "./session-compact-thresholds.js";
28
+ import { getClaudeResumeBlockerFromReader, } from "./session-claude-resume-guard.js";
21
29
  const SESSION_DETAIL_SLOW_LOG_MS = 250;
22
- const CLAUDE_RESUME_API_ERROR_RECOVERY = "handoff-required";
23
- const CLAUDE_RESUME_API_ERROR_MESSAGE = "Claude session cannot be safely resumed because the Claude SDK recorded an API-error response as the latest assistant message. Start a handoff session instead.";
30
+ const DEFAULT_SESSION_DETAIL_TAIL_COMPACTIONS = 2;
31
+ const LARGE_FULL_HISTORY_MESSAGE_THRESHOLD = 1000;
32
+ async function getSessionSlashCommands(process, provider) {
33
+ if (process?.supportsDynamicCommands) {
34
+ const commands = await process.supportedCommands();
35
+ if (commands)
36
+ return commands;
37
+ }
38
+ return getStaticSlashCommandsForProvider(provider);
39
+ }
24
40
  function roundedMs(value) {
25
41
  return Math.round(value * 10) / 10;
26
42
  }
@@ -39,242 +55,116 @@ function isQueuedResponse(result) {
39
55
  function isQueueFullResponse(result) {
40
56
  return "error" in result && result.error === "queue_full";
41
57
  }
42
- function getClaudeResumeApiErrorBlocker(messages) {
43
- const { activeBranch } = buildDag(messages);
44
- for (let i = activeBranch.length - 1; i >= 0; i--) {
45
- const raw = activeBranch[i]?.raw;
46
- if (raw?.type !== "assistant") {
47
- continue;
48
- }
49
- if (raw.isApiErrorMessage !== true) {
50
- return null;
51
- }
52
- const apiError = raw;
53
- return {
54
- error: CLAUDE_RESUME_API_ERROR_MESSAGE,
55
- recovery: CLAUDE_RESUME_API_ERROR_RECOVERY,
56
- messageId: raw.message.id,
57
- apiErrorStatus: apiError.apiErrorStatus,
58
- };
59
- }
60
- return null;
61
- }
62
- async function getClaudeResumeBlockerFromReader(reader, sessionId, projectId) {
63
- const session = await reader.getSession(sessionId, projectId);
64
- if (!session) {
65
- return null;
66
- }
67
- if (session.data.provider !== "claude" &&
68
- session.data.provider !== "claude-ollama") {
69
- return null;
70
- }
71
- return getClaudeResumeApiErrorBlocker(session.data.session.messages);
72
- }
73
- function parseOptionalExecutor(rawExecutor) {
74
- if (rawExecutor === undefined || rawExecutor === null) {
75
- return { executor: undefined };
76
- }
77
- if (typeof rawExecutor !== "string") {
78
- return { executor: undefined, error: "executor must be a string" };
79
- }
80
- const executor = normalizeSshHostAlias(rawExecutor);
81
- if (!executor) {
82
- return { executor: undefined };
83
- }
84
- if (!isValidSshHostAlias(executor)) {
85
- return {
86
- executor: undefined,
87
- error: "executor must be a valid SSH host alias",
88
- };
89
- }
90
- return { executor };
91
- }
92
- function normalizeOptionalServiceTier(rawServiceTier) {
93
- if (typeof rawServiceTier !== "string") {
94
- return undefined;
95
- }
96
- const serviceTier = rawServiceTier.trim();
97
- return /^[A-Za-z0-9_-]{1,64}$/.test(serviceTier)
98
- ? serviceTier
99
- : undefined;
100
- }
101
- function parseOptionalRecapMode(rawMode) {
102
- if (rawMode === undefined || rawMode === null || rawMode === "") {
103
- return { recapMode: undefined };
104
- }
105
- if (typeof rawMode !== "string" ||
106
- !RECAP_MODES.includes(rawMode)) {
107
- return {
108
- recapMode: undefined,
109
- error: "recapMode must be one of: off, native, side-session",
110
- };
111
- }
112
- return { recapMode: rawMode };
113
- }
114
- function parseOptionalPromptSuggestionMode(rawMode) {
115
- if (rawMode === undefined || rawMode === null || rawMode === "") {
116
- return { promptSuggestionMode: undefined };
117
- }
118
- if (typeof rawMode !== "string" ||
119
- !PROMPT_SUGGESTION_MODES.includes(rawMode)) {
120
- return {
121
- promptSuggestionMode: undefined,
122
- error: "promptSuggestionMode must be one of: off, native",
123
- };
124
- }
125
- return { promptSuggestionMode: rawMode };
126
- }
127
- function parseOptionalHelperSideModel(rawModel) {
128
- if (rawModel === undefined || rawModel === null || rawModel === "") {
129
- return { helperSideModel: undefined };
130
- }
131
- if (typeof rawModel !== "string") {
132
- return { helperSideModel: undefined, error: "helperSideModel must be a string" };
133
- }
134
- const trimmed = rawModel.trim();
135
- if (!trimmed) {
136
- return { helperSideModel: undefined };
137
- }
138
- return {
139
- helperSideModel: trimmed === HELPER_SIDE_MODEL_SAME_AS_MAIN
140
- ? HELPER_SIDE_MODEL_SAME_AS_MAIN
141
- : trimmed === HELPER_SIDE_MODEL_CHEAPEST
142
- ? HELPER_SIDE_MODEL_CHEAPEST
143
- : trimmed.slice(0, 200),
144
- };
145
- }
146
- function parseHelperSettings(body) {
147
- const recap = parseOptionalRecapMode(body.recapMode);
148
- if (recap.error) {
149
- return {
150
- recapMode: undefined,
151
- promptSuggestionMode: undefined,
152
- helperSideModel: undefined,
153
- error: recap.error,
154
- };
155
- }
156
- const promptSuggestion = parseOptionalPromptSuggestionMode(body.promptSuggestionMode);
157
- if (promptSuggestion.error) {
158
- return {
159
- recapMode: undefined,
160
- promptSuggestionMode: undefined,
161
- helperSideModel: undefined,
162
- error: promptSuggestion.error,
163
- };
164
- }
165
- const helperModel = parseOptionalHelperSideModel(body.helperSideModel);
166
- if (helperModel.error) {
167
- return {
168
- recapMode: undefined,
169
- promptSuggestionMode: undefined,
170
- helperSideModel: undefined,
171
- error: helperModel.error,
172
- };
173
- }
174
- return {
175
- recapMode: recap.recapMode,
176
- promptSuggestionMode: promptSuggestion.promptSuggestionMode,
177
- helperSideModel: helperModel.helperSideModel,
178
- };
179
- }
180
58
  function isCodexProviderName(provider) {
181
59
  return provider === "codex" || provider === "codex-oss";
182
60
  }
183
- function parseDeferredPlacement(body) {
184
- const beforeTempId = typeof body.insertBeforeTempId === "string" &&
185
- body.insertBeforeTempId.trim()
186
- ? body.insertBeforeTempId.trim()
187
- : undefined;
188
- const afterTempId = typeof body.insertAfterTempId === "string" && body.insertAfterTempId.trim()
189
- ? body.insertAfterTempId.trim()
190
- : undefined;
191
- const replaceTempId = typeof body.replaceDeferredTempId === "string" &&
192
- body.replaceDeferredTempId.trim()
193
- ? body.replaceDeferredTempId.trim()
194
- : undefined;
195
- if (!beforeTempId && !afterTempId && !replaceTempId) {
196
- return undefined;
197
- }
198
- return {
199
- ...(afterTempId ? { afterTempId } : {}),
200
- ...(beforeTempId ? { beforeTempId } : {}),
201
- ...(replaceTempId ? { replaceTempId } : {}),
202
- };
203
- }
204
- const USER_MESSAGE_DELIVERY_INTENTS = new Set(["direct", "steer", "deferred", "patient"]);
205
61
  const AUTO_COMPACT_CONTEXT_PERCENT_THRESHOLD = 85;
206
62
  const AUTO_COMPACT_MODEL_PREFIXES = ["gpt-5.3-codex-spark"];
207
63
  const AUTO_COMPACT_TARGET_PROVIDERS = new Set([
208
64
  "codex",
209
65
  "codex-oss",
210
66
  ]);
211
- function isRecord(value) {
212
- return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ function providerResolutionDeps(deps) {
68
+ return {
69
+ readerFactory: deps.readerFactory,
70
+ sessionIndexService: deps.sessionIndexService,
71
+ codexSessionsDir: deps.codexSessionsDir,
72
+ codexReaderFactory: deps.codexReaderFactory,
73
+ geminiSessionsDir: deps.geminiSessionsDir,
74
+ geminiReaderFactory: deps.geminiReaderFactory,
75
+ geminiHashToCwd: deps.geminiScanner?.getHashToCwd(),
76
+ grokSessionsDir: deps.grokSessionsDir,
77
+ grokReaderFactory: deps.grokReaderFactory,
78
+ piSessionsDir: deps.piSessionsDir,
79
+ piReaderFactory: deps.piReaderFactory,
80
+ };
213
81
  }
214
- function parseIsoTimestamp(value) {
215
- if (typeof value !== "string" || !value.trim())
216
- return undefined;
217
- return Number.isNaN(Date.parse(value)) ? undefined : value;
82
+ function persistedPatientQueueSummary(item) {
83
+ const attachmentCount = (item.message.attachments?.length ?? 0) +
84
+ (item.message.images?.length ?? 0) +
85
+ (item.message.documents?.length ?? 0);
86
+ const tempId = item.message.tempId ?? item.source?.tempId;
87
+ return {
88
+ id: item.id,
89
+ ...(tempId ? { tempId } : {}),
90
+ content: item.message.text,
91
+ timestamp: item.queuedAt,
92
+ ...(item.message.attachments?.length
93
+ ? { attachments: item.message.attachments }
94
+ : {}),
95
+ ...(attachmentCount > 0 ? { attachmentCount } : {}),
96
+ ...(item.message.metadata ? { metadata: item.message.metadata } : {}),
97
+ kind: "patient",
98
+ status: item.status === "paused-after-restart" ? item.status : "queued",
99
+ sessionId: item.sessionId,
100
+ projectId: item.projectId,
101
+ queuedAt: item.queuedAt,
102
+ createdAt: item.createdAt,
103
+ updatedAt: item.updatedAt,
104
+ };
218
105
  }
219
- function parseDeliveryIntent(value) {
220
- return typeof value === "string" &&
221
- USER_MESSAGE_DELIVERY_INTENTS.has(value)
222
- ? value
223
- : undefined;
106
+ function isApprovalAuditLogEnabled(deps) {
107
+ return (deps.serverSettingsService?.getSetting("approvalAuditLogEnabled") === true);
224
108
  }
225
- function parseShortString(value, maxLength) {
226
- if (typeof value !== "string")
227
- return undefined;
228
- const trimmed = value.trim();
229
- return trimmed ? trimmed.slice(0, maxLength) : undefined;
109
+ function recoveredPatientQueueSummaries(deps, sessionId) {
110
+ return recoveredPatientQueueItems(deps, sessionId).map(persistedPatientQueueSummary);
230
111
  }
231
- function parseStringList(value, maxItems, maxLength) {
232
- if (!Array.isArray(value))
233
- return undefined;
234
- const parsed = value
235
- .map((entry) => parseShortString(entry, maxLength))
236
- .filter((entry) => entry !== undefined)
237
- .slice(0, maxItems);
238
- return parsed.length > 0 ? parsed : undefined;
239
- }
240
- function buildUserMessageMetadata(body, serverTimestamp, fallbackIntent) {
241
- const rawMetadata = isRecord(body.messageMetadata) ? body.messageMetadata : {};
242
- const rawComposition = isRecord(rawMetadata.composition)
243
- ? rawMetadata.composition
244
- : {};
245
- const composition = {
246
- typingStartedAt: parseIsoTimestamp(rawComposition.typingStartedAt),
247
- typingEndedAt: parseIsoTimestamp(rawComposition.typingEndedAt),
248
- lastEditedAt: parseIsoTimestamp(rawComposition.lastEditedAt),
249
- submittedAt: parseIsoTimestamp(rawComposition.submittedAt),
112
+ function recoveredPatientQueueItems(deps, sessionId) {
113
+ if (!deps.sessionQueuePersistenceService) {
114
+ return [];
115
+ }
116
+ return deps.sessionQueuePersistenceService
117
+ .listSession(sessionId)
118
+ .filter((item) => item.kind === "patient" && item.status === "paused-after-restart")
119
+ .sort((left, right) => left.queuedAt.localeCompare(right.queuedAt));
120
+ }
121
+ function sessionQueueSummaries(deps, sessionId, process) {
122
+ const recovered = recoveredPatientQueueSummaries(deps, sessionId);
123
+ if (process && typeof process.getDeferredQueueSummary === "function") {
124
+ const messages = [
125
+ ...process.getDeferredQueueSummary(),
126
+ ...recovered,
127
+ ];
128
+ return messages.sort((left, right) => (left.queuedAt ?? left.timestamp).localeCompare(right.queuedAt ?? right.timestamp));
129
+ }
130
+ if (process) {
131
+ return recovered;
132
+ }
133
+ return recovered;
134
+ }
135
+ function recoveredPatientUserMessage(item) {
136
+ const mode = item.message.mode ?? item.mode;
137
+ const metadata = {
138
+ ...item.message.metadata,
139
+ deliveryIntent: "patient",
250
140
  };
251
- const cleanComposition = Object.fromEntries(Object.entries(composition).filter(([, value]) => value !== undefined));
252
- const clientTimestamp = typeof body.clientTimestamp === "number" && Number.isFinite(body.clientTimestamp)
253
- ? body.clientTimestamp
254
- : undefined;
255
- const rawSpeech = isRecord(rawMetadata.speech) ? rawMetadata.speech : {};
256
- const speechClientTurnId = parseShortString(rawSpeech.clientTurnId, 120);
257
- const speechTranscriptionIds = parseStringList(rawSpeech.transcriptionIds, 20, 120);
258
- const speech = speechClientTurnId || speechTranscriptionIds
259
- ? {
260
- ...(speechClientTurnId
261
- ? { clientTurnId: speechClientTurnId }
262
- : {}),
263
- ...(speechTranscriptionIds
264
- ? { transcriptionIds: speechTranscriptionIds }
265
- : {}),
266
- }
267
- : undefined;
141
+ // Live-queue chip actions (cancel, steer) address entries by tempId, so a
142
+ // recovered entry that was persisted without one gets a stable fallback.
143
+ const tempId = item.message.tempId ?? item.source?.tempId ?? `recovered-${item.id}`;
268
144
  return {
269
- deliveryIntent: parseDeliveryIntent(rawMetadata.deliveryIntent) ?? fallbackIntent,
270
- ...(Object.keys(cleanComposition).length > 0
271
- ? { composition: cleanComposition }
272
- : {}),
273
- ...(speech ? { speech } : {}),
274
- ...(clientTimestamp !== undefined ? { clientTimestamp } : {}),
275
- serverReceivedAt: new Date(serverTimestamp).toISOString(),
145
+ ...item.message,
146
+ tempId,
147
+ ...(mode ? { mode } : {}),
148
+ metadata,
276
149
  };
277
150
  }
151
+ /**
152
+ * Count live patient queue entries composed after the given recovered entry.
153
+ * Resuming or steering the recovered entry would deliver its older content
154
+ * behind these newer ones, so callers reject while any exist. Regular
155
+ * deferred entries never block recovered work: the regular lane delivers on
156
+ * turn boundaries and may pass patient work by design.
157
+ */
158
+ function livePatientEntriesNewerThan(process, queuedAt) {
159
+ const live = process?.getDeferredQueueSummary() ?? [];
160
+ return live.filter((entry) => entry.metadata?.deliveryIntent === "patient" &&
161
+ entry.timestamp.localeCompare(queuedAt) > 0).length;
162
+ }
163
+ async function resolveSessionReaderForAgentContent({ deps, project, sessionId, projectId, }) {
164
+ const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
165
+ const resolved = await findSessionSummaryAcrossProviders(project, sessionId, projectId, providerResolutionDeps(deps), metadataProvider, { readMode: "head" });
166
+ return resolved?.source.reader ?? deps.readerFactory(project);
167
+ }
278
168
  const RESTART_HANDOFF_MAX_CHARS = 40_000;
279
169
  const RESTART_HANDOFF_JSON_MAX_CHARS = 2_000;
280
170
  const RESTART_HANDOFF_COMPACT_MAX_CHARS = 10_000;
@@ -336,7 +226,9 @@ function renderRestartContent(content) {
336
226
  case "text":
337
227
  return typed.text ?? "";
338
228
  case "thinking":
339
- return typed.thinking ? `[thinking]\n${typed.thinking}` : "[thinking]";
229
+ return typed.thinking
230
+ ? `[thinking]\n${typed.thinking}`
231
+ : "[thinking]";
340
232
  case "tool_use":
341
233
  return `[tool_use ${typed.name ?? "unknown"}]\n${stringifyForRestart(typed.input, RESTART_HANDOFF_JSON_MAX_CHARS)}`;
342
234
  case "tool_result":
@@ -372,6 +264,33 @@ function messageContent(message) {
372
264
  const nested = message.message;
373
265
  return nested?.content ?? message.content;
374
266
  }
267
+ function messageId(message) {
268
+ if (!message) {
269
+ return undefined;
270
+ }
271
+ return ((typeof message.uuid === "string" && message.uuid) ||
272
+ (typeof message.id === "string" && message.id) ||
273
+ undefined);
274
+ }
275
+ function findDurableRecapCursor(afterMessageId, recaps) {
276
+ if (!afterMessageId) {
277
+ return undefined;
278
+ }
279
+ return recaps.find((recap) => recap.uuid === afterMessageId || recap.id === afterMessageId);
280
+ }
281
+ function sliceAfterDurableRecapCursor(params) {
282
+ const index = params.messages.findIndex((message) => {
283
+ const id = messageId(message);
284
+ return (id === params.cursorId ||
285
+ id === params.recap.uuid ||
286
+ id === params.recap.id ||
287
+ hasEquivalentRecapMessage([message], params.recap));
288
+ });
289
+ if (index < 0) {
290
+ return { messages: params.messages, found: false };
291
+ }
292
+ return { messages: params.messages.slice(index + 1), found: true };
293
+ }
375
294
  function messageHasToolResult(message) {
376
295
  if (message.toolUseResult !== undefined) {
377
296
  return true;
@@ -382,6 +301,32 @@ function messageHasToolResult(message) {
382
301
  typeof block === "object" &&
383
302
  block.type === "tool_result"));
384
303
  }
304
+ function messageTextContent(message) {
305
+ const content = messageContent(message);
306
+ if (typeof content === "string") {
307
+ return content;
308
+ }
309
+ if (!Array.isArray(content)) {
310
+ return undefined;
311
+ }
312
+ const textBlocks = content
313
+ .map((block) => block &&
314
+ typeof block === "object" &&
315
+ block.type === "text" &&
316
+ typeof block.text === "string"
317
+ ? block.text
318
+ : "")
319
+ .filter(Boolean);
320
+ return textBlocks.length > 0 ? textBlocks.join("\n") : undefined;
321
+ }
322
+ function isSlashCommandSkillBodyUserMessage(message) {
323
+ if (message.isMeta !== true) {
324
+ return false;
325
+ }
326
+ return (messageTextContent(message)
327
+ ?.trimStart()
328
+ .startsWith("Base directory for this skill:") === true);
329
+ }
385
330
  function isRestartInternalCompactCommand(message) {
386
331
  if (messageRole(message) !== "user" || messageHasToolResult(message)) {
387
332
  return false;
@@ -751,8 +696,111 @@ function isHumanUserMessage(message) {
751
696
  message.type;
752
697
  return role === "user" && !messageHasToolResult(message);
753
698
  }
699
+ function isCompactSummaryUserMessage(message) {
700
+ return message.isCompactSummary === true;
701
+ }
702
+ function isUserAuthoredRequest(message) {
703
+ return (isHumanUserMessage(message) &&
704
+ !isCompactSummaryUserMessage(message) &&
705
+ !isSlashCommandSkillBodyUserMessage(message));
706
+ }
707
+ function resolveForkAfterBoundary(messages, sourceMessageId, sourceIsBusy) {
708
+ const sourceIndex = messages.findIndex((message) => messageId(message) === sourceMessageId);
709
+ const sourceMessage = messages[sourceIndex];
710
+ if (sourceIndex < 0 || !sourceMessage) {
711
+ return { error: "Selected source message was not found", status: 404 };
712
+ }
713
+ if (!isUserAuthoredRequest(sourceMessage)) {
714
+ return {
715
+ error: "sourceMessageId must identify a user-authored request",
716
+ status: 400,
717
+ };
718
+ }
719
+ let nextUserIndex = -1;
720
+ for (let index = sourceIndex + 1; index < messages.length; index += 1) {
721
+ const candidate = messages[index];
722
+ if (candidate && isUserAuthoredRequest(candidate)) {
723
+ nextUserIndex = index;
724
+ break;
725
+ }
726
+ }
727
+ if (nextUserIndex < 0 && sourceIsBusy) {
728
+ return {
729
+ error: "The selected turn is still in progress",
730
+ status: 409,
731
+ };
732
+ }
733
+ const searchEnd = nextUserIndex >= 0 ? nextUserIndex - 1 : messages.length - 1;
734
+ let hasAssistantResponse = false;
735
+ for (let index = sourceIndex + 1; index <= searchEnd; index += 1) {
736
+ const candidate = messages[index];
737
+ if (candidate && messageRole(candidate) === "assistant") {
738
+ hasAssistantResponse = true;
739
+ break;
740
+ }
741
+ }
742
+ if (!hasAssistantResponse) {
743
+ return {
744
+ error: "The selected request has no completed assistant response",
745
+ status: 409,
746
+ };
747
+ }
748
+ let boundary;
749
+ for (let index = searchEnd; index > sourceIndex; index -= 1) {
750
+ const candidate = messages[index];
751
+ const role = candidate ? messageRole(candidate) : undefined;
752
+ if (candidate &&
753
+ messageId(candidate) &&
754
+ (role === "user" || role === "assistant")) {
755
+ boundary = candidate;
756
+ break;
757
+ }
758
+ }
759
+ const retainedThroughMessageId = messageId(boundary);
760
+ if (!boundary || !retainedThroughMessageId) {
761
+ return { error: "Completed turn boundary has no message id", status: 409 };
762
+ }
763
+ const placementAfterMessageId = [...messages].reverse().map(messageId).find(Boolean) ??
764
+ retainedThroughMessageId;
765
+ const rendered = renderRestartContent(messageContent(boundary)).trim();
766
+ return {
767
+ placementAfterMessageId,
768
+ retainedThroughMessageId,
769
+ retainedThroughContext: rendered
770
+ ? truncateForRestart(rendered, 1200)
771
+ : undefined,
772
+ };
773
+ }
774
+ function forkSummaryTitle(summary, fallback) {
775
+ const firstLine = summary
776
+ .split("\n")
777
+ .map((line) => line.trim())
778
+ .find(Boolean);
779
+ const candidate = firstLine
780
+ ?.replace(/^#{1,6}\s*/u, "")
781
+ .replace(/^title:\s*/iu, "")
782
+ .trim()
783
+ .replace(/[.!?]+$/u, "");
784
+ return truncateSessionTitle(candidate || fallback || "Forked session");
785
+ }
786
+ function generatedRetitleCandidate(title) {
787
+ const firstLine = title
788
+ .split("\n")
789
+ .map((line) => line.trim())
790
+ .find(Boolean);
791
+ if (!firstLine) {
792
+ return undefined;
793
+ }
794
+ const candidate = normalizeRestartTitleCandidate(firstLine
795
+ .replace(/^title:\s*/iu, "")
796
+ .replace(/^["'“”‘’]+|["'“”‘’]+$/gu, "")
797
+ .replace(/[.!?]+$/u, "")
798
+ .trim());
799
+ return candidate ? truncateSessionTitle(candidate) : undefined;
800
+ }
754
801
  function messageTitleCandidate(message) {
755
- if (!isHumanUserMessage(message) || isRestartInternalCompactCommand(message)) {
802
+ if (!isHumanUserMessage(message) ||
803
+ isRestartInternalCompactCommand(message)) {
756
804
  return undefined;
757
805
  }
758
806
  const nested = message.message;
@@ -793,6 +841,18 @@ function deriveRestartTitle(params) {
793
841
  const title = /^Handoff:/i.test(base) ? base : `Handoff: ${base}`;
794
842
  return truncateRestartTitle(title);
795
843
  }
844
+ function deriveForkTitle(params) {
845
+ const candidates = [
846
+ params.preferredTitle,
847
+ params.sourceSession.customTitle,
848
+ params.sourceSession.title,
849
+ latestUserTitleCandidate(params.sourceSession.messages),
850
+ ];
851
+ const base = candidates.map(normalizeRestartTitleCandidate).find(Boolean) ??
852
+ "forked session";
853
+ const title = /^Fork:/i.test(base) ? base : `Fork: ${base}`;
854
+ return truncateRestartTitle(title);
855
+ }
796
856
  function buildRestartHandoff(params) {
797
857
  const { handoffTitle, sourceSession, sourceProvider, sourceModel, sourceProcess, compactAttempt, projectPath, reason, omittedCount, transcript, } = params;
798
858
  const oldProcessLine = sourceProcess
@@ -847,12 +907,14 @@ function isRestartReplacementActivity(message) {
847
907
  function sdkMessagesToClientMessages(sdkMessages) {
848
908
  const messages = [];
849
909
  for (const msg of sdkMessages) {
910
+ const rawFields = msg;
850
911
  if (isCompactBoundaryMessage(msg)) {
851
912
  const content = (typeof msg.message?.content === "string"
852
913
  ? msg.message.content
853
914
  : undefined) ??
854
915
  (typeof msg.content === "string" ? msg.content : "Context compacted");
855
916
  messages.push({
917
+ ...rawFields,
856
918
  id: msg.uuid ?? `msg-${Date.now()}-${messages.length}`,
857
919
  type: "system",
858
920
  role: "system",
@@ -889,6 +951,7 @@ function sdkMessagesToClientMessages(sdkMessages) {
889
951
  continue;
890
952
  }
891
953
  messages.push({
954
+ ...rawFields,
892
955
  id: msg.uuid ?? `msg-${Date.now()}-${messages.length}`,
893
956
  type: msg.type,
894
957
  role: msg.type,
@@ -1005,6 +1068,31 @@ function extractContextUsageFromSDKMessages(sdkMessages, model, provider, resolv
1005
1068
  }
1006
1069
  export function createSessionsRoutes(deps) {
1007
1070
  const routes = new Hono();
1071
+ const activeForkSummaryJobs = new Map();
1072
+ const emitTranscriptDisplayObjects = (sessionId) => {
1073
+ deps.eventBus?.emit({
1074
+ type: "session-metadata-changed",
1075
+ sessionId,
1076
+ transcriptDisplayObjects: deps.sessionMetadataService?.getTranscriptDisplayObjects(sessionId) ??
1077
+ [],
1078
+ timestamp: new Date().toISOString(),
1079
+ });
1080
+ };
1081
+ const updateForkSummaryChildMetadata = async (childSessionId, parentSessionId, title, archived) => {
1082
+ await deps.sessionMetadataService?.updateMetadata(childSessionId, {
1083
+ title,
1084
+ archived,
1085
+ parentSessionId,
1086
+ });
1087
+ deps.eventBus?.emit({
1088
+ type: "session-metadata-changed",
1089
+ sessionId: childSessionId,
1090
+ title,
1091
+ archived,
1092
+ parentSessionId,
1093
+ timestamp: new Date().toISOString(),
1094
+ });
1095
+ };
1008
1096
  const getCodexReader = (projectPath) => deps.codexReaderFactory?.(projectPath) ??
1009
1097
  (deps.codexSessionsDir
1010
1098
  ? new CodexSessionReader({
@@ -1012,13 +1100,6 @@ export function createSessionsRoutes(deps) {
1012
1100
  projectPath,
1013
1101
  })
1014
1102
  : null);
1015
- const getGrokReader = (projectPath) => deps.grokReaderFactory?.(projectPath) ??
1016
- (deps.grokSessionsDir
1017
- ? new GrokSessionReader({
1018
- sessionsDir: deps.grokSessionsDir,
1019
- projectPath,
1020
- })
1021
- : null);
1022
1103
  let unscopedGrokReader;
1023
1104
  const getUnscopedGrokReader = () => {
1024
1105
  if (unscopedGrokReader !== undefined) {
@@ -1045,8 +1126,78 @@ export function createSessionsRoutes(deps) {
1045
1126
  const search = new URL(requestUrl).search;
1046
1127
  return `/api/projects/${canonicalProjectId}/sessions/${encodeURIComponent(sessionId)}${suffix}${search}`;
1047
1128
  };
1048
- const getGlobalInstructions = () => deps.serverSettingsService?.getSetting("globalInstructions") || undefined;
1049
- const persistLaunchMetadata = async (sessionId, provider, executor, initialPrompt) => {
1129
+ const buildSessionProjectRedirectPath = (canonicalProjectId, sessionId, suffix, requestUrl) => {
1130
+ const search = new URL(requestUrl).search;
1131
+ return `/api/projects/${canonicalProjectId}/sessions/${encodeURIComponent(sessionId)}${suffix}${search}`;
1132
+ };
1133
+ const resolveSessionProjectRouting = async (requestProjectId, sessionId) => {
1134
+ const workingProjectId = deps.sessionMetadataService?.getMetadata(sessionId)?.workingProjectId;
1135
+ if (workingProjectId && workingProjectId !== requestProjectId) {
1136
+ return { redirectProjectId: workingProjectId };
1137
+ }
1138
+ const activeProcess = deps.supervisor.getProcessForSession(sessionId);
1139
+ if (activeProcess &&
1140
+ typeof activeProcess.projectId === "string" &&
1141
+ isUrlProjectId(activeProcess.projectId) &&
1142
+ activeProcess.projectId !== requestProjectId) {
1143
+ return { redirectProjectId: activeProcess.projectId };
1144
+ }
1145
+ const workingProject = await deps.scanner.getOrCreateProject(requestProjectId);
1146
+ if (!workingProject) {
1147
+ return { error: "Project not found", status: 404 };
1148
+ }
1149
+ const metadata = deps.sessionMetadataService?.getMetadata(sessionId);
1150
+ const transcriptProjectId = metadata?.workingProjectId === requestProjectId &&
1151
+ metadata.transcriptProjectId
1152
+ ? metadata.transcriptProjectId
1153
+ : requestProjectId;
1154
+ const transcriptProject = transcriptProjectId === requestProjectId
1155
+ ? workingProject
1156
+ : await deps.scanner.getOrCreateProject(transcriptProjectId);
1157
+ if (!transcriptProject) {
1158
+ return { error: "Transcript project not found", status: 404 };
1159
+ }
1160
+ return {
1161
+ transcriptProject,
1162
+ transcriptProjectId,
1163
+ workingProject,
1164
+ workingProjectId: requestProjectId,
1165
+ };
1166
+ };
1167
+ const getGlobalInstructions = () => buildEffectiveAgentContext({
1168
+ globalInstructions: deps.serverSettingsService?.getSetting("globalInstructions"),
1169
+ hints: deps.serverSettingsService?.getSetting("agentContextHints"),
1170
+ });
1171
+ const resolveLaunchWorkstreamTarget = (project, rawWorkstreamId) => {
1172
+ if (rawWorkstreamId === undefined) {
1173
+ return { projectPath: project.path };
1174
+ }
1175
+ if (!isWorkstreamId(rawWorkstreamId)) {
1176
+ return { error: "Invalid workstream ID format", status: 400 };
1177
+ }
1178
+ if (deps.serverSettingsService?.getSetting("workstreamsEnabled") !== true) {
1179
+ return { error: "Workstreams are disabled", status: 404 };
1180
+ }
1181
+ if (!deps.workstreamService) {
1182
+ return { error: "Workstreams unavailable", status: 503 };
1183
+ }
1184
+ const workstreamId = rawWorkstreamId;
1185
+ if (workstreamId === mainWorkstreamId(project.id)) {
1186
+ return { projectPath: project.path };
1187
+ }
1188
+ if (!deps.sessionMetadataService) {
1189
+ return { error: "Session metadata unavailable", status: 503 };
1190
+ }
1191
+ const workstream = deps.workstreamService.getWorkstream(project.id, workstreamId);
1192
+ if (workstream?.kind !== "checkout") {
1193
+ return { error: "Workstream not found", status: 404 };
1194
+ }
1195
+ if (workstream.status !== "active") {
1196
+ return { error: "Workstream is not active", status: 409 };
1197
+ }
1198
+ return { projectPath: workstream.path, workstreamId };
1199
+ };
1200
+ const persistLaunchMetadata = async (sessionId, provider, executor, initialPrompt, requestedModel, promptSuggestionMode, recapAfterSeconds, workstreamId) => {
1050
1201
  if (!deps.sessionMetadataService) {
1051
1202
  return;
1052
1203
  }
@@ -1059,18 +1210,40 @@ export function createSessionsRoutes(deps) {
1059
1210
  if (initialPrompt?.trim()) {
1060
1211
  await deps.sessionMetadataService.setInitialPrompt(sessionId, initialPrompt);
1061
1212
  }
1213
+ // Persist the requested YA model id (the launch alias, incl. "default") so
1214
+ // per-model settings still key by it after a server restart, instead of
1215
+ // falling back to the reported model. See topics/provider-abstraction.md.
1216
+ if (requestedModel) {
1217
+ await deps.sessionMetadataService.setRequestedModel(sessionId, requestedModel);
1218
+ }
1219
+ // Persist the resolved prompt-suggestion mode so a later resume (which may
1220
+ // omit it from the request body) recovers the per-session preference
1221
+ // instead of falling back to the provider's native default.
1222
+ if (promptSuggestionMode !== undefined) {
1223
+ await deps.sessionMetadataService.updateMetadata(sessionId, {
1224
+ promptSuggestionMode,
1225
+ });
1226
+ }
1227
+ if (recapAfterSeconds !== undefined) {
1228
+ await deps.sessionMetadataService.updateMetadata(sessionId, {
1229
+ recapAfterSeconds,
1230
+ });
1231
+ }
1232
+ if (workstreamId !== undefined) {
1233
+ await deps.sessionMetadataService.setWorkstream(sessionId, workstreamId);
1234
+ }
1235
+ };
1236
+ const loadProviderSession = async (project, sessionId, projectId, preferredProvider, afterMessageId, options) => {
1237
+ for (const source of getSessionSources(project, providerResolutionDeps(deps), preferredProvider)) {
1238
+ const loaded = await source.reader.getSession(sessionId, projectId, afterMessageId, options);
1239
+ if (loaded) {
1240
+ return loaded;
1241
+ }
1242
+ }
1243
+ return null;
1062
1244
  };
1063
1245
  const loadRestartSourceSession = async (project, sessionId, projectId, preferredProvider, process) => {
1064
- const resolved = await findSessionSummaryAcrossProviders(project, sessionId, projectId, {
1065
- readerFactory: deps.readerFactory,
1066
- codexSessionsDir: deps.codexSessionsDir,
1067
- codexReaderFactory: deps.codexReaderFactory,
1068
- geminiSessionsDir: deps.geminiSessionsDir,
1069
- geminiReaderFactory: deps.geminiReaderFactory,
1070
- geminiHashToCwd: deps.geminiScanner?.getHashToCwd(),
1071
- grokSessionsDir: deps.grokSessionsDir,
1072
- grokReaderFactory: deps.grokReaderFactory,
1073
- }, preferredProvider);
1246
+ const resolved = await findSessionSummaryAcrossProviders(project, sessionId, projectId, providerResolutionDeps(deps), preferredProvider, { readMode: "head" });
1074
1247
  if (resolved) {
1075
1248
  const loaded = await resolved.source.reader.getSession(sessionId, projectId, undefined, { includeOrphans: false });
1076
1249
  if (loaded) {
@@ -1092,6 +1265,7 @@ export function createSessionsRoutes(deps) {
1092
1265
  processId: process.id,
1093
1266
  permissionMode: process.permissionMode,
1094
1267
  modeVersion: process.modeVersion,
1268
+ recapAfterSeconds: process.recapAfterSeconds,
1095
1269
  },
1096
1270
  provider: process.provider,
1097
1271
  model: process.resolvedModel ?? process.model,
@@ -1127,7 +1301,8 @@ export function createSessionsRoutes(deps) {
1127
1301
  unsubscribe = null;
1128
1302
  };
1129
1303
  unsubscribe = replacement.subscribe((event) => {
1130
- if (event.type === "message" && isRestartReplacementActivity(event.message)) {
1304
+ if (event.type === "message" &&
1305
+ isRestartReplacementActivity(event.message)) {
1131
1306
  cleanup();
1132
1307
  void deps.supervisor.abortProcess(oldProcess.id).catch((error) => {
1133
1308
  console.warn(`[restart] Failed to abort old process ${oldProcess.id}:`, error);
@@ -1157,11 +1332,19 @@ export function createSessionsRoutes(deps) {
1157
1332
  if (!isUrlProjectId(projectId)) {
1158
1333
  return c.json({ error: "Invalid project ID format" }, 400);
1159
1334
  }
1160
- const project = await deps.scanner.getOrCreateProject(projectId);
1161
- if (!project) {
1162
- return c.json({ error: "Project not found" }, 404);
1335
+ const routing = await resolveSessionProjectRouting(projectId, c.req.param("sessionId"));
1336
+ if ("redirectProjectId" in routing) {
1337
+ return c.redirect(buildSessionProjectRedirectPath(routing.redirectProjectId, c.req.param("sessionId"), "", c.req.url), 307);
1338
+ }
1339
+ if ("error" in routing) {
1340
+ return c.json({ error: routing.error }, routing.status);
1163
1341
  }
1164
- const reader = deps.readerFactory(project);
1342
+ const reader = await resolveSessionReaderForAgentContent({
1343
+ deps,
1344
+ project: routing.transcriptProject,
1345
+ sessionId: c.req.param("sessionId"),
1346
+ projectId: routing.transcriptProjectId,
1347
+ });
1165
1348
  const mappings = await reader.getAgentMappings();
1166
1349
  return c.json({ mappings });
1167
1350
  });
@@ -1174,17 +1357,30 @@ export function createSessionsRoutes(deps) {
1174
1357
  if (!isUrlProjectId(projectId)) {
1175
1358
  return c.json({ error: "Invalid project ID format" }, 400);
1176
1359
  }
1177
- const project = await deps.scanner.getOrCreateProject(projectId);
1178
- if (!project) {
1179
- return c.json({ error: "Project not found" }, 404);
1360
+ const routing = await resolveSessionProjectRouting(projectId, c.req.param("sessionId"));
1361
+ if ("redirectProjectId" in routing) {
1362
+ return c.redirect(buildSessionProjectRedirectPath(routing.redirectProjectId, c.req.param("sessionId"), "", c.req.url), 307);
1180
1363
  }
1181
- const reader = deps.readerFactory(project);
1364
+ if ("error" in routing) {
1365
+ return c.json({ error: routing.error }, routing.status);
1366
+ }
1367
+ const reader = await resolveSessionReaderForAgentContent({
1368
+ deps,
1369
+ project: routing.transcriptProject,
1370
+ sessionId: c.req.param("sessionId"),
1371
+ projectId: routing.transcriptProjectId,
1372
+ });
1182
1373
  const agentSession = await reader.getAgentSession(agentId);
1183
1374
  if (!agentSession) {
1184
1375
  return c.json({ error: "Agent session not found" }, 404);
1185
1376
  }
1186
1377
  // Add server-rendered HTML to text blocks for markdown display
1187
- await augmentTextBlocks(agentSession.messages);
1378
+ await augmentTextBlocks(agentSession.messages, {
1379
+ projectFileLinks: {
1380
+ projectId: routing.workingProjectId,
1381
+ projectPath: routing.workingProject.path,
1382
+ },
1383
+ });
1188
1384
  return c.json(agentSession);
1189
1385
  });
1190
1386
  // GET /api/projects/:projectId/sessions/:sessionId/metadata - Get session metadata only (no messages)
@@ -1196,10 +1392,16 @@ export function createSessionsRoutes(deps) {
1196
1392
  if (!isUrlProjectId(projectId)) {
1197
1393
  return c.json({ error: "Invalid project ID format" }, 400);
1198
1394
  }
1199
- const project = await deps.scanner.getOrCreateProject(projectId);
1200
- if (!project) {
1201
- return c.json({ error: "Project not found" }, 404);
1395
+ const routing = await resolveSessionProjectRouting(projectId, sessionId);
1396
+ if ("redirectProjectId" in routing) {
1397
+ return c.redirect(buildSessionProjectRedirectPath(routing.redirectProjectId, sessionId, "/metadata", c.req.url), 307);
1398
+ }
1399
+ if ("error" in routing) {
1400
+ return c.json({ error: routing.error }, routing.status);
1202
1401
  }
1402
+ const project = routing.workingProject;
1403
+ const transcriptProject = routing.transcriptProject;
1404
+ const transcriptProjectId = routing.transcriptProjectId;
1203
1405
  // Check if session is actively owned by a process
1204
1406
  const process = deps.supervisor.getProcessForSession(sessionId);
1205
1407
  // Check if session is being controlled by an external program
@@ -1211,6 +1413,7 @@ export function createSessionsRoutes(deps) {
1211
1413
  processId: process.id,
1212
1414
  permissionMode: process.permissionMode,
1213
1415
  modeVersion: process.modeVersion,
1416
+ recapAfterSeconds: process.recapAfterSeconds,
1214
1417
  }
1215
1418
  : isExternal
1216
1419
  ? { owner: "external" }
@@ -1222,23 +1425,18 @@ export function createSessionsRoutes(deps) {
1222
1425
  const lastSeenAt = lastSeenEntry?.timestamp;
1223
1426
  // Get pending input request from active process
1224
1427
  const pendingInputRequest = process?.state.type === "waiting-input" ? process.state.request : null;
1225
- // Get available slash commands from active process
1226
- const slashCommands = process?.supportsDynamicCommands
1227
- ? await process.supportedCommands()
1228
- : null;
1428
+ const providerRuntimeStatus = process?.getProviderRuntimeStatus() ?? null;
1229
1429
  // Read minimal session info from disk (just for title/timestamps, no messages)
1230
- const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
1231
- const sessionSummaryResult = await findSessionSummaryAcrossProviders(project, sessionId, projectId, {
1232
- readerFactory: deps.readerFactory,
1233
- codexSessionsDir: deps.codexSessionsDir,
1234
- codexReaderFactory: deps.codexReaderFactory,
1235
- geminiSessionsDir: deps.geminiSessionsDir,
1236
- geminiReaderFactory: deps.geminiReaderFactory,
1237
- geminiHashToCwd: deps.geminiScanner?.getHashToCwd(),
1238
- grokSessionsDir: deps.grokSessionsDir,
1239
- grokReaderFactory: deps.grokReaderFactory,
1240
- }, metadataProvider ?? process?.provider);
1241
- const sessionSummary = sessionSummaryResult?.summary ?? null;
1430
+ const metadataProvider = metadata?.provider ??
1431
+ deps.sessionMetadataService?.getProvider(sessionId);
1432
+ const slashCommands = await getSessionSlashCommands(process, process?.provider ?? metadataProvider ?? project.provider);
1433
+ const deferredMessages = sessionQueueSummaries(deps, sessionId, process);
1434
+ const sessionSummaryResult = await findSessionSummaryAcrossProviders(transcriptProject, sessionId, transcriptProjectId, providerResolutionDeps(deps), metadataProvider ?? process?.provider);
1435
+ const rawSessionSummary = sessionSummaryResult?.summary ?? null;
1436
+ const recapMessages = deps.sessionMetadataService?.getRecapMessages?.(sessionId) ?? [];
1437
+ const sessionSummary = rawSessionSummary
1438
+ ? applyRecapOverlayToSummary(rawSessionSummary, recapMessages)
1439
+ : null;
1242
1440
  if (!sessionSummary && !process) {
1243
1441
  const canonicalProjectId = await getGrokNativeProjectId(sessionId, projectId);
1244
1442
  if (canonicalProjectId) {
@@ -1246,14 +1444,13 @@ export function createSessionsRoutes(deps) {
1246
1444
  }
1247
1445
  return c.json({ error: "Session not found" }, 404);
1248
1446
  }
1249
- // Calculate hasUnread if we have session summary
1250
- const hasUnread = deps.notificationService && sessionSummary
1251
- ? deps.notificationService.hasUnread(sessionId, sessionSummary.updatedAt)
1447
+ const hasUnread = rawSessionSummary
1448
+ ? hasUnreadProviderContent(deps.notificationService, sessionId, rawSessionSummary.updatedAt)
1252
1449
  : undefined;
1253
1450
  const response = {
1254
1451
  session: {
1255
1452
  id: sessionId,
1256
- projectId: projectId,
1453
+ projectId: routing.workingProjectId,
1257
1454
  title: sessionSummary?.title ?? null,
1258
1455
  fullTitle: sessionSummary?.fullTitle ?? null,
1259
1456
  createdAt: sessionSummary?.createdAt ?? new Date().toISOString(),
@@ -1279,16 +1476,142 @@ export function createSessionsRoutes(deps) {
1279
1476
  heartbeatTurnsAfterMinutes: metadata?.heartbeatTurnsAfterMinutes,
1280
1477
  heartbeatTurnText: metadata?.heartbeatTurnText,
1281
1478
  heartbeatForceAfterMinutes: metadata?.heartbeatForceAfterMinutes ?? undefined,
1479
+ promptSuggestionMode: metadata?.promptSuggestionMode,
1480
+ recapAfterSeconds: metadata?.recapAfterSeconds,
1481
+ workingProjectId: metadata?.workingProjectId,
1482
+ transcriptProjectId: metadata?.transcriptProjectId,
1483
+ workstreamId: metadata?.workstreamId,
1484
+ transcriptDisplayObjects: metadata?.transcriptDisplayObjects,
1282
1485
  lastSeenAt,
1283
1486
  hasUnread,
1284
1487
  },
1285
1488
  ownership,
1286
1489
  processState: process?.state.type ?? null,
1490
+ providerRuntimeStatus,
1287
1491
  pendingInputRequest,
1288
1492
  slashCommands,
1493
+ ...(deferredMessages.length > 0 ? { deferredMessages } : {}),
1289
1494
  };
1290
1495
  return c.json(response);
1291
1496
  });
1497
+ // PUT /api/projects/:projectId/sessions/:sessionId/project
1498
+ // Reclassify a session under a different YA project without sending any
1499
+ // provider-visible turn. The provider transcript remains under
1500
+ // transcriptProjectId; relative file links and UI routing use projectId.
1501
+ routes.put("/projects/:projectId/sessions/:sessionId/project", async (c) => {
1502
+ const projectId = c.req.param("projectId");
1503
+ const sessionId = c.req.param("sessionId");
1504
+ if (!isUrlProjectId(projectId)) {
1505
+ return c.json({ error: "Invalid project ID format" }, 400);
1506
+ }
1507
+ if (!deps.sessionMetadataService) {
1508
+ return c.json({ error: "Session metadata service unavailable" }, 503);
1509
+ }
1510
+ let body;
1511
+ try {
1512
+ body = await c.req.json();
1513
+ }
1514
+ catch {
1515
+ return c.json({ error: "Invalid JSON body" }, 400);
1516
+ }
1517
+ const targetProjectId = body.projectId;
1518
+ if (typeof targetProjectId !== "string" ||
1519
+ !isUrlProjectId(targetProjectId)) {
1520
+ return c.json({ error: "Invalid target project ID format" }, 400);
1521
+ }
1522
+ const targetProject = await deps.scanner.getOrCreateProject(targetProjectId);
1523
+ if (!targetProject) {
1524
+ return c.json({ error: "Target project not found" }, 404);
1525
+ }
1526
+ const metadata = deps.sessionMetadataService.getMetadata(sessionId);
1527
+ const process = deps.supervisor.getProcessForSession(sessionId);
1528
+ const transcriptProjectId = metadata?.transcriptProjectId ??
1529
+ process?.projectId ??
1530
+ projectId;
1531
+ const transcriptProject = transcriptProjectId === targetProjectId
1532
+ ? targetProject
1533
+ : await deps.scanner.getOrCreateProject(transcriptProjectId);
1534
+ if (!transcriptProject) {
1535
+ return c.json({ error: "Transcript project not found" }, 404);
1536
+ }
1537
+ if (!process) {
1538
+ const metadataProvider = metadata?.provider ??
1539
+ deps.sessionMetadataService.getProvider(sessionId);
1540
+ const summary = await findSessionSummaryAcrossProviders(transcriptProject, sessionId, transcriptProjectId, providerResolutionDeps(deps), metadataProvider, { readMode: "head" });
1541
+ if (!summary) {
1542
+ return c.json({ error: "Session not found" }, 404);
1543
+ }
1544
+ }
1545
+ const storedWorkingProjectId = targetProjectId === transcriptProjectId ? undefined : targetProjectId;
1546
+ const storedTranscriptProjectId = targetProjectId === transcriptProjectId ? undefined : transcriptProjectId;
1547
+ await deps.sessionMetadataService.setWorkingProject(sessionId, storedWorkingProjectId, storedTranscriptProjectId);
1548
+ deps.eventBus?.emit({
1549
+ type: "session-metadata-changed",
1550
+ sessionId,
1551
+ projectId: targetProjectId,
1552
+ transcriptProjectId: storedTranscriptProjectId ?? null,
1553
+ timestamp: new Date().toISOString(),
1554
+ });
1555
+ return c.json({
1556
+ updated: true,
1557
+ projectId: targetProjectId,
1558
+ transcriptProjectId: storedTranscriptProjectId ?? null,
1559
+ });
1560
+ });
1561
+ // POST /api/projects/:projectId/sessions/:sessionId/refresh-preview
1562
+ // On-demand recompute of the hover-card recent-activity excerpt for a
1563
+ // non-running session (its live state is otherwise only refreshed when the
1564
+ // session is resumed). Fast reverse-scan of the JSONL, then push the result
1565
+ // to lists/hovers via a session-updated event so the preview updates in
1566
+ // place without flicker. See topics/session-hovercard-recent-activity.md.
1567
+ routes.post("/projects/:projectId/sessions/:sessionId/refresh-preview", async (c) => {
1568
+ const projectId = c.req.param("projectId");
1569
+ const sessionId = c.req.param("sessionId");
1570
+ if (!isUrlProjectId(projectId)) {
1571
+ return c.json({ error: "Invalid project ID format" }, 400);
1572
+ }
1573
+ const routing = await resolveSessionProjectRouting(projectId, sessionId);
1574
+ if ("redirectProjectId" in routing) {
1575
+ return c.redirect(buildSessionProjectRedirectPath(routing.redirectProjectId, sessionId, "/refresh-preview", c.req.url), 307);
1576
+ }
1577
+ if ("error" in routing) {
1578
+ return c.json({ error: routing.error }, routing.status);
1579
+ }
1580
+ const transcriptProject = routing.transcriptProject;
1581
+ const transcriptProjectId = routing.transcriptProjectId;
1582
+ const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
1583
+ const recapMessages = deps.sessionMetadataService?.getRecapMessages?.(sessionId) ?? [];
1584
+ let lastAgentText;
1585
+ if (recapMessages.length > 0) {
1586
+ const summaryResult = await findSessionSummaryAcrossProviders(transcriptProject, sessionId, transcriptProjectId, providerResolutionDeps(deps), metadataProvider);
1587
+ if (summaryResult?.summary) {
1588
+ lastAgentText = applyRecapOverlayToSummary(summaryResult.summary, recapMessages).lastAgentText;
1589
+ }
1590
+ }
1591
+ // Claude uses the fast reverse-scan when there is no durable overlay to
1592
+ // compare; every other provider goes through the cross-provider load +
1593
+ // normalize, then a shared extractor on the uniform message form.
1594
+ const reader = deps.readerFactory(transcriptProject);
1595
+ lastAgentText ??= reader.getLastAgentExcerpt
1596
+ ? await reader.getLastAgentExcerpt(sessionId)
1597
+ : undefined;
1598
+ if (lastAgentText === undefined) {
1599
+ const normalized = await loadRestartSourceSession(transcriptProject, sessionId, transcriptProjectId, metadataProvider);
1600
+ if (normalized) {
1601
+ lastAgentText = extractLastAgentExcerpt(normalized.messages);
1602
+ }
1603
+ }
1604
+ if (lastAgentText !== undefined) {
1605
+ deps.eventBus?.emit({
1606
+ type: "session-updated",
1607
+ sessionId,
1608
+ projectId: routing.workingProjectId,
1609
+ lastAgentText,
1610
+ timestamp: new Date().toISOString(),
1611
+ });
1612
+ }
1613
+ return c.json({ lastAgentText: lastAgentText ?? null });
1614
+ });
1292
1615
  // GET /api/projects/:projectId/sessions/:sessionId - Get session detail
1293
1616
  // Optional query params:
1294
1617
  // ?afterMessageId=<id> - incremental forward-fetch (append new messages)
@@ -1296,12 +1619,15 @@ export function createSessionsRoutes(deps) {
1296
1619
  // ?beforeMessageId=<id> - cursor for loading older chunks (used with tailCompactions)
1297
1620
  // ?tailTurns=<n> - aggressive opt-in client memory cap by recent user turns
1298
1621
  // ?tailFrom=<id> - aggressive opt-in client memory cap from a user message id
1622
+ // ?fullHistory=1 - explicit opt-in to the full transcript
1299
1623
  routes.get("/projects/:projectId/sessions/:sessionId", async (c) => {
1300
1624
  const requestStartMs = performance.now();
1301
1625
  const projectId = c.req.param("projectId");
1302
1626
  const sessionId = c.req.param("sessionId");
1303
1627
  const afterMessageId = c.req.query("afterMessageId");
1304
1628
  const publicShare = c.req.query("publicShare") === "1";
1629
+ const fullHistory = c.req.query("fullHistory") === "1";
1630
+ const fullHistoryReason = c.req.query("fullHistoryReason");
1305
1631
  const tailCompactionsParam = c.req.query("tailCompactions");
1306
1632
  const beforeMessageId = c.req.query("beforeMessageId");
1307
1633
  const tailTurnsParam = c.req.query("tailTurns");
@@ -1312,15 +1638,38 @@ export function createSessionsRoutes(deps) {
1312
1638
  const tailTurns = tailTurnsParam !== undefined
1313
1639
  ? Number.parseInt(tailTurnsParam, 10)
1314
1640
  : undefined;
1641
+ const requestedTailCompactions = tailCompactions !== undefined &&
1642
+ !Number.isNaN(tailCompactions) &&
1643
+ tailCompactions > 0
1644
+ ? tailCompactions
1645
+ : undefined;
1646
+ const requestedTailTurns = tailTurns !== undefined && !Number.isNaN(tailTurns) && tailTurns > 0
1647
+ ? tailTurns
1648
+ : undefined;
1649
+ const defaultedToCompactTail = !fullHistory &&
1650
+ !afterMessageId &&
1651
+ !tailFrom &&
1652
+ requestedTailTurns === undefined &&
1653
+ requestedTailCompactions === undefined;
1654
+ const effectiveTailCompactions = requestedTailCompactions ??
1655
+ (defaultedToCompactTail
1656
+ ? DEFAULT_SESSION_DETAIL_TAIL_COMPACTIONS
1657
+ : undefined);
1315
1658
  // Validate projectId format at API boundary
1316
1659
  if (!isUrlProjectId(projectId)) {
1317
1660
  return c.json({ error: "Invalid project ID format" }, 400);
1318
1661
  }
1319
- // Use getOrCreateProject to support Codex projects that may not be in the scan cache yet
1320
- const project = await deps.scanner.getOrCreateProject(projectId);
1321
- if (!project) {
1322
- return c.json({ error: "Project not found" }, 404);
1662
+ const routing = await resolveSessionProjectRouting(projectId, sessionId);
1663
+ if ("redirectProjectId" in routing) {
1664
+ return c.redirect(buildSessionProjectRedirectPath(routing.redirectProjectId, sessionId, "", c.req.url), 307);
1323
1665
  }
1666
+ if ("error" in routing) {
1667
+ return c.json({ error: routing.error }, routing.status);
1668
+ }
1669
+ const project = routing.workingProject;
1670
+ const effectiveProjectId = routing.workingProjectId;
1671
+ const transcriptProject = routing.transcriptProject;
1672
+ const transcriptProjectId = routing.transcriptProjectId;
1324
1673
  const projectResolvedMs = performance.now();
1325
1674
  // Check if session is actively owned by a process
1326
1675
  const process = deps.supervisor.getProcessForSession(sessionId);
@@ -1330,70 +1679,30 @@ export function createSessionsRoutes(deps) {
1330
1679
  // Only mark tools as "aborted" if we owned the session and know it terminated
1331
1680
  const wasEverOwned = deps.supervisor.wasEverOwned(sessionId);
1332
1681
  const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
1333
- // Always try to read from disk first (even for owned sessions)
1334
- const reader = deps.readerFactory(project);
1335
- let loadedSession = await reader.getSession(sessionId, project.id, afterMessageId, {
1682
+ const recapMessages = deps.sessionMetadataService?.getRecapMessages?.(sessionId) ?? [];
1683
+ const recapCursor = findDurableRecapCursor(afterMessageId, recapMessages);
1684
+ const providerAfterMessageId = recapCursor ? undefined : afterMessageId;
1685
+ const primaryReaderAfterMessageId = isClaudeSdkProviderName(project.provider) ||
1686
+ isClaudeSdkProviderName(metadataProvider)
1687
+ ? undefined
1688
+ : providerAfterMessageId;
1689
+ const loadedSession = await loadProviderSession(transcriptProject, sessionId, transcriptProjectId, metadataProvider ?? process?.provider, primaryReaderAfterMessageId, {
1336
1690
  // Only include orphaned tool info if:
1337
1691
  // 1. We previously owned this session (not external)
1338
1692
  // 2. No active process (tools aren't potentially in progress)
1339
1693
  // When we own the session, tools without results might be pending approval
1340
1694
  includeOrphans: wasEverOwned && !process,
1341
1695
  });
1342
- // For Claude projects, also check for Codex sessions if primary reader didn't find it
1343
- // This handles mixed projects that have sessions from multiple providers
1344
- if (!loadedSession &&
1345
- project.provider === "claude" &&
1346
- (deps.codexReaderFactory || deps.codexSessionsDir)) {
1347
- const codexReader = deps.codexReaderFactory?.(project.path) ??
1348
- (deps.codexSessionsDir
1349
- ? new CodexSessionReader({
1350
- sessionsDir: deps.codexSessionsDir,
1351
- projectPath: project.path,
1352
- })
1353
- : null);
1354
- if (codexReader) {
1355
- loadedSession = await codexReader.getSession(sessionId, project.id, afterMessageId, { includeOrphans: wasEverOwned && !process });
1356
- }
1357
- }
1358
- // For Claude/Codex projects, also check for Gemini sessions if still not found
1359
- // This handles mixed projects that have sessions from multiple providers
1360
- if (!loadedSession &&
1361
- (project.provider === "claude" || project.provider === "codex") &&
1362
- (deps.geminiReaderFactory || deps.geminiSessionsDir)) {
1363
- const geminiReader = deps.geminiReaderFactory?.(project.path) ??
1364
- (deps.geminiSessionsDir
1365
- ? new GeminiSessionReader({
1366
- sessionsDir: deps.geminiSessionsDir,
1367
- projectPath: project.path,
1368
- hashToCwd: deps.geminiScanner?.getHashToCwd(),
1369
- })
1370
- : null);
1371
- if (geminiReader) {
1372
- loadedSession = await geminiReader.getSession(sessionId, project.id, afterMessageId, { includeOrphans: wasEverOwned && !process });
1373
- }
1374
- }
1375
- // For mixed-provider projects, a restarted YA process may need session
1376
- // metadata to rediscover an OpenCode-native ses_* transcript.
1377
- if (!loadedSession && metadataProvider === "opencode") {
1378
- const opencodeReader = deps.readerFactory({
1379
- ...project,
1380
- provider: "opencode",
1381
- });
1382
- loadedSession = await opencodeReader.getSession(sessionId, project.id, afterMessageId, { includeOrphans: wasEverOwned && !process });
1383
- }
1384
- if (!loadedSession) {
1385
- const grokReader = getGrokReader(project.path);
1386
- if (grokReader) {
1387
- loadedSession = await grokReader.getSession(sessionId, project.id, afterMessageId, { includeOrphans: wasEverOwned && !process });
1388
- }
1389
- }
1390
1696
  const readEndMs = performance.now();
1391
1697
  let session = loadedSession ? normalizeSession(loadedSession) : null;
1392
1698
  const normalizedMessageCount = session?.messages.length ?? 0;
1393
1699
  const normalizeEndMs = performance.now();
1700
+ if (session && isClaudeSdkProviderName(session.provider)) {
1701
+ augmentTaskListSnapshots(session.messages);
1702
+ }
1394
1703
  let incrementalAnchorFound = false;
1395
- if (session && afterMessageId) {
1396
- const sliced = sliceAfterMessageIdWithMatch(session.messages, afterMessageId);
1704
+ if (session && providerAfterMessageId) {
1705
+ const sliced = sliceAfterMessageIdWithMatch(session.messages, providerAfterMessageId);
1397
1706
  session = {
1398
1707
  ...session,
1399
1708
  messages: sliced.messages,
@@ -1407,6 +1716,7 @@ export function createSessionsRoutes(deps) {
1407
1716
  processId: process.id,
1408
1717
  permissionMode: process.permissionMode,
1409
1718
  modeVersion: process.modeVersion,
1719
+ recapAfterSeconds: process.recapAfterSeconds,
1410
1720
  }
1411
1721
  : isExternal
1412
1722
  ? { owner: "external" }
@@ -1414,12 +1724,16 @@ export function createSessionsRoutes(deps) {
1414
1724
  // Get pending input request from active process (for tool approval prompts)
1415
1725
  // This ensures clients get pending requests immediately without waiting for SSE
1416
1726
  const pendingInputRequest = process?.state.type === "waiting-input" ? process.state.request : null;
1417
- // Get available slash commands from active process (for "/" button in toolbar)
1727
+ const providerRuntimeStatus = process?.getProviderRuntimeStatus() ?? null;
1728
+ // Get available slash commands (for "/" button and typed slash menu)
1418
1729
  // The init message that normally carries these gets discarded from the SSE buffer
1419
- // after ~30s, so we attach them to the REST response for reliable delivery.
1420
- const slashCommands = process?.supportsDynamicCommands
1421
- ? await process.supportedCommands()
1422
- : null;
1730
+ // after ~30s, so we attach them to the REST response. Providers with known
1731
+ // native built-ins, such as Codex, can expose those while stopped.
1732
+ const slashCommands = await getSessionSlashCommands(process, process?.provider ??
1733
+ session?.provider ??
1734
+ metadataProvider ??
1735
+ project.provider);
1736
+ const deferredMessages = sessionQueueSummaries(deps, sessionId, process);
1423
1737
  if (!session) {
1424
1738
  // Session file doesn't exist yet - only valid if we own the process
1425
1739
  if (process) {
@@ -1436,22 +1750,23 @@ export function createSessionsRoutes(deps) {
1436
1750
  : mis
1437
1751
  ? (m, p) => mis.getContextWindow(m, p)
1438
1752
  : undefined);
1439
- // Cache SDK-reported context window for future JSONL reads
1440
- if (mis && sdkContextWindow && process.resolvedModel) {
1441
- mis.recordContextWindow(process.resolvedModel, sdkContextWindow, process.provider);
1442
- }
1753
+ // (Durable recording happens at the observation point in Process via
1754
+ // onContextWindowObserved, not as a side effect of this GET.)
1443
1755
  // Get metadata even for new sessions (in case it was set before file was written)
1444
1756
  const metadata = deps.sessionMetadataService?.getMetadata(sessionId);
1757
+ const recapMessages = deps.sessionMetadataService?.getRecapMessages?.(sessionId) ?? [];
1758
+ const visibleProcessMessages = mergeRecapMessages(processMessages, recapMessages);
1445
1759
  // Get notification data for new sessions too
1446
1760
  const lastSeenEntry = deps.notificationService?.getLastSeen(sessionId);
1447
- const newSessionUpdatedAt = new Date().toISOString();
1761
+ const latestRecap = latestRecapMessage(recapMessages);
1762
+ const newSessionUpdatedAt = latestRecap?.timestamp ?? new Date().toISOString();
1448
1763
  const hasUnread = deps.notificationService
1449
1764
  ? deps.notificationService.hasUnread(sessionId, newSessionUpdatedAt)
1450
1765
  : undefined;
1451
1766
  return c.json({
1452
1767
  session: {
1453
1768
  id: sessionId,
1454
- projectId,
1769
+ projectId: effectiveProjectId,
1455
1770
  title: null,
1456
1771
  createdAt: new Date().toISOString(),
1457
1772
  updatedAt: newSessionUpdatedAt,
@@ -1466,16 +1781,24 @@ export function createSessionsRoutes(deps) {
1466
1781
  heartbeatTurnsAfterMinutes: metadata?.heartbeatTurnsAfterMinutes,
1467
1782
  heartbeatTurnText: metadata?.heartbeatTurnText,
1468
1783
  heartbeatForceAfterMinutes: metadata?.heartbeatForceAfterMinutes,
1784
+ promptSuggestionMode: metadata?.promptSuggestionMode,
1785
+ recapAfterSeconds: metadata?.recapAfterSeconds,
1786
+ workingProjectId: metadata?.workingProjectId,
1787
+ transcriptProjectId: metadata?.transcriptProjectId,
1788
+ workstreamId: metadata?.workstreamId,
1789
+ transcriptDisplayObjects: metadata?.transcriptDisplayObjects,
1469
1790
  lastSeenAt: lastSeenEntry?.timestamp,
1470
1791
  hasUnread,
1471
1792
  provider: process.provider,
1472
1793
  model: process.resolvedModel,
1473
1794
  contextUsage,
1474
1795
  },
1475
- messages: processMessages,
1796
+ messages: visibleProcessMessages,
1476
1797
  ownership,
1798
+ providerRuntimeStatus,
1477
1799
  pendingInputRequest,
1478
1800
  slashCommands,
1801
+ ...(deferredMessages.length > 0 ? { deferredMessages } : {}),
1479
1802
  });
1480
1803
  }
1481
1804
  const canonicalProjectId = await getGrokNativeProjectId(sessionId, projectId);
@@ -1489,37 +1812,32 @@ export function createSessionsRoutes(deps) {
1489
1812
  // Get notification data (lastSeenAt, hasUnread)
1490
1813
  const lastSeenEntry = deps.notificationService?.getLastSeen(sessionId);
1491
1814
  const lastSeenAt = lastSeenEntry?.timestamp;
1492
- const hasUnread = deps.notificationService
1493
- ? deps.notificationService.hasUnread(sessionId, session.updatedAt)
1494
- : undefined;
1495
1815
  // Apply pagination if requested (BEFORE expensive augmentation). tailTurns
1496
1816
  // is an opt-in stronger client memory cap, so it wins over compact tails.
1497
1817
  // Skip both when afterMessageId is present since that's an incremental
1498
1818
  // forward-fetch use case.
1499
1819
  let paginationInfo;
1500
1820
  if (!afterMessageId &&
1501
- (tailFrom ||
1502
- (tailTurns !== undefined && !Number.isNaN(tailTurns) && tailTurns > 0))) {
1503
- const sliced = sliceAtUserTurnBoundary(session.messages, tailTurns !== undefined && !Number.isNaN(tailTurns) && tailTurns > 0
1504
- ? tailTurns
1505
- : 20, tailFrom);
1821
+ (tailFrom || requestedTailTurns !== undefined)) {
1822
+ const sliced = sliceAtUserTurnBoundary(session.messages, requestedTailTurns ?? 20, tailFrom);
1506
1823
  session = { ...session, messages: sliced.messages };
1507
1824
  paginationInfo = sliced.pagination;
1508
1825
  }
1509
- else if (tailCompactions !== undefined &&
1510
- !Number.isNaN(tailCompactions) &&
1511
- tailCompactions > 0 &&
1826
+ else if (effectiveTailCompactions !== undefined &&
1512
1827
  !afterMessageId) {
1513
- const sliced = sliceAtCompactBoundaries(session.messages, tailCompactions, beforeMessageId);
1828
+ const sliced = sliceAtCompactBoundaries(session.messages, effectiveTailCompactions, beforeMessageId);
1514
1829
  session = { ...session, messages: sliced.messages };
1515
1830
  paginationInfo = sliced.pagination;
1516
1831
  }
1832
+ if (isClaudeSdkProviderName(session.provider)) {
1833
+ pruneTaskListSnapshotsToLatest(session.messages);
1834
+ }
1517
1835
  const sliceEndMs = performance.now();
1518
1836
  // Codex normalized IDs can drift between stream and JSONL. If an
1519
1837
  // incremental request misses its anchor, never return the full historical
1520
1838
  // session into a compact-tail client; bound the fallback to the same tail
1521
1839
  // window used for initial loads.
1522
- if (afterMessageId && !incrementalAnchorFound) {
1840
+ if (providerAfterMessageId && !incrementalAnchorFound) {
1523
1841
  const sliced = sliceAtCompactBoundaries(session.messages, 2);
1524
1842
  session = { ...session, messages: sliced.messages };
1525
1843
  paginationInfo = sliced.pagination;
@@ -1529,9 +1847,33 @@ export function createSessionsRoutes(deps) {
1529
1847
  await augmentEditToolUses(session.messages);
1530
1848
  }
1531
1849
  else {
1532
- await augmentPersistedSessionMessages(session.messages);
1850
+ await augmentPersistedSessionMessages(session.messages, {
1851
+ projectFileLinks: {
1852
+ projectId: effectiveProjectId,
1853
+ projectPath: project.path,
1854
+ },
1855
+ });
1856
+ }
1857
+ // The overlay reassigns `session` below; hasUnreadProviderContent needs
1858
+ // the pre-overlay timestamp.
1859
+ const preRecapUpdatedAt = session.updatedAt;
1860
+ if (!beforeMessageId) {
1861
+ session = applyRecapOverlayToSession(session, recapMessages);
1862
+ if (recapCursor && afterMessageId) {
1863
+ const sliced = sliceAfterDurableRecapCursor({
1864
+ messages: session.messages,
1865
+ recap: recapCursor,
1866
+ cursorId: afterMessageId,
1867
+ });
1868
+ session = { ...session, messages: sliced.messages };
1869
+ incrementalAnchorFound = sliced.found;
1870
+ }
1871
+ }
1872
+ else {
1873
+ session = applyRecapOverlayToSummary(session, recapMessages);
1533
1874
  }
1534
1875
  const augmentEndMs = performance.now();
1876
+ const hasUnread = hasUnreadProviderContent(deps.notificationService, sessionId, preRecapUpdatedAt);
1535
1877
  // Override context usage with SDK-reported context window from live process
1536
1878
  // The reader uses hardcoded defaults; the process captures the real value at runtime
1537
1879
  let { contextUsage } = session;
@@ -1542,29 +1884,80 @@ export function createSessionsRoutes(deps) {
1542
1884
  percentage: Math.round((contextUsage.inputTokens / cw) * 100),
1543
1885
  contextWindow: cw,
1544
1886
  };
1545
- // Cache for future reads without a live process
1546
- deps.modelInfoService?.recordContextWindow(process.resolvedModel ?? session.model ?? "", cw, process.provider);
1887
+ // Durable recording happens at the observation point in Process via
1888
+ // onContextWindowObserved; this block only overrides the displayed value.
1547
1889
  }
1548
1890
  const { messages: _messages, ...sessionMetadata } = session;
1549
1891
  const totalMs = performance.now() - requestStartMs;
1892
+ if (defaultedToCompactTail &&
1893
+ normalizedMessageCount >= LARGE_FULL_HISTORY_MESSAGE_THRESHOLD) {
1894
+ getLogger().warn({
1895
+ beforeMessageId: beforeMessageId ?? null,
1896
+ defaultTailCompactions: DEFAULT_SESSION_DETAIL_TAIL_COMPACTIONS,
1897
+ event: "session_detail_unbounded_defaulted_to_tail",
1898
+ normalizedMessageCount,
1899
+ projectId: effectiveProjectId,
1900
+ transcriptProjectId,
1901
+ provider: session.provider,
1902
+ publicShare,
1903
+ returnedMessageCount: session.messages.length,
1904
+ sessionId,
1905
+ timings: {
1906
+ augmentMs: roundedMs(augmentEndMs - sliceEndMs),
1907
+ normalizeMs: roundedMs(normalizeEndMs - readEndMs),
1908
+ projectMs: roundedMs(projectResolvedMs - requestStartMs),
1909
+ readMs: roundedMs(readEndMs - projectResolvedMs),
1910
+ routeMs: roundedMs(sliceEndMs - normalizeEndMs),
1911
+ totalMs: roundedMs(totalMs),
1912
+ },
1913
+ totalMessageCount: session.messageCount,
1914
+ }, "SESSION_DETAIL: defaulted unbounded request to compact tail");
1915
+ }
1916
+ if (fullHistory &&
1917
+ session.messages.length >= LARGE_FULL_HISTORY_MESSAGE_THRESHOLD) {
1918
+ getLogger().warn({
1919
+ event: "session_detail_full_history_large",
1920
+ fullHistoryReason: fullHistoryReason ?? null,
1921
+ normalizedMessageCount,
1922
+ projectId: effectiveProjectId,
1923
+ transcriptProjectId,
1924
+ provider: session.provider,
1925
+ publicShare,
1926
+ returnedMessageCount: session.messages.length,
1927
+ sessionId,
1928
+ timings: {
1929
+ augmentMs: roundedMs(augmentEndMs - sliceEndMs),
1930
+ normalizeMs: roundedMs(normalizeEndMs - readEndMs),
1931
+ projectMs: roundedMs(projectResolvedMs - requestStartMs),
1932
+ readMs: roundedMs(readEndMs - projectResolvedMs),
1933
+ routeMs: roundedMs(sliceEndMs - normalizeEndMs),
1934
+ totalMs: roundedMs(totalMs),
1935
+ },
1936
+ totalMessageCount: session.messageCount,
1937
+ }, "SESSION_DETAIL: large explicit full-history request");
1938
+ }
1550
1939
  if (totalMs >= SESSION_DETAIL_SLOW_LOG_MS) {
1551
1940
  getLogger().warn({
1552
1941
  afterMessageId: afterMessageId ?? null,
1553
1942
  beforeMessageId: beforeMessageId ?? null,
1554
1943
  event: "session_detail_slow",
1944
+ defaultedToCompactTail,
1945
+ fullHistory,
1946
+ fullHistoryReason: fullHistoryReason ?? null,
1555
1947
  incrementalAnchorFound: afterMessageId
1556
1948
  ? incrementalAnchorFound
1557
1949
  : null,
1558
1950
  normalizedMessageCount,
1559
1951
  owned: Boolean(process),
1560
1952
  processState: process?.state.type ?? null,
1561
- projectId,
1953
+ projectId: effectiveProjectId,
1954
+ transcriptProjectId,
1562
1955
  provider: session.provider,
1563
1956
  publicShare,
1564
1957
  returnedMessageCount: session.messages.length,
1565
1958
  sessionId,
1566
- tailCompactions: tailCompactions ?? null,
1567
- tailTurns: tailTurns ?? null,
1959
+ tailCompactions: effectiveTailCompactions ?? null,
1960
+ tailTurns: requestedTailTurns ?? null,
1568
1961
  timings: {
1569
1962
  augmentMs: roundedMs(augmentEndMs - sliceEndMs),
1570
1963
  normalizeMs: roundedMs(normalizeEndMs - readEndMs),
@@ -1579,6 +1972,7 @@ export function createSessionsRoutes(deps) {
1579
1972
  return c.json({
1580
1973
  session: {
1581
1974
  ...sessionMetadata,
1975
+ projectId: effectiveProjectId,
1582
1976
  ownership,
1583
1977
  contextUsage,
1584
1978
  customTitle: metadata?.customTitle,
@@ -1590,6 +1984,12 @@ export function createSessionsRoutes(deps) {
1590
1984
  heartbeatTurnsAfterMinutes: metadata?.heartbeatTurnsAfterMinutes,
1591
1985
  heartbeatTurnText: metadata?.heartbeatTurnText,
1592
1986
  heartbeatForceAfterMinutes: metadata?.heartbeatForceAfterMinutes,
1987
+ promptSuggestionMode: metadata?.promptSuggestionMode,
1988
+ recapAfterSeconds: metadata?.recapAfterSeconds,
1989
+ workingProjectId: metadata?.workingProjectId,
1990
+ transcriptProjectId: metadata?.transcriptProjectId,
1991
+ workstreamId: metadata?.workstreamId,
1992
+ transcriptDisplayObjects: metadata?.transcriptDisplayObjects,
1593
1993
  // Model comes from the session reader (extracted from JSONL)
1594
1994
  model: session.model,
1595
1995
  lastSeenAt,
@@ -1597,9 +1997,11 @@ export function createSessionsRoutes(deps) {
1597
1997
  },
1598
1998
  messages: session.messages,
1599
1999
  ownership,
2000
+ providerRuntimeStatus,
1600
2001
  pendingInputRequest,
1601
2002
  slashCommands,
1602
2003
  ...(paginationInfo && { pagination: paginationInfo }),
2004
+ ...(deferredMessages.length > 0 ? { deferredMessages } : {}),
1603
2005
  });
1604
2006
  });
1605
2007
  // POST /api/projects/:projectId/sessions - Start new session
@@ -1632,6 +2034,10 @@ export function createSessionsRoutes(deps) {
1632
2034
  if (helperSettings.error) {
1633
2035
  return c.json({ error: helperSettings.error }, 400);
1634
2036
  }
2037
+ const workstreamTarget = resolveLaunchWorkstreamTarget(project, body.workstreamId);
2038
+ if ("error" in workstreamTarget) {
2039
+ return c.json({ error: workstreamTarget.error }, workstreamTarget.status);
2040
+ }
1635
2041
  const serverTimestamp = Date.now();
1636
2042
  const userMessage = {
1637
2043
  text: body.message,
@@ -1644,7 +2050,7 @@ export function createSessionsRoutes(deps) {
1644
2050
  };
1645
2051
  // Convert thinking option to SDK config
1646
2052
  const { thinking, effort } = body.thinking
1647
- ? thinkingOptionToConfig(body.thinking)
2053
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
1648
2054
  : { thinking: undefined, effort: undefined };
1649
2055
  // Convert model option (undefined or "default" means use CLI default)
1650
2056
  const model = body.model && body.model !== "default" ? body.model : undefined;
@@ -1656,7 +2062,7 @@ export function createSessionsRoutes(deps) {
1656
2062
  model: body.model,
1657
2063
  serviceTier,
1658
2064
  });
1659
- const result = await deps.supervisor.startSession(project.path, userMessage, body.mode, {
2065
+ const result = await deps.supervisor.startSession(workstreamTarget.projectPath, userMessage, body.mode, {
1660
2066
  model,
1661
2067
  serviceTier,
1662
2068
  thinking,
@@ -1666,8 +2072,12 @@ export function createSessionsRoutes(deps) {
1666
2072
  globalInstructions: getGlobalInstructions(),
1667
2073
  permissions: body.permissions,
1668
2074
  recapMode: helperSettings.recapMode,
2075
+ recapAfterSeconds: helperSettings.recapAfterSeconds,
1669
2076
  promptSuggestionMode: helperSettings.promptSuggestionMode,
1670
2077
  helperSideModel: helperSettings.helperSideModel,
2078
+ }, {
2079
+ projectId: project.id,
2080
+ workstreamId: workstreamTarget.workstreamId,
1671
2081
  });
1672
2082
  // Check if queue is full
1673
2083
  if (isQueueFullResponse(result)) {
@@ -1677,13 +2087,14 @@ export function createSessionsRoutes(deps) {
1677
2087
  if (isQueuedResponse(result)) {
1678
2088
  return c.json({ ...result, serverTimestamp }, 202); // 202 Accepted - queued for processing
1679
2089
  }
1680
- await persistLaunchMetadata(result.sessionId, body.provider, executor, body.message);
2090
+ await persistLaunchMetadata(result.sessionId, body.provider, executor, body.message, body.model, result.promptSuggestionMode, helperSettings.recapAfterSeconds, workstreamTarget.workstreamId);
1681
2091
  return c.json({
1682
2092
  sessionId: result.sessionId,
1683
2093
  processId: result.id,
1684
2094
  projectId: result.projectId,
1685
2095
  permissionMode: result.permissionMode,
1686
2096
  modeVersion: result.modeVersion,
2097
+ recapAfterSeconds: result.recapAfterSeconds,
1687
2098
  serverTimestamp,
1688
2099
  });
1689
2100
  });
@@ -1715,14 +2126,18 @@ export function createSessionsRoutes(deps) {
1715
2126
  if (helperSettings.error) {
1716
2127
  return c.json({ error: helperSettings.error }, 400);
1717
2128
  }
2129
+ const workstreamTarget = resolveLaunchWorkstreamTarget(project, body.workstreamId);
2130
+ if ("error" in workstreamTarget) {
2131
+ return c.json({ error: workstreamTarget.error }, workstreamTarget.status);
2132
+ }
1718
2133
  // Convert thinking option to SDK config
1719
2134
  const { thinking, effort } = body.thinking
1720
- ? thinkingOptionToConfig(body.thinking)
2135
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
1721
2136
  : { thinking: undefined, effort: undefined };
1722
2137
  // Convert model option (undefined or "default" means use CLI default)
1723
2138
  const model = body.model && body.model !== "default" ? body.model : undefined;
1724
2139
  const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
1725
- const result = await deps.supervisor.createSession(project.path, body.mode, {
2140
+ const result = await deps.supervisor.createSession(workstreamTarget.projectPath, body.mode, {
1726
2141
  model,
1727
2142
  serviceTier,
1728
2143
  thinking,
@@ -1732,8 +2147,12 @@ export function createSessionsRoutes(deps) {
1732
2147
  globalInstructions: getGlobalInstructions(),
1733
2148
  permissions: body.permissions,
1734
2149
  recapMode: helperSettings.recapMode,
2150
+ recapAfterSeconds: helperSettings.recapAfterSeconds,
1735
2151
  promptSuggestionMode: helperSettings.promptSuggestionMode,
1736
2152
  helperSideModel: helperSettings.helperSideModel,
2153
+ }, {
2154
+ projectId: project.id,
2155
+ workstreamId: workstreamTarget.workstreamId,
1737
2156
  });
1738
2157
  // Check if queue is full
1739
2158
  if (isQueueFullResponse(result)) {
@@ -1743,13 +2162,14 @@ export function createSessionsRoutes(deps) {
1743
2162
  if (isQueuedResponse(result)) {
1744
2163
  return c.json({ ...result, serverTimestamp: Date.now() }, 202); // 202 Accepted - queued for processing
1745
2164
  }
1746
- await persistLaunchMetadata(result.sessionId, body.provider, executor);
2165
+ await persistLaunchMetadata(result.sessionId, body.provider, executor, undefined, body.model, result.promptSuggestionMode, helperSettings.recapAfterSeconds, workstreamTarget.workstreamId);
1747
2166
  return c.json({
1748
2167
  sessionId: result.sessionId,
1749
2168
  processId: result.id,
1750
2169
  projectId: result.projectId,
1751
2170
  permissionMode: result.permissionMode,
1752
2171
  modeVersion: result.modeVersion,
2172
+ recapAfterSeconds: result.recapAfterSeconds,
1753
2173
  serverTimestamp: Date.now(),
1754
2174
  });
1755
2175
  });
@@ -1785,7 +2205,7 @@ export function createSessionsRoutes(deps) {
1785
2205
  metadata: buildUserMessageMetadata(body, serverTimestamp, "direct"),
1786
2206
  };
1787
2207
  const { thinking, effort } = body.thinking
1788
- ? thinkingOptionToConfig(body.thinking)
2208
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
1789
2209
  : { thinking: undefined, effort: undefined };
1790
2210
  const model = body.model && body.model !== "default" ? body.model : undefined;
1791
2211
  const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
@@ -1799,6 +2219,7 @@ export function createSessionsRoutes(deps) {
1799
2219
  globalInstructions: getGlobalInstructions(),
1800
2220
  permissions: body.permissions,
1801
2221
  recapMode: helperSettings.recapMode,
2222
+ recapAfterSeconds: helperSettings.recapAfterSeconds,
1802
2223
  promptSuggestionMode: helperSettings.promptSuggestionMode,
1803
2224
  helperSideModel: helperSettings.helperSideModel,
1804
2225
  });
@@ -1808,13 +2229,14 @@ export function createSessionsRoutes(deps) {
1808
2229
  if (isQueuedResponse(result)) {
1809
2230
  return c.json({ ...result, serverTimestamp: Date.now() }, 202);
1810
2231
  }
1811
- await persistLaunchMetadata(result.sessionId, body.provider, executor, body.message);
2232
+ await persistLaunchMetadata(result.sessionId, body.provider, executor, body.message, body.model, result.promptSuggestionMode, helperSettings.recapAfterSeconds);
1812
2233
  return c.json({
1813
2234
  sessionId: result.sessionId,
1814
2235
  processId: result.id,
1815
2236
  projectId: result.projectId,
1816
2237
  permissionMode: result.permissionMode,
1817
2238
  modeVersion: result.modeVersion,
2239
+ recapAfterSeconds: result.recapAfterSeconds,
1818
2240
  serverTimestamp,
1819
2241
  });
1820
2242
  });
@@ -1837,7 +2259,7 @@ export function createSessionsRoutes(deps) {
1837
2259
  }
1838
2260
  const projectPath = await ensureDetachedProjectPath(executor);
1839
2261
  const { thinking, effort } = body.thinking
1840
- ? thinkingOptionToConfig(body.thinking)
2262
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
1841
2263
  : { thinking: undefined, effort: undefined };
1842
2264
  const model = body.model && body.model !== "default" ? body.model : undefined;
1843
2265
  const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
@@ -1851,6 +2273,7 @@ export function createSessionsRoutes(deps) {
1851
2273
  globalInstructions: getGlobalInstructions(),
1852
2274
  permissions: body.permissions,
1853
2275
  recapMode: helperSettings.recapMode,
2276
+ recapAfterSeconds: helperSettings.recapAfterSeconds,
1854
2277
  promptSuggestionMode: helperSettings.promptSuggestionMode,
1855
2278
  helperSideModel: helperSettings.helperSideModel,
1856
2279
  });
@@ -1860,13 +2283,14 @@ export function createSessionsRoutes(deps) {
1860
2283
  if (isQueuedResponse(result)) {
1861
2284
  return c.json({ ...result, serverTimestamp: Date.now() }, 202);
1862
2285
  }
1863
- await persistLaunchMetadata(result.sessionId, body.provider, executor);
2286
+ await persistLaunchMetadata(result.sessionId, body.provider, executor, undefined, body.model, result.promptSuggestionMode, helperSettings.recapAfterSeconds);
1864
2287
  return c.json({
1865
2288
  sessionId: result.sessionId,
1866
2289
  processId: result.id,
1867
2290
  projectId: result.projectId,
1868
2291
  permissionMode: result.permissionMode,
1869
2292
  modeVersion: result.modeVersion,
2293
+ recapAfterSeconds: result.recapAfterSeconds,
1870
2294
  serverTimestamp: Date.now(),
1871
2295
  });
1872
2296
  });
@@ -1901,6 +2325,11 @@ export function createSessionsRoutes(deps) {
1901
2325
  if (helperSettings.error) {
1902
2326
  return c.json({ error: helperSettings.error }, 400);
1903
2327
  }
2328
+ const parsedResumeMode = parseOptionalResumeMode(body.resumeMode);
2329
+ if (parsedResumeMode.error) {
2330
+ return c.json({ error: parsedResumeMode.error }, 400);
2331
+ }
2332
+ const resumeMode = parsedResumeMode.resumeMode ?? "full";
1904
2333
  const serverTimestamp = Date.now();
1905
2334
  const userMessage = {
1906
2335
  text: body.message,
@@ -1913,10 +2342,16 @@ export function createSessionsRoutes(deps) {
1913
2342
  };
1914
2343
  // Convert thinking option to SDK config
1915
2344
  const { thinking, effort } = body.thinking
1916
- ? thinkingOptionToConfig(body.thinking)
2345
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
1917
2346
  : { thinking: undefined, effort: undefined };
1918
- // Convert model option (undefined or "default" means use CLI default)
1919
- const model = body.model && body.model !== "default" ? body.model : undefined;
2347
+ // Convert model option (undefined or "default" means use CLI default). When
2348
+ // the client sends no model (e.g. resume after a server restart), recover the
2349
+ // YA model id persisted at launch so the process keeps its requested alias
2350
+ // and per-model settings stay keyed by it. See topics/provider-abstraction.md.
2351
+ const requestedModel = body.model ?? deps.sessionMetadataService?.getRequestedModel(sessionId);
2352
+ const model = requestedModel && requestedModel !== "default"
2353
+ ? requestedModel
2354
+ : undefined;
1920
2355
  const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
1921
2356
  // Use client-provided executor, falling back to saved executor from metadata.
1922
2357
  let executor = parsedBodyExecutor.executor;
@@ -1945,23 +2380,14 @@ export function createSessionsRoutes(deps) {
1945
2380
  await deps.sessionMetadataService.setExecutor(sessionId, executor);
1946
2381
  }
1947
2382
  }
1948
- const globalInstructions = deps.serverSettingsService?.getSetting("globalInstructions") || undefined;
2383
+ const globalInstructions = getGlobalInstructions();
1949
2384
  // Look up the session's original provider so we resume with the correct one
1950
2385
  // (e.g., claude-ollama sessions need the Ollama provider, not default Claude).
1951
2386
  // Check metadata first (explicitly saved on creation), then fall back to reader.
1952
2387
  const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
1953
2388
  let providerName = metadataProvider ?? body.provider;
1954
2389
  if (!providerName) {
1955
- const sessionSummaryResult = await findSessionSummaryAcrossProviders(project, sessionId, projectId, {
1956
- readerFactory: deps.readerFactory,
1957
- codexSessionsDir: deps.codexSessionsDir,
1958
- codexReaderFactory: deps.codexReaderFactory,
1959
- geminiSessionsDir: deps.geminiSessionsDir,
1960
- geminiReaderFactory: deps.geminiReaderFactory,
1961
- geminiHashToCwd: deps.geminiScanner?.getHashToCwd(),
1962
- grokSessionsDir: deps.grokSessionsDir,
1963
- grokReaderFactory: deps.grokReaderFactory,
1964
- }, metadataProvider ?? body.provider);
2390
+ const sessionSummaryResult = await findSessionSummaryAcrossProviders(project, sessionId, projectId, providerResolutionDeps(deps), metadataProvider ?? body.provider, { readMode: "head" });
1965
2391
  const sessionSummary = sessionSummaryResult?.summary ?? null;
1966
2392
  providerName =
1967
2393
  sessionSummary?.provider ??
@@ -1969,6 +2395,20 @@ export function createSessionsRoutes(deps) {
1969
2395
  body.provider ??
1970
2396
  project.provider;
1971
2397
  }
2398
+ const previousProcess = deps.supervisor.getProcessForSession?.(sessionId);
2399
+ const resumeDiagnostics = {
2400
+ requestedMode: resumeMode,
2401
+ provider: providerName,
2402
+ previousProcess: previousProcess
2403
+ ? {
2404
+ processId: previousProcess.id,
2405
+ state: previousProcess.state.type,
2406
+ provider: previousProcess.provider,
2407
+ supportsDynamicCommands: previousProcess.supportsDynamicCommands,
2408
+ }
2409
+ : null,
2410
+ };
2411
+ let resumeSessionAt;
1972
2412
  if (isClaudeSdkProviderName(providerName)) {
1973
2413
  let blocker = null;
1974
2414
  try {
@@ -1983,7 +2423,21 @@ export function createSessionsRoutes(deps) {
1983
2423
  error: error instanceof Error ? error.message : String(error),
1984
2424
  }, "Failed to check Claude session for resume-blocking API error");
1985
2425
  }
1986
- if (blocker) {
2426
+ if (blocker?.resumeAtMessageId) {
2427
+ // Recoverable: resume up to the last good assistant message,
2428
+ // dropping the API-error tail, instead of forcing a handoff.
2429
+ resumeSessionAt = blocker.resumeAtMessageId;
2430
+ getLogger().info({
2431
+ event: "claude_resume_truncated_after_api_error",
2432
+ sessionId,
2433
+ projectId,
2434
+ providerName,
2435
+ messageId: blocker.messageId,
2436
+ apiErrorStatus: blocker.apiErrorStatus,
2437
+ resumeSessionAt,
2438
+ }, "Resuming Claude session before SDK API-error tail");
2439
+ }
2440
+ else if (blocker) {
1987
2441
  getLogger().warn({
1988
2442
  event: "claude_resume_blocked_after_api_error",
1989
2443
  sessionId,
@@ -1995,36 +2449,243 @@ export function createSessionsRoutes(deps) {
1995
2449
  return c.json({
1996
2450
  error: blocker.error,
1997
2451
  recovery: blocker.recovery,
2452
+ resume: {
2453
+ ...resumeDiagnostics,
2454
+ blockedReason: "claude-api-error-tail",
2455
+ },
1998
2456
  }, 409);
1999
2457
  }
2000
2458
  }
2001
- const result = await deps.supervisor.resumeSession(sessionId, project.path, userMessage, body.mode, {
2002
- model,
2003
- serviceTier,
2004
- thinking,
2005
- effort,
2006
- providerName,
2007
- executor,
2008
- globalInstructions,
2009
- permissions: body.permissions,
2010
- recapMode: helperSettings.recapMode,
2011
- promptSuggestionMode: helperSettings.promptSuggestionMode,
2012
- helperSideModel: helperSettings.helperSideModel,
2013
- });
2459
+ let result;
2460
+ try {
2461
+ result = await deps.supervisor.resumeSession(sessionId, project.path, userMessage, body.mode, {
2462
+ model,
2463
+ serviceTier,
2464
+ thinking,
2465
+ effort,
2466
+ providerName,
2467
+ executor,
2468
+ globalInstructions,
2469
+ permissions: body.permissions,
2470
+ recapMode: helperSettings.recapMode,
2471
+ recapAfterSeconds: helperSettings.recapAfterSeconds ??
2472
+ deps.sessionMetadataService?.getRecapAfterSeconds?.(sessionId),
2473
+ // Body value wins; otherwise recover the per-session preference from
2474
+ // metadata so a body-less resume does not default back to native.
2475
+ promptSuggestionMode: helperSettings.promptSuggestionMode ??
2476
+ deps.sessionMetadataService?.getPromptSuggestionMode?.(sessionId),
2477
+ helperSideModel: helperSettings.helperSideModel,
2478
+ resumeMode,
2479
+ resumeSessionAt,
2480
+ });
2481
+ }
2482
+ catch (error) {
2483
+ if (error instanceof ResumeCompactionError) {
2484
+ getLogger().warn({
2485
+ event: "resume_compaction_failed",
2486
+ sessionId,
2487
+ projectId,
2488
+ providerName,
2489
+ attempt: error.attempt,
2490
+ }, "Compact-first resume failed");
2491
+ return c.json({
2492
+ error: error.message,
2493
+ recovery: error.recovery,
2494
+ resume: {
2495
+ ...resumeDiagnostics,
2496
+ compaction: error.attempt,
2497
+ },
2498
+ }, 409);
2499
+ }
2500
+ throw error;
2501
+ }
2014
2502
  // Check if queue is full
2015
2503
  if (isQueueFullResponse(result)) {
2016
2504
  return c.json({ error: "Queue is full", maxQueueSize: result.maxQueueSize }, 503);
2017
2505
  }
2018
2506
  // Check if request was queued
2019
2507
  if (isQueuedResponse(result)) {
2020
- return c.json({ ...result, serverTimestamp: Date.now() }, 202); // 202 Accepted - queued for processing
2508
+ return c.json({
2509
+ ...result,
2510
+ serverTimestamp: Date.now(),
2511
+ resume: { ...resumeDiagnostics, outcome: "queued" },
2512
+ }, 202); // 202 Accepted - queued for processing
2021
2513
  }
2022
2514
  return c.json({
2023
2515
  processId: result.id,
2024
2516
  permissionMode: result.permissionMode,
2025
2517
  modeVersion: result.modeVersion,
2518
+ recapAfterSeconds: result.recapAfterSeconds,
2026
2519
  serverTimestamp,
2520
+ resume: {
2521
+ ...resumeDiagnostics,
2522
+ outcome: "started",
2523
+ compaction: resumeMode === "compact-first" ? { status: "completed" } : undefined,
2524
+ },
2525
+ });
2526
+ });
2527
+ // POST /api/projects/:projectId/sessions/:sessionId/reactivate
2528
+ // Spawn a live harness process bound to the session WITHOUT delivering a turn,
2529
+ // so the client can read live process state (model options) before messaging.
2530
+ // Idempotent: returns the existing process if the session is already owned.
2531
+ routes.post("/projects/:projectId/sessions/:sessionId/reactivate", async (c) => {
2532
+ const projectId = c.req.param("projectId");
2533
+ const sessionId = c.req.param("sessionId");
2534
+ if (!isUrlProjectId(projectId)) {
2535
+ return c.json({ error: "Invalid project ID format" }, 400);
2536
+ }
2537
+ const project = await deps.scanner.getOrCreateProject(projectId);
2538
+ if (!project) {
2539
+ return c.json({ error: "Project not found or path does not exist" }, 404);
2540
+ }
2541
+ // Already owned by a live process - return it (idempotent).
2542
+ const existing = deps.supervisor.getProcessForSession(sessionId);
2543
+ if (existing) {
2544
+ return c.json({
2545
+ processId: existing.id,
2546
+ permissionMode: existing.permissionMode,
2547
+ modeVersion: existing.modeVersion,
2548
+ recapAfterSeconds: existing.recapAfterSeconds,
2549
+ serverTimestamp: Date.now(),
2550
+ });
2551
+ }
2552
+ // Body is optional - allows overriding mode/model/executor.
2553
+ let body = {};
2554
+ try {
2555
+ body = await c.req.json();
2556
+ }
2557
+ catch {
2558
+ // No body - resume with the session's saved settings.
2559
+ }
2560
+ const parsedBodyExecutor = parseOptionalExecutor(body.executor);
2561
+ if (parsedBodyExecutor.error) {
2562
+ return c.json({ error: parsedBodyExecutor.error }, 400);
2563
+ }
2564
+ const helperSettings = parseHelperSettings(body);
2565
+ if (helperSettings.error) {
2566
+ return c.json({ error: helperSettings.error }, 400);
2567
+ }
2568
+ // Resolve provider/model/executor from the YA launch record (persisted on
2569
+ // launch) so reactivation resumes with the correct backend and model.
2570
+ const metadata = deps.sessionMetadataService?.getMetadata(sessionId);
2571
+ const providerName = metadata?.provider ??
2572
+ body.provider ??
2573
+ project.provider;
2574
+ const executor = parsedBodyExecutor.executor ?? metadata?.executor;
2575
+ // Prefer an explicit override, else the YA id the session was launched
2576
+ // with; "default" means let the backend pick (pass undefined).
2577
+ const rawModel = body.model && body.model !== "default"
2578
+ ? body.model
2579
+ : metadata?.requestedModel;
2580
+ const model = rawModel && rawModel !== "default" ? rawModel : undefined;
2581
+ const { thinking, effort } = body.thinking
2582
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
2583
+ : { thinking: undefined, effort: undefined };
2584
+ let process;
2585
+ try {
2586
+ process = await deps.supervisor.reactivateSession(project.path, sessionId, body.mode, {
2587
+ model,
2588
+ thinking,
2589
+ effort,
2590
+ providerName,
2591
+ executor,
2592
+ globalInstructions: getGlobalInstructions(),
2593
+ recapAfterSeconds: helperSettings.recapAfterSeconds ?? metadata?.recapAfterSeconds,
2594
+ recapMode: helperSettings.recapMode ?? metadata?.recapMode,
2595
+ });
2596
+ }
2597
+ catch (error) {
2598
+ return c.json({
2599
+ error: error instanceof Error
2600
+ ? error.message
2601
+ : "Failed to reactivate session",
2602
+ }, 503);
2603
+ }
2604
+ // Keep the launch record current (provider/executor) for this session.
2605
+ await persistLaunchMetadata(sessionId, providerName, executor);
2606
+ return c.json({
2607
+ processId: process.id,
2608
+ permissionMode: process.permissionMode,
2609
+ modeVersion: process.modeVersion,
2610
+ recapAfterSeconds: process.recapAfterSeconds,
2611
+ serverTimestamp: Date.now(),
2612
+ });
2613
+ });
2614
+ // POST /api/projects/:projectId/sessions/:sessionId/recap
2615
+ // Away-recap trigger, keyed by session (not process) so it survives a server
2616
+ // restart that killed the process. A live process recaps directly in its own
2617
+ // mode; a cold fork-mode session is revived — without ever preempting a live
2618
+ // worker — and recapped from its on-disk transcript. See topics/fork-recap.md.
2619
+ routes.post("/projects/:projectId/sessions/:sessionId/recap", async (c) => {
2620
+ const projectId = c.req.param("projectId");
2621
+ const sessionId = c.req.param("sessionId");
2622
+ if (!isUrlProjectId(projectId)) {
2623
+ return c.json({ error: "Invalid project ID format" }, 400);
2624
+ }
2625
+ let sinceMs = null;
2626
+ try {
2627
+ const body = await c.req.json();
2628
+ if (typeof body.hiddenSinceMs === "number" &&
2629
+ Number.isFinite(body.hiddenSinceMs)) {
2630
+ sinceMs = body.hiddenSinceMs;
2631
+ }
2632
+ }
2633
+ catch {
2634
+ // Empty body is accepted.
2635
+ }
2636
+ // Live process: recap directly in whatever mode it runs.
2637
+ const live = deps.supervisor.getProcessForSession(sessionId);
2638
+ if (live && !live.isTerminated) {
2639
+ const result = await deps.supervisor.requestRecap(live.id, { sinceMs });
2640
+ return c.json(result);
2641
+ }
2642
+ // Cold session: only fork mode can recap from the on-disk transcript —
2643
+ // side-session/native recaps need in-memory recent text a revived process
2644
+ // lacks. Other modes simply skip until the session is live again.
2645
+ const recapMode = deps.sessionMetadataService?.getRecapMode(sessionId);
2646
+ if (recapMode !== "fork") {
2647
+ return c.json({
2648
+ supported: true,
2649
+ emitted: false,
2650
+ reason: "recap requires a live process for this recap mode",
2651
+ });
2652
+ }
2653
+ const project = await deps.scanner.getOrCreateProject(projectId);
2654
+ if (!project) {
2655
+ return c.json({ error: "Project not found or path does not exist" }, 404);
2656
+ }
2657
+ const metadata = deps.sessionMetadataService?.getMetadata(sessionId);
2658
+ const providerName = metadata?.provider ?? project.provider;
2659
+ const rawModel = metadata?.requestedModel;
2660
+ const model = rawModel && rawModel !== "default" ? rawModel : undefined;
2661
+ let process;
2662
+ try {
2663
+ process = await deps.supervisor.reactivateSession(project.path, sessionId, undefined, {
2664
+ model,
2665
+ providerName,
2666
+ executor: metadata?.executor,
2667
+ globalInstructions: getGlobalInstructions(),
2668
+ recapAfterSeconds: metadata?.recapAfterSeconds,
2669
+ recapMode: "fork",
2670
+ }, { preempt: false });
2671
+ }
2672
+ catch (error) {
2673
+ // At capacity with no idle worker to drop (background recaps never
2674
+ // preempt), or reactivation failed — skip the recap rather than error.
2675
+ const atCapacity = error instanceof Error && error.message.includes("worker capacity");
2676
+ return c.json({
2677
+ supported: true,
2678
+ emitted: false,
2679
+ reason: atCapacity
2680
+ ? "recap skipped: at worker capacity"
2681
+ : "recap skipped: session could not be revived",
2682
+ });
2683
+ }
2684
+ const result = await deps.supervisor.requestRecap(process.id, {
2685
+ sinceMs,
2686
+ revived: true,
2027
2687
  });
2688
+ return c.json(result);
2028
2689
  });
2029
2690
  // POST /api/projects/:projectId/sessions/:sessionId/restart
2030
2691
  // Start a fresh session from a bounded handoff, then terminate the old YA-owned process.
@@ -2061,10 +2722,18 @@ export function createSessionsRoutes(deps) {
2061
2722
  }
2062
2723
  executor = parsedSavedExecutor.executor;
2063
2724
  }
2725
+ if (body.restartMode !== undefined &&
2726
+ body.restartMode !== "handoff" &&
2727
+ body.restartMode !== "fork") {
2728
+ return c.json({ error: 'restartMode must be "handoff" or "fork"' }, 400);
2729
+ }
2730
+ const restartMode = body.restartMode ?? "handoff";
2064
2731
  const oldProcess = deps.supervisor.getProcessForSession(sessionId);
2065
2732
  const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
2066
2733
  const preferredSourceProvider = metadataProvider ?? oldProcess?.provider ?? project.provider;
2067
- const compactAttempt = await tryRestartCompact(oldProcess);
2734
+ // Fork copies the transcript as-is; pre-restart compaction is a
2735
+ // handoff-only mitigation.
2736
+ const compactAttempt = restartMode === "fork" ? undefined : await tryRestartCompact(oldProcess);
2068
2737
  const oldProcessInterrupted = await interruptOldProcessForHandoff(oldProcess);
2069
2738
  const sourceSession = await loadRestartSourceSession(project, sessionId, projectId, preferredSourceProvider, oldProcess);
2070
2739
  if (!sourceSession) {
@@ -2073,6 +2742,105 @@ export function createSessionsRoutes(deps) {
2073
2742
  const sourceProvider = sourceSession.provider ?? preferredSourceProvider;
2074
2743
  const providerName = body.provider ?? sourceProvider;
2075
2744
  const originalMetadata = deps.sessionMetadataService?.getMetadata(sessionId);
2745
+ const { thinking, effort } = body.thinking
2746
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
2747
+ : { thinking: undefined, effort: undefined };
2748
+ const model = body.model && body.model !== "default" ? body.model : undefined;
2749
+ const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
2750
+ if (restartMode === "fork") {
2751
+ if (body.provider && body.provider !== sourceProvider) {
2752
+ return c.json({
2753
+ error: "Fork keeps the source provider; omit provider or match the source session",
2754
+ }, 400);
2755
+ }
2756
+ if (!deps.supervisor.supportsForkSession(sourceProvider)) {
2757
+ return c.json({ error: `${sourceProvider} does not support transcript fork` }, 400);
2758
+ }
2759
+ const forkTitle = deriveForkTitle({
2760
+ preferredTitle: originalMetadata?.customTitle,
2761
+ sourceSession,
2762
+ });
2763
+ let fork;
2764
+ try {
2765
+ fork = await deps.supervisor.forkSession({
2766
+ sessionId,
2767
+ projectPath: project.path,
2768
+ providerName: sourceProvider,
2769
+ upToMessageId: body.forkUpToMessageId,
2770
+ title: forkTitle,
2771
+ });
2772
+ }
2773
+ catch (error) {
2774
+ getLogger().warn({
2775
+ event: "restart_fork_failed",
2776
+ sessionId,
2777
+ projectId,
2778
+ sourceProvider,
2779
+ upToMessageId: body.forkUpToMessageId,
2780
+ error: error instanceof Error ? error.message : String(error),
2781
+ }, "Transcript fork failed");
2782
+ return c.json({
2783
+ error: error instanceof Error ? error.message : "Transcript fork failed",
2784
+ }, 500);
2785
+ }
2786
+ const forkMessage = body.reason?.trim() || "Continue from this fork point.";
2787
+ const result = await deps.supervisor.resumeSession(fork.sessionId, project.path, { text: forkMessage, mode: body.mode }, body.mode, {
2788
+ model,
2789
+ serviceTier,
2790
+ thinking,
2791
+ effort,
2792
+ providerName: sourceProvider,
2793
+ executor,
2794
+ globalInstructions: getGlobalInstructions(),
2795
+ permissions: body.permissions,
2796
+ recapMode: helperSettings.recapMode,
2797
+ recapAfterSeconds: helperSettings.recapAfterSeconds ??
2798
+ originalMetadata?.recapAfterSeconds,
2799
+ // Inherit the source session's preference unless the body overrides.
2800
+ promptSuggestionMode: helperSettings.promptSuggestionMode ??
2801
+ originalMetadata?.promptSuggestionMode,
2802
+ helperSideModel: helperSettings.helperSideModel,
2803
+ });
2804
+ if (isQueueFullResponse(result)) {
2805
+ return c.json({ error: "Queue is full", maxQueueSize: result.maxQueueSize }, 503);
2806
+ }
2807
+ if (isQueuedResponse(result)) {
2808
+ deps.supervisor.cancelQueuedRequest(result.queueId);
2809
+ return c.json({
2810
+ error: "Fork could not start immediately; old process was left running",
2811
+ }, 503);
2812
+ }
2813
+ await persistLaunchMetadata(result.sessionId, sourceProvider, executor, undefined, body.model ?? deps.sessionMetadataService?.getRequestedModel(sessionId), result.promptSuggestionMode, result.recapAfterSeconds);
2814
+ if (deps.sessionMetadataService) {
2815
+ await deps.sessionMetadataService.updateMetadata(result.sessionId, {
2816
+ title: forkTitle,
2817
+ });
2818
+ deps.eventBus?.emit({
2819
+ type: "session-metadata-changed",
2820
+ sessionId: result.sessionId,
2821
+ title: forkTitle,
2822
+ timestamp: new Date().toISOString(),
2823
+ });
2824
+ }
2825
+ const oldProcessAbortDeferred = abortOldProcessAfterReplacementActivity(oldProcess, result);
2826
+ return c.json({
2827
+ sessionId: result.sessionId,
2828
+ processId: result.id,
2829
+ projectId: result.projectId,
2830
+ provider: result.provider,
2831
+ model: result.resolvedModel ?? result.model,
2832
+ title: forkTitle,
2833
+ permissionMode: result.permissionMode,
2834
+ modeVersion: result.modeVersion,
2835
+ recapAfterSeconds: result.recapAfterSeconds,
2836
+ restartedFrom: sessionId,
2837
+ forkUpToMessageId: body.forkUpToMessageId,
2838
+ oldProcessId: oldProcess?.id,
2839
+ oldProcessInterrupted,
2840
+ oldProcessAbortDeferred,
2841
+ oldProcessAborted: false,
2842
+ });
2843
+ }
2076
2844
  const handoffTitle = deriveRestartTitle({
2077
2845
  preferredTitle: originalMetadata?.customTitle,
2078
2846
  sourceSession,
@@ -2090,11 +2858,6 @@ export function createSessionsRoutes(deps) {
2090
2858
  omittedCount,
2091
2859
  transcript,
2092
2860
  });
2093
- const { thinking, effort } = body.thinking
2094
- ? thinkingOptionToConfig(body.thinking)
2095
- : { thinking: undefined, effort: undefined };
2096
- const model = body.model && body.model !== "default" ? body.model : undefined;
2097
- const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
2098
2861
  const result = await deps.supervisor.startSession(project.path, {
2099
2862
  text: handoff,
2100
2863
  mode: body.mode,
@@ -2109,7 +2872,11 @@ export function createSessionsRoutes(deps) {
2109
2872
  globalInstructions: getGlobalInstructions(),
2110
2873
  permissions: body.permissions,
2111
2874
  recapMode: helperSettings.recapMode,
2112
- promptSuggestionMode: helperSettings.promptSuggestionMode,
2875
+ recapAfterSeconds: helperSettings.recapAfterSeconds ??
2876
+ originalMetadata?.recapAfterSeconds,
2877
+ // Inherit the source session's preference unless the body overrides.
2878
+ promptSuggestionMode: helperSettings.promptSuggestionMode ??
2879
+ originalMetadata?.promptSuggestionMode,
2113
2880
  helperSideModel: helperSettings.helperSideModel,
2114
2881
  });
2115
2882
  if (isQueueFullResponse(result)) {
@@ -2121,7 +2888,7 @@ export function createSessionsRoutes(deps) {
2121
2888
  error: "Restart could not start immediately; old process was left running",
2122
2889
  }, 503);
2123
2890
  }
2124
- await persistLaunchMetadata(result.sessionId, providerName, executor);
2891
+ await persistLaunchMetadata(result.sessionId, providerName, executor, undefined, body.model ?? deps.sessionMetadataService?.getRequestedModel(sessionId), result.promptSuggestionMode, result.recapAfterSeconds);
2125
2892
  if (deps.sessionMetadataService) {
2126
2893
  await deps.sessionMetadataService.updateMetadata(result.sessionId, {
2127
2894
  title: handoffTitle,
@@ -2143,6 +2910,7 @@ export function createSessionsRoutes(deps) {
2143
2910
  title: handoffTitle,
2144
2911
  permissionMode: result.permissionMode,
2145
2912
  modeVersion: result.modeVersion,
2913
+ recapAfterSeconds: result.recapAfterSeconds,
2146
2914
  restartedFrom: sessionId,
2147
2915
  oldProcessId: oldProcess?.id,
2148
2916
  oldProcessInterrupted,
@@ -2150,43 +2918,617 @@ export function createSessionsRoutes(deps) {
2150
2918
  oldProcessAborted: false,
2151
2919
  });
2152
2920
  });
2153
- // POST /api/sessions/:sessionId/messages - Queue message
2154
- routes.post("/sessions/:sessionId/messages", async (c) => {
2921
+ // POST /api/projects/:projectId/sessions/:sessionId/fork
2922
+ // Fork the provider transcript into a new resumable session without
2923
+ // starting a process or sending any message ("fork from here" / rewind).
2924
+ // The forked session opens cold; the next user send resumes it normally.
2925
+ routes.post("/projects/:projectId/sessions/:sessionId/fork", async (c) => {
2926
+ const projectId = c.req.param("projectId");
2155
2927
  const sessionId = c.req.param("sessionId");
2156
- const process = deps.supervisor.getProcessForSession(sessionId);
2157
- if (!process) {
2158
- return c.json({ error: "No active process for session" }, 404);
2928
+ if (!isUrlProjectId(projectId)) {
2929
+ return c.json({ error: "Invalid project ID format" }, 400);
2159
2930
  }
2160
- let body;
2931
+ const project = await deps.scanner.getOrCreateProject(projectId);
2932
+ if (!project) {
2933
+ return c.json({ error: "Project not found or path does not exist" }, 404);
2934
+ }
2935
+ let body = {};
2161
2936
  try {
2162
2937
  body = await c.req.json();
2163
2938
  }
2164
2939
  catch {
2165
- return c.json({ error: "Invalid JSON body" }, 400);
2940
+ // Body is optional; full-transcript fork.
2166
2941
  }
2167
- if (!body.message) {
2168
- return c.json({ error: "Message is required" }, 400);
2942
+ const upToMessageId = typeof body.upToMessageId === "string" && body.upToMessageId
2943
+ ? body.upToMessageId
2944
+ : undefined;
2945
+ const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
2946
+ const sourceProcess = deps.supervisor.getProcessForSession(sessionId);
2947
+ const sessionSummaryResult = await findSessionSummaryAcrossProviders(project, sessionId, projectId, providerResolutionDeps(deps), sourceProcess?.provider ?? metadataProvider, { readMode: "head" });
2948
+ const sessionSummary = sessionSummaryResult?.summary ?? null;
2949
+ const providerName = sourceProcess?.provider ??
2950
+ metadataProvider ??
2951
+ sessionSummary?.provider ??
2952
+ project.provider;
2953
+ if (!deps.supervisor.supportsForkSession(providerName)) {
2954
+ return c.json({ error: `${providerName} does not support transcript fork` }, 400);
2169
2955
  }
2170
- const serverTimestamp = Date.now();
2171
- const userMessage = {
2172
- text: body.message,
2173
- images: body.images,
2174
- documents: body.documents,
2175
- attachments: body.attachments,
2176
- mode: body.mode,
2177
- tempId: body.tempId,
2178
- metadata: buildUserMessageMetadata(body, serverTimestamp, body.deferred ? "deferred" : "direct"),
2179
- };
2180
- // Check if process is terminated
2181
- if (process.isTerminated) {
2956
+ const originalMetadata = deps.sessionMetadataService?.getMetadata(sessionId);
2957
+ let baseTitle = normalizeRestartTitleCandidate(originalMetadata?.customTitle);
2958
+ if (!baseTitle) {
2959
+ try {
2960
+ const summary = await deps
2961
+ .readerFactory(project)
2962
+ .getSessionSummary(sessionId, projectId, { readMode: "head" });
2963
+ baseTitle = normalizeRestartTitleCandidate(summary?.title);
2964
+ }
2965
+ catch {
2966
+ // Title is cosmetic; fall through to the provider default.
2967
+ }
2968
+ }
2969
+ const forkTitle = baseTitle
2970
+ ? truncateSessionTitle(/^Fork:/i.test(baseTitle) ? baseTitle : `Fork: ${baseTitle}`)
2971
+ : undefined;
2972
+ let fork;
2973
+ try {
2974
+ fork = await deps.supervisor.forkSession({
2975
+ sessionId,
2976
+ projectPath: project.path,
2977
+ providerName,
2978
+ upToMessageId,
2979
+ title: forkTitle,
2980
+ });
2981
+ }
2982
+ catch (error) {
2983
+ getLogger().warn({
2984
+ event: "session_fork_failed",
2985
+ sessionId,
2986
+ projectId,
2987
+ providerName,
2988
+ upToMessageId,
2989
+ error: error instanceof Error ? error.message : String(error),
2990
+ }, "Transcript fork failed");
2182
2991
  return c.json({
2183
- error: "Process terminated",
2184
- reason: process.terminationReason,
2185
- }, 410); // 410 Gone
2992
+ error: error instanceof Error ? error.message : "Transcript fork failed",
2993
+ }, 500);
2186
2994
  }
2187
- const resolvedModel = body.model && body.model !== "default"
2188
- ? body.model
2189
- : process.resolvedModel ?? process.model;
2995
+ const savedExecutor = parseOptionalExecutor(deps.sessionMetadataService?.getExecutor(sessionId)).executor;
2996
+ await persistLaunchMetadata(fork.sessionId, providerName, savedExecutor, undefined, deps.sessionMetadataService?.getRequestedModel(sessionId), originalMetadata?.promptSuggestionMode, originalMetadata?.recapAfterSeconds);
2997
+ if (deps.sessionMetadataService) {
2998
+ await deps.sessionMetadataService.updateMetadata(fork.sessionId, {
2999
+ title: forkTitle,
3000
+ parentSessionId: sessionId,
3001
+ });
3002
+ deps.eventBus?.emit({
3003
+ type: "session-metadata-changed",
3004
+ sessionId: fork.sessionId,
3005
+ title: forkTitle,
3006
+ parentSessionId: sessionId,
3007
+ timestamp: new Date().toISOString(),
3008
+ });
3009
+ }
3010
+ return c.json({
3011
+ sessionId: fork.sessionId,
3012
+ projectId,
3013
+ provider: providerName,
3014
+ title: forkTitle,
3015
+ forkedFrom: sessionId,
3016
+ upToMessageId,
3017
+ });
3018
+ });
3019
+ // POST /api/projects/:projectId/sessions/:sessionId/retitle
3020
+ // Generate a proposed session title through an archived helper fork. This
3021
+ // never updates the source session title; the client must explicitly accept
3022
+ // the returned proposal through the ordinary metadata update route.
3023
+ routes.post("/projects/:projectId/sessions/:sessionId/retitle", async (c) => {
3024
+ const projectId = c.req.param("projectId");
3025
+ const sessionId = c.req.param("sessionId");
3026
+ if (!isUrlProjectId(projectId)) {
3027
+ return c.json({ error: "Invalid project ID format" }, 400);
3028
+ }
3029
+ const project = await deps.scanner.getOrCreateProject(projectId);
3030
+ if (!project) {
3031
+ return c.json({ error: "Project not found or path does not exist" }, 404);
3032
+ }
3033
+ if (!deps.sessionMetadataService) {
3034
+ return c.json({ error: "Session metadata service not available" }, 503);
3035
+ }
3036
+ let body = {};
3037
+ try {
3038
+ body = await c.req.json();
3039
+ }
3040
+ catch {
3041
+ // Body is optional.
3042
+ }
3043
+ const currentTitle = typeof body.currentTitle === "string"
3044
+ ? body.currentTitle.trim().slice(0, 200)
3045
+ : undefined;
3046
+ let lengthTarget;
3047
+ if (body.lengthTarget !== undefined) {
3048
+ if (typeof body.lengthTarget !== "number" ||
3049
+ !Number.isInteger(body.lengthTarget) ||
3050
+ body.lengthTarget < 20 ||
3051
+ body.lengthTarget > 132) {
3052
+ return c.json({ error: "lengthTarget must be an integer between 20 and 132" }, 400);
3053
+ }
3054
+ lengthTarget = body.lengthTarget;
3055
+ }
3056
+ const metadataProvider = deps.sessionMetadataService.getProvider(sessionId);
3057
+ let sourceProcess = deps.supervisor.getProcessForSession(sessionId);
3058
+ const liveSourceProcess = sourceProcess && !sourceProcess.isTerminated ? sourceProcess : undefined;
3059
+ const sessionSummaryResult = await findSessionSummaryAcrossProviders(project, sessionId, projectId, providerResolutionDeps(deps), liveSourceProcess?.provider ?? metadataProvider, { readMode: "head" });
3060
+ const sessionSummary = sessionSummaryResult?.summary ?? null;
3061
+ const providerName = liveSourceProcess?.provider ??
3062
+ metadataProvider ??
3063
+ sessionSummary?.provider ??
3064
+ sourceProcess?.provider ??
3065
+ project.provider;
3066
+ if (!deps.supervisor.supportsForkSession(providerName)) {
3067
+ return c.json({ error: `${providerName} does not support transcript fork` }, 400);
3068
+ }
3069
+ const savedExecutor = parseOptionalExecutor(deps.sessionMetadataService.getExecutor(sessionId)).executor;
3070
+ let requestedModel = deps.sessionMetadataService.getRequestedModel(sessionId) ??
3071
+ liveSourceProcess?.model;
3072
+ const sourceMetadata = deps.sessionMetadataService.getMetadata(sessionId);
3073
+ const promptSuggestionMode = sourceMetadata?.promptSuggestionMode;
3074
+ const recapAfterSeconds = sourceMetadata?.recapAfterSeconds;
3075
+ const abortController = new AbortController();
3076
+ const abortFromRequest = () => abortController.abort();
3077
+ if (c.req.raw.signal.aborted) {
3078
+ abortController.abort();
3079
+ }
3080
+ else {
3081
+ c.req.raw.signal.addEventListener("abort", abortFromRequest, {
3082
+ once: true,
3083
+ });
3084
+ }
3085
+ let generatorSessionId;
3086
+ try {
3087
+ if (!liveSourceProcess) {
3088
+ sourceProcess = await deps.supervisor.reactivateSession(project.path, sessionId, undefined, {
3089
+ model: requestedModel && requestedModel !== "default"
3090
+ ? requestedModel
3091
+ : undefined,
3092
+ providerName,
3093
+ executor: savedExecutor,
3094
+ globalInstructions: getGlobalInstructions(),
3095
+ promptSuggestionMode,
3096
+ recapAfterSeconds,
3097
+ });
3098
+ requestedModel = requestedModel ?? sourceProcess.model;
3099
+ }
3100
+ const generator = await deps.supervisor.forkSession({
3101
+ sessionId,
3102
+ projectPath: project.path,
3103
+ providerName,
3104
+ title: "Retitle generator",
3105
+ });
3106
+ generatorSessionId = generator.sessionId;
3107
+ await updateForkSummaryChildMetadata(generator.sessionId, sessionId, "Retitle generator", true);
3108
+ await persistLaunchMetadata(generator.sessionId, providerName, savedExecutor, undefined, requestedModel, promptSuggestionMode, recapAfterSeconds);
3109
+ if (abortController.signal.aborted) {
3110
+ throw new DOMException("Retitle cancelled", "AbortError");
3111
+ }
3112
+ const generated = await deps.supervisor.generateSummary(providerName, {
3113
+ purpose: "session-retitle",
3114
+ strategy: "fork",
3115
+ generatorSessionId: generator.sessionId,
3116
+ cwd: project.path,
3117
+ currentTitle,
3118
+ lengthTarget,
3119
+ signal: abortController.signal,
3120
+ });
3121
+ const title = generatedRetitleCandidate(generated.text);
3122
+ if (!title) {
3123
+ throw new Error("Retitle generation returned empty title");
3124
+ }
3125
+ return c.json({ title, generatorSessionId: generator.sessionId });
3126
+ }
3127
+ catch (error) {
3128
+ const cancelled = abortController.signal.aborted ||
3129
+ (error instanceof DOMException && error.name === "AbortError");
3130
+ const message = error instanceof Error ? error.message : "Retitle generation failed";
3131
+ getLogger().warn({
3132
+ event: cancelled
3133
+ ? "session_retitle_cancelled"
3134
+ : "session_retitle_failed",
3135
+ sessionId,
3136
+ projectId,
3137
+ providerName,
3138
+ generatorSessionId,
3139
+ error: message,
3140
+ }, cancelled ? "Session retitle cancelled" : "Session retitle failed");
3141
+ return c.json({ error: message }, cancelled ? 400 : 500);
3142
+ }
3143
+ finally {
3144
+ c.req.raw.signal.removeEventListener("abort", abortFromRequest);
3145
+ abortController.abort();
3146
+ }
3147
+ });
3148
+ // POST /api/projects/:projectId/sessions/:sessionId/fork-summary
3149
+ // Start a server-owned whole-context fork-after-summary job. The response
3150
+ // returns after durable job creation; generation continues independently of
3151
+ // the requesting client connection.
3152
+ routes.post("/projects/:projectId/sessions/:sessionId/fork-summary", async (c) => {
3153
+ const projectId = c.req.param("projectId");
3154
+ const sessionId = c.req.param("sessionId");
3155
+ if (!isUrlProjectId(projectId)) {
3156
+ return c.json({ error: "Invalid project ID format" }, 400);
3157
+ }
3158
+ const project = await deps.scanner.getOrCreateProject(projectId);
3159
+ if (!project) {
3160
+ return c.json({ error: "Project not found or path does not exist" }, 404);
3161
+ }
3162
+ let body;
3163
+ try {
3164
+ body = await c.req.json();
3165
+ }
3166
+ catch {
3167
+ return c.json({ error: "Invalid JSON body" }, 400);
3168
+ }
3169
+ const sourceMessageId = typeof body.sourceMessageId === "string" && body.sourceMessageId.trim()
3170
+ ? body.sourceMessageId.trim()
3171
+ : undefined;
3172
+ if (!sourceMessageId) {
3173
+ return c.json({ error: "sourceMessageId is required" }, 400);
3174
+ }
3175
+ if (body.mode !== undefined &&
3176
+ (typeof body.mode !== "string" ||
3177
+ !ALL_PERMISSION_MODES.includes(body.mode))) {
3178
+ return c.json({ error: "Invalid permission mode" }, 400);
3179
+ }
3180
+ if (body.autoOpenWhenReady !== undefined &&
3181
+ typeof body.autoOpenWhenReady !== "boolean") {
3182
+ return c.json({ error: "autoOpenWhenReady must be a boolean" }, 400);
3183
+ }
3184
+ const mode = body.mode;
3185
+ const instructions = typeof body.instructions === "string" ? body.instructions : undefined;
3186
+ const autoOpenWhenReady = body.autoOpenWhenReady === true;
3187
+ const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
3188
+ const sourceProcess = deps.supervisor.getProcessForSession(sessionId);
3189
+ const providerName = metadataProvider ?? sourceProcess?.provider ?? project.provider;
3190
+ if (!deps.supervisor.supportsForkSession(providerName)) {
3191
+ return c.json({ error: `${providerName} does not support transcript fork` }, 400);
3192
+ }
3193
+ if (!deps.sessionMetadataService) {
3194
+ return c.json({ error: "Session metadata service not available" }, 503);
3195
+ }
3196
+ if (activeForkSummaryJobs.has(sessionId)) {
3197
+ return c.json({ error: "A fork summary is already generating for this session" }, 409);
3198
+ }
3199
+ const sourceSession = sourceProcess
3200
+ ? {
3201
+ messages: sdkMessagesToClientMessages(sourceProcess.getMessageHistory()),
3202
+ }
3203
+ : await loadRestartSourceSession(project, sessionId, projectId, providerName);
3204
+ if (!sourceSession) {
3205
+ return c.json({ error: "Source session not found" }, 404);
3206
+ }
3207
+ const boundary = resolveForkAfterBoundary(sourceSession.messages, sourceMessageId, sourceProcess?.state.type === "in-turn" ||
3208
+ sourceProcess?.state.type === "waiting-input");
3209
+ if ("error" in boundary) {
3210
+ return c.json({ error: boundary.error }, boundary.status);
3211
+ }
3212
+ const displayObject = {
3213
+ id: randomUUID(),
3214
+ kind: "fork-summary",
3215
+ createdAt: new Date().toISOString(),
3216
+ placementAfterMessageId: boundary.placementAfterMessageId,
3217
+ sourceMessageId,
3218
+ retainedThroughMessageId: boundary.retainedThroughMessageId,
3219
+ status: "generating",
3220
+ autoOpenWhenReady: autoOpenWhenReady || undefined,
3221
+ };
3222
+ const abortController = new AbortController();
3223
+ activeForkSummaryJobs.set(sessionId, {
3224
+ objectId: displayObject.id,
3225
+ abortController,
3226
+ });
3227
+ try {
3228
+ await deps.sessionMetadataService.addTranscriptDisplayObject(sessionId, displayObject);
3229
+ }
3230
+ catch (error) {
3231
+ activeForkSummaryJobs.delete(sessionId);
3232
+ throw error;
3233
+ }
3234
+ emitTranscriptDisplayObjects(sessionId);
3235
+ const originalMetadata = deps.sessionMetadataService.getMetadata(sessionId);
3236
+ const baseTitle = normalizeRestartTitleCandidate(originalMetadata?.customTitle ?? sourceSession.title);
3237
+ const fallbackTitle = baseTitle
3238
+ ? truncateSessionTitle(/^Fork:/i.test(baseTitle) ? baseTitle : `Fork: ${baseTitle}`)
3239
+ : undefined;
3240
+ const savedExecutor = parseOptionalExecutor(deps.sessionMetadataService.getExecutor(sessionId)).executor;
3241
+ const requestedModel = deps.sessionMetadataService.getRequestedModel(sessionId) ??
3242
+ sourceProcess?.model;
3243
+ void (async () => {
3244
+ let generatorSessionId;
3245
+ let targetSessionId;
3246
+ let targetTitle;
3247
+ let targetProcessId;
3248
+ let completed = false;
3249
+ try {
3250
+ const generator = await deps.supervisor.forkSession({
3251
+ sessionId,
3252
+ projectPath: project.path,
3253
+ providerName,
3254
+ title: "Fork summary generator",
3255
+ });
3256
+ generatorSessionId = generator.sessionId;
3257
+ await updateForkSummaryChildMetadata(generator.sessionId, sessionId, "Fork summary generator", true);
3258
+ await persistLaunchMetadata(generator.sessionId, providerName, savedExecutor, undefined, requestedModel, originalMetadata?.promptSuggestionMode, originalMetadata?.recapAfterSeconds);
3259
+ if (abortController.signal.aborted) {
3260
+ throw new DOMException("Fork summary cancelled", "AbortError");
3261
+ }
3262
+ const generated = await deps.supervisor.generateSummary(providerName, {
3263
+ purpose: "fork-after-summary",
3264
+ strategy: "fork",
3265
+ generatorSessionId: generator.sessionId,
3266
+ cwd: project.path,
3267
+ afterTurnMessageId: boundary.retainedThroughMessageId,
3268
+ afterTurnContext: boundary.retainedThroughContext,
3269
+ instructions,
3270
+ signal: abortController.signal,
3271
+ });
3272
+ if (abortController.signal.aborted) {
3273
+ throw new DOMException("Fork summary cancelled", "AbortError");
3274
+ }
3275
+ const title = forkSummaryTitle(generated.text, fallbackTitle);
3276
+ targetTitle = title;
3277
+ const target = await deps.supervisor.forkSession({
3278
+ sessionId,
3279
+ projectPath: project.path,
3280
+ providerName,
3281
+ upToMessageId: boundary.retainedThroughMessageId,
3282
+ title,
3283
+ });
3284
+ targetSessionId = target.sessionId;
3285
+ await updateForkSummaryChildMetadata(target.sessionId, sessionId, title, true);
3286
+ await persistLaunchMetadata(target.sessionId, providerName, savedExecutor, undefined, requestedModel, originalMetadata?.promptSuggestionMode, originalMetadata?.recapAfterSeconds);
3287
+ if (abortController.signal.aborted) {
3288
+ throw new DOMException("Fork summary cancelled", "AbortError");
3289
+ }
3290
+ const result = await deps.supervisor.resumeSession(target.sessionId, project.path, { text: generated.text, mode }, mode, {
3291
+ providerName,
3292
+ executor: savedExecutor,
3293
+ globalInstructions: getGlobalInstructions(),
3294
+ model: requestedModel,
3295
+ promptSuggestionMode: originalMetadata?.promptSuggestionMode,
3296
+ recapAfterSeconds: originalMetadata?.recapAfterSeconds,
3297
+ });
3298
+ if (isQueueFullResponse(result)) {
3299
+ throw new Error("Queue is full");
3300
+ }
3301
+ if (isQueuedResponse(result)) {
3302
+ deps.supervisor.cancelQueuedRequest(result.queueId);
3303
+ throw new Error("Fork summary could not start immediately");
3304
+ }
3305
+ targetProcessId = result.id;
3306
+ if (abortController.signal.aborted) {
3307
+ await deps.supervisor.abortProcess(result.id);
3308
+ targetProcessId = undefined;
3309
+ throw new DOMException("Fork summary cancelled", "AbortError");
3310
+ }
3311
+ await persistLaunchMetadata(result.sessionId, providerName, savedExecutor, undefined, requestedModel, result.promptSuggestionMode, result.recapAfterSeconds);
3312
+ await updateForkSummaryChildMetadata(result.sessionId, sessionId, title, false);
3313
+ await deps.sessionMetadataService?.updateTranscriptDisplayObject(sessionId, displayObject.id, (object) => ({
3314
+ ...object,
3315
+ status: "ready",
3316
+ targetSessionId: result.sessionId,
3317
+ title,
3318
+ error: undefined,
3319
+ }));
3320
+ emitTranscriptDisplayObjects(sessionId);
3321
+ completed = true;
3322
+ }
3323
+ catch (error) {
3324
+ const cancelled = abortController.signal.aborted ||
3325
+ (error instanceof DOMException && error.name === "AbortError");
3326
+ const logCleanupFailure = (stage, cleanupError) => {
3327
+ getLogger().warn({
3328
+ event: "fork_after_summary_cleanup_failed",
3329
+ sessionId,
3330
+ projectId,
3331
+ providerName,
3332
+ stage,
3333
+ error: cleanupError instanceof Error
3334
+ ? cleanupError.message
3335
+ : String(cleanupError),
3336
+ }, "Fork-after-summary cleanup failed");
3337
+ };
3338
+ if (generatorSessionId) {
3339
+ try {
3340
+ await updateForkSummaryChildMetadata(generatorSessionId, sessionId, "Fork summary generator", true);
3341
+ }
3342
+ catch (cleanupError) {
3343
+ logCleanupFailure("archive-generator", cleanupError);
3344
+ }
3345
+ }
3346
+ if (targetSessionId && targetTitle) {
3347
+ try {
3348
+ await updateForkSummaryChildMetadata(targetSessionId, sessionId, targetTitle, true);
3349
+ }
3350
+ catch (cleanupError) {
3351
+ logCleanupFailure("archive-target", cleanupError);
3352
+ }
3353
+ }
3354
+ if (!completed && targetProcessId) {
3355
+ try {
3356
+ await deps.supervisor.abortProcess(targetProcessId);
3357
+ }
3358
+ catch (cleanupError) {
3359
+ logCleanupFailure("abort-target-process", cleanupError);
3360
+ }
3361
+ }
3362
+ if (!cancelled) {
3363
+ const message = error instanceof Error
3364
+ ? error.message
3365
+ : "Summary generation failed";
3366
+ getLogger().warn({
3367
+ event: "fork_after_summary_failed",
3368
+ sessionId,
3369
+ projectId,
3370
+ providerName,
3371
+ sourceMessageId,
3372
+ error: message,
3373
+ }, "Fork-after-summary job failed");
3374
+ try {
3375
+ await deps.sessionMetadataService?.updateTranscriptDisplayObject(sessionId, displayObject.id, (object) => ({
3376
+ ...object,
3377
+ status: "error",
3378
+ error: message,
3379
+ }));
3380
+ emitTranscriptDisplayObjects(sessionId);
3381
+ }
3382
+ catch (cleanupError) {
3383
+ logCleanupFailure("persist-error-state", cleanupError);
3384
+ }
3385
+ }
3386
+ }
3387
+ finally {
3388
+ const active = activeForkSummaryJobs.get(sessionId);
3389
+ if (active?.objectId === displayObject.id) {
3390
+ activeForkSummaryJobs.delete(sessionId);
3391
+ }
3392
+ }
3393
+ })().catch((error) => {
3394
+ getLogger().error({
3395
+ event: "fork_after_summary_unhandled_failure",
3396
+ sessionId,
3397
+ projectId,
3398
+ providerName,
3399
+ sourceMessageId,
3400
+ error: error instanceof Error ? error.message : String(error),
3401
+ }, "Fork-after-summary job escaped its guarded workflow");
3402
+ });
3403
+ return c.json({ displayObject }, 202);
3404
+ });
3405
+ routes.post("/projects/:projectId/sessions/:sessionId/fork-summary/:objectId/cancel", async (c) => {
3406
+ const projectId = c.req.param("projectId");
3407
+ const sessionId = c.req.param("sessionId");
3408
+ const objectId = c.req.param("objectId");
3409
+ if (!isUrlProjectId(projectId)) {
3410
+ return c.json({ error: "Invalid project ID format" }, 400);
3411
+ }
3412
+ const active = activeForkSummaryJobs.get(sessionId);
3413
+ const object = deps.sessionMetadataService
3414
+ ?.getTranscriptDisplayObjects(sessionId)
3415
+ .find((candidate) => candidate.id === objectId);
3416
+ if (!object) {
3417
+ return c.json({ error: "Transcript display object not found" }, 404);
3418
+ }
3419
+ if (active?.objectId === objectId && object.status === "generating") {
3420
+ active.abortController.abort();
3421
+ }
3422
+ else if (object.status !== "error") {
3423
+ return c.json({ error: "Fork summary job is not active" }, 409);
3424
+ }
3425
+ await deps.sessionMetadataService?.removeTranscriptDisplayObject(sessionId, objectId);
3426
+ emitTranscriptDisplayObjects(sessionId);
3427
+ return c.json({
3428
+ transcriptDisplayObjects: deps.sessionMetadataService?.getTranscriptDisplayObjects(sessionId) ??
3429
+ [],
3430
+ });
3431
+ });
3432
+ routes.patch("/projects/:projectId/sessions/:sessionId/fork-summary/:objectId", async (c) => {
3433
+ const projectId = c.req.param("projectId");
3434
+ const sessionId = c.req.param("sessionId");
3435
+ const objectId = c.req.param("objectId");
3436
+ if (!isUrlProjectId(projectId)) {
3437
+ return c.json({ error: "Invalid project ID format" }, 400);
3438
+ }
3439
+ if (!deps.sessionMetadataService) {
3440
+ return c.json({ error: "Session metadata service not available" }, 503);
3441
+ }
3442
+ let body;
3443
+ try {
3444
+ body = await c.req.json();
3445
+ }
3446
+ catch {
3447
+ return c.json({ error: "Invalid JSON body" }, 400);
3448
+ }
3449
+ const action = body.action === "opened" || body.action === "clicked"
3450
+ ? body.action
3451
+ : undefined;
3452
+ if (body.autoOpenWhenReady !== undefined &&
3453
+ typeof body.autoOpenWhenReady !== "boolean") {
3454
+ return c.json({ error: "autoOpenWhenReady must be a boolean" }, 400);
3455
+ }
3456
+ if (body.action !== undefined && !action) {
3457
+ return c.json({ error: "action must be opened or clicked" }, 400);
3458
+ }
3459
+ const now = new Date().toISOString();
3460
+ const updated = await deps.sessionMetadataService.updateTranscriptDisplayObject(sessionId, objectId, (object) => ({
3461
+ ...object,
3462
+ ...(typeof body.autoOpenWhenReady === "boolean"
3463
+ ? { autoOpenWhenReady: body.autoOpenWhenReady || undefined }
3464
+ : {}),
3465
+ ...(action === "opened" ? { openedAt: now } : {}),
3466
+ ...(action === "clicked" ? { clickedAt: now } : {}),
3467
+ }));
3468
+ if (!updated) {
3469
+ return c.json({ error: "Transcript display object not found" }, 404);
3470
+ }
3471
+ emitTranscriptDisplayObjects(sessionId);
3472
+ return c.json({
3473
+ displayObject: updated,
3474
+ transcriptDisplayObjects: deps.sessionMetadataService.getTranscriptDisplayObjects(sessionId),
3475
+ });
3476
+ });
3477
+ // POST /api/sessions/:sessionId/messages - Queue message
3478
+ routes.post("/sessions/:sessionId/messages", async (c) => {
3479
+ const sessionId = c.req.param("sessionId");
3480
+ const process = deps.supervisor.getProcessForSession(sessionId);
3481
+ if (!process) {
3482
+ return c.json({ error: "No active process for session" }, 404);
3483
+ }
3484
+ let body;
3485
+ try {
3486
+ body = await c.req.json();
3487
+ }
3488
+ catch {
3489
+ return c.json({ error: "Invalid JSON body" }, 400);
3490
+ }
3491
+ if (!body.message) {
3492
+ return c.json({ error: "Message is required" }, 400);
3493
+ }
3494
+ const serverTimestamp = Date.now();
3495
+ const userMessage = {
3496
+ text: body.message,
3497
+ images: body.images,
3498
+ documents: body.documents,
3499
+ attachments: body.attachments,
3500
+ mode: body.mode,
3501
+ tempId: body.tempId,
3502
+ metadata: buildUserMessageMetadata(body, serverTimestamp, body.deferred ? "deferred" : "direct"),
3503
+ };
3504
+ // Check if process is terminated
3505
+ if (process.isTerminated) {
3506
+ return c.json({
3507
+ error: "Process terminated",
3508
+ reason: process.terminationReason,
3509
+ }, 410); // 410 Gone
3510
+ }
3511
+ // Provider-native slash commands (e.g. Codex `/compact`) are dispatched
3512
+ // through the provider's own protocol rather than delivered as turn text the
3513
+ // model would never interpret. Claude's `/compact` reports handled:false and
3514
+ // falls through to normal delivery so the SDK handles it as a regular turn
3515
+ // (and any trailing focus instructions reach the SDK verbatim). A deferred
3516
+ // submission keeps normal queue semantics.
3517
+ if (!body.deferred) {
3518
+ const parsed = parseSlashCommandSubmission(body.message);
3519
+ if (parsed) {
3520
+ const providerResult = await process.runProviderCommand(parsed.name, parsed.argument);
3521
+ if (providerResult.handled) {
3522
+ if (providerResult.error) {
3523
+ return c.json({ error: "Failed to run command", reason: providerResult.error }, 409);
3524
+ }
3525
+ return c.json({ queued: true, serverTimestamp });
3526
+ }
3527
+ }
3528
+ }
3529
+ const resolvedModel = body.model && body.model !== "default"
3530
+ ? body.model
3531
+ : (process.resolvedModel ?? process.model);
2190
3532
  const modelInfoService = deps.modelInfoService;
2191
3533
  const resolveContextWindow = modelInfoService
2192
3534
  ? (model, provider) => modelInfoService.getContextWindow(model, provider)
@@ -2210,7 +3552,6 @@ export function createSessionsRoutes(deps) {
2210
3552
  await process.primeSupportedCommandsForMessage(userMessage);
2211
3553
  const deferredResult = process.deferMessage(userMessage, {
2212
3554
  promoteIfReady: true,
2213
- placement: parseDeferredPlacement(body),
2214
3555
  });
2215
3556
  if (!deferredResult.success) {
2216
3557
  return c.json({
@@ -2218,6 +3559,7 @@ export function createSessionsRoutes(deps) {
2218
3559
  reason: deferredResult.error,
2219
3560
  }, 410);
2220
3561
  }
3562
+ await process.waitForPatientQueuePersistenceIdle();
2221
3563
  return c.json({
2222
3564
  queued: true,
2223
3565
  deferred: deferredResult.deferred,
@@ -2229,7 +3571,7 @@ export function createSessionsRoutes(deps) {
2229
3571
  }
2230
3572
  // Convert thinking option to SDK config
2231
3573
  const { thinking, effort } = body.thinking
2232
- ? thinkingOptionToConfig(body.thinking)
3574
+ ? thinkingOptionToConfig(body.thinking, body.showThinking)
2233
3575
  : { thinking: undefined, effort: undefined };
2234
3576
  const metadataProvider = deps.sessionMetadataService?.getProvider(sessionId);
2235
3577
  const metadataExecutor = parseOptionalExecutor(deps.sessionMetadataService?.getExecutor(sessionId));
@@ -2244,9 +3586,26 @@ export function createSessionsRoutes(deps) {
2244
3586
  ? body.model
2245
3587
  : (process.resolvedModel ?? process.model);
2246
3588
  const serviceTier = normalizeOptionalServiceTier(body.serviceTier);
3589
+ // Per-model preemptive-compaction threshold (task 029). The route holds the
3590
+ // settings; the Supervisor stays settings-agnostic. Key strictly by the YA
3591
+ // model id: the alias the user picked this turn, else the live requested
3592
+ // alias, else the alias persisted at launch (survives restart), else the
3593
+ // provider's reported→YA-id helper for sessions YA didn't start. No family
3594
+ // fallback. See topics/provider-abstraction.md § Per-model settings keying.
3595
+ const yaModelId = body.model ??
3596
+ process.requestedModel ??
3597
+ deps.sessionMetadataService?.getRequestedModel(sessionId) ??
3598
+ getProvider(process.provider)?.yaModelIdForReported?.(process.resolvedModel);
3599
+ const compactAtContextPercent = resolveCompactPercent(deps.serverSettingsService?.getSetting("clientDefaults")
3600
+ ?.compactAtContextPercent, yaModelId);
3601
+ // Resolve the effective window here too — process.contextWindow is
3602
+ // unreliable (often undefined) and the base resolver ignores always-1M.
3603
+ const compactAtContextWindow = compactAtContextPercent === undefined
3604
+ ? undefined
3605
+ : resolveCompactWindow(process.provider, [yaModelId, process.resolvedModel, process.model], resolveContextWindow);
2247
3606
  // Use queueMessageToSession which handles thinking mode changes
2248
3607
  // If thinking mode changed, it will restart the process automatically
2249
- const queueGlobalInstructions = deps.serverSettingsService?.getSetting("globalInstructions") || undefined;
3608
+ const queueGlobalInstructions = getGlobalInstructions();
2250
3609
  const result = await deps.supervisor.queueMessageToSession(sessionId, process.projectPath, userMessage, body.mode, {
2251
3610
  model,
2252
3611
  serviceTier,
@@ -2259,6 +3618,8 @@ export function createSessionsRoutes(deps) {
2259
3618
  undefined,
2260
3619
  globalInstructions: queueGlobalInstructions,
2261
3620
  permissions: body.permissions,
3621
+ compactAtContextPercent,
3622
+ compactAtContextWindow,
2262
3623
  });
2263
3624
  if (!result.success) {
2264
3625
  return c.json({
@@ -2274,52 +3635,267 @@ export function createSessionsRoutes(deps) {
2274
3635
  serverTimestamp,
2275
3636
  });
2276
3637
  });
2277
- // DELETE /api/sessions/:sessionId/deferred/:tempId - Cancel a deferred message
2278
- routes.delete("/sessions/:sessionId/deferred/:tempId", (c) => {
3638
+ // DELETE /api/sessions/:sessionId/recovered-queue/:queueId - Delete a
3639
+ // restart-paused patient queue entry by durable queue id.
3640
+ routes.delete("/sessions/:sessionId/recovered-queue/:queueId", async (c) => {
2279
3641
  const sessionId = c.req.param("sessionId");
2280
- const tempId = c.req.param("tempId");
2281
- const process = deps.supervisor.getProcessForSession(sessionId);
3642
+ const queueId = c.req.param("queueId");
3643
+ const service = deps.sessionQueuePersistenceService;
3644
+ if (!service) {
3645
+ return c.json({ error: "Session queue persistence unavailable" }, 503);
3646
+ }
3647
+ const item = service
3648
+ .listSession(sessionId)
3649
+ .find((candidate) => candidate.id === queueId);
3650
+ if (item?.kind !== "patient" || item.status !== "paused-after-restart") {
3651
+ return c.json({ error: "Recovered queued message not found" }, 404);
3652
+ }
3653
+ await service.deleteItem(queueId);
3654
+ return c.json({
3655
+ deleted: true,
3656
+ deferredMessages: recoveredPatientQueueSummaries(deps, sessionId),
3657
+ });
3658
+ });
3659
+ // A recovered patient queue entry may need a fresh process before it can
3660
+ // rejoin the live queue. Shared by the recovered-queue resume/steer routes.
3661
+ const ensureProcessForRecoveredItem = async (sessionId, item, existing) => {
3662
+ let process = existing;
3663
+ const mode = item.message.mode ?? item.mode;
3664
+ const metadata = deps.sessionMetadataService?.getMetadata(sessionId);
3665
+ const parsedExecutor = parseOptionalExecutor(item.executor ?? metadata?.executor);
3666
+ if (parsedExecutor.error) {
3667
+ return { error: parsedExecutor.error, status: 400 };
3668
+ }
3669
+ const rawModel = item.model ?? metadata?.requestedModel;
3670
+ const model = rawModel && rawModel !== "default" ? rawModel : undefined;
2282
3671
  if (!process) {
2283
- return c.json({ error: "No active process for session" }, 404);
3672
+ try {
3673
+ process = await deps.supervisor.reactivateSession(item.projectPath, sessionId, mode, {
3674
+ model,
3675
+ serviceTier: item.serviceTier,
3676
+ providerName: item.provider,
3677
+ executor: parsedExecutor.executor,
3678
+ globalInstructions: getGlobalInstructions(),
3679
+ recapAfterSeconds: metadata?.recapAfterSeconds,
3680
+ promptSuggestionMode: metadata?.promptSuggestionMode,
3681
+ });
3682
+ }
3683
+ catch (error) {
3684
+ return {
3685
+ error: error instanceof Error
3686
+ ? error.message
3687
+ : "Failed to reactivate session",
3688
+ status: 503,
3689
+ };
3690
+ }
3691
+ await persistLaunchMetadata(sessionId, item.provider, parsedExecutor.executor, undefined, item.model ?? metadata?.requestedModel, metadata?.promptSuggestionMode, metadata?.recapAfterSeconds);
2284
3692
  }
2285
- const cancelled = process.cancelDeferredMessage(tempId);
2286
- if (!cancelled) {
2287
- return c.json({ error: "Deferred message not found" }, 404);
3693
+ else if (mode) {
3694
+ process.setPermissionMode(mode);
2288
3695
  }
2289
- return c.json({ cancelled: true });
3696
+ return { process };
3697
+ };
3698
+ // Move recovered entries back into the live patient queue in queuedAt
3699
+ // order. On a mid-group failure the earlier resumes stand; the caller
3700
+ // reports the failed entry.
3701
+ const resumeRecoveredGroup = async (process, group, options) => {
3702
+ let last;
3703
+ let lastMessage;
3704
+ for (const item of group) {
3705
+ const userMessage = recoveredPatientUserMessage(item);
3706
+ await process.primeSupportedCommandsForMessage(userMessage);
3707
+ const result = process.deferMessage(userMessage, {
3708
+ promoteIfReady: options.promoteIfReady,
3709
+ persistedQueueId: item.id,
3710
+ timestamp: item.queuedAt,
3711
+ });
3712
+ if (!result.success) {
3713
+ return {
3714
+ last,
3715
+ lastMessage,
3716
+ error: result.error ?? "Failed to queue message",
3717
+ };
3718
+ }
3719
+ last = result;
3720
+ lastMessage = userMessage;
3721
+ }
3722
+ return { last, lastMessage };
3723
+ };
3724
+ const reportableProcessState = (process) => process.state.type === "waiting-input" || process.state.type === "in-turn"
3725
+ ? process.state.type
3726
+ : "idle";
3727
+ // Shared prologue for the recovered-queue resume/steer routes: resolve the
3728
+ // target and the recovered group composed before it, refuse while newer
3729
+ // live patient entries exist, and ensure a live process. The newer-entries
3730
+ // guard runs again after the ensure — its await is a window for new
3731
+ // arrivals, and recovered context must never jump a newer patient entry.
3732
+ const resolveRecoveredGroupForDelivery = async (sessionId, queueId, refusal) => {
3733
+ if (!deps.sessionQueuePersistenceService) {
3734
+ return {
3735
+ ok: false,
3736
+ status: 503,
3737
+ body: { error: "Session queue persistence unavailable" },
3738
+ };
3739
+ }
3740
+ const recoveredItems = recoveredPatientQueueItems(deps, sessionId);
3741
+ const targetIndex = recoveredItems.findIndex((candidate) => candidate.id === queueId);
3742
+ const target = recoveredItems[targetIndex];
3743
+ if (!target) {
3744
+ return {
3745
+ ok: false,
3746
+ status: 404,
3747
+ body: { error: "Recovered queued message not found" },
3748
+ };
3749
+ }
3750
+ const group = recoveredItems.slice(0, targetIndex + 1);
3751
+ let process = deps.supervisor.getProcessForSession(sessionId);
3752
+ if (process?.isTerminated) {
3753
+ process = undefined;
3754
+ }
3755
+ const refuseIfNewerEntries = (candidate) => livePatientEntriesNewerThan(candidate, target.queuedAt) > 0
3756
+ ? {
3757
+ ok: false,
3758
+ status: 409,
3759
+ body: {
3760
+ error: refusal,
3761
+ headQueueId: target.id,
3762
+ deferredMessages: sessionQueueSummaries(deps, sessionId, candidate),
3763
+ },
3764
+ }
3765
+ : null;
3766
+ const refusedBefore = refuseIfNewerEntries(process);
3767
+ if (refusedBefore) {
3768
+ return refusedBefore;
3769
+ }
3770
+ const ensured = await ensureProcessForRecoveredItem(sessionId, target, process);
3771
+ if ("error" in ensured) {
3772
+ return {
3773
+ ok: false,
3774
+ status: ensured.status,
3775
+ body: { error: ensured.error },
3776
+ };
3777
+ }
3778
+ process = ensured.process;
3779
+ const refusedAfter = refuseIfNewerEntries(process);
3780
+ if (refusedAfter) {
3781
+ return refusedAfter;
3782
+ }
3783
+ return { ok: true, process, group };
3784
+ };
3785
+ // POST /api/sessions/:sessionId/recovered-queue/:queueId/resume - Move
3786
+ // restart-paused patient queue entries back into the live patient queue,
3787
+ // through the requested entry: resuming a non-head entry also resumes every
3788
+ // recovered entry before it, so compose order is preserved.
3789
+ routes.post("/sessions/:sessionId/recovered-queue/:queueId/resume", async (c) => {
3790
+ const sessionId = c.req.param("sessionId");
3791
+ const queueId = c.req.param("queueId");
3792
+ const resolved = await resolveRecoveredGroupForDelivery(sessionId, queueId, "Drain or cancel newer queued messages before resuming recovered work.");
3793
+ if (!resolved.ok) {
3794
+ return c.json(resolved.body, resolved.status);
3795
+ }
3796
+ const { process, group } = resolved;
3797
+ const { last, error } = await resumeRecoveredGroup(process, group, {
3798
+ promoteIfReady: true,
3799
+ });
3800
+ if (error || !last) {
3801
+ return c.json({
3802
+ error: "Failed to resume recovered queued message",
3803
+ reason: error,
3804
+ deferredMessages: sessionQueueSummaries(deps, sessionId, process),
3805
+ }, 409);
3806
+ }
3807
+ await process.waitForPatientQueuePersistenceIdle();
3808
+ return c.json({
3809
+ resumed: true,
3810
+ resumedCount: group.length,
3811
+ deferred: last.deferred,
3812
+ promoted: last.promoted,
3813
+ position: last.position,
3814
+ processId: process.id,
3815
+ processState: reportableProcessState(process),
3816
+ permissionMode: process.permissionMode,
3817
+ modeVersion: process.modeVersion,
3818
+ recapAfterSeconds: process.recapAfterSeconds,
3819
+ deferredMessages: sessionQueueSummaries(deps, sessionId, process),
3820
+ serverTimestamp: Date.now(),
3821
+ });
2290
3822
  });
2291
- // POST /api/sessions/:sessionId/deferred/:tempId/edit - Take a deferred message for editing
2292
- routes.post("/sessions/:sessionId/deferred/:tempId/edit", (c) => {
3823
+ // POST /api/sessions/:sessionId/recovered-queue/:queueId/steer - Steer a
3824
+ // restart-paused patient queue entry into the session now, without waiting
3825
+ // for verified quiet: the entry and every recovered or live patient entry
3826
+ // composed before it rejoin the live queue and steer in compose order.
3827
+ routes.post("/sessions/:sessionId/recovered-queue/:queueId/steer", async (c) => {
3828
+ const sessionId = c.req.param("sessionId");
3829
+ const queueId = c.req.param("queueId");
3830
+ const resolved = await resolveRecoveredGroupForDelivery(sessionId, queueId, "Drain or cancel newer queued messages before steering recovered work.");
3831
+ if (!resolved.ok) {
3832
+ return c.json(resolved.body, resolved.status);
3833
+ }
3834
+ const { process, group } = resolved;
3835
+ const { lastMessage, error } = await resumeRecoveredGroup(process, group, { promoteIfReady: false });
3836
+ if (error || !lastMessage?.tempId) {
3837
+ return c.json({
3838
+ error: "Failed to steer recovered queued message",
3839
+ reason: error,
3840
+ deferredMessages: sessionQueueSummaries(deps, sessionId, process),
3841
+ }, 409);
3842
+ }
3843
+ const steerResult = process.steerPatientDeferredMessagesThrough(lastMessage.tempId);
3844
+ if (!steerResult.success) {
3845
+ return c.json({
3846
+ error: steerResult.error ?? "Failed to steer recovered queued message",
3847
+ deferredMessages: sessionQueueSummaries(deps, sessionId, process),
3848
+ }, 409);
3849
+ }
3850
+ await process.waitForPatientQueuePersistenceIdle();
3851
+ return c.json({
3852
+ steered: true,
3853
+ count: steerResult.steered,
3854
+ processId: process.id,
3855
+ processState: reportableProcessState(process),
3856
+ permissionMode: process.permissionMode,
3857
+ modeVersion: process.modeVersion,
3858
+ recapAfterSeconds: process.recapAfterSeconds,
3859
+ deferredMessages: sessionQueueSummaries(deps, sessionId, process),
3860
+ serverTimestamp: Date.now(),
3861
+ });
3862
+ });
3863
+ // DELETE /api/sessions/:sessionId/deferred/:tempId - Cancel a deferred message
3864
+ routes.delete("/sessions/:sessionId/deferred/:tempId", async (c) => {
2293
3865
  const sessionId = c.req.param("sessionId");
2294
3866
  const tempId = c.req.param("tempId");
2295
3867
  const process = deps.supervisor.getProcessForSession(sessionId);
2296
3868
  if (!process) {
2297
3869
  return c.json({ error: "No active process for session" }, 404);
2298
3870
  }
2299
- const taken = process.takeDeferredMessage(tempId);
2300
- if (!taken) {
3871
+ const cancelled = process.cancelDeferredMessage(tempId);
3872
+ if (!cancelled) {
2301
3873
  return c.json({ error: "Deferred message not found" }, 404);
2302
3874
  }
2303
- return c.json({
2304
- message: taken.message.text,
2305
- tempId: taken.message.tempId,
2306
- mode: taken.message.mode,
2307
- attachments: taken.message.attachments,
2308
- placement: taken.placement,
2309
- });
3875
+ await process.waitForPatientQueuePersistenceIdle();
3876
+ return c.json({ cancelled: true });
2310
3877
  });
2311
- // POST /api/sessions/:sessionId/deferred/:tempId/edit/release - Release a queued edit barrier
2312
- routes.post("/sessions/:sessionId/deferred/:tempId/edit/release", (c) => {
3878
+ // POST /api/sessions/:sessionId/deferred/:tempId/steer - Steer a patient
3879
+ // queued message, and every patient entry ahead of it, into the session now.
3880
+ routes.post("/sessions/:sessionId/deferred/:tempId/steer", async (c) => {
2313
3881
  const sessionId = c.req.param("sessionId");
2314
3882
  const tempId = c.req.param("tempId");
2315
3883
  const process = deps.supervisor.getProcessForSession(sessionId);
2316
3884
  if (!process) {
2317
3885
  return c.json({ error: "No active process for session" }, 404);
2318
3886
  }
2319
- const released = process.releaseDeferredEditBarrier(tempId);
3887
+ const result = process.steerPatientDeferredMessagesThrough(tempId);
3888
+ if (!result.success) {
3889
+ const status = result.error === "Deferred message not found" ? 404 : 409;
3890
+ return c.json({ error: result.error }, status);
3891
+ }
3892
+ await process.waitForPatientQueuePersistenceIdle();
2320
3893
  return c.json({
2321
- released,
2322
- deferredMessages: process.getDeferredQueueSummary(),
3894
+ steered: true,
3895
+ count: result.steered,
3896
+ // Include any still-paused recovered entries so the client's list does
3897
+ // not lose their chips when a live entry is steered.
3898
+ deferredMessages: sessionQueueSummaries(deps, sessionId, process),
2323
3899
  });
2324
3900
  });
2325
3901
  // PUT /api/sessions/:sessionId/mode - Update permission mode without sending a message
@@ -2387,32 +3963,65 @@ export function createSessionsRoutes(deps) {
2387
3963
  const requestBefore = process.getPendingInputRequest();
2388
3964
  const permissionModeBefore = process.permissionMode;
2389
3965
  if (process.state.type !== "waiting-input") {
2390
- try {
2391
- await appendApprovalAuditLog(deps.dataDir, {
2392
- timestamp: new Date().toISOString(),
2393
- sessionId,
2394
- processId: process.id,
2395
- provider: process.provider,
2396
- requestId: body.requestId,
2397
- request: requestBefore,
2398
- response: body.response,
2399
- normalizedResponse,
2400
- answers: body.answers,
2401
- feedback: body.feedback,
2402
- accepted: false,
2403
- failure: "No pending input request",
2404
- permissionModeBefore,
2405
- permissionModeAfter: process.permissionMode,
2406
- });
2407
- }
2408
- catch (error) {
2409
- console.warn("[approval-audit] Failed to append audit log:", error);
3966
+ if (isApprovalAuditLogEnabled(deps)) {
3967
+ try {
3968
+ await appendApprovalAuditLog(deps.dataDir, {
3969
+ timestamp: new Date().toISOString(),
3970
+ sessionId,
3971
+ processId: process.id,
3972
+ provider: process.provider,
3973
+ requestId: body.requestId,
3974
+ request: requestBefore,
3975
+ response: body.response,
3976
+ normalizedResponse,
3977
+ answers: body.answers,
3978
+ feedback: body.feedback,
3979
+ accepted: false,
3980
+ failure: "No pending input request",
3981
+ permissionModeBefore,
3982
+ permissionModeAfter: process.permissionMode,
3983
+ });
3984
+ }
3985
+ catch (error) {
3986
+ console.warn("[approval-audit] Failed to append audit log:", error);
3987
+ }
2410
3988
  }
2411
3989
  return c.json({ error: "No pending input request" }, 400);
2412
3990
  }
2413
3991
  // Call respondToInput which resolves the SDK's canUseTool promise
2414
3992
  const accepted = process.respondToInput(body.requestId, normalizedResponse, body.answers, body.feedback);
2415
3993
  if (!accepted) {
3994
+ if (isApprovalAuditLogEnabled(deps)) {
3995
+ try {
3996
+ await appendApprovalAuditLog(deps.dataDir, {
3997
+ timestamp: new Date().toISOString(),
3998
+ sessionId,
3999
+ processId: process.id,
4000
+ provider: process.provider,
4001
+ requestId: body.requestId,
4002
+ request: requestBefore,
4003
+ response: body.response,
4004
+ normalizedResponse,
4005
+ answers: body.answers,
4006
+ feedback: body.feedback,
4007
+ accepted: false,
4008
+ failure: "Invalid request ID or no pending request",
4009
+ permissionModeBefore,
4010
+ permissionModeAfter: process.permissionMode,
4011
+ });
4012
+ }
4013
+ catch (error) {
4014
+ console.warn("[approval-audit] Failed to append audit log:", error);
4015
+ }
4016
+ }
4017
+ return c.json({ error: "Invalid request ID or no pending request" }, 400);
4018
+ }
4019
+ // If approve_accept_edits, switch the permission mode
4020
+ if (isApproveAcceptEdits) {
4021
+ process.setPermissionMode("acceptEdits");
4022
+ }
4023
+ const pendingInputRequest = process.getPendingInputRequest();
4024
+ if (isApprovalAuditLogEnabled(deps)) {
2416
4025
  try {
2417
4026
  await appendApprovalAuditLog(deps.dataDir, {
2418
4027
  timestamp: new Date().toISOString(),
@@ -2425,8 +4034,7 @@ export function createSessionsRoutes(deps) {
2425
4034
  normalizedResponse,
2426
4035
  answers: body.answers,
2427
4036
  feedback: body.feedback,
2428
- accepted: false,
2429
- failure: "Invalid request ID or no pending request",
4037
+ accepted: true,
2430
4038
  permissionModeBefore,
2431
4039
  permissionModeAfter: process.permissionMode,
2432
4040
  });
@@ -2434,32 +4042,6 @@ export function createSessionsRoutes(deps) {
2434
4042
  catch (error) {
2435
4043
  console.warn("[approval-audit] Failed to append audit log:", error);
2436
4044
  }
2437
- return c.json({ error: "Invalid request ID or no pending request" }, 400);
2438
- }
2439
- // If approve_accept_edits, switch the permission mode
2440
- if (isApproveAcceptEdits) {
2441
- process.setPermissionMode("acceptEdits");
2442
- }
2443
- const pendingInputRequest = process.getPendingInputRequest();
2444
- try {
2445
- await appendApprovalAuditLog(deps.dataDir, {
2446
- timestamp: new Date().toISOString(),
2447
- sessionId,
2448
- processId: process.id,
2449
- provider: process.provider,
2450
- requestId: body.requestId,
2451
- request: requestBefore,
2452
- response: body.response,
2453
- normalizedResponse,
2454
- answers: body.answers,
2455
- feedback: body.feedback,
2456
- accepted: true,
2457
- permissionModeBefore,
2458
- permissionModeAfter: process.permissionMode,
2459
- });
2460
- }
2461
- catch (error) {
2462
- console.warn("[approval-audit] Failed to append audit log:", error);
2463
4045
  }
2464
4046
  return c.json({ accepted: true, pendingInputRequest });
2465
4047
  });
@@ -2542,7 +4124,9 @@ export function createSessionsRoutes(deps) {
2542
4124
  body.heartbeatTurnsEnabled === undefined &&
2543
4125
  body.heartbeatTurnsAfterMinutes === undefined &&
2544
4126
  body.heartbeatTurnText === undefined &&
2545
- body.heartbeatForceAfterMinutes === undefined) {
4127
+ body.heartbeatForceAfterMinutes === undefined &&
4128
+ body.promptSuggestionMode === undefined &&
4129
+ body.recapAfterSeconds === undefined) {
2546
4130
  return c.json({
2547
4131
  error: "At least one session metadata field must be provided",
2548
4132
  }, 400);
@@ -2606,6 +4190,36 @@ export function createSessionsRoutes(deps) {
2606
4190
  : typeof body.parentSessionId === "string"
2607
4191
  ? body.parentSessionId.trim() || null
2608
4192
  : null;
4193
+ // promptSuggestionMode: null/"" clears the preference (revert to default);
4194
+ // a valid enum value is stored; any other value is rejected.
4195
+ let promptSuggestionMode;
4196
+ if (body.promptSuggestionMode !== undefined) {
4197
+ if (body.promptSuggestionMode === null ||
4198
+ body.promptSuggestionMode === "") {
4199
+ promptSuggestionMode = null;
4200
+ }
4201
+ else if (typeof body.promptSuggestionMode === "string" &&
4202
+ PROMPT_SUGGESTION_MODES.includes(body.promptSuggestionMode)) {
4203
+ promptSuggestionMode =
4204
+ body.promptSuggestionMode;
4205
+ }
4206
+ else {
4207
+ return c.json({ error: "promptSuggestionMode must be one of: off, native" }, 400);
4208
+ }
4209
+ }
4210
+ let recapAfterSeconds;
4211
+ if (body.recapAfterSeconds !== undefined) {
4212
+ if (body.recapAfterSeconds === null || body.recapAfterSeconds === "") {
4213
+ recapAfterSeconds = null;
4214
+ }
4215
+ else if (typeof body.recapAfterSeconds === "number" &&
4216
+ Number.isFinite(body.recapAfterSeconds)) {
4217
+ recapAfterSeconds = clampRecapAfterSeconds(body.recapAfterSeconds);
4218
+ }
4219
+ else {
4220
+ return c.json({ error: "recapAfterSeconds must be null or a finite number" }, 400);
4221
+ }
4222
+ }
2609
4223
  await deps.sessionMetadataService.updateMetadata(sessionId, {
2610
4224
  title: body.title,
2611
4225
  archived: body.archived,
@@ -2615,6 +4229,8 @@ export function createSessionsRoutes(deps) {
2615
4229
  heartbeatTurnsAfterMinutes,
2616
4230
  heartbeatTurnText,
2617
4231
  heartbeatForceAfterMinutes,
4232
+ promptSuggestionMode,
4233
+ recapAfterSeconds,
2618
4234
  });
2619
4235
  // Emit SSE event so sidebar and other clients can update
2620
4236
  if (deps.eventBus) {
@@ -2629,6 +4245,8 @@ export function createSessionsRoutes(deps) {
2629
4245
  heartbeatTurnsAfterMinutes,
2630
4246
  heartbeatTurnText,
2631
4247
  heartbeatForceAfterMinutes,
4248
+ promptSuggestionMode: promptSuggestionMode ?? undefined,
4249
+ recapAfterSeconds: recapAfterSeconds ?? undefined,
2632
4250
  timestamp: new Date().toISOString(),
2633
4251
  });
2634
4252
  }
@@ -2674,7 +4292,7 @@ export function createSessionsRoutes(deps) {
2674
4292
  }
2675
4293
  // Get original session to extract title for the clone
2676
4294
  const reader = deps.readerFactory(project);
2677
- let originalSession = await reader.getSessionSummary(sessionId, projectId);
4295
+ let originalSession = await reader.getSessionSummary(sessionId, projectId, { readMode: "head" });
2678
4296
  let cloneProvider = project.provider;
2679
4297
  let result;
2680
4298
  const shouldCloneFromCodex = isCodexProviderName(body.provider) ||
@@ -2691,7 +4309,9 @@ export function createSessionsRoutes(deps) {
2691
4309
  }
2692
4310
  originalSession =
2693
4311
  originalSession ??
2694
- (await codexReader.getSessionSummary(sessionId, projectId)) ??
4312
+ (await codexReader.getSessionSummary(sessionId, projectId, {
4313
+ readMode: "head",
4314
+ })) ??
2695
4315
  null;
2696
4316
  cloneProvider =
2697
4317
  originalSession?.provider ??
@@ -2734,36 +4354,6 @@ export function createSessionsRoutes(deps) {
2734
4354
  return c.json({ error: message }, 500);
2735
4355
  }
2736
4356
  });
2737
- // ============ Worker Queue Endpoints ============
2738
- // GET /api/status/workers - Get worker activity for safe restart indicator
2739
- routes.get("/status/workers", (c) => {
2740
- const activity = deps.supervisor.getWorkerActivity();
2741
- return c.json(activity);
2742
- });
2743
- // GET /api/queue - Get all queued requests
2744
- routes.get("/queue", (c) => {
2745
- const queue = deps.supervisor.getQueueInfo();
2746
- const poolStatus = deps.supervisor.getWorkerPoolStatus();
2747
- return c.json({ queue, ...poolStatus });
2748
- });
2749
- // GET /api/queue/:queueId - Get specific queue entry position
2750
- routes.get("/queue/:queueId", (c) => {
2751
- const queueId = c.req.param("queueId");
2752
- const position = deps.supervisor.getQueuePosition(queueId);
2753
- if (position === undefined) {
2754
- return c.json({ error: "Queue entry not found" }, 404);
2755
- }
2756
- return c.json({ queueId, position });
2757
- });
2758
- // DELETE /api/queue/:queueId - Cancel a queued request
2759
- routes.delete("/queue/:queueId", (c) => {
2760
- const queueId = c.req.param("queueId");
2761
- const cancelled = deps.supervisor.cancelQueuedRequest(queueId);
2762
- if (!cancelled) {
2763
- return c.json({ error: "Queue entry not found or already processed" }, 404);
2764
- }
2765
- return c.json({ cancelled: true });
2766
- });
2767
4357
  return routes;
2768
4358
  }
2769
4359
  //# sourceMappingURL=sessions.js.map