@prestyj/cli 5.10.0 → 5.11.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 (455) hide show
  1. package/README.md +3 -4
  2. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  3. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  4. package/dist/app-sidecar-auth-broadcast.test.js +282 -0
  5. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  6. package/dist/app-sidecar-sessions.d.ts +5 -2
  7. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  8. package/dist/app-sidecar-sessions.js +34 -8
  9. package/dist/app-sidecar-sessions.js.map +1 -1
  10. package/dist/app-sidecar-sessions.test.js +95 -0
  11. package/dist/app-sidecar-sessions.test.js.map +1 -1
  12. package/dist/app-sidecar.js +497 -128
  13. package/dist/app-sidecar.js.map +1 -1
  14. package/dist/cli/command-routing.d.ts +1 -1
  15. package/dist/cli/command-routing.d.ts.map +1 -1
  16. package/dist/cli/command-routing.js +1 -0
  17. package/dist/cli/command-routing.js.map +1 -1
  18. package/dist/cli/command-routing.test.js +1 -0
  19. package/dist/cli/command-routing.test.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +167 -47
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +2 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +10 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/config.test.js +10 -1
  28. package/dist/config.test.js.map +1 -1
  29. package/dist/core/agent-notifications.d.ts +62 -0
  30. package/dist/core/agent-notifications.d.ts.map +1 -0
  31. package/dist/core/agent-notifications.js +92 -0
  32. package/dist/core/agent-notifications.js.map +1 -0
  33. package/dist/core/agent-notifications.test.d.ts +2 -0
  34. package/dist/core/agent-notifications.test.d.ts.map +1 -0
  35. package/dist/core/agent-notifications.test.js +97 -0
  36. package/dist/core/agent-notifications.test.js.map +1 -0
  37. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  38. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  39. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  40. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  41. package/dist/core/agent-session-compaction.test.js +323 -6
  42. package/dist/core/agent-session-compaction.test.js.map +1 -1
  43. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  44. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  45. package/dist/core/agent-session-crash-durability.test.js +102 -0
  46. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  47. package/dist/core/agent-session-marker-anchors.test.js +181 -2
  48. package/dist/core/agent-session-marker-anchors.test.js.map +1 -1
  49. package/dist/core/agent-session-model-switch.test.d.ts +2 -0
  50. package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  51. package/dist/core/agent-session-model-switch.test.js +161 -0
  52. package/dist/core/agent-session-model-switch.test.js.map +1 -0
  53. package/dist/core/agent-session-notifications.test.d.ts +2 -0
  54. package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
  55. package/dist/core/agent-session-notifications.test.js +114 -0
  56. package/dist/core/agent-session-notifications.test.js.map +1 -0
  57. package/dist/core/agent-session-process-gate.test.d.ts +2 -0
  58. package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
  59. package/dist/core/agent-session-process-gate.test.js +92 -0
  60. package/dist/core/agent-session-process-gate.test.js.map +1 -0
  61. package/dist/core/agent-session-queue.test.js +65 -0
  62. package/dist/core/agent-session-queue.test.js.map +1 -1
  63. package/dist/core/agent-session-review-coverage.test.js +80 -23
  64. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  65. package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
  66. package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
  67. package/dist/core/agent-session-slash-restore.test.js +121 -0
  68. package/dist/core/agent-session-slash-restore.test.js.map +1 -0
  69. package/dist/core/agent-session.d.ts +214 -10
  70. package/dist/core/agent-session.d.ts.map +1 -1
  71. package/dist/core/agent-session.js +978 -212
  72. package/dist/core/agent-session.js.map +1 -1
  73. package/dist/core/compaction/active-context.test.js +21 -0
  74. package/dist/core/compaction/active-context.test.js.map +1 -1
  75. package/dist/core/compaction/compactor.d.ts +108 -8
  76. package/dist/core/compaction/compactor.d.ts.map +1 -1
  77. package/dist/core/compaction/compactor.js +423 -119
  78. package/dist/core/compaction/compactor.js.map +1 -1
  79. package/dist/core/compaction/compactor.test.js +360 -8
  80. package/dist/core/compaction/compactor.test.js.map +1 -1
  81. package/dist/core/compaction/policy.d.ts +17 -0
  82. package/dist/core/compaction/policy.d.ts.map +1 -0
  83. package/dist/core/compaction/policy.js +22 -0
  84. package/dist/core/compaction/policy.js.map +1 -0
  85. package/dist/core/compaction/policy.test.d.ts +2 -0
  86. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  87. package/dist/core/compaction/policy.test.js +35 -0
  88. package/dist/core/compaction/policy.test.js.map +1 -0
  89. package/dist/core/compaction/query-aware-selector.d.ts +33 -0
  90. package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
  91. package/dist/core/compaction/query-aware-selector.js +202 -0
  92. package/dist/core/compaction/query-aware-selector.js.map +1 -0
  93. package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
  94. package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
  95. package/dist/core/compaction/query-aware-selector.test.js +96 -0
  96. package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
  97. package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
  98. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
  99. package/dist/core/compaction/tool-result-pruner.js +98 -26
  100. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  101. package/dist/core/compaction/tool-result-pruner.test.js +178 -6
  102. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
  103. package/dist/core/event-bus.d.ts +23 -0
  104. package/dist/core/event-bus.d.ts.map +1 -1
  105. package/dist/core/event-bus.js +10 -0
  106. package/dist/core/event-bus.js.map +1 -1
  107. package/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/dist/core/extensions/loader.js +28 -8
  109. package/dist/core/extensions/loader.js.map +1 -1
  110. package/dist/core/extensions/plugin-bundles.d.ts +23 -0
  111. package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
  112. package/dist/core/extensions/plugin-bundles.js +228 -0
  113. package/dist/core/extensions/plugin-bundles.js.map +1 -0
  114. package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
  115. package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
  116. package/dist/core/extensions/plugin-bundles.test.js +110 -0
  117. package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
  118. package/dist/core/foreign-session-import.d.ts +120 -0
  119. package/dist/core/foreign-session-import.d.ts.map +1 -0
  120. package/dist/core/foreign-session-import.js +598 -0
  121. package/dist/core/foreign-session-import.js.map +1 -0
  122. package/dist/core/foreign-session-import.test.d.ts +2 -0
  123. package/dist/core/foreign-session-import.test.d.ts.map +1 -0
  124. package/dist/core/foreign-session-import.test.js +220 -0
  125. package/dist/core/foreign-session-import.test.js.map +1 -0
  126. package/dist/core/foreign-transcript-blocks.d.ts +51 -0
  127. package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
  128. package/dist/core/foreign-transcript-blocks.js +151 -0
  129. package/dist/core/foreign-transcript-blocks.js.map +1 -0
  130. package/dist/core/ideal-review.d.ts +20 -1
  131. package/dist/core/ideal-review.d.ts.map +1 -1
  132. package/dist/core/ideal-review.js +37 -2
  133. package/dist/core/ideal-review.js.map +1 -1
  134. package/dist/core/ideal-review.test.js +99 -3
  135. package/dist/core/ideal-review.test.js.map +1 -1
  136. package/dist/core/loop-breaker.d.ts.map +1 -1
  137. package/dist/core/loop-breaker.js +1 -0
  138. package/dist/core/loop-breaker.js.map +1 -1
  139. package/dist/core/lsp/client.d.ts +15 -0
  140. package/dist/core/lsp/client.d.ts.map +1 -1
  141. package/dist/core/lsp/client.js +39 -1
  142. package/dist/core/lsp/client.js.map +1 -1
  143. package/dist/core/lsp/manager.d.ts +47 -9
  144. package/dist/core/lsp/manager.d.ts.map +1 -1
  145. package/dist/core/lsp/manager.js +96 -69
  146. package/dist/core/lsp/manager.js.map +1 -1
  147. package/dist/core/lsp/manager.test.js +18 -0
  148. package/dist/core/lsp/manager.test.js.map +1 -1
  149. package/dist/core/lsp/pool.d.ts +113 -0
  150. package/dist/core/lsp/pool.d.ts.map +1 -0
  151. package/dist/core/lsp/pool.js +255 -0
  152. package/dist/core/lsp/pool.js.map +1 -0
  153. package/dist/core/lsp/pool.test.d.ts +2 -0
  154. package/dist/core/lsp/pool.test.d.ts.map +1 -0
  155. package/dist/core/lsp/pool.test.js +286 -0
  156. package/dist/core/lsp/pool.test.js.map +1 -0
  157. package/dist/core/lsp/servers.d.ts.map +1 -1
  158. package/dist/core/lsp/servers.js +43 -3
  159. package/dist/core/lsp/servers.js.map +1 -1
  160. package/dist/core/lsp/servers.test.d.ts +2 -0
  161. package/dist/core/lsp/servers.test.d.ts.map +1 -0
  162. package/dist/core/lsp/servers.test.js +67 -0
  163. package/dist/core/lsp/servers.test.js.map +1 -0
  164. package/dist/core/lsp/windows.test.js +2 -14
  165. package/dist/core/lsp/windows.test.js.map +1 -1
  166. package/dist/core/mcp/catalog-cache.d.ts +63 -0
  167. package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
  168. package/dist/core/mcp/catalog-cache.js +163 -0
  169. package/dist/core/mcp/catalog-cache.js.map +1 -0
  170. package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
  171. package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
  172. package/dist/core/mcp/catalog-cache.test.js +99 -0
  173. package/dist/core/mcp/catalog-cache.test.js.map +1 -0
  174. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  175. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  176. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  177. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  178. package/dist/core/mcp/client.d.ts +158 -0
  179. package/dist/core/mcp/client.d.ts.map +1 -1
  180. package/dist/core/mcp/client.js +476 -35
  181. package/dist/core/mcp/client.js.map +1 -1
  182. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  183. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  184. package/dist/core/mcp/elicitation-bridge.js +52 -0
  185. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  186. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  187. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  188. package/dist/core/mcp/elicitation.test.js +158 -0
  189. package/dist/core/mcp/elicitation.test.js.map +1 -0
  190. package/dist/core/mcp/index.d.ts +5 -1
  191. package/dist/core/mcp/index.d.ts.map +1 -1
  192. package/dist/core/mcp/index.js +2 -0
  193. package/dist/core/mcp/index.js.map +1 -1
  194. package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
  195. package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
  196. package/dist/core/mcp/legacy-negotiation.test.js +80 -0
  197. package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
  198. package/dist/core/mcp/oauth-provider.d.ts +1 -2
  199. package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
  200. package/dist/core/mcp/oauth-provider.js +6 -2
  201. package/dist/core/mcp/oauth-provider.js.map +1 -1
  202. package/dist/core/mcp/oauth-store.d.ts +1 -1
  203. package/dist/core/mcp/oauth-store.d.ts.map +1 -1
  204. package/dist/core/mcp/oauth-store.js.map +1 -1
  205. package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
  206. package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
  207. package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
  208. package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
  209. package/dist/core/mcp/shared-pool.d.ts +158 -0
  210. package/dist/core/mcp/shared-pool.d.ts.map +1 -0
  211. package/dist/core/mcp/shared-pool.js +235 -0
  212. package/dist/core/mcp/shared-pool.js.map +1 -0
  213. package/dist/core/mcp/shared-pool.test.d.ts +2 -0
  214. package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
  215. package/dist/core/mcp/shared-pool.test.js +425 -0
  216. package/dist/core/mcp/shared-pool.test.js.map +1 -0
  217. package/dist/core/mcp/types.d.ts +12 -0
  218. package/dist/core/mcp/types.d.ts.map +1 -1
  219. package/dist/core/mcp/when-connected.test.d.ts +2 -0
  220. package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
  221. package/dist/core/mcp/when-connected.test.js +46 -0
  222. package/dist/core/mcp/when-connected.test.js.map +1 -0
  223. package/dist/core/nolan-context.d.ts.map +1 -1
  224. package/dist/core/nolan-context.js +8 -0
  225. package/dist/core/nolan-context.js.map +1 -1
  226. package/dist/core/nolan-context.test.js +40 -0
  227. package/dist/core/nolan-context.test.js.map +1 -1
  228. package/dist/core/nolan-prompt.js +3 -0
  229. package/dist/core/nolan-prompt.js.map +1 -1
  230. package/dist/core/nolan-prompt.test.js +5 -0
  231. package/dist/core/nolan-prompt.test.js.map +1 -1
  232. package/dist/core/persistent-shell.d.ts +3 -2
  233. package/dist/core/persistent-shell.d.ts.map +1 -1
  234. package/dist/core/persistent-shell.js +10 -3
  235. package/dist/core/persistent-shell.js.map +1 -1
  236. package/dist/core/process-gate.d.ts +45 -0
  237. package/dist/core/process-gate.d.ts.map +1 -0
  238. package/dist/core/process-gate.js +79 -0
  239. package/dist/core/process-gate.js.map +1 -0
  240. package/dist/core/process-gate.test.d.ts +2 -0
  241. package/dist/core/process-gate.test.d.ts.map +1 -0
  242. package/dist/core/process-gate.test.js +87 -0
  243. package/dist/core/process-gate.test.js.map +1 -0
  244. package/dist/core/process-manager-notifications.test.d.ts +2 -0
  245. package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
  246. package/dist/core/process-manager-notifications.test.js +217 -0
  247. package/dist/core/process-manager-notifications.test.js.map +1 -0
  248. package/dist/core/process-manager.d.ts +50 -1
  249. package/dist/core/process-manager.d.ts.map +1 -1
  250. package/dist/core/process-manager.js +189 -2
  251. package/dist/core/process-manager.js.map +1 -1
  252. package/dist/core/project-discovery.d.ts +31 -6
  253. package/dist/core/project-discovery.d.ts.map +1 -1
  254. package/dist/core/project-discovery.js +287 -31
  255. package/dist/core/project-discovery.js.map +1 -1
  256. package/dist/core/project-discovery.test.js +124 -0
  257. package/dist/core/project-discovery.test.js.map +1 -1
  258. package/dist/core/regrounding.d.ts.map +1 -1
  259. package/dist/core/regrounding.js +1 -0
  260. package/dist/core/regrounding.js.map +1 -1
  261. package/dist/core/run-claim.d.ts +37 -0
  262. package/dist/core/run-claim.d.ts.map +1 -0
  263. package/dist/core/run-claim.js +46 -0
  264. package/dist/core/run-claim.js.map +1 -0
  265. package/dist/core/run-claim.test.d.ts +2 -0
  266. package/dist/core/run-claim.test.d.ts.map +1 -0
  267. package/dist/core/run-claim.test.js +107 -0
  268. package/dist/core/run-claim.test.js.map +1 -0
  269. package/dist/core/run-journal.test.d.ts +2 -0
  270. package/dist/core/run-journal.test.d.ts.map +1 -0
  271. package/dist/core/run-journal.test.js +195 -0
  272. package/dist/core/run-journal.test.js.map +1 -0
  273. package/dist/core/run-lifecycle.d.ts +22 -3
  274. package/dist/core/run-lifecycle.d.ts.map +1 -1
  275. package/dist/core/run-lifecycle.js +12 -3
  276. package/dist/core/run-lifecycle.js.map +1 -1
  277. package/dist/core/sandbox-domains.d.ts +14 -0
  278. package/dist/core/sandbox-domains.d.ts.map +1 -0
  279. package/dist/core/sandbox-domains.js +76 -0
  280. package/dist/core/sandbox-domains.js.map +1 -0
  281. package/dist/core/sandbox-feedback.d.ts +22 -0
  282. package/dist/core/sandbox-feedback.d.ts.map +1 -0
  283. package/dist/core/sandbox-feedback.js +69 -0
  284. package/dist/core/sandbox-feedback.js.map +1 -0
  285. package/dist/core/sandbox-feedback.test.d.ts +2 -0
  286. package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
  287. package/dist/core/sandbox-feedback.test.js +32 -0
  288. package/dist/core/sandbox-feedback.test.js.map +1 -0
  289. package/dist/core/sandbox.d.ts +91 -0
  290. package/dist/core/sandbox.d.ts.map +1 -0
  291. package/dist/core/sandbox.js +254 -0
  292. package/dist/core/sandbox.js.map +1 -0
  293. package/dist/core/sandbox.test.d.ts +2 -0
  294. package/dist/core/sandbox.test.d.ts.map +1 -0
  295. package/dist/core/sandbox.test.js +157 -0
  296. package/dist/core/sandbox.test.js.map +1 -0
  297. package/dist/core/session-compaction.d.ts +6 -0
  298. package/dist/core/session-compaction.d.ts.map +1 -1
  299. package/dist/core/session-compaction.js +11 -1
  300. package/dist/core/session-compaction.js.map +1 -1
  301. package/dist/core/session-compaction.test.js +18 -1
  302. package/dist/core/session-compaction.test.js.map +1 -1
  303. package/dist/core/session-export.d.ts.map +1 -1
  304. package/dist/core/session-export.js +12 -4
  305. package/dist/core/session-export.js.map +1 -1
  306. package/dist/core/session-history.d.ts +95 -6
  307. package/dist/core/session-history.d.ts.map +1 -1
  308. package/dist/core/session-history.js +261 -17
  309. package/dist/core/session-history.js.map +1 -1
  310. package/dist/core/session-history.test.js +173 -2
  311. package/dist/core/session-history.test.js.map +1 -1
  312. package/dist/core/session-manager.d.ts +223 -6
  313. package/dist/core/session-manager.d.ts.map +1 -1
  314. package/dist/core/session-manager.js +515 -26
  315. package/dist/core/session-manager.js.map +1 -1
  316. package/dist/core/session-manager.test.js +147 -3
  317. package/dist/core/session-manager.test.js.map +1 -1
  318. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  319. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  320. package/dist/core/session-notification-visibility.test.js +113 -0
  321. package/dist/core/session-notification-visibility.test.js.map +1 -0
  322. package/dist/core/session-preview.d.ts +5 -4
  323. package/dist/core/session-preview.d.ts.map +1 -1
  324. package/dist/core/session-preview.js +12 -5
  325. package/dist/core/session-preview.js.map +1 -1
  326. package/dist/core/session-restore-display.test.js +25 -0
  327. package/dist/core/session-restore-display.test.js.map +1 -1
  328. package/dist/core/session-storage.d.ts.map +1 -1
  329. package/dist/core/session-storage.js +39 -3
  330. package/dist/core/session-storage.js.map +1 -1
  331. package/dist/core/session-storage.test.js +6 -1
  332. package/dist/core/session-storage.test.js.map +1 -1
  333. package/dist/core/settings-manager.d.ts +8 -2
  334. package/dist/core/settings-manager.d.ts.map +1 -1
  335. package/dist/core/settings-manager.js +26 -0
  336. package/dist/core/settings-manager.js.map +1 -1
  337. package/dist/core/steering.d.ts +12 -0
  338. package/dist/core/steering.d.ts.map +1 -1
  339. package/dist/core/steering.js +16 -0
  340. package/dist/core/steering.js.map +1 -1
  341. package/dist/core/subagent-manager.d.ts +13 -0
  342. package/dist/core/subagent-manager.d.ts.map +1 -1
  343. package/dist/core/subagent-manager.js +39 -1
  344. package/dist/core/subagent-manager.js.map +1 -1
  345. package/dist/core/subagent-manager.test.js +43 -7
  346. package/dist/core/subagent-manager.test.js.map +1 -1
  347. package/dist/core/verification-evidence.d.ts +17 -0
  348. package/dist/core/verification-evidence.d.ts.map +1 -0
  349. package/dist/core/verification-evidence.js +283 -0
  350. package/dist/core/verification-evidence.js.map +1 -0
  351. package/dist/core/verification-evidence.test.d.ts +2 -0
  352. package/dist/core/verification-evidence.test.d.ts.map +1 -0
  353. package/dist/core/verification-evidence.test.js +103 -0
  354. package/dist/core/verification-evidence.test.js.map +1 -0
  355. package/dist/interactive.d.ts.map +1 -1
  356. package/dist/interactive.js +17 -3
  357. package/dist/interactive.js.map +1 -1
  358. package/dist/modes/acp-mode.d.ts +122 -0
  359. package/dist/modes/acp-mode.d.ts.map +1 -0
  360. package/dist/modes/acp-mode.js +1366 -0
  361. package/dist/modes/acp-mode.js.map +1 -0
  362. package/dist/modes/acp-mode.test.d.ts +2 -0
  363. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  364. package/dist/modes/acp-mode.test.js +1065 -0
  365. package/dist/modes/acp-mode.test.js.map +1 -0
  366. package/dist/modes/index.d.ts +1 -0
  367. package/dist/modes/index.d.ts.map +1 -1
  368. package/dist/modes/index.js +1 -0
  369. package/dist/modes/index.js.map +1 -1
  370. package/dist/modes/json-mode.d.ts +28 -0
  371. package/dist/modes/json-mode.d.ts.map +1 -1
  372. package/dist/modes/json-mode.js +37 -0
  373. package/dist/modes/json-mode.js.map +1 -1
  374. package/dist/modes/json-mode.test.d.ts +2 -0
  375. package/dist/modes/json-mode.test.d.ts.map +1 -0
  376. package/dist/modes/json-mode.test.js +84 -0
  377. package/dist/modes/json-mode.test.js.map +1 -0
  378. package/dist/modes/subagent-worker-mode.d.ts +3 -0
  379. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  380. package/dist/modes/subagent-worker-mode.js +91 -11
  381. package/dist/modes/subagent-worker-mode.js.map +1 -1
  382. package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
  383. package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
  384. package/dist/modes/subagent-worker-mode.test.js +52 -0
  385. package/dist/modes/subagent-worker-mode.test.js.map +1 -0
  386. package/dist/session.d.ts +29 -0
  387. package/dist/session.d.ts.map +1 -1
  388. package/dist/session.js +41 -0
  389. package/dist/session.js.map +1 -1
  390. package/dist/tools/bash.d.ts +2 -1
  391. package/dist/tools/bash.d.ts.map +1 -1
  392. package/dist/tools/bash.js +55 -16
  393. package/dist/tools/bash.js.map +1 -1
  394. package/dist/tools/goals.d.ts +2 -2
  395. package/dist/tools/grep.d.ts.map +1 -1
  396. package/dist/tools/grep.js +2 -1
  397. package/dist/tools/grep.js.map +1 -1
  398. package/dist/tools/index.d.ts +13 -2
  399. package/dist/tools/index.d.ts.map +1 -1
  400. package/dist/tools/index.js +20 -17
  401. package/dist/tools/index.js.map +1 -1
  402. package/dist/tools/index.test.d.ts +2 -0
  403. package/dist/tools/index.test.d.ts.map +1 -0
  404. package/dist/tools/index.test.js +55 -0
  405. package/dist/tools/index.test.js.map +1 -0
  406. package/dist/tools/read-only-bash.d.ts +3 -0
  407. package/dist/tools/read-only-bash.d.ts.map +1 -1
  408. package/dist/tools/read-only-bash.js +7 -9
  409. package/dist/tools/read-only-bash.js.map +1 -1
  410. package/dist/tools/subagent-control.d.ts.map +1 -1
  411. package/dist/tools/subagent-control.js +6 -2
  412. package/dist/tools/subagent-control.js.map +1 -1
  413. package/dist/tools/subagent-shared.d.ts +6 -0
  414. package/dist/tools/subagent-shared.d.ts.map +1 -1
  415. package/dist/tools/subagent-shared.js +6 -0
  416. package/dist/tools/subagent-shared.js.map +1 -1
  417. package/dist/tools/subagent.d.ts.map +1 -1
  418. package/dist/tools/subagent.js +43 -8
  419. package/dist/tools/subagent.js.map +1 -1
  420. package/dist/tools/subagent.test.js +65 -3
  421. package/dist/tools/subagent.test.js.map +1 -1
  422. package/dist/tools/task-output.d.ts.map +1 -1
  423. package/dist/tools/task-output.js +3 -1
  424. package/dist/tools/task-output.js.map +1 -1
  425. package/dist/tools/tool-search.d.ts +12 -1
  426. package/dist/tools/tool-search.d.ts.map +1 -1
  427. package/dist/tools/tool-search.js +22 -3
  428. package/dist/tools/tool-search.js.map +1 -1
  429. package/dist/tools/tool-search.test.d.ts +2 -0
  430. package/dist/tools/tool-search.test.d.ts.map +1 -0
  431. package/dist/tools/tool-search.test.js +69 -0
  432. package/dist/tools/tool-search.test.js.map +1 -0
  433. package/dist/tools/web-fetch.d.ts +1 -1
  434. package/dist/tools/web-fetch.d.ts.map +1 -1
  435. package/dist/tools/web-fetch.js +5 -2
  436. package/dist/tools/web-fetch.js.map +1 -1
  437. package/dist/types.d.ts +7 -0
  438. package/dist/types.d.ts.map +1 -1
  439. package/dist/ui/App.d.ts.map +1 -1
  440. package/dist/ui/App.js +27 -1
  441. package/dist/ui/App.js.map +1 -1
  442. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  443. package/dist/ui/hooks/useAgentLoop.js +18 -4
  444. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  445. package/dist/utils/github.test.js +3 -3
  446. package/dist/utils/github.test.js.map +1 -1
  447. package/dist/utils/image.d.ts +26 -3
  448. package/dist/utils/image.d.ts.map +1 -1
  449. package/dist/utils/image.js +83 -14
  450. package/dist/utils/image.js.map +1 -1
  451. package/dist/utils/image.test.d.ts +2 -0
  452. package/dist/utils/image.test.d.ts.map +1 -0
  453. package/dist/utils/image.test.js +139 -0
  454. package/dist/utils/image.test.js.map +1 -0
  455. package/package.json +7 -6
@@ -5,6 +5,7 @@ import crypto from "node:crypto";
5
5
  import { environmentSecrets, redactValue, } from "@prestyj/ai";
6
6
  import { log } from "./logger.js";
7
7
  import { encodeCwd } from "./encode-cwd.js";
8
+ import { getUserSessionPrompt } from "./session-preview.js";
8
9
  import { archiveColdSession, archiveSessionPath, cleanupOldSessionTemps, COLD_SESSION_AGE_DAYS, emptyStorageNormalizationMetrics, hydrateSessionEntry, isSessionPath, normalizeSessionEntryForStorage, openSessionReadStream, plainSessionPath, resolveSessionPath, sessionAssetDir, thawSessionArchive, } from "./session-storage.js";
9
10
  export const DISPLAY_ITEM_CUSTOM_KIND = "display_item";
10
11
  export const TURN_METRIC_CUSTOM_KIND = "turn_metric";
@@ -71,12 +72,29 @@ export const AUTOPILOT_MARKER_CUSTOM_KIND = "autopilot_marker";
71
72
  * so the LLM never sees it, anchored by `afterMessageCount` so the host can
72
73
  * interleave it back into the transcript on resume. */
73
74
  export const APP_MARKER_CUSTOM_KIND = "app_transcript_marker";
75
+ /**
76
+ * Run journal — a matched pair of custom entries bracketing every provider run.
77
+ *
78
+ * Same not-on-the-DAG treatment as Nolan turns (`parentId: null`): the model never
79
+ * sees them, and they can't race the message branch's leaf pointer. A
80
+ * `run_started` with no matching `run_finished` is the on-disk signature of a
81
+ * run that died mid-flight — the host surfaces it on load instead of silently
82
+ * resuming work whose tools already half-mutated the repo.
83
+ *
84
+ * `generation` is the `RunLifecycle` generation, so the pairing inherits that
85
+ * class's generation-safety: a stale run cannot close a newer one's journal.
86
+ */
87
+ export const RUN_STARTED_CUSTOM_KIND = "run_started";
88
+ export const RUN_FINISHED_CUSTOM_KIND = "run_finished";
74
89
  function isCompletedItemLike(value) {
75
90
  return (typeof value === "object" &&
76
91
  value !== null &&
77
92
  typeof value.kind === "string" &&
78
93
  typeof value.id === "string");
79
94
  }
95
+ const COMPACTION_COORDINATION_DIR = ".compaction-coordination";
96
+ const CORRUPT_LEASE_STALE_MS = 60_000;
97
+ const LEASE_POLL_MS = 50;
80
98
  // ── Session Manager ────────────────────────────────────────
81
99
  export class SessionManager {
82
100
  static activePathsByRoot = new Map();
@@ -88,6 +106,115 @@ export class SessionManager {
88
106
  constructor(sessionsDir) {
89
107
  this.sessionsDir = path.resolve(sessionsDir);
90
108
  }
109
+ coordinationKey(conversationId) {
110
+ return crypto.createHash("sha256").update(conversationId).digest("hex");
111
+ }
112
+ coordinationRoot() {
113
+ return path.join(this.sessionsDir, COMPACTION_COORDINATION_DIR);
114
+ }
115
+ async leaseOwner(lockPath) {
116
+ try {
117
+ const parsed = JSON.parse(await fs.readFile(path.join(lockPath, "owner.json"), "utf-8"));
118
+ return typeof parsed.token === "string" &&
119
+ typeof parsed.pid === "number" &&
120
+ typeof parsed.createdAt === "string"
121
+ ? parsed
122
+ : null;
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ processIsAlive(pid) {
129
+ try {
130
+ process.kill(pid, 0);
131
+ return true;
132
+ }
133
+ catch (error) {
134
+ return error.code !== "ESRCH";
135
+ }
136
+ }
137
+ async waitForLease(signal) {
138
+ if (signal?.aborted)
139
+ throw new DOMException("Aborted", "AbortError");
140
+ await new Promise((resolve, reject) => {
141
+ const finish = () => {
142
+ signal?.removeEventListener("abort", onAbort);
143
+ resolve();
144
+ };
145
+ const timer = setTimeout(finish, LEASE_POLL_MS);
146
+ const onAbort = () => {
147
+ clearTimeout(timer);
148
+ signal?.removeEventListener("abort", onAbort);
149
+ reject(new DOMException("Aborted", "AbortError"));
150
+ };
151
+ signal?.addEventListener("abort", onAbort, { once: true });
152
+ timer.unref?.();
153
+ });
154
+ }
155
+ /** Serialize compaction work across processes for one logical conversation. */
156
+ async withCompactionLease(conversationId, signal, work) {
157
+ const root = this.coordinationRoot();
158
+ await fs.mkdir(root, { recursive: true });
159
+ const lockPath = path.join(root, `${this.coordinationKey(conversationId)}.lock`);
160
+ const token = crypto.randomUUID();
161
+ while (true) {
162
+ try {
163
+ await fs.mkdir(lockPath);
164
+ const owner = {
165
+ token,
166
+ pid: process.pid,
167
+ createdAt: new Date().toISOString(),
168
+ };
169
+ await fs.writeFile(path.join(lockPath, "owner.json"), JSON.stringify(owner), "utf-8");
170
+ break;
171
+ }
172
+ catch (error) {
173
+ if (error.code !== "EEXIST")
174
+ throw error;
175
+ const owner = await this.leaseOwner(lockPath);
176
+ const stat = await fs.stat(lockPath).catch(() => null);
177
+ const corruptAndOld = !owner && stat !== null && Date.now() - stat.mtimeMs > CORRUPT_LEASE_STALE_MS;
178
+ const deadOwner = owner !== null && !this.processIsAlive(owner.pid);
179
+ if (deadOwner || corruptAndOld) {
180
+ await fs.rm(lockPath, { recursive: true, force: true });
181
+ continue;
182
+ }
183
+ await this.waitForLease(signal);
184
+ }
185
+ }
186
+ try {
187
+ return await work();
188
+ }
189
+ finally {
190
+ const owner = await this.leaseOwner(lockPath);
191
+ if (owner?.token === token)
192
+ await fs.rm(lockPath, { recursive: true, force: true });
193
+ }
194
+ }
195
+ async readCompactionAttemptState(conversationId) {
196
+ const statePath = path.join(this.coordinationRoot(), `${this.coordinationKey(conversationId)}.state.json`);
197
+ try {
198
+ const state = JSON.parse(await fs.readFile(statePath, "utf-8"));
199
+ return typeof state.fingerprint === "string" &&
200
+ typeof state.policyKey === "string" &&
201
+ ["success", "failed", "noop"].includes(state.outcome) &&
202
+ typeof state.updatedAt === "string"
203
+ ? state
204
+ : null;
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ }
210
+ async writeCompactionAttemptState(conversationId, state) {
211
+ const root = this.coordinationRoot();
212
+ await fs.mkdir(root, { recursive: true });
213
+ const statePath = path.join(root, `${this.coordinationKey(conversationId)}.state.json`);
214
+ const temporaryPath = `${statePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
215
+ await fs.writeFile(temporaryPath, JSON.stringify(state), "utf-8");
216
+ await fs.rename(temporaryPath, statePath);
217
+ }
91
218
  /**
92
219
  * Session persistence must never crash a live session. Disk-full (ENOSPC),
93
220
  * permission, or quota errors during transcript writes are reported once
@@ -124,6 +251,12 @@ export class SessionManager {
124
251
  version: 2,
125
252
  id,
126
253
  conversationId: options?.conversationId ?? id,
254
+ generation: options?.generation ?? 0,
255
+ ...(options?.parentSessionId ? { parentSessionId: options.parentSessionId } : {}),
256
+ ...(options?.sourceFingerprint ? { sourceFingerprint: options.sourceFingerprint } : {}),
257
+ ...(typeof options?.retainedMessageCount === "number"
258
+ ? { retainedMessageCount: options.retainedMessageCount }
259
+ : {}),
127
260
  ...(safePreview ? { preview: safePreview } : {}),
128
261
  timestamp,
129
262
  cwd,
@@ -163,7 +296,68 @@ export class SessionManager {
163
296
  async load(sessionPath) {
164
297
  // Resuming is a write operation, so transparently thaw a gzip archive and
165
298
  // return the effective plain path every future append must use.
166
- const effectivePath = await thawSessionArchive(sessionPath);
299
+ const canonicalPath = (await this.resolveCanonicalSession(sessionPath)) ?? sessionPath;
300
+ const effectivePath = await thawSessionArchive(canonicalPath);
301
+ return this.loadPhysicalCheckpoint(effectivePath);
302
+ }
303
+ /**
304
+ * Load the contiguous checkpoint ancestry ending at the canonical newest file.
305
+ *
306
+ * The returned order is oldest → newest. A missing, corrupt, cyclic, or
307
+ * cross-conversation parent stops traversal at the oldest readable checkpoint,
308
+ * allowing display callers to retain that checkpoint's compaction summary as a
309
+ * fallback. Parent archives are read in place rather than thawed because this
310
+ * API is for history reconstruction, not resuming writes.
311
+ */
312
+ async loadCheckpointChain(sessionPath) {
313
+ const requestedSummary = await this.readSessionSummary(sessionPath);
314
+ // A resumed session can compact under a different cwd than its parent (for
315
+ // example, a remote ACP client reconnecting from another workspace). The
316
+ // physical ancestry is therefore machine-wide, not confined to the newest
317
+ // checkpoint's encoded-cwd directory.
318
+ const directories = await this.storageDirectories();
319
+ const candidates = (await Promise.all(directories.map((directory) => this.sessionCandidates(directory)))).flat();
320
+ const summaries = (await Promise.all(candidates.map((candidate) => this.readSessionSummary(candidate)))).filter((summary) => summary !== null);
321
+ const requestedIdentity = requestedSummary?.conversationId ??
322
+ summaries.find((summary) => summary.id === sessionPath)?.conversationId;
323
+ const newestSummary = requestedIdentity
324
+ ? summaries
325
+ .filter((summary) => summary.conversationId === requestedIdentity)
326
+ .sort((left, right) => SessionManager.compareCheckpoints(right, left))[0]
327
+ : undefined;
328
+ const newestPath = newestSummary?.path ?? sessionPath;
329
+ // Resuming the newest checkpoint is a write operation, so thaw only that
330
+ // generation. Historical parents stay archived and read-only.
331
+ const newest = await this.loadPhysicalCheckpoint(await thawSessionArchive(newestPath));
332
+ const newestConversationId = newest.header.conversationId ?? newest.header.id;
333
+ const byPhysicalId = new Map(summaries.map((summary) => [summary.id, summary.path]));
334
+ const newestResolvedPath = path.resolve(newest.path);
335
+ const visited = new Set([newest.header.id]);
336
+ const chain = [newest];
337
+ let current = newest;
338
+ while (current.header.parentSessionId) {
339
+ const parentId = current.header.parentSessionId;
340
+ if (visited.has(parentId))
341
+ break;
342
+ const parentPath = byPhysicalId.get(parentId);
343
+ if (!parentPath || path.resolve(parentPath) === newestResolvedPath)
344
+ break;
345
+ try {
346
+ const parent = await this.loadPhysicalCheckpoint(parentPath, true);
347
+ if ((parent.header.conversationId ?? parent.header.id) !== newestConversationId)
348
+ break;
349
+ visited.add(parentId);
350
+ chain.unshift(parent);
351
+ current = parent;
352
+ }
353
+ catch {
354
+ break;
355
+ }
356
+ }
357
+ return chain;
358
+ }
359
+ async loadPhysicalCheckpoint(sessionPath, rejectMalformedLines = false) {
360
+ const effectivePath = await resolveSessionPath(sessionPath);
167
361
  const { stream } = await openSessionReadStream(effectivePath);
168
362
  let header = null;
169
363
  const entries = [];
@@ -184,6 +378,7 @@ export class SessionManager {
184
378
  version: 2,
185
379
  id: v1.id,
186
380
  conversationId: v1.id,
381
+ generation: 0,
187
382
  timestamp: v1.timestamp,
188
383
  cwd: v1.cwd,
189
384
  provider: v1.provider,
@@ -200,7 +395,9 @@ export class SessionManager {
200
395
  }
201
396
  entries.push(await hydrateSessionEntry(entry, effectivePath));
202
397
  }
203
- catch {
398
+ catch (error) {
399
+ if (rejectMalformedLines)
400
+ throw error;
204
401
  // Skip malformed JSON lines — cold migration preserves their raw bytes
205
402
  // so a future recovery tool still has a chance to repair them.
206
403
  }
@@ -218,6 +415,7 @@ export class SessionManager {
218
415
  let first = null;
219
416
  let messageCount = 0;
220
417
  let lastActivity = null;
418
+ let preview;
221
419
  for await (const line of rl) {
222
420
  if (!line)
223
421
  continue;
@@ -227,11 +425,24 @@ export class SessionManager {
227
425
  if (parsed.type !== "session")
228
426
  break;
229
427
  first = parsed;
428
+ // v2 headers usually carry the preview already; when they do,
429
+ // nothing below has to look for one.
430
+ preview = parsed.preview?.trim() || undefined;
230
431
  }
231
432
  else if (parsed.type === "message") {
232
433
  messageCount += 1;
233
434
  if (parsed.timestamp)
234
435
  lastActivity = parsed.timestamp;
436
+ // Recover a title for the sessions whose header predates `preview`.
437
+ // Free here: this pass already reads every line. `getUserSessionPrompt`
438
+ // rejects compaction summaries and autopilot/status injections, so a
439
+ // session is titled by what its user actually asked.
440
+ if (!preview && parsed.message?.role === "user") {
441
+ preview =
442
+ getUserSessionPrompt(parsed.message.content, parsed.message.provenance)
443
+ ?.replace(/\s+/g, " ")
444
+ .trim() || undefined;
445
+ }
235
446
  }
236
447
  }
237
448
  catch {
@@ -245,11 +456,13 @@ export class SessionManager {
245
456
  conversationId: first.version === 2
246
457
  ? (first.conversationId ?? first.id)
247
458
  : first.id,
459
+ generation: first.version === 2 ? (first.generation ?? 0) : 0,
248
460
  path: resolvedPath,
249
461
  timestamp: first.timestamp,
250
462
  lastActivity: lastActivity ?? first.timestamp,
251
463
  cwd: first.cwd,
252
464
  messageCount,
465
+ ...(preview ? { preview: preview.slice(0, 100) } : {}),
253
466
  };
254
467
  }
255
468
  catch {
@@ -266,9 +479,92 @@ export class SessionManager {
266
479
  }
267
480
  return files.filter(isSessionPath).map((file) => path.join(directory, file));
268
481
  }
269
- async list(cwd) {
270
- const candidates = await this.sessionCandidates(this.dirForCwd(cwd));
271
- const summaries = await Promise.all(candidates.map((file) => this.readSessionInfo(file)));
482
+ /**
483
+ * Read just enough of a session file to summarize it.
484
+ *
485
+ * Stops at the first user prompt (or the first message, when the header
486
+ * already carries a preview) instead of parsing the whole transcript — this
487
+ * is what makes listing every session on the machine cheap. Files whose
488
+ * first user message is far down (long tool runs before the user speaks)
489
+ * are capped rather than allowed to stall the list.
490
+ */
491
+ async readSessionSummary(candidatePath) {
492
+ try {
493
+ const resolvedPath = await resolveSessionPath(candidatePath);
494
+ const { stream } = await openSessionReadStream(resolvedPath);
495
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
496
+ let first = null;
497
+ let preview;
498
+ let hasMessages = false;
499
+ let lines = 0;
500
+ const MAX_SCAN_LINES = 500;
501
+ try {
502
+ for await (const line of rl) {
503
+ if (!line)
504
+ continue;
505
+ if (++lines > MAX_SCAN_LINES)
506
+ break;
507
+ try {
508
+ const parsed = JSON.parse(line);
509
+ if (!first) {
510
+ if (parsed.type !== "session")
511
+ break;
512
+ first = parsed;
513
+ preview = parsed.preview?.trim() || undefined;
514
+ }
515
+ else if (parsed.type === "message") {
516
+ hasMessages = true;
517
+ // Header preview + any message: done. Otherwise keep scanning for
518
+ // the first user-authored prompt to title by.
519
+ if (preview)
520
+ break;
521
+ if (parsed.message?.role === "user") {
522
+ preview =
523
+ getUserSessionPrompt(parsed.message.content, parsed.message.provenance)
524
+ ?.replace(/\s+/g, " ")
525
+ .trim() || undefined;
526
+ if (preview)
527
+ break;
528
+ }
529
+ }
530
+ }
531
+ catch {
532
+ // Skip malformed lines while retaining readable entries around them.
533
+ }
534
+ }
535
+ }
536
+ finally {
537
+ rl.close();
538
+ stream.destroy();
539
+ }
540
+ if (!first || first.type !== "session")
541
+ return null;
542
+ const stat = await fs.stat(resolvedPath);
543
+ return {
544
+ id: first.id,
545
+ conversationId: first.version === 2
546
+ ? (first.conversationId ?? first.id)
547
+ : first.id,
548
+ generation: first.version === 2 ? (first.generation ?? 0) : 0,
549
+ path: resolvedPath,
550
+ timestamp: first.timestamp,
551
+ lastActivity: stat.mtime.toISOString(),
552
+ cwd: first.cwd,
553
+ hasMessages,
554
+ ...(preview ? { preview: preview.slice(0, 100) } : {}),
555
+ };
556
+ }
557
+ catch {
558
+ return null;
559
+ }
560
+ }
561
+ /**
562
+ * Keep the newest file per conversation and sort newest first.
563
+ *
564
+ * A conversation can span several files (compaction forks a fresh one), so
565
+ * without this collapse a resumed thread appears once per checkpoint.
566
+ */
567
+ static dedupeByConversation(summaries) {
272
568
  const byConversation = new Map();
273
569
  const seenResolvedPaths = new Set();
274
570
  for (const summary of summaries) {
@@ -276,25 +572,109 @@ export class SessionManager {
276
572
  continue;
277
573
  seenResolvedPaths.add(summary.path);
278
574
  const current = byConversation.get(summary.conversationId);
279
- if (!current || summary.lastActivity > current.lastActivity) {
280
- const { conversationId: _conversationId, ...info } = summary;
281
- byConversation.set(summary.conversationId, info);
575
+ if (!current || SessionManager.compareCheckpoints(summary, current) > 0) {
576
+ byConversation.set(summary.conversationId, summary);
282
577
  }
283
578
  }
284
- return [...byConversation.values()].sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
579
+ return [...byConversation.values()]
580
+ .sort((a, b) => b.lastActivity.localeCompare(a.lastActivity) ||
581
+ b.timestamp.localeCompare(a.timestamp) ||
582
+ b.path.localeCompare(a.path))
583
+ .map(({ conversationId: _conversationId, generation: _generation, ...info }) => info);
584
+ }
585
+ static compareCheckpoints(left, right) {
586
+ return ((left.generation ?? 0) - (right.generation ?? 0) ||
587
+ left.lastActivity.localeCompare(right.lastActivity) ||
588
+ left.timestamp.localeCompare(right.timestamp) ||
589
+ left.path.localeCompare(right.path));
590
+ }
591
+ async list(cwd) {
592
+ return this.summarize(await this.sessionCandidates(this.dirForCwd(cwd)));
593
+ }
594
+ /**
595
+ * One project's sessions, newest first, using the early-exit summary read.
596
+ *
597
+ * For callers that render a list rather than exact message counts — on a
598
+ * project with hundreds of sessions this is the difference between instant
599
+ * and a noticeable stall.
600
+ */
601
+ async listSummaries(cwd) {
602
+ const candidates = await this.sessionCandidates(this.dirForCwd(cwd));
603
+ const summaries = await Promise.all(candidates.map((file) => this.readSessionSummary(file)));
604
+ return SessionManager.dedupeByConversation(summaries);
605
+ }
606
+ /**
607
+ * Every session on this machine, across every project, newest first.
608
+ *
609
+ * A remote client is not browsing one checkout the way the TUI is — it is
610
+ * asking "what have I been working on?", and the answer spans projects. Each
611
+ * entry carries its own `cwd`, so the caller can group by project. Uses the
612
+ * early-exit summary read, because "every session on the machine" is exactly
613
+ * where a full parse of each file becomes a multi-second stall.
614
+ */
615
+ async listAllSummaries() {
616
+ const directories = await this.storageDirectories();
617
+ const candidates = await Promise.all(directories.map((directory) => this.sessionCandidates(directory)));
618
+ const summaries = await Promise.all(candidates.flat().map((file) => this.readSessionSummary(file)));
619
+ return SessionManager.dedupeByConversation(summaries);
620
+ }
621
+ /**
622
+ * Summarize session files, keeping the newest file per conversation.
623
+ *
624
+ * A conversation can span several files (compaction forks a fresh one), so
625
+ * without this collapse a resumed thread appears once per checkpoint.
626
+ */
627
+ async summarize(candidates) {
628
+ const summaries = await Promise.all(candidates.map((file) => this.readSessionInfo(file)));
629
+ return SessionManager.dedupeByConversation(summaries);
285
630
  }
286
631
  async getMostRecent(cwd) {
287
632
  const sessions = await this.list(cwd);
288
633
  return sessions.find((session) => session.messageCount > 0)?.path ?? null;
289
634
  }
635
+ /** Resolve an id, conversation id, or stale physical path to the newest checkpoint. */
636
+ async resolveCanonicalSession(requested, cwd) {
637
+ const looksLikePath = path.isAbsolute(requested) || requested.includes(path.sep) || isSessionPath(requested);
638
+ let candidates;
639
+ let requestedSummary = null;
640
+ if (looksLikePath) {
641
+ requestedSummary = await this.readSessionSummary(requested);
642
+ if (!requestedSummary)
643
+ return null;
644
+ candidates = await this.sessionCandidates(path.dirname(requestedSummary.path));
645
+ }
646
+ else if (cwd) {
647
+ candidates = await this.sessionCandidates(this.dirForCwd(cwd));
648
+ }
649
+ else {
650
+ const directories = await this.storageDirectories();
651
+ candidates = (await Promise.all(directories.map((dir) => this.sessionCandidates(dir)))).flat();
652
+ }
653
+ const summaries = (await Promise.all(candidates.map((candidate) => this.readSessionSummary(candidate)))).filter((summary) => summary !== null);
654
+ if (requestedSummary && !summaries.some((summary) => summary.path === requestedSummary?.path)) {
655
+ summaries.push(requestedSummary);
656
+ }
657
+ const identityMatches = requestedSummary
658
+ ? [requestedSummary]
659
+ : summaries.filter((summary) => summary.id === requested || summary.conversationId === requested);
660
+ if (identityMatches.length === 0)
661
+ return null;
662
+ const requestedIdentity = identityMatches.sort((a, b) => SessionManager.compareCheckpoints(b, a))[0].conversationId;
663
+ const checkpoints = summaries.filter((summary) => summary.conversationId === requestedIdentity);
664
+ return (checkpoints.sort((a, b) => SessionManager.compareCheckpoints(b, a))[0]?.path ??
665
+ identityMatches[0].path);
666
+ }
290
667
  async findById(cwd, sessionId) {
291
- const candidates = await this.sessionCandidates(this.dirForCwd(cwd));
292
- for (const candidate of candidates) {
293
- const summary = await this.readSessionInfo(candidate);
294
- if (summary?.id === sessionId || summary?.conversationId === sessionId)
295
- return summary.path;
668
+ return this.resolveCanonicalSession(sessionId, cwd);
669
+ }
670
+ /** Locate and canonicalize a session identity across every project directory. */
671
+ async findAnyById(sessionId, cwd) {
672
+ if (cwd) {
673
+ const local = await this.resolveCanonicalSession(sessionId, cwd);
674
+ if (local)
675
+ return local;
296
676
  }
297
- return null;
677
+ return this.resolveCanonicalSession(sessionId);
298
678
  }
299
679
  async storageDirectories() {
300
680
  let entries;
@@ -305,7 +685,7 @@ export class SessionManager {
305
685
  return [];
306
686
  }
307
687
  return entries
308
- .filter((entry) => entry.isDirectory())
688
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
309
689
  .map((entry) => path.join(this.sessionsDir, entry.name));
310
690
  }
311
691
  async logicalSessionBases(directory) {
@@ -471,6 +851,84 @@ export class SessionManager {
471
851
  };
472
852
  await this.appendEntry(sessionPath, entry);
473
853
  }
854
+ /** Open the run journal for one `RunLifecycle` generation. */
855
+ async appendRunStarted(sessionPath, payload) {
856
+ await this.appendEntry(sessionPath, {
857
+ type: "custom",
858
+ kind: RUN_STARTED_CUSTOM_KIND,
859
+ id: crypto.randomUUID(),
860
+ parentId: null,
861
+ timestamp: new Date().toISOString(),
862
+ data: payload,
863
+ });
864
+ }
865
+ /** Close the run journal for one generation. Its absence marks a crashed run. */
866
+ async appendRunFinished(sessionPath, payload) {
867
+ await this.appendEntry(sessionPath, {
868
+ type: "custom",
869
+ kind: RUN_FINISHED_CUSTOM_KIND,
870
+ id: crypto.randomUUID(),
871
+ parentId: null,
872
+ timestamp: new Date().toISOString(),
873
+ data: payload,
874
+ });
875
+ }
876
+ /**
877
+ * Reconstruct the run journal in file order, pairing each `run_started` with
878
+ * the `run_finished` carrying the same generation.
879
+ *
880
+ * Generations are NOT unique across a session file. `RunLifecycle` counts
881
+ * from zero per instance, and a resumed session builds a fresh one — so the
882
+ * first run after every app restart is generation 1 again. Pairing therefore
883
+ * tracks only the runs still OPEN: a `run_finished` closes its generation and
884
+ * releases the number, and a later `run_started` reusing it opens a new run.
885
+ *
886
+ * Without that release, a crash in a resumed session was invisible: the
887
+ * reused `run_started` looked like a duplicate and was dropped, which is
888
+ * exactly the case this journal exists to catch.
889
+ *
890
+ * A repeat `run_started` for a generation that is still open IS ignored, so a
891
+ * truncated or replayed log reports one unfinished run — never a phantom pile.
892
+ */
893
+ getRunJournal(entries) {
894
+ const runs = [];
895
+ const openByGeneration = new Map();
896
+ for (const entry of entries) {
897
+ if (entry.type !== "custom")
898
+ continue;
899
+ if (entry.kind === RUN_STARTED_CUSTOM_KIND) {
900
+ const p = entry.data;
901
+ if (p?.version !== 1 || typeof p.generation !== "number")
902
+ continue;
903
+ if (openByGeneration.has(p.generation))
904
+ continue;
905
+ const run = {
906
+ generation: p.generation,
907
+ startedAt: typeof p.startedAt === "string" ? p.startedAt : entry.timestamp,
908
+ afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
909
+ };
910
+ openByGeneration.set(p.generation, run);
911
+ runs.push(run);
912
+ continue;
913
+ }
914
+ if (entry.kind === RUN_FINISHED_CUSTOM_KIND) {
915
+ const p = entry.data;
916
+ if (p?.version !== 1 || typeof p.generation !== "number")
917
+ continue;
918
+ const run = openByGeneration.get(p.generation);
919
+ if (!run || !p.outcome)
920
+ continue;
921
+ run.outcome = p.outcome;
922
+ // Closed — the number is free for the next app run to reuse.
923
+ openByGeneration.delete(p.generation);
924
+ }
925
+ }
926
+ return runs;
927
+ }
928
+ /** Runs that opened the journal but never closed it — i.e. crashed mid-flight. */
929
+ getUnfinishedRuns(entries) {
930
+ return this.getRunJournal(entries).filter((run) => run.outcome === undefined);
931
+ }
474
932
  async updateLeaf(sessionPath, leafId) {
475
933
  try {
476
934
  const writablePath = await thawSessionArchive(sessionPath);
@@ -540,11 +998,36 @@ export class SessionManager {
540
998
  return isCompletedItemLike(item) ? [item] : [];
541
999
  });
542
1000
  }
1001
+ /**
1002
+ * Walk entries in file order, tracking how many branch (non-system) messages
1003
+ * have been written so far, and hand each custom entry that count. This is
1004
+ * the independent position estimate behind {@link RecordedPosition}.
1005
+ */
1006
+ mapCustomEntriesInFileOrder(entries, leafId, project) {
1007
+ // Only branch messages count — an off-branch fork's entries are not part of
1008
+ // the restored transcript the anchors are measured against.
1009
+ const onBranch = leafId
1010
+ ? new Set(this.getBranch(entries, leafId).map((e) => e.id))
1011
+ : new Set(entries.map((e) => e.id));
1012
+ const out = [];
1013
+ let messagesSoFar = 0;
1014
+ for (const entry of entries) {
1015
+ if (entry.type === "message") {
1016
+ if (onBranch.has(entry.id) && entry.message.role !== "system")
1017
+ messagesSoFar++;
1018
+ continue;
1019
+ }
1020
+ if (entry.type !== "custom")
1021
+ continue;
1022
+ out.push(...project(entry, messagesSoFar));
1023
+ }
1024
+ return out;
1025
+ }
543
1026
  /** Read all persisted Nolan turns in file order. Returns them regardless of
544
1027
  * branch (Nolan turns are not chained into the DAG), validated + normalized. */
545
- getNolanTurns(entries) {
546
- return entries.flatMap((entry) => {
547
- if (entry.type !== "custom" || entry.kind !== NOLAN_TURN_CUSTOM_KIND)
1028
+ getNolanTurns(entries, leafId) {
1029
+ return this.mapCustomEntriesInFileOrder(entries, leafId, (entry, recordedAfterMessageCount) => {
1030
+ if (entry.kind !== NOLAN_TURN_CUSTOM_KIND)
548
1031
  return [];
549
1032
  const p = entry.data;
550
1033
  if (p?.version === 1 && typeof p.question === "string" && typeof p.reply === "string") {
@@ -554,6 +1037,7 @@ export class SessionManager {
554
1037
  question: p.question,
555
1038
  reply: p.reply,
556
1039
  afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
1040
+ recordedAfterMessageCount,
557
1041
  },
558
1042
  ];
559
1043
  }
@@ -562,9 +1046,9 @@ export class SessionManager {
562
1046
  }
563
1047
  /** Read all persisted app transcript markers in file order, validated +
564
1048
  * normalized (same not-on-the-DAG treatment as Nolan turns). */
565
- getAppMarkers(entries) {
566
- return entries.flatMap((entry) => {
567
- if (entry.type !== "custom" || entry.kind !== APP_MARKER_CUSTOM_KIND)
1049
+ getAppMarkers(entries, leafId) {
1050
+ return this.mapCustomEntriesInFileOrder(entries, leafId, (entry, recordedAfterMessageCount) => {
1051
+ if (entry.kind !== APP_MARKER_CUSTOM_KIND)
568
1052
  return [];
569
1053
  const p = entry.data;
570
1054
  const kind = p?.kind;
@@ -574,13 +1058,17 @@ export class SessionManager {
574
1058
  kind === "error" ||
575
1059
  kind === "user_hint" ||
576
1060
  kind === "compaction" ||
577
- kind === "agent_handoff")) {
1061
+ kind === "agent_handoff" ||
1062
+ kind === "model_switch" ||
1063
+ kind === "import" ||
1064
+ kind === "interrupted_run")) {
578
1065
  return [
579
1066
  {
580
1067
  version: 1,
581
1068
  kind,
582
1069
  afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
583
1070
  data: typeof p.data === "object" && p.data !== null ? p.data : {},
1071
+ recordedAfterMessageCount,
584
1072
  },
585
1073
  ];
586
1074
  }
@@ -598,9 +1086,9 @@ export class SessionManager {
598
1086
  }
599
1087
  /** Read all persisted autopilot markers in file order, validated + normalized
600
1088
  * (same not-on-the-DAG treatment as Nolan turns). */
601
- getAutopilotMarkers(entries) {
602
- return entries.flatMap((entry) => {
603
- if (entry.type !== "custom" || entry.kind !== AUTOPILOT_MARKER_CUSTOM_KIND)
1089
+ getAutopilotMarkers(entries, leafId) {
1090
+ return this.mapCustomEntriesInFileOrder(entries, leafId, (entry, recordedAfterMessageCount) => {
1091
+ if (entry.kind !== AUTOPILOT_MARKER_CUSTOM_KIND)
604
1092
  return [];
605
1093
  const p = entry.data;
606
1094
  const phase = p?.phase;
@@ -617,6 +1105,7 @@ export class SessionManager {
617
1105
  ...(typeof p.reason === "string" ? { reason: p.reason } : {}),
618
1106
  ...(typeof p.body === "string" ? { body: p.body } : {}),
619
1107
  afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
1108
+ recordedAfterMessageCount,
620
1109
  },
621
1110
  ];
622
1111
  }