@prestyj/cli 5.10.0 → 5.12.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 (703) hide show
  1. package/README.md +3 -4
  2. package/assets/skills/compliance-guard/SKILL.md +251 -0
  3. package/assets/skills/compliance-guard/references/artifacts.md +186 -0
  4. package/assets/skills/compliance-guard/references/eu-uk.md +151 -0
  5. package/assets/skills/compliance-guard/references/exposure-triage.md +161 -0
  6. package/assets/skills/compliance-guard/references/lawsuit-vectors.md +141 -0
  7. package/assets/skills/compliance-guard/references/provenance.md +42 -0
  8. package/assets/skills/compliance-guard/references/sector-gates.md +173 -0
  9. package/assets/skills/compliance-guard/references/security-baseline.md +130 -0
  10. package/assets/skills/compliance-guard/references/trigger-map.md +163 -0
  11. package/assets/skills/compliance-guard/references/us.md +164 -0
  12. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  13. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  14. package/dist/app-sidecar-auth-broadcast.test.js +348 -0
  15. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  16. package/dist/app-sidecar-sessions.d.ts +5 -2
  17. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  18. package/dist/app-sidecar-sessions.js +34 -8
  19. package/dist/app-sidecar-sessions.js.map +1 -1
  20. package/dist/app-sidecar-sessions.test.js +96 -1
  21. package/dist/app-sidecar-sessions.test.js.map +1 -1
  22. package/dist/app-sidecar.js +616 -173
  23. package/dist/app-sidecar.js.map +1 -1
  24. package/dist/chat-agents/jiwa.test.js +1 -1
  25. package/dist/cli/auth.d.ts.map +1 -1
  26. package/dist/cli/auth.js +33 -36
  27. package/dist/cli/auth.js.map +1 -1
  28. package/dist/cli/command-routing.d.ts +1 -1
  29. package/dist/cli/command-routing.d.ts.map +1 -1
  30. package/dist/cli/command-routing.js +1 -0
  31. package/dist/cli/command-routing.js.map +1 -1
  32. package/dist/cli/command-routing.test.js +1 -0
  33. package/dist/cli/command-routing.test.js.map +1 -1
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +179 -48
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config.d.ts +18 -5
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +45 -76
  40. package/dist/config.js.map +1 -1
  41. package/dist/config.test.js +30 -13
  42. package/dist/config.test.js.map +1 -1
  43. package/dist/core/agent-notifications.d.ts +62 -0
  44. package/dist/core/agent-notifications.d.ts.map +1 -0
  45. package/dist/core/agent-notifications.js +92 -0
  46. package/dist/core/agent-notifications.js.map +1 -0
  47. package/dist/core/agent-notifications.test.d.ts +2 -0
  48. package/dist/core/agent-notifications.test.d.ts.map +1 -0
  49. package/dist/core/agent-notifications.test.js +97 -0
  50. package/dist/core/agent-notifications.test.js.map +1 -0
  51. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  52. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  53. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  54. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  55. package/dist/core/agent-session-compaction.test.js +323 -6
  56. package/dist/core/agent-session-compaction.test.js.map +1 -1
  57. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  58. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  59. package/dist/core/agent-session-crash-durability.test.js +102 -0
  60. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  61. package/dist/core/agent-session-marker-anchors.test.js +181 -2
  62. package/dist/core/agent-session-marker-anchors.test.js.map +1 -1
  63. package/dist/core/agent-session-model-switch.test.d.ts +2 -0
  64. package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  65. package/dist/core/agent-session-model-switch.test.js +161 -0
  66. package/dist/core/agent-session-model-switch.test.js.map +1 -0
  67. package/dist/core/agent-session-notifications.test.d.ts +2 -0
  68. package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
  69. package/dist/core/agent-session-notifications.test.js +114 -0
  70. package/dist/core/agent-session-notifications.test.js.map +1 -0
  71. package/dist/core/agent-session-process-gate.test.d.ts +2 -0
  72. package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
  73. package/dist/core/agent-session-process-gate.test.js +98 -0
  74. package/dist/core/agent-session-process-gate.test.js.map +1 -0
  75. package/dist/core/agent-session-queue.test.js +65 -0
  76. package/dist/core/agent-session-queue.test.js.map +1 -1
  77. package/dist/core/agent-session-review-coverage.test.js +80 -23
  78. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  79. package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
  80. package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
  81. package/dist/core/agent-session-slash-restore.test.js +121 -0
  82. package/dist/core/agent-session-slash-restore.test.js.map +1 -0
  83. package/dist/core/agent-session-tool-tiers.test.d.ts +2 -0
  84. package/dist/core/agent-session-tool-tiers.test.d.ts.map +1 -0
  85. package/dist/core/agent-session-tool-tiers.test.js +138 -0
  86. package/dist/core/agent-session-tool-tiers.test.js.map +1 -0
  87. package/dist/core/agent-session.d.ts +257 -13
  88. package/dist/core/agent-session.d.ts.map +1 -1
  89. package/dist/core/agent-session.js +1118 -241
  90. package/dist/core/agent-session.js.map +1 -1
  91. package/dist/core/agents.d.ts +29 -1
  92. package/dist/core/agents.d.ts.map +1 -1
  93. package/dist/core/agents.js +47 -97
  94. package/dist/core/agents.js.map +1 -1
  95. package/dist/core/agents.test.js +87 -7
  96. package/dist/core/agents.test.js.map +1 -1
  97. package/dist/core/api-benchmark.d.ts +3 -3
  98. package/dist/core/api-benchmark.js +7 -7
  99. package/dist/core/auth-providers.d.ts +37 -1
  100. package/dist/core/auth-providers.d.ts.map +1 -1
  101. package/dist/core/auth-providers.js +83 -3
  102. package/dist/core/auth-providers.js.map +1 -1
  103. package/dist/core/auto-update.test.js +1 -1
  104. package/dist/core/autopilot-gate.d.ts +2 -2
  105. package/dist/core/autopilot-gate.js +1 -1
  106. package/dist/core/bash-spawn-benchmark.js +6 -6
  107. package/dist/core/bundled-agents.d.ts +3 -0
  108. package/dist/core/bundled-agents.d.ts.map +1 -0
  109. package/dist/core/bundled-agents.js +252 -0
  110. package/dist/core/bundled-agents.js.map +1 -0
  111. package/dist/core/cache-warm-benchmark.d.ts +2 -2
  112. package/dist/core/cache-warm-benchmark.js +5 -5
  113. package/dist/core/code-retrieval-chunkers.d.ts +27 -0
  114. package/dist/core/code-retrieval-chunkers.d.ts.map +1 -0
  115. package/dist/core/code-retrieval-chunkers.js +264 -0
  116. package/dist/core/code-retrieval-chunkers.js.map +1 -0
  117. package/dist/core/code-retrieval-chunkers.test.d.ts +2 -0
  118. package/dist/core/code-retrieval-chunkers.test.d.ts.map +1 -0
  119. package/dist/core/code-retrieval-chunkers.test.js +203 -0
  120. package/dist/core/code-retrieval-chunkers.test.js.map +1 -0
  121. package/dist/core/code-retrieval.d.ts +6 -0
  122. package/dist/core/code-retrieval.d.ts.map +1 -1
  123. package/dist/core/code-retrieval.js +38 -1
  124. package/dist/core/code-retrieval.js.map +1 -1
  125. package/dist/core/compaction/active-context.test.js +21 -0
  126. package/dist/core/compaction/active-context.test.js.map +1 -1
  127. package/dist/core/compaction/compactor.d.ts +108 -8
  128. package/dist/core/compaction/compactor.d.ts.map +1 -1
  129. package/dist/core/compaction/compactor.js +423 -119
  130. package/dist/core/compaction/compactor.js.map +1 -1
  131. package/dist/core/compaction/compactor.test.js +360 -8
  132. package/dist/core/compaction/compactor.test.js.map +1 -1
  133. package/dist/core/compaction/policy.d.ts +17 -0
  134. package/dist/core/compaction/policy.d.ts.map +1 -0
  135. package/dist/core/compaction/policy.js +22 -0
  136. package/dist/core/compaction/policy.js.map +1 -0
  137. package/dist/core/compaction/policy.test.d.ts +2 -0
  138. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  139. package/dist/core/compaction/policy.test.js +35 -0
  140. package/dist/core/compaction/policy.test.js.map +1 -0
  141. package/dist/core/compaction/query-aware-selector.d.ts +33 -0
  142. package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
  143. package/dist/core/compaction/query-aware-selector.js +202 -0
  144. package/dist/core/compaction/query-aware-selector.js.map +1 -0
  145. package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
  146. package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
  147. package/dist/core/compaction/query-aware-selector.test.js +96 -0
  148. package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
  149. package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
  150. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
  151. package/dist/core/compaction/tool-result-pruner.js +98 -26
  152. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  153. package/dist/core/compaction/tool-result-pruner.test.js +178 -6
  154. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
  155. package/dist/core/encode-cwd.test.js +1 -1
  156. package/dist/core/event-bus.d.ts +23 -0
  157. package/dist/core/event-bus.d.ts.map +1 -1
  158. package/dist/core/event-bus.js +10 -0
  159. package/dist/core/event-bus.js.map +1 -1
  160. package/dist/core/extensions/loader.d.ts.map +1 -1
  161. package/dist/core/extensions/loader.js +28 -8
  162. package/dist/core/extensions/loader.js.map +1 -1
  163. package/dist/core/extensions/plugin-bundles.d.ts +23 -0
  164. package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
  165. package/dist/core/extensions/plugin-bundles.js +228 -0
  166. package/dist/core/extensions/plugin-bundles.js.map +1 -0
  167. package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
  168. package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
  169. package/dist/core/extensions/plugin-bundles.test.js +110 -0
  170. package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
  171. package/dist/core/fast-apply-benchmark.d.ts +2 -2
  172. package/dist/core/fast-apply-benchmark.js +6 -6
  173. package/dist/core/foreign-session-import.d.ts +120 -0
  174. package/dist/core/foreign-session-import.d.ts.map +1 -0
  175. package/dist/core/foreign-session-import.js +598 -0
  176. package/dist/core/foreign-session-import.js.map +1 -0
  177. package/dist/core/foreign-session-import.test.d.ts +2 -0
  178. package/dist/core/foreign-session-import.test.d.ts.map +1 -0
  179. package/dist/core/foreign-session-import.test.js +220 -0
  180. package/dist/core/foreign-session-import.test.js.map +1 -0
  181. package/dist/core/foreign-transcript-blocks.d.ts +51 -0
  182. package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
  183. package/dist/core/foreign-transcript-blocks.js +151 -0
  184. package/dist/core/foreign-transcript-blocks.js.map +1 -0
  185. package/dist/core/goal-lifecycle-smoke.test.js +2 -2
  186. package/dist/core/goal-store.js +1 -1
  187. package/dist/core/goal-store.test.js +2 -2
  188. package/dist/core/goal-verifier.test.js +2 -2
  189. package/dist/core/goal-worker-dev-server-lifecycle.test.js +1 -1
  190. package/dist/core/goal-worker.test.js +2 -2
  191. package/dist/core/hashline-edit-benchmark.d.ts +2 -2
  192. package/dist/core/hashline-edit-benchmark.js +7 -7
  193. package/dist/core/ideal-review.d.ts +20 -1
  194. package/dist/core/ideal-review.d.ts.map +1 -1
  195. package/dist/core/ideal-review.js +37 -2
  196. package/dist/core/ideal-review.js.map +1 -1
  197. package/dist/core/ideal-review.test.js +99 -3
  198. package/dist/core/ideal-review.test.js.map +1 -1
  199. package/dist/core/local-endpoint-store.test.js +1 -1
  200. package/dist/core/logger.d.ts +1 -1
  201. package/dist/core/logger.js +2 -2
  202. package/dist/core/logger.js.map +1 -1
  203. package/dist/core/loop-breaker.d.ts.map +1 -1
  204. package/dist/core/loop-breaker.js +1 -0
  205. package/dist/core/loop-breaker.js.map +1 -1
  206. package/dist/core/lsp/client-navigation.test.d.ts +2 -0
  207. package/dist/core/lsp/client-navigation.test.d.ts.map +1 -0
  208. package/dist/core/lsp/client-navigation.test.js +135 -0
  209. package/dist/core/lsp/client-navigation.test.js.map +1 -0
  210. package/dist/core/lsp/client.d.ts +84 -0
  211. package/dist/core/lsp/client.d.ts.map +1 -1
  212. package/dist/core/lsp/client.js +190 -3
  213. package/dist/core/lsp/client.js.map +1 -1
  214. package/dist/core/lsp/integration.test.js +2 -2
  215. package/dist/core/lsp/manager-navigation.test.d.ts +2 -0
  216. package/dist/core/lsp/manager-navigation.test.d.ts.map +1 -0
  217. package/dist/core/lsp/manager-navigation.test.js +121 -0
  218. package/dist/core/lsp/manager-navigation.test.js.map +1 -0
  219. package/dist/core/lsp/manager.d.ts +81 -9
  220. package/dist/core/lsp/manager.d.ts.map +1 -1
  221. package/dist/core/lsp/manager.js +183 -64
  222. package/dist/core/lsp/manager.js.map +1 -1
  223. package/dist/core/lsp/manager.test.js +18 -0
  224. package/dist/core/lsp/manager.test.js.map +1 -1
  225. package/dist/core/lsp/pool.d.ts +113 -0
  226. package/dist/core/lsp/pool.d.ts.map +1 -0
  227. package/dist/core/lsp/pool.js +255 -0
  228. package/dist/core/lsp/pool.js.map +1 -0
  229. package/dist/core/lsp/pool.test.d.ts +2 -0
  230. package/dist/core/lsp/pool.test.d.ts.map +1 -0
  231. package/dist/core/lsp/pool.test.js +286 -0
  232. package/dist/core/lsp/pool.test.js.map +1 -0
  233. package/dist/core/lsp/servers.d.ts.map +1 -1
  234. package/dist/core/lsp/servers.js +43 -3
  235. package/dist/core/lsp/servers.js.map +1 -1
  236. package/dist/core/lsp/servers.test.d.ts +2 -0
  237. package/dist/core/lsp/servers.test.d.ts.map +1 -0
  238. package/dist/core/lsp/servers.test.js +67 -0
  239. package/dist/core/lsp/servers.test.js.map +1 -0
  240. package/dist/core/lsp/windows.test.js +6 -18
  241. package/dist/core/lsp/windows.test.js.map +1 -1
  242. package/dist/core/mcp/catalog-cache.d.ts +63 -0
  243. package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
  244. package/dist/core/mcp/catalog-cache.js +163 -0
  245. package/dist/core/mcp/catalog-cache.js.map +1 -0
  246. package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
  247. package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
  248. package/dist/core/mcp/catalog-cache.test.js +99 -0
  249. package/dist/core/mcp/catalog-cache.test.js.map +1 -0
  250. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  251. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  252. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  253. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  254. package/dist/core/mcp/client.d.ts +158 -0
  255. package/dist/core/mcp/client.d.ts.map +1 -1
  256. package/dist/core/mcp/client.js +478 -45
  257. package/dist/core/mcp/client.js.map +1 -1
  258. package/dist/core/mcp/content.d.ts +19 -0
  259. package/dist/core/mcp/content.d.ts.map +1 -0
  260. package/dist/core/mcp/content.js +172 -0
  261. package/dist/core/mcp/content.js.map +1 -0
  262. package/dist/core/mcp/content.test.d.ts +2 -0
  263. package/dist/core/mcp/content.test.d.ts.map +1 -0
  264. package/dist/core/mcp/content.test.js +135 -0
  265. package/dist/core/mcp/content.test.js.map +1 -0
  266. package/dist/core/mcp/deferred-catalog.d.ts +1 -1
  267. package/dist/core/mcp/deferred-catalog.js +1 -1
  268. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  269. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  270. package/dist/core/mcp/elicitation-bridge.js +52 -0
  271. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  272. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  273. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  274. package/dist/core/mcp/elicitation.test.js +158 -0
  275. package/dist/core/mcp/elicitation.test.js.map +1 -0
  276. package/dist/core/mcp/index.d.ts +5 -1
  277. package/dist/core/mcp/index.d.ts.map +1 -1
  278. package/dist/core/mcp/index.js +2 -0
  279. package/dist/core/mcp/index.js.map +1 -1
  280. package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
  281. package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
  282. package/dist/core/mcp/legacy-negotiation.test.js +80 -0
  283. package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
  284. package/dist/core/mcp/oauth-provider.d.ts +1 -2
  285. package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
  286. package/dist/core/mcp/oauth-provider.js +6 -2
  287. package/dist/core/mcp/oauth-provider.js.map +1 -1
  288. package/dist/core/mcp/oauth-store.d.ts +1 -1
  289. package/dist/core/mcp/oauth-store.d.ts.map +1 -1
  290. package/dist/core/mcp/oauth-store.js.map +1 -1
  291. package/dist/core/mcp/oauth-store.test.js +2 -2
  292. package/dist/core/mcp/resolve-stdio.test.js +5 -5
  293. package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
  294. package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
  295. package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
  296. package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
  297. package/dist/core/mcp/shared-pool.d.ts +158 -0
  298. package/dist/core/mcp/shared-pool.d.ts.map +1 -0
  299. package/dist/core/mcp/shared-pool.js +235 -0
  300. package/dist/core/mcp/shared-pool.js.map +1 -0
  301. package/dist/core/mcp/shared-pool.test.d.ts +2 -0
  302. package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
  303. package/dist/core/mcp/shared-pool.test.js +425 -0
  304. package/dist/core/mcp/shared-pool.test.js.map +1 -0
  305. package/dist/core/mcp/store.test.js +5 -5
  306. package/dist/core/mcp/types.d.ts +12 -0
  307. package/dist/core/mcp/types.d.ts.map +1 -1
  308. package/dist/core/mcp/when-connected.test.d.ts +2 -0
  309. package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
  310. package/dist/core/mcp/when-connected.test.js +46 -0
  311. package/dist/core/mcp/when-connected.test.js.map +1 -0
  312. package/dist/core/nolan-context.d.ts.map +1 -1
  313. package/dist/core/nolan-context.js +8 -0
  314. package/dist/core/nolan-context.js.map +1 -1
  315. package/dist/core/nolan-context.test.js +40 -0
  316. package/dist/core/nolan-context.test.js.map +1 -1
  317. package/dist/core/nolan-model.test.js +1 -1
  318. package/dist/core/nolan-prompt.js +8 -5
  319. package/dist/core/nolan-prompt.js.map +1 -1
  320. package/dist/core/nolan-prompt.test.js +5 -0
  321. package/dist/core/nolan-prompt.test.js.map +1 -1
  322. package/dist/core/oauth/xai.d.ts +2 -0
  323. package/dist/core/oauth/xai.d.ts.map +1 -0
  324. package/dist/core/oauth/xai.js +4 -0
  325. package/dist/core/oauth/xai.js.map +1 -0
  326. package/dist/core/persistent-shell.d.ts +3 -2
  327. package/dist/core/persistent-shell.d.ts.map +1 -1
  328. package/dist/core/persistent-shell.js +11 -4
  329. package/dist/core/persistent-shell.js.map +1 -1
  330. package/dist/core/persistent-shell.test.js +6 -6
  331. package/dist/core/process-gate.d.ts +45 -0
  332. package/dist/core/process-gate.d.ts.map +1 -0
  333. package/dist/core/process-gate.js +79 -0
  334. package/dist/core/process-gate.js.map +1 -0
  335. package/dist/core/process-gate.test.d.ts +2 -0
  336. package/dist/core/process-gate.test.d.ts.map +1 -0
  337. package/dist/core/process-gate.test.js +87 -0
  338. package/dist/core/process-gate.test.js.map +1 -0
  339. package/dist/core/process-manager-dev-server-repro.test.js +51 -12
  340. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  341. package/dist/core/process-manager-notifications.test.d.ts +2 -0
  342. package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
  343. package/dist/core/process-manager-notifications.test.js +227 -0
  344. package/dist/core/process-manager-notifications.test.js.map +1 -0
  345. package/dist/core/process-manager.d.ts +72 -1
  346. package/dist/core/process-manager.d.ts.map +1 -1
  347. package/dist/core/process-manager.js +247 -4
  348. package/dist/core/process-manager.js.map +1 -1
  349. package/dist/core/progress/git-xp.test.js +2 -2
  350. package/dist/core/progress/ranks.d.ts +14 -3
  351. package/dist/core/progress/ranks.d.ts.map +1 -1
  352. package/dist/core/progress/ranks.js +181 -12
  353. package/dist/core/progress/ranks.js.map +1 -1
  354. package/dist/core/progress/ranks.test.js +48 -7
  355. package/dist/core/progress/ranks.test.js.map +1 -1
  356. package/dist/core/progress/rebuild.test.js +1 -1
  357. package/dist/core/progress/store.test.js +1 -1
  358. package/dist/core/progress/types.d.ts +2 -1
  359. package/dist/core/progress/types.d.ts.map +1 -1
  360. package/dist/core/project-discovery.d.ts +31 -6
  361. package/dist/core/project-discovery.d.ts.map +1 -1
  362. package/dist/core/project-discovery.js +349 -79
  363. package/dist/core/project-discovery.js.map +1 -1
  364. package/dist/core/project-discovery.test.js +128 -4
  365. package/dist/core/project-discovery.test.js.map +1 -1
  366. package/dist/core/prompt-commands.js +3 -3
  367. package/dist/core/radio.d.ts +1 -1
  368. package/dist/core/radio.d.ts.map +1 -1
  369. package/dist/core/radio.js +90 -6
  370. package/dist/core/radio.js.map +1 -1
  371. package/dist/core/radio.test.js +15 -2
  372. package/dist/core/radio.test.js.map +1 -1
  373. package/dist/core/regrounding.d.ts.map +1 -1
  374. package/dist/core/regrounding.js +1 -0
  375. package/dist/core/regrounding.js.map +1 -1
  376. package/dist/core/run-claim.d.ts +37 -0
  377. package/dist/core/run-claim.d.ts.map +1 -0
  378. package/dist/core/run-claim.js +46 -0
  379. package/dist/core/run-claim.js.map +1 -0
  380. package/dist/core/run-claim.test.d.ts +2 -0
  381. package/dist/core/run-claim.test.d.ts.map +1 -0
  382. package/dist/core/run-claim.test.js +107 -0
  383. package/dist/core/run-claim.test.js.map +1 -0
  384. package/dist/core/run-journal.test.d.ts +2 -0
  385. package/dist/core/run-journal.test.d.ts.map +1 -0
  386. package/dist/core/run-journal.test.js +195 -0
  387. package/dist/core/run-journal.test.js.map +1 -0
  388. package/dist/core/run-lifecycle.d.ts +22 -3
  389. package/dist/core/run-lifecycle.d.ts.map +1 -1
  390. package/dist/core/run-lifecycle.js +12 -3
  391. package/dist/core/run-lifecycle.js.map +1 -1
  392. package/dist/core/sandbox-domains.d.ts +14 -0
  393. package/dist/core/sandbox-domains.d.ts.map +1 -0
  394. package/dist/core/sandbox-domains.js +76 -0
  395. package/dist/core/sandbox-domains.js.map +1 -0
  396. package/dist/core/sandbox-feedback.d.ts +22 -0
  397. package/dist/core/sandbox-feedback.d.ts.map +1 -0
  398. package/dist/core/sandbox-feedback.js +69 -0
  399. package/dist/core/sandbox-feedback.js.map +1 -0
  400. package/dist/core/sandbox-feedback.test.d.ts +2 -0
  401. package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
  402. package/dist/core/sandbox-feedback.test.js +32 -0
  403. package/dist/core/sandbox-feedback.test.js.map +1 -0
  404. package/dist/core/sandbox.d.ts +91 -0
  405. package/dist/core/sandbox.d.ts.map +1 -0
  406. package/dist/core/sandbox.js +254 -0
  407. package/dist/core/sandbox.js.map +1 -0
  408. package/dist/core/sandbox.test.d.ts +2 -0
  409. package/dist/core/sandbox.test.d.ts.map +1 -0
  410. package/dist/core/sandbox.test.js +157 -0
  411. package/dist/core/sandbox.test.js.map +1 -0
  412. package/dist/core/semantic-search-benchmark.d.ts +2 -2
  413. package/dist/core/semantic-search-benchmark.js +5 -5
  414. package/dist/core/session-compaction.d.ts +6 -0
  415. package/dist/core/session-compaction.d.ts.map +1 -1
  416. package/dist/core/session-compaction.js +11 -1
  417. package/dist/core/session-compaction.js.map +1 -1
  418. package/dist/core/session-compaction.test.js +19 -2
  419. package/dist/core/session-compaction.test.js.map +1 -1
  420. package/dist/core/session-export.d.ts.map +1 -1
  421. package/dist/core/session-export.js +12 -4
  422. package/dist/core/session-export.js.map +1 -1
  423. package/dist/core/session-history.d.ts +95 -6
  424. package/dist/core/session-history.d.ts.map +1 -1
  425. package/dist/core/session-history.js +261 -17
  426. package/dist/core/session-history.js.map +1 -1
  427. package/dist/core/session-history.test.js +173 -2
  428. package/dist/core/session-history.test.js.map +1 -1
  429. package/dist/core/session-manager.d.ts +223 -6
  430. package/dist/core/session-manager.d.ts.map +1 -1
  431. package/dist/core/session-manager.js +579 -74
  432. package/dist/core/session-manager.js.map +1 -1
  433. package/dist/core/session-manager.test.js +154 -10
  434. package/dist/core/session-manager.test.js.map +1 -1
  435. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  436. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  437. package/dist/core/session-notification-visibility.test.js +113 -0
  438. package/dist/core/session-notification-visibility.test.js.map +1 -0
  439. package/dist/core/session-preview.d.ts +5 -4
  440. package/dist/core/session-preview.d.ts.map +1 -1
  441. package/dist/core/session-preview.js +12 -5
  442. package/dist/core/session-preview.js.map +1 -1
  443. package/dist/core/session-restore-display.test.js +25 -0
  444. package/dist/core/session-restore-display.test.js.map +1 -1
  445. package/dist/core/session-storage-fd-leak.test.d.ts +2 -0
  446. package/dist/core/session-storage-fd-leak.test.d.ts.map +1 -0
  447. package/dist/core/session-storage-fd-leak.test.js +116 -0
  448. package/dist/core/session-storage-fd-leak.test.js.map +1 -0
  449. package/dist/core/session-storage.d.ts +22 -2
  450. package/dist/core/session-storage.d.ts.map +1 -1
  451. package/dist/core/session-storage.js +72 -9
  452. package/dist/core/session-storage.js.map +1 -1
  453. package/dist/core/session-storage.test.js +8 -3
  454. package/dist/core/session-storage.test.js.map +1 -1
  455. package/dist/core/settings-manager.d.ts +10 -2
  456. package/dist/core/settings-manager.d.ts.map +1 -1
  457. package/dist/core/settings-manager.js +36 -1
  458. package/dist/core/settings-manager.js.map +1 -1
  459. package/dist/core/shell-path.js +1 -1
  460. package/dist/core/shell.d.ts +1 -1
  461. package/dist/core/shell.js +2 -2
  462. package/dist/core/shell.test.js +2 -2
  463. package/dist/core/shutdown.d.ts +71 -0
  464. package/dist/core/shutdown.d.ts.map +1 -0
  465. package/dist/core/shutdown.js +144 -0
  466. package/dist/core/shutdown.js.map +1 -0
  467. package/dist/core/shutdown.test.d.ts +2 -0
  468. package/dist/core/shutdown.test.d.ts.map +1 -0
  469. package/dist/core/shutdown.test.js +160 -0
  470. package/dist/core/shutdown.test.js.map +1 -0
  471. package/dist/core/sidecar-error-reporter.js +1 -1
  472. package/dist/core/sidecar-error-reporter.test.js +2 -2
  473. package/dist/core/skills-routing.test.js +96 -0
  474. package/dist/core/skills-routing.test.js.map +1 -1
  475. package/dist/core/skills.d.ts.map +1 -1
  476. package/dist/core/skills.js +3 -0
  477. package/dist/core/skills.js.map +1 -1
  478. package/dist/core/steering.d.ts +12 -0
  479. package/dist/core/steering.d.ts.map +1 -1
  480. package/dist/core/steering.js +16 -0
  481. package/dist/core/steering.js.map +1 -1
  482. package/dist/core/subagent-manager.d.ts +22 -0
  483. package/dist/core/subagent-manager.d.ts.map +1 -1
  484. package/dist/core/subagent-manager.js +84 -13
  485. package/dist/core/subagent-manager.js.map +1 -1
  486. package/dist/core/subagent-manager.test.js +57 -15
  487. package/dist/core/subagent-manager.test.js.map +1 -1
  488. package/dist/core/subagent-store.test.js +3 -3
  489. package/dist/core/task-store.js +1 -1
  490. package/dist/core/verification-evidence.d.ts +17 -0
  491. package/dist/core/verification-evidence.d.ts.map +1 -0
  492. package/dist/core/verification-evidence.js +283 -0
  493. package/dist/core/verification-evidence.js.map +1 -0
  494. package/dist/core/verification-evidence.test.d.ts +2 -0
  495. package/dist/core/verification-evidence.test.d.ts.map +1 -0
  496. package/dist/core/verification-evidence.test.js +103 -0
  497. package/dist/core/verification-evidence.test.js.map +1 -0
  498. package/dist/index.js +1 -1
  499. package/dist/index.js.map +1 -1
  500. package/dist/interactive.d.ts.map +1 -1
  501. package/dist/interactive.js +17 -3
  502. package/dist/interactive.js.map +1 -1
  503. package/dist/modes/acp-mode.d.ts +122 -0
  504. package/dist/modes/acp-mode.d.ts.map +1 -0
  505. package/dist/modes/acp-mode.js +1366 -0
  506. package/dist/modes/acp-mode.js.map +1 -0
  507. package/dist/modes/acp-mode.test.d.ts +2 -0
  508. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  509. package/dist/modes/acp-mode.test.js +1065 -0
  510. package/dist/modes/acp-mode.test.js.map +1 -0
  511. package/dist/modes/agent-home-mode.d.ts.map +1 -1
  512. package/dist/modes/agent-home-mode.js +15 -11
  513. package/dist/modes/agent-home-mode.js.map +1 -1
  514. package/dist/modes/index.d.ts +1 -0
  515. package/dist/modes/index.d.ts.map +1 -1
  516. package/dist/modes/index.js +1 -0
  517. package/dist/modes/index.js.map +1 -1
  518. package/dist/modes/json-mode.d.ts +36 -0
  519. package/dist/modes/json-mode.d.ts.map +1 -1
  520. package/dist/modes/json-mode.js +39 -0
  521. package/dist/modes/json-mode.js.map +1 -1
  522. package/dist/modes/json-mode.test.d.ts +2 -0
  523. package/dist/modes/json-mode.test.d.ts.map +1 -0
  524. package/dist/modes/json-mode.test.js +84 -0
  525. package/dist/modes/json-mode.test.js.map +1 -0
  526. package/dist/modes/serve-mode.d.ts +1 -1
  527. package/dist/modes/serve-mode.d.ts.map +1 -1
  528. package/dist/modes/serve-mode.js +14 -9
  529. package/dist/modes/serve-mode.js.map +1 -1
  530. package/dist/modes/subagent-worker-mode.d.ts +8 -0
  531. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  532. package/dist/modes/subagent-worker-mode.js +91 -11
  533. package/dist/modes/subagent-worker-mode.js.map +1 -1
  534. package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
  535. package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
  536. package/dist/modes/subagent-worker-mode.test.js +52 -0
  537. package/dist/modes/subagent-worker-mode.test.js.map +1 -0
  538. package/dist/session.d.ts +29 -0
  539. package/dist/session.d.ts.map +1 -1
  540. package/dist/session.js +41 -0
  541. package/dist/session.js.map +1 -1
  542. package/dist/session.test.js +1 -1
  543. package/dist/system-prompt.d.ts +37 -1
  544. package/dist/system-prompt.d.ts.map +1 -1
  545. package/dist/system-prompt.js +123 -11
  546. package/dist/system-prompt.js.map +1 -1
  547. package/dist/system-prompt.test.js +90 -11
  548. package/dist/system-prompt.test.js.map +1 -1
  549. package/dist/system-prompt.tiering.test.d.ts +2 -0
  550. package/dist/system-prompt.tiering.test.d.ts.map +1 -0
  551. package/dist/system-prompt.tiering.test.js +105 -0
  552. package/dist/system-prompt.tiering.test.js.map +1 -0
  553. package/dist/tools/bash.d.ts +2 -1
  554. package/dist/tools/bash.d.ts.map +1 -1
  555. package/dist/tools/bash.js +55 -16
  556. package/dist/tools/bash.js.map +1 -1
  557. package/dist/tools/code-nav.d.ts +30 -0
  558. package/dist/tools/code-nav.d.ts.map +1 -0
  559. package/dist/tools/code-nav.js +320 -0
  560. package/dist/tools/code-nav.js.map +1 -0
  561. package/dist/tools/code-nav.test.d.ts +2 -0
  562. package/dist/tools/code-nav.test.d.ts.map +1 -0
  563. package/dist/tools/code-nav.test.js +136 -0
  564. package/dist/tools/code-nav.test.js.map +1 -0
  565. package/dist/tools/edit.test.js +1 -1
  566. package/dist/tools/generate-image.d.ts +1 -1
  567. package/dist/tools/goals.d.ts +4 -4
  568. package/dist/tools/goals.test.js +2 -2
  569. package/dist/tools/grep-parity.test.d.ts +2 -0
  570. package/dist/tools/grep-parity.test.d.ts.map +1 -0
  571. package/dist/tools/grep-parity.test.js +92 -0
  572. package/dist/tools/grep-parity.test.js.map +1 -0
  573. package/dist/tools/grep-recall.test.d.ts +2 -0
  574. package/dist/tools/grep-recall.test.d.ts.map +1 -0
  575. package/dist/tools/grep-recall.test.js +114 -0
  576. package/dist/tools/grep-recall.test.js.map +1 -0
  577. package/dist/tools/grep-spawn.test.d.ts +2 -0
  578. package/dist/tools/grep-spawn.test.d.ts.map +1 -0
  579. package/dist/tools/grep-spawn.test.js +66 -0
  580. package/dist/tools/grep-spawn.test.js.map +1 -0
  581. package/dist/tools/grep.d.ts +13 -3
  582. package/dist/tools/grep.d.ts.map +1 -1
  583. package/dist/tools/grep.js +342 -41
  584. package/dist/tools/grep.js.map +1 -1
  585. package/dist/tools/grep.test.js +17 -14
  586. package/dist/tools/grep.test.js.map +1 -1
  587. package/dist/tools/index.d.ts +20 -2
  588. package/dist/tools/index.d.ts.map +1 -1
  589. package/dist/tools/index.js +28 -18
  590. package/dist/tools/index.js.map +1 -1
  591. package/dist/tools/index.test.d.ts +2 -0
  592. package/dist/tools/index.test.d.ts.map +1 -0
  593. package/dist/tools/index.test.js +55 -0
  594. package/dist/tools/index.test.js.map +1 -0
  595. package/dist/tools/plan-mode.test.js +1 -1
  596. package/dist/tools/prompt-hints.d.ts +9 -0
  597. package/dist/tools/prompt-hints.d.ts.map +1 -1
  598. package/dist/tools/prompt-hints.js +61 -8
  599. package/dist/tools/prompt-hints.js.map +1 -1
  600. package/dist/tools/read-only-bash.d.ts +3 -0
  601. package/dist/tools/read-only-bash.d.ts.map +1 -1
  602. package/dist/tools/read-only-bash.js +7 -9
  603. package/dist/tools/read-only-bash.js.map +1 -1
  604. package/dist/tools/safe-env.js +1 -1
  605. package/dist/tools/search-code.d.ts.map +1 -1
  606. package/dist/tools/search-code.js +7 -6
  607. package/dist/tools/search-code.js.map +1 -1
  608. package/dist/tools/search-code.test.js +8 -2
  609. package/dist/tools/search-code.test.js.map +1 -1
  610. package/dist/tools/skill.js +2 -1
  611. package/dist/tools/skill.js.map +1 -1
  612. package/dist/tools/subagent-control.d.ts.map +1 -1
  613. package/dist/tools/subagent-control.js +25 -6
  614. package/dist/tools/subagent-control.js.map +1 -1
  615. package/dist/tools/subagent-control.test.js +34 -1
  616. package/dist/tools/subagent-control.test.js.map +1 -1
  617. package/dist/tools/subagent-shared.d.ts +26 -1
  618. package/dist/tools/subagent-shared.d.ts.map +1 -1
  619. package/dist/tools/subagent-shared.js +40 -7
  620. package/dist/tools/subagent-shared.js.map +1 -1
  621. package/dist/tools/subagent-shared.test.js +43 -9
  622. package/dist/tools/subagent-shared.test.js.map +1 -1
  623. package/dist/tools/subagent.d.ts.map +1 -1
  624. package/dist/tools/subagent.js +54 -16
  625. package/dist/tools/subagent.js.map +1 -1
  626. package/dist/tools/subagent.test.js +68 -3
  627. package/dist/tools/subagent.test.js.map +1 -1
  628. package/dist/tools/task-output.d.ts.map +1 -1
  629. package/dist/tools/task-output.js +3 -1
  630. package/dist/tools/task-output.js.map +1 -1
  631. package/dist/tools/task-send.test.js +19 -10
  632. package/dist/tools/task-send.test.js.map +1 -1
  633. package/dist/tools/tool-search.d.ts +12 -1
  634. package/dist/tools/tool-search.d.ts.map +1 -1
  635. package/dist/tools/tool-search.js +24 -4
  636. package/dist/tools/tool-search.js.map +1 -1
  637. package/dist/tools/tool-search.test.d.ts +2 -0
  638. package/dist/tools/tool-search.test.d.ts.map +1 -0
  639. package/dist/tools/tool-search.test.js +69 -0
  640. package/dist/tools/tool-search.test.js.map +1 -0
  641. package/dist/tools/tool-tiers.d.ts +42 -0
  642. package/dist/tools/tool-tiers.d.ts.map +1 -0
  643. package/dist/tools/tool-tiers.js +78 -0
  644. package/dist/tools/tool-tiers.js.map +1 -0
  645. package/dist/tools/tool-tiers.test.d.ts +2 -0
  646. package/dist/tools/tool-tiers.test.d.ts.map +1 -0
  647. package/dist/tools/tool-tiers.test.js +48 -0
  648. package/dist/tools/tool-tiers.test.js.map +1 -0
  649. package/dist/tools/web-fetch.d.ts +1 -1
  650. package/dist/tools/web-fetch.d.ts.map +1 -1
  651. package/dist/tools/web-fetch.js +5 -2
  652. package/dist/tools/web-fetch.js.map +1 -1
  653. package/dist/tools/write.test.js +1 -1
  654. package/dist/types.d.ts +7 -0
  655. package/dist/types.d.ts.map +1 -1
  656. package/dist/ui/App.d.ts.map +1 -1
  657. package/dist/ui/App.js +27 -1
  658. package/dist/ui/App.js.map +1 -1
  659. package/dist/ui/components/Banner.d.ts.map +1 -1
  660. package/dist/ui/components/Banner.js +24 -16
  661. package/dist/ui/components/Banner.js.map +1 -1
  662. package/dist/ui/components/ToolExecution.js +42 -34
  663. package/dist/ui/components/ToolExecution.js.map +1 -1
  664. package/dist/ui/components/force-color.d.ts +2 -0
  665. package/dist/ui/components/force-color.d.ts.map +1 -0
  666. package/dist/ui/components/force-color.js +11 -0
  667. package/dist/ui/components/force-color.js.map +1 -0
  668. package/dist/ui/components/themed-output.test.d.ts +2 -0
  669. package/dist/ui/components/themed-output.test.d.ts.map +1 -0
  670. package/dist/ui/components/themed-output.test.js +111 -0
  671. package/dist/ui/components/themed-output.test.js.map +1 -0
  672. package/dist/ui/error-item.d.ts +1 -1
  673. package/dist/ui/error-item.js +3 -3
  674. package/dist/ui/error-item.js.map +1 -1
  675. package/dist/ui/hooks/useAgentLoop.d.ts +3 -0
  676. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  677. package/dist/ui/hooks/useAgentLoop.js +45 -6
  678. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  679. package/dist/ui/login.js +1 -1
  680. package/dist/ui/login.js.map +1 -1
  681. package/dist/ui/render.d.ts +1 -1
  682. package/dist/ui/render.js +6 -6
  683. package/dist/ui/theme/dark-ansi.json +6 -0
  684. package/dist/ui/theme/dark-daltonized.json +6 -0
  685. package/dist/ui/theme/dark.json +6 -0
  686. package/dist/ui/theme/light-ansi.json +6 -0
  687. package/dist/ui/theme/light-daltonized.json +6 -0
  688. package/dist/ui/theme/light.json +6 -0
  689. package/dist/ui/theme/theme.d.ts +6 -0
  690. package/dist/ui/theme/theme.d.ts.map +1 -1
  691. package/dist/ui/tui-terminal-recorder.test.js +1 -1
  692. package/dist/utils/git.test.js +3 -3
  693. package/dist/utils/github.test.js +3 -3
  694. package/dist/utils/github.test.js.map +1 -1
  695. package/dist/utils/image.d.ts +26 -3
  696. package/dist/utils/image.d.ts.map +1 -1
  697. package/dist/utils/image.js +83 -14
  698. package/dist/utils/image.js.map +1 -1
  699. package/dist/utils/image.test.d.ts +2 -0
  700. package/dist/utils/image.test.d.ts.map +1 -0
  701. package/dist/utils/image.test.js +139 -0
  702. package/dist/utils/image.test.js.map +1 -0
  703. 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,48 +296,119 @@ 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);
167
- const { stream } = await openSessionReadStream(effectivePath);
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);
361
+ const { stream, close } = await openSessionReadStream(effectivePath);
168
362
  let header = null;
169
363
  const entries = [];
170
364
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
171
- for await (const line of rl) {
172
- if (!line)
173
- continue;
174
- try {
175
- const parsed = JSON.parse(line);
176
- if (parsed.type === "session") {
177
- if (parsed.version === 2) {
178
- header = parsed;
365
+ // rejectMalformedLines can throw mid-transcript; close() on the way out.
366
+ try {
367
+ for await (const line of rl) {
368
+ if (!line)
369
+ continue;
370
+ try {
371
+ const parsed = JSON.parse(line);
372
+ if (parsed.type === "session") {
373
+ if (parsed.version === 2) {
374
+ header = parsed;
375
+ }
376
+ else {
377
+ const v1 = parsed;
378
+ header = {
379
+ type: "session",
380
+ version: 2,
381
+ id: v1.id,
382
+ conversationId: v1.id,
383
+ generation: 0,
384
+ timestamp: v1.timestamp,
385
+ cwd: v1.cwd,
386
+ provider: v1.provider,
387
+ model: v1.model,
388
+ leafId: null,
389
+ };
390
+ }
391
+ continue;
179
392
  }
180
- else {
181
- const v1 = parsed;
182
- header = {
183
- type: "session",
184
- version: 2,
185
- id: v1.id,
186
- conversationId: v1.id,
187
- timestamp: v1.timestamp,
188
- cwd: v1.cwd,
189
- provider: v1.provider,
190
- model: v1.model,
191
- leafId: null,
192
- };
393
+ const entry = parsed;
394
+ if (entry.type === "message" && !entry.id) {
395
+ entry.id = crypto.randomUUID();
396
+ entry.parentId = null;
193
397
  }
194
- continue;
398
+ entries.push(await hydrateSessionEntry(entry, effectivePath));
195
399
  }
196
- const entry = parsed;
197
- if (entry.type === "message" && !entry.id) {
198
- entry.id = crypto.randomUUID();
199
- entry.parentId = null;
400
+ catch (error) {
401
+ if (rejectMalformedLines)
402
+ throw error;
403
+ // Skip malformed JSON lines — cold migration preserves their raw bytes
404
+ // so a future recovery tool still has a chance to repair them.
200
405
  }
201
- entries.push(await hydrateSessionEntry(entry, effectivePath));
202
- }
203
- catch {
204
- // Skip malformed JSON lines — cold migration preserves their raw bytes
205
- // so a future recovery tool still has a chance to repair them.
206
406
  }
207
407
  }
408
+ finally {
409
+ rl.close();
410
+ close();
411
+ }
208
412
  if (!header) {
209
413
  throw new Error(`Invalid session file: no header found in ${sessionPath}`);
210
414
  }
@@ -213,30 +417,50 @@ export class SessionManager {
213
417
  async readSessionInfo(candidatePath) {
214
418
  try {
215
419
  const resolvedPath = await resolveSessionPath(candidatePath);
216
- const { stream } = await openSessionReadStream(resolvedPath);
420
+ const { stream, close } = await openSessionReadStream(resolvedPath);
217
421
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
218
422
  let first = null;
219
423
  let messageCount = 0;
220
424
  let lastActivity = null;
221
- for await (const line of rl) {
222
- if (!line)
223
- continue;
224
- try {
225
- const parsed = JSON.parse(line);
226
- if (!first) {
227
- if (parsed.type !== "session")
228
- break;
229
- first = parsed;
425
+ let preview;
426
+ try {
427
+ for await (const line of rl) {
428
+ if (!line)
429
+ continue;
430
+ try {
431
+ const parsed = JSON.parse(line);
432
+ if (!first) {
433
+ if (parsed.type !== "session")
434
+ break;
435
+ first = parsed;
436
+ // v2 headers usually carry the preview already; when they do,
437
+ // nothing below has to look for one.
438
+ preview = parsed.preview?.trim() || undefined;
439
+ }
440
+ else if (parsed.type === "message") {
441
+ messageCount += 1;
442
+ if (parsed.timestamp)
443
+ lastActivity = parsed.timestamp;
444
+ // Recover a title for the sessions whose header predates `preview`.
445
+ // Free here: this pass already reads every line. `getUserSessionPrompt`
446
+ // rejects compaction summaries and autopilot/status injections, so a
447
+ // session is titled by what its user actually asked.
448
+ if (!preview && parsed.message?.role === "user") {
449
+ preview =
450
+ getUserSessionPrompt(parsed.message.content, parsed.message.provenance)
451
+ ?.replace(/\s+/g, " ")
452
+ .trim() || undefined;
453
+ }
454
+ }
230
455
  }
231
- else if (parsed.type === "message") {
232
- messageCount += 1;
233
- if (parsed.timestamp)
234
- lastActivity = parsed.timestamp;
456
+ catch {
457
+ // Skip malformed lines while retaining readable entries around them.
235
458
  }
236
459
  }
237
- catch {
238
- // Skip malformed lines while retaining readable entries around them.
239
- }
460
+ }
461
+ finally {
462
+ rl.close();
463
+ close();
240
464
  }
241
465
  if (!first || first.type !== "session")
242
466
  return null;
@@ -245,11 +469,13 @@ export class SessionManager {
245
469
  conversationId: first.version === 2
246
470
  ? (first.conversationId ?? first.id)
247
471
  : first.id,
472
+ generation: first.version === 2 ? (first.generation ?? 0) : 0,
248
473
  path: resolvedPath,
249
474
  timestamp: first.timestamp,
250
475
  lastActivity: lastActivity ?? first.timestamp,
251
476
  cwd: first.cwd,
252
477
  messageCount,
478
+ ...(preview ? { preview: preview.slice(0, 100) } : {}),
253
479
  };
254
480
  }
255
481
  catch {
@@ -266,9 +492,95 @@ export class SessionManager {
266
492
  }
267
493
  return files.filter(isSessionPath).map((file) => path.join(directory, file));
268
494
  }
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)));
495
+ /**
496
+ * Read just enough of a session file to summarize it.
497
+ *
498
+ * Stops at the first user prompt (or the first message, when the header
499
+ * already carries a preview) instead of parsing the whole transcript — this
500
+ * is what makes listing every session on the machine cheap. Files whose
501
+ * first user message is far down (long tool runs before the user speaks)
502
+ * are capped rather than allowed to stall the list.
503
+ */
504
+ async readSessionSummary(candidatePath) {
505
+ try {
506
+ const resolvedPath = await resolveSessionPath(candidatePath);
507
+ const { stream, close } = await openSessionReadStream(resolvedPath);
508
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
509
+ let first = null;
510
+ let preview;
511
+ let hasMessages = false;
512
+ let lines = 0;
513
+ const MAX_SCAN_LINES = 500;
514
+ try {
515
+ for await (const line of rl) {
516
+ if (!line)
517
+ continue;
518
+ if (++lines > MAX_SCAN_LINES)
519
+ break;
520
+ try {
521
+ const parsed = JSON.parse(line);
522
+ if (!first) {
523
+ if (parsed.type !== "session")
524
+ break;
525
+ first = parsed;
526
+ preview = parsed.preview?.trim() || undefined;
527
+ }
528
+ else if (parsed.type === "message") {
529
+ hasMessages = true;
530
+ // Header preview + any message: done. Otherwise keep scanning for
531
+ // the first user-authored prompt to title by.
532
+ if (preview)
533
+ break;
534
+ if (parsed.message?.role === "user") {
535
+ preview =
536
+ getUserSessionPrompt(parsed.message.content, parsed.message.provenance)
537
+ ?.replace(/\s+/g, " ")
538
+ .trim() || undefined;
539
+ if (preview)
540
+ break;
541
+ }
542
+ }
543
+ }
544
+ catch {
545
+ // Skip malformed lines while retaining readable entries around them.
546
+ }
547
+ }
548
+ }
549
+ finally {
550
+ // close() destroys the gunzip AND its source. Destroying only the
551
+ // stream orphans the underlying fd — this listing runs over every
552
+ // session on the machine, so that leaked one fd per archive per call.
553
+ rl.close();
554
+ close();
555
+ }
556
+ if (!first || first.type !== "session")
557
+ return null;
558
+ const stat = await fs.stat(resolvedPath);
559
+ return {
560
+ id: first.id,
561
+ conversationId: first.version === 2
562
+ ? (first.conversationId ?? first.id)
563
+ : first.id,
564
+ generation: first.version === 2 ? (first.generation ?? 0) : 0,
565
+ path: resolvedPath,
566
+ timestamp: first.timestamp,
567
+ lastActivity: stat.mtime.toISOString(),
568
+ cwd: first.cwd,
569
+ hasMessages,
570
+ ...(preview ? { preview: preview.slice(0, 100) } : {}),
571
+ };
572
+ }
573
+ catch {
574
+ return null;
575
+ }
576
+ }
577
+ /**
578
+ * Keep the newest file per conversation and sort newest first.
579
+ *
580
+ * A conversation can span several files (compaction forks a fresh one), so
581
+ * without this collapse a resumed thread appears once per checkpoint.
582
+ */
583
+ static dedupeByConversation(summaries) {
272
584
  const byConversation = new Map();
273
585
  const seenResolvedPaths = new Set();
274
586
  for (const summary of summaries) {
@@ -276,25 +588,109 @@ export class SessionManager {
276
588
  continue;
277
589
  seenResolvedPaths.add(summary.path);
278
590
  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);
591
+ if (!current || SessionManager.compareCheckpoints(summary, current) > 0) {
592
+ byConversation.set(summary.conversationId, summary);
282
593
  }
283
594
  }
284
- return [...byConversation.values()].sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
595
+ return [...byConversation.values()]
596
+ .sort((a, b) => b.lastActivity.localeCompare(a.lastActivity) ||
597
+ b.timestamp.localeCompare(a.timestamp) ||
598
+ b.path.localeCompare(a.path))
599
+ .map(({ conversationId: _conversationId, generation: _generation, ...info }) => info);
600
+ }
601
+ static compareCheckpoints(left, right) {
602
+ return ((left.generation ?? 0) - (right.generation ?? 0) ||
603
+ left.lastActivity.localeCompare(right.lastActivity) ||
604
+ left.timestamp.localeCompare(right.timestamp) ||
605
+ left.path.localeCompare(right.path));
606
+ }
607
+ async list(cwd) {
608
+ return this.summarize(await this.sessionCandidates(this.dirForCwd(cwd)));
609
+ }
610
+ /**
611
+ * One project's sessions, newest first, using the early-exit summary read.
612
+ *
613
+ * For callers that render a list rather than exact message counts — on a
614
+ * project with hundreds of sessions this is the difference between instant
615
+ * and a noticeable stall.
616
+ */
617
+ async listSummaries(cwd) {
618
+ const candidates = await this.sessionCandidates(this.dirForCwd(cwd));
619
+ const summaries = await Promise.all(candidates.map((file) => this.readSessionSummary(file)));
620
+ return SessionManager.dedupeByConversation(summaries);
621
+ }
622
+ /**
623
+ * Every session on this machine, across every project, newest first.
624
+ *
625
+ * A remote client is not browsing one checkout the way the TUI is — it is
626
+ * asking "what have I been working on?", and the answer spans projects. Each
627
+ * entry carries its own `cwd`, so the caller can group by project. Uses the
628
+ * early-exit summary read, because "every session on the machine" is exactly
629
+ * where a full parse of each file becomes a multi-second stall.
630
+ */
631
+ async listAllSummaries() {
632
+ const directories = await this.storageDirectories();
633
+ const candidates = await Promise.all(directories.map((directory) => this.sessionCandidates(directory)));
634
+ const summaries = await Promise.all(candidates.flat().map((file) => this.readSessionSummary(file)));
635
+ return SessionManager.dedupeByConversation(summaries);
636
+ }
637
+ /**
638
+ * Summarize session files, keeping the newest file per conversation.
639
+ *
640
+ * A conversation can span several files (compaction forks a fresh one), so
641
+ * without this collapse a resumed thread appears once per checkpoint.
642
+ */
643
+ async summarize(candidates) {
644
+ const summaries = await Promise.all(candidates.map((file) => this.readSessionInfo(file)));
645
+ return SessionManager.dedupeByConversation(summaries);
285
646
  }
286
647
  async getMostRecent(cwd) {
287
648
  const sessions = await this.list(cwd);
288
649
  return sessions.find((session) => session.messageCount > 0)?.path ?? null;
289
650
  }
651
+ /** Resolve an id, conversation id, or stale physical path to the newest checkpoint. */
652
+ async resolveCanonicalSession(requested, cwd) {
653
+ const looksLikePath = path.isAbsolute(requested) || requested.includes(path.sep) || isSessionPath(requested);
654
+ let candidates;
655
+ let requestedSummary = null;
656
+ if (looksLikePath) {
657
+ requestedSummary = await this.readSessionSummary(requested);
658
+ if (!requestedSummary)
659
+ return null;
660
+ candidates = await this.sessionCandidates(path.dirname(requestedSummary.path));
661
+ }
662
+ else if (cwd) {
663
+ candidates = await this.sessionCandidates(this.dirForCwd(cwd));
664
+ }
665
+ else {
666
+ const directories = await this.storageDirectories();
667
+ candidates = (await Promise.all(directories.map((dir) => this.sessionCandidates(dir)))).flat();
668
+ }
669
+ const summaries = (await Promise.all(candidates.map((candidate) => this.readSessionSummary(candidate)))).filter((summary) => summary !== null);
670
+ if (requestedSummary && !summaries.some((summary) => summary.path === requestedSummary?.path)) {
671
+ summaries.push(requestedSummary);
672
+ }
673
+ const identityMatches = requestedSummary
674
+ ? [requestedSummary]
675
+ : summaries.filter((summary) => summary.id === requested || summary.conversationId === requested);
676
+ if (identityMatches.length === 0)
677
+ return null;
678
+ const requestedIdentity = identityMatches.sort((a, b) => SessionManager.compareCheckpoints(b, a))[0].conversationId;
679
+ const checkpoints = summaries.filter((summary) => summary.conversationId === requestedIdentity);
680
+ return (checkpoints.sort((a, b) => SessionManager.compareCheckpoints(b, a))[0]?.path ??
681
+ identityMatches[0].path);
682
+ }
290
683
  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;
684
+ return this.resolveCanonicalSession(sessionId, cwd);
685
+ }
686
+ /** Locate and canonicalize a session identity across every project directory. */
687
+ async findAnyById(sessionId, cwd) {
688
+ if (cwd) {
689
+ const local = await this.resolveCanonicalSession(sessionId, cwd);
690
+ if (local)
691
+ return local;
296
692
  }
297
- return null;
693
+ return this.resolveCanonicalSession(sessionId);
298
694
  }
299
695
  async storageDirectories() {
300
696
  let entries;
@@ -305,7 +701,7 @@ export class SessionManager {
305
701
  return [];
306
702
  }
307
703
  return entries
308
- .filter((entry) => entry.isDirectory())
704
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
309
705
  .map((entry) => path.join(this.sessionsDir, entry.name));
310
706
  }
311
707
  async logicalSessionBases(directory) {
@@ -471,6 +867,84 @@ export class SessionManager {
471
867
  };
472
868
  await this.appendEntry(sessionPath, entry);
473
869
  }
870
+ /** Open the run journal for one `RunLifecycle` generation. */
871
+ async appendRunStarted(sessionPath, payload) {
872
+ await this.appendEntry(sessionPath, {
873
+ type: "custom",
874
+ kind: RUN_STARTED_CUSTOM_KIND,
875
+ id: crypto.randomUUID(),
876
+ parentId: null,
877
+ timestamp: new Date().toISOString(),
878
+ data: payload,
879
+ });
880
+ }
881
+ /** Close the run journal for one generation. Its absence marks a crashed run. */
882
+ async appendRunFinished(sessionPath, payload) {
883
+ await this.appendEntry(sessionPath, {
884
+ type: "custom",
885
+ kind: RUN_FINISHED_CUSTOM_KIND,
886
+ id: crypto.randomUUID(),
887
+ parentId: null,
888
+ timestamp: new Date().toISOString(),
889
+ data: payload,
890
+ });
891
+ }
892
+ /**
893
+ * Reconstruct the run journal in file order, pairing each `run_started` with
894
+ * the `run_finished` carrying the same generation.
895
+ *
896
+ * Generations are NOT unique across a session file. `RunLifecycle` counts
897
+ * from zero per instance, and a resumed session builds a fresh one — so the
898
+ * first run after every app restart is generation 1 again. Pairing therefore
899
+ * tracks only the runs still OPEN: a `run_finished` closes its generation and
900
+ * releases the number, and a later `run_started` reusing it opens a new run.
901
+ *
902
+ * Without that release, a crash in a resumed session was invisible: the
903
+ * reused `run_started` looked like a duplicate and was dropped, which is
904
+ * exactly the case this journal exists to catch.
905
+ *
906
+ * A repeat `run_started` for a generation that is still open IS ignored, so a
907
+ * truncated or replayed log reports one unfinished run — never a phantom pile.
908
+ */
909
+ getRunJournal(entries) {
910
+ const runs = [];
911
+ const openByGeneration = new Map();
912
+ for (const entry of entries) {
913
+ if (entry.type !== "custom")
914
+ continue;
915
+ if (entry.kind === RUN_STARTED_CUSTOM_KIND) {
916
+ const p = entry.data;
917
+ if (p?.version !== 1 || typeof p.generation !== "number")
918
+ continue;
919
+ if (openByGeneration.has(p.generation))
920
+ continue;
921
+ const run = {
922
+ generation: p.generation,
923
+ startedAt: typeof p.startedAt === "string" ? p.startedAt : entry.timestamp,
924
+ afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
925
+ };
926
+ openByGeneration.set(p.generation, run);
927
+ runs.push(run);
928
+ continue;
929
+ }
930
+ if (entry.kind === RUN_FINISHED_CUSTOM_KIND) {
931
+ const p = entry.data;
932
+ if (p?.version !== 1 || typeof p.generation !== "number")
933
+ continue;
934
+ const run = openByGeneration.get(p.generation);
935
+ if (!run || !p.outcome)
936
+ continue;
937
+ run.outcome = p.outcome;
938
+ // Closed — the number is free for the next app run to reuse.
939
+ openByGeneration.delete(p.generation);
940
+ }
941
+ }
942
+ return runs;
943
+ }
944
+ /** Runs that opened the journal but never closed it — i.e. crashed mid-flight. */
945
+ getUnfinishedRuns(entries) {
946
+ return this.getRunJournal(entries).filter((run) => run.outcome === undefined);
947
+ }
474
948
  async updateLeaf(sessionPath, leafId) {
475
949
  try {
476
950
  const writablePath = await thawSessionArchive(sessionPath);
@@ -540,11 +1014,36 @@ export class SessionManager {
540
1014
  return isCompletedItemLike(item) ? [item] : [];
541
1015
  });
542
1016
  }
1017
+ /**
1018
+ * Walk entries in file order, tracking how many branch (non-system) messages
1019
+ * have been written so far, and hand each custom entry that count. This is
1020
+ * the independent position estimate behind {@link RecordedPosition}.
1021
+ */
1022
+ mapCustomEntriesInFileOrder(entries, leafId, project) {
1023
+ // Only branch messages count — an off-branch fork's entries are not part of
1024
+ // the restored transcript the anchors are measured against.
1025
+ const onBranch = leafId
1026
+ ? new Set(this.getBranch(entries, leafId).map((e) => e.id))
1027
+ : new Set(entries.map((e) => e.id));
1028
+ const out = [];
1029
+ let messagesSoFar = 0;
1030
+ for (const entry of entries) {
1031
+ if (entry.type === "message") {
1032
+ if (onBranch.has(entry.id) && entry.message.role !== "system")
1033
+ messagesSoFar++;
1034
+ continue;
1035
+ }
1036
+ if (entry.type !== "custom")
1037
+ continue;
1038
+ out.push(...project(entry, messagesSoFar));
1039
+ }
1040
+ return out;
1041
+ }
543
1042
  /** Read all persisted Nolan turns in file order. Returns them regardless of
544
1043
  * 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)
1044
+ getNolanTurns(entries, leafId) {
1045
+ return this.mapCustomEntriesInFileOrder(entries, leafId, (entry, recordedAfterMessageCount) => {
1046
+ if (entry.kind !== NOLAN_TURN_CUSTOM_KIND)
548
1047
  return [];
549
1048
  const p = entry.data;
550
1049
  if (p?.version === 1 && typeof p.question === "string" && typeof p.reply === "string") {
@@ -554,6 +1053,7 @@ export class SessionManager {
554
1053
  question: p.question,
555
1054
  reply: p.reply,
556
1055
  afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
1056
+ recordedAfterMessageCount,
557
1057
  },
558
1058
  ];
559
1059
  }
@@ -562,9 +1062,9 @@ export class SessionManager {
562
1062
  }
563
1063
  /** Read all persisted app transcript markers in file order, validated +
564
1064
  * 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)
1065
+ getAppMarkers(entries, leafId) {
1066
+ return this.mapCustomEntriesInFileOrder(entries, leafId, (entry, recordedAfterMessageCount) => {
1067
+ if (entry.kind !== APP_MARKER_CUSTOM_KIND)
568
1068
  return [];
569
1069
  const p = entry.data;
570
1070
  const kind = p?.kind;
@@ -574,13 +1074,17 @@ export class SessionManager {
574
1074
  kind === "error" ||
575
1075
  kind === "user_hint" ||
576
1076
  kind === "compaction" ||
577
- kind === "agent_handoff")) {
1077
+ kind === "agent_handoff" ||
1078
+ kind === "model_switch" ||
1079
+ kind === "import" ||
1080
+ kind === "interrupted_run")) {
578
1081
  return [
579
1082
  {
580
1083
  version: 1,
581
1084
  kind,
582
1085
  afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
583
1086
  data: typeof p.data === "object" && p.data !== null ? p.data : {},
1087
+ recordedAfterMessageCount,
584
1088
  },
585
1089
  ];
586
1090
  }
@@ -598,9 +1102,9 @@ export class SessionManager {
598
1102
  }
599
1103
  /** Read all persisted autopilot markers in file order, validated + normalized
600
1104
  * (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)
1105
+ getAutopilotMarkers(entries, leafId) {
1106
+ return this.mapCustomEntriesInFileOrder(entries, leafId, (entry, recordedAfterMessageCount) => {
1107
+ if (entry.kind !== AUTOPILOT_MARKER_CUSTOM_KIND)
604
1108
  return [];
605
1109
  const p = entry.data;
606
1110
  const phase = p?.phase;
@@ -617,6 +1121,7 @@ export class SessionManager {
617
1121
  ...(typeof p.reason === "string" ? { reason: p.reason } : {}),
618
1122
  ...(typeof p.body === "string" ? { body: p.body } : {}),
619
1123
  afterMessageCount: typeof p.afterMessageCount === "number" ? p.afterMessageCount : 0,
1124
+ recordedAfterMessageCount,
620
1125
  },
621
1126
  ];
622
1127
  }