lattice-orchestrator 0.7.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 (440) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +58 -0
  3. package/config/logrotate.conf +15 -0
  4. package/dist/cli-parser.d.ts +11 -0
  5. package/dist/cli-parser.d.ts.map +1 -0
  6. package/dist/cli-parser.js +48 -0
  7. package/dist/cli-parser.js.map +1 -0
  8. package/dist/lattice-server.d.ts +70 -0
  9. package/dist/lattice-server.d.ts.map +1 -0
  10. package/dist/lattice-server.js +969 -0
  11. package/dist/lattice-server.js.map +1 -0
  12. package/dist/mcp-server/index.d.ts +3 -0
  13. package/dist/mcp-server/index.d.ts.map +1 -0
  14. package/dist/mcp-server/index.js +190 -0
  15. package/dist/mcp-server/index.js.map +1 -0
  16. package/dist/mcp-server/lattice-tools.d.ts +15 -0
  17. package/dist/mcp-server/lattice-tools.d.ts.map +1 -0
  18. package/dist/mcp-server/lattice-tools.js +366 -0
  19. package/dist/mcp-server/lattice-tools.js.map +1 -0
  20. package/dist/middleware/cors-setup.d.ts +7 -0
  21. package/dist/middleware/cors-setup.d.ts.map +1 -0
  22. package/dist/middleware/cors-setup.js +8 -0
  23. package/dist/middleware/cors-setup.js.map +1 -0
  24. package/dist/middleware/error-handler.d.ts +4 -0
  25. package/dist/middleware/error-handler.d.ts.map +1 -0
  26. package/dist/middleware/error-handler.js +27 -0
  27. package/dist/middleware/error-handler.js.map +1 -0
  28. package/dist/middleware/query-parser.d.ts +11 -0
  29. package/dist/middleware/query-parser.d.ts.map +1 -0
  30. package/dist/middleware/query-parser.js +68 -0
  31. package/dist/middleware/query-parser.js.map +1 -0
  32. package/dist/middleware/request-logger.d.ts +4 -0
  33. package/dist/middleware/request-logger.d.ts.map +1 -0
  34. package/dist/middleware/request-logger.js +6 -0
  35. package/dist/middleware/request-logger.js.map +1 -0
  36. package/dist/process-daemon/index.d.ts +14 -0
  37. package/dist/process-daemon/index.d.ts.map +1 -0
  38. package/dist/process-daemon/index.js +51 -0
  39. package/dist/process-daemon/index.js.map +1 -0
  40. package/dist/process-daemon/process-daemon.d.ts +101 -0
  41. package/dist/process-daemon/process-daemon.d.ts.map +1 -0
  42. package/dist/process-daemon/process-daemon.js +846 -0
  43. package/dist/process-daemon/process-daemon.js.map +1 -0
  44. package/dist/process-daemon/process-manager-client.d.ts +123 -0
  45. package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
  46. package/dist/process-daemon/process-manager-client.js +329 -0
  47. package/dist/process-daemon/process-manager-client.js.map +1 -0
  48. package/dist/process-daemon/process-manager-interface.d.ts +61 -0
  49. package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
  50. package/dist/process-daemon/process-manager-interface.js +8 -0
  51. package/dist/process-daemon/process-manager-interface.js.map +1 -0
  52. package/dist/process-daemon/test-daemon.d.ts +12 -0
  53. package/dist/process-daemon/test-daemon.d.ts.map +1 -0
  54. package/dist/process-daemon/test-daemon.js +81 -0
  55. package/dist/process-daemon/test-daemon.js.map +1 -0
  56. package/dist/process-daemon/types.d.ts +97 -0
  57. package/dist/process-daemon/types.d.ts.map +1 -0
  58. package/dist/process-daemon/types.js +8 -0
  59. package/dist/process-daemon/types.js.map +1 -0
  60. package/dist/routes/analysis.routes.d.ts +13 -0
  61. package/dist/routes/analysis.routes.d.ts.map +1 -0
  62. package/dist/routes/analysis.routes.js +520 -0
  63. package/dist/routes/analysis.routes.js.map +1 -0
  64. package/dist/routes/config.routes.d.ts +4 -0
  65. package/dist/routes/config.routes.d.ts.map +1 -0
  66. package/dist/routes/config.routes.js +27 -0
  67. package/dist/routes/config.routes.js.map +1 -0
  68. package/dist/routes/conversation.routes.d.ts +43 -0
  69. package/dist/routes/conversation.routes.d.ts.map +1 -0
  70. package/dist/routes/conversation.routes.js +79 -0
  71. package/dist/routes/conversation.routes.js.map +1 -0
  72. package/dist/routes/filesystem.routes.d.ts +4 -0
  73. package/dist/routes/filesystem.routes.d.ts.map +1 -0
  74. package/dist/routes/filesystem.routes.js +86 -0
  75. package/dist/routes/filesystem.routes.js.map +1 -0
  76. package/dist/routes/insights.routes.d.ts +17 -0
  77. package/dist/routes/insights.routes.d.ts.map +1 -0
  78. package/dist/routes/insights.routes.js +633 -0
  79. package/dist/routes/insights.routes.js.map +1 -0
  80. package/dist/routes/lattice.routes.d.ts +10 -0
  81. package/dist/routes/lattice.routes.d.ts.map +1 -0
  82. package/dist/routes/lattice.routes.js +123 -0
  83. package/dist/routes/lattice.routes.js.map +1 -0
  84. package/dist/routes/license.routes.d.ts +3 -0
  85. package/dist/routes/license.routes.d.ts.map +1 -0
  86. package/dist/routes/license.routes.js +95 -0
  87. package/dist/routes/license.routes.js.map +1 -0
  88. package/dist/routes/log.routes.d.ts +3 -0
  89. package/dist/routes/log.routes.d.ts.map +1 -0
  90. package/dist/routes/log.routes.js +184 -0
  91. package/dist/routes/log.routes.js.map +1 -0
  92. package/dist/routes/pending-question.routes.d.ts +9 -0
  93. package/dist/routes/pending-question.routes.d.ts.map +1 -0
  94. package/dist/routes/pending-question.routes.js +162 -0
  95. package/dist/routes/pending-question.routes.js.map +1 -0
  96. package/dist/routes/permission.routes.d.ts +18 -0
  97. package/dist/routes/permission.routes.d.ts.map +1 -0
  98. package/dist/routes/permission.routes.js +370 -0
  99. package/dist/routes/permission.routes.js.map +1 -0
  100. package/dist/routes/process-events.routes.d.ts +9 -0
  101. package/dist/routes/process-events.routes.d.ts.map +1 -0
  102. package/dist/routes/process-events.routes.js +141 -0
  103. package/dist/routes/process-events.routes.js.map +1 -0
  104. package/dist/routes/prototype.routes.d.ts +9 -0
  105. package/dist/routes/prototype.routes.d.ts.map +1 -0
  106. package/dist/routes/prototype.routes.js +757 -0
  107. package/dist/routes/prototype.routes.js.map +1 -0
  108. package/dist/routes/question.routes.d.ts +8 -0
  109. package/dist/routes/question.routes.d.ts.map +1 -0
  110. package/dist/routes/question.routes.js +83 -0
  111. package/dist/routes/question.routes.js.map +1 -0
  112. package/dist/routes/session-control.routes.d.ts +29 -0
  113. package/dist/routes/session-control.routes.d.ts.map +1 -0
  114. package/dist/routes/session-control.routes.js +455 -0
  115. package/dist/routes/session-control.routes.js.map +1 -0
  116. package/dist/routes/session-lifecycle.routes.d.ts +21 -0
  117. package/dist/routes/session-lifecycle.routes.d.ts.map +1 -0
  118. package/dist/routes/session-lifecycle.routes.js +256 -0
  119. package/dist/routes/session-lifecycle.routes.js.map +1 -0
  120. package/dist/routes/session-query.routes.d.ts +25 -0
  121. package/dist/routes/session-query.routes.d.ts.map +1 -0
  122. package/dist/routes/session-query.routes.js +363 -0
  123. package/dist/routes/session-query.routes.js.map +1 -0
  124. package/dist/routes/session-stream.routes.d.ts +21 -0
  125. package/dist/routes/session-stream.routes.d.ts.map +1 -0
  126. package/dist/routes/session-stream.routes.js +235 -0
  127. package/dist/routes/session-stream.routes.js.map +1 -0
  128. package/dist/routes/streaming.routes.d.ts +4 -0
  129. package/dist/routes/streaming.routes.d.ts.map +1 -0
  130. package/dist/routes/streaming.routes.js +33 -0
  131. package/dist/routes/streaming.routes.js.map +1 -0
  132. package/dist/routes/system.routes.d.ts +7 -0
  133. package/dist/routes/system.routes.d.ts.map +1 -0
  134. package/dist/routes/system.routes.js +214 -0
  135. package/dist/routes/system.routes.js.map +1 -0
  136. package/dist/routes/walkthrough.routes.d.ts +19 -0
  137. package/dist/routes/walkthrough.routes.d.ts.map +1 -0
  138. package/dist/routes/walkthrough.routes.js +688 -0
  139. package/dist/routes/walkthrough.routes.js.map +1 -0
  140. package/dist/routes/working-directories.routes.d.ts +4 -0
  141. package/dist/routes/working-directories.routes.d.ts.map +1 -0
  142. package/dist/routes/working-directories.routes.js +25 -0
  143. package/dist/routes/working-directories.routes.js.map +1 -0
  144. package/dist/server.d.ts +3 -0
  145. package/dist/server.d.ts.map +1 -0
  146. package/dist/server.js +34 -0
  147. package/dist/server.js.map +1 -0
  148. package/dist/services/ToolMetricsService.d.ts +53 -0
  149. package/dist/services/ToolMetricsService.d.ts.map +1 -0
  150. package/dist/services/ToolMetricsService.js +230 -0
  151. package/dist/services/ToolMetricsService.js.map +1 -0
  152. package/dist/services/claude-router-service.d.ts +19 -0
  153. package/dist/services/claude-router-service.d.ts.map +1 -0
  154. package/dist/services/claude-router-service.js +160 -0
  155. package/dist/services/claude-router-service.js.map +1 -0
  156. package/dist/services/commands-service.d.ts +20 -0
  157. package/dist/services/commands-service.d.ts.map +1 -0
  158. package/dist/services/commands-service.js +115 -0
  159. package/dist/services/commands-service.js.map +1 -0
  160. package/dist/services/connection-debug-logger.d.ts +85 -0
  161. package/dist/services/connection-debug-logger.d.ts.map +1 -0
  162. package/dist/services/connection-debug-logger.js +221 -0
  163. package/dist/services/connection-debug-logger.js.map +1 -0
  164. package/dist/services/debug-log.d.ts +6 -0
  165. package/dist/services/debug-log.d.ts.map +1 -0
  166. package/dist/services/debug-log.js +27 -0
  167. package/dist/services/debug-log.js.map +1 -0
  168. package/dist/services/gemini-service.d.ts +35 -0
  169. package/dist/services/gemini-service.d.ts.map +1 -0
  170. package/dist/services/gemini-service.js +256 -0
  171. package/dist/services/gemini-service.js.map +1 -0
  172. package/dist/services/infrastructure/config-service.d.ts +79 -0
  173. package/dist/services/infrastructure/config-service.d.ts.map +1 -0
  174. package/dist/services/infrastructure/config-service.js +431 -0
  175. package/dist/services/infrastructure/config-service.js.map +1 -0
  176. package/dist/services/infrastructure/cost-tracker.d.ts +112 -0
  177. package/dist/services/infrastructure/cost-tracker.d.ts.map +1 -0
  178. package/dist/services/infrastructure/cost-tracker.js +423 -0
  179. package/dist/services/infrastructure/cost-tracker.js.map +1 -0
  180. package/dist/services/infrastructure/file-system-service.d.ts +61 -0
  181. package/dist/services/infrastructure/file-system-service.d.ts.map +1 -0
  182. package/dist/services/infrastructure/file-system-service.js +348 -0
  183. package/dist/services/infrastructure/file-system-service.js.map +1 -0
  184. package/dist/services/infrastructure/log-formatter.d.ts +5 -0
  185. package/dist/services/infrastructure/log-formatter.d.ts.map +1 -0
  186. package/dist/services/infrastructure/log-formatter.js +77 -0
  187. package/dist/services/infrastructure/log-formatter.js.map +1 -0
  188. package/dist/services/infrastructure/log-stream-buffer.d.ts +11 -0
  189. package/dist/services/infrastructure/log-stream-buffer.d.ts.map +1 -0
  190. package/dist/services/infrastructure/log-stream-buffer.js +36 -0
  191. package/dist/services/infrastructure/log-stream-buffer.js.map +1 -0
  192. package/dist/services/infrastructure/logger.d.ts +71 -0
  193. package/dist/services/infrastructure/logger.d.ts.map +1 -0
  194. package/dist/services/infrastructure/logger.js +215 -0
  195. package/dist/services/infrastructure/logger.js.map +1 -0
  196. package/dist/services/infrastructure/service-registry.d.ts +86 -0
  197. package/dist/services/infrastructure/service-registry.d.ts.map +1 -0
  198. package/dist/services/infrastructure/service-registry.js +162 -0
  199. package/dist/services/infrastructure/service-registry.js.map +1 -0
  200. package/dist/services/infrastructure/stream-manager.d.ts +87 -0
  201. package/dist/services/infrastructure/stream-manager.d.ts.map +1 -0
  202. package/dist/services/infrastructure/stream-manager.js +436 -0
  203. package/dist/services/infrastructure/stream-manager.js.map +1 -0
  204. package/dist/services/infrastructure/timing.d.ts +72 -0
  205. package/dist/services/infrastructure/timing.d.ts.map +1 -0
  206. package/dist/services/infrastructure/timing.js +115 -0
  207. package/dist/services/infrastructure/timing.js.map +1 -0
  208. package/dist/services/insights/anthropic-service.d.ts +224 -0
  209. package/dist/services/insights/anthropic-service.d.ts.map +1 -0
  210. package/dist/services/insights/anthropic-service.js +1062 -0
  211. package/dist/services/insights/anthropic-service.js.map +1 -0
  212. package/dist/services/insights/insight-audit-repository.d.ts +119 -0
  213. package/dist/services/insights/insight-audit-repository.d.ts.map +1 -0
  214. package/dist/services/insights/insight-audit-repository.js +242 -0
  215. package/dist/services/insights/insight-audit-repository.js.map +1 -0
  216. package/dist/services/insights/insight-queue.d.ts +99 -0
  217. package/dist/services/insights/insight-queue.d.ts.map +1 -0
  218. package/dist/services/insights/insight-queue.js +277 -0
  219. package/dist/services/insights/insight-queue.js.map +1 -0
  220. package/dist/services/insights/insights-computer.d.ts +132 -0
  221. package/dist/services/insights/insights-computer.d.ts.map +1 -0
  222. package/dist/services/insights/insights-computer.js +936 -0
  223. package/dist/services/insights/insights-computer.js.map +1 -0
  224. package/dist/services/insights/insights-coordinator.d.ts +165 -0
  225. package/dist/services/insights/insights-coordinator.d.ts.map +1 -0
  226. package/dist/services/insights/insights-coordinator.js +1678 -0
  227. package/dist/services/insights/insights-coordinator.js.map +1 -0
  228. package/dist/services/insights/insights-event-log.d.ts +196 -0
  229. package/dist/services/insights/insights-event-log.d.ts.map +1 -0
  230. package/dist/services/insights/insights-event-log.js +319 -0
  231. package/dist/services/insights/insights-event-log.js.map +1 -0
  232. package/dist/services/lattice-service.d.ts +77 -0
  233. package/dist/services/lattice-service.d.ts.map +1 -0
  234. package/dist/services/lattice-service.js +195 -0
  235. package/dist/services/lattice-service.js.map +1 -0
  236. package/dist/services/license-service.d.ts +69 -0
  237. package/dist/services/license-service.d.ts.map +1 -0
  238. package/dist/services/license-service.js +330 -0
  239. package/dist/services/license-service.js.map +1 -0
  240. package/dist/services/mcp-config-generator.d.ts +32 -0
  241. package/dist/services/mcp-config-generator.d.ts.map +1 -0
  242. package/dist/services/mcp-config-generator.js +126 -0
  243. package/dist/services/mcp-config-generator.js.map +1 -0
  244. package/dist/services/message-filter.d.ts +22 -0
  245. package/dist/services/message-filter.d.ts.map +1 -0
  246. package/dist/services/message-filter.js +57 -0
  247. package/dist/services/message-filter.js.map +1 -0
  248. package/dist/services/notification-service.d.ts +45 -0
  249. package/dist/services/notification-service.d.ts.map +1 -0
  250. package/dist/services/notification-service.js +184 -0
  251. package/dist/services/notification-service.js.map +1 -0
  252. package/dist/services/pending-question-service.d.ts +97 -0
  253. package/dist/services/pending-question-service.d.ts.map +1 -0
  254. package/dist/services/pending-question-service.js +223 -0
  255. package/dist/services/pending-question-service.js.map +1 -0
  256. package/dist/services/permission-event-log.d.ts +136 -0
  257. package/dist/services/permission-event-log.d.ts.map +1 -0
  258. package/dist/services/permission-event-log.js +252 -0
  259. package/dist/services/permission-event-log.js.map +1 -0
  260. package/dist/services/permission-pattern-matcher.d.ts +82 -0
  261. package/dist/services/permission-pattern-matcher.d.ts.map +1 -0
  262. package/dist/services/permission-pattern-matcher.js +294 -0
  263. package/dist/services/permission-pattern-matcher.js.map +1 -0
  264. package/dist/services/permission-tracker.d.ts +67 -0
  265. package/dist/services/permission-tracker.d.ts.map +1 -0
  266. package/dist/services/permission-tracker.js +162 -0
  267. package/dist/services/permission-tracker.js.map +1 -0
  268. package/dist/services/process/claude-process-manager.d.ts +142 -0
  269. package/dist/services/process/claude-process-manager.d.ts.map +1 -0
  270. package/dist/services/process/claude-process-manager.js +1218 -0
  271. package/dist/services/process/claude-process-manager.js.map +1 -0
  272. package/dist/services/process/conversation-status-manager.d.ts +110 -0
  273. package/dist/services/process/conversation-status-manager.d.ts.map +1 -0
  274. package/dist/services/process/conversation-status-manager.js +349 -0
  275. package/dist/services/process/conversation-status-manager.js.map +1 -0
  276. package/dist/services/process/json-lines-parser.d.ts +19 -0
  277. package/dist/services/process/json-lines-parser.d.ts.map +1 -0
  278. package/dist/services/process/json-lines-parser.js +59 -0
  279. package/dist/services/process/json-lines-parser.js.map +1 -0
  280. package/dist/services/process/process-event-log.d.ts +263 -0
  281. package/dist/services/process/process-event-log.d.ts.map +1 -0
  282. package/dist/services/process/process-event-log.js +509 -0
  283. package/dist/services/process/process-event-log.js.map +1 -0
  284. package/dist/services/process/process-manager-factory.d.ts +109 -0
  285. package/dist/services/process/process-manager-factory.d.ts.map +1 -0
  286. package/dist/services/process/process-manager-factory.js +338 -0
  287. package/dist/services/process/process-manager-factory.js.map +1 -0
  288. package/dist/services/question-tracker.d.ts +51 -0
  289. package/dist/services/question-tracker.d.ts.map +1 -0
  290. package/dist/services/question-tracker.js +111 -0
  291. package/dist/services/question-tracker.js.map +1 -0
  292. package/dist/services/sessions/claude-history-reader.d.ts +139 -0
  293. package/dist/services/sessions/claude-history-reader.d.ts.map +1 -0
  294. package/dist/services/sessions/claude-history-reader.js +864 -0
  295. package/dist/services/sessions/claude-history-reader.js.map +1 -0
  296. package/dist/services/sessions/conversation-cache.d.ts +98 -0
  297. package/dist/services/sessions/conversation-cache.d.ts.map +1 -0
  298. package/dist/services/sessions/conversation-cache.js +329 -0
  299. package/dist/services/sessions/conversation-cache.js.map +1 -0
  300. package/dist/services/sessions/session-activity-watcher.d.ts +67 -0
  301. package/dist/services/sessions/session-activity-watcher.d.ts.map +1 -0
  302. package/dist/services/sessions/session-activity-watcher.js +236 -0
  303. package/dist/services/sessions/session-activity-watcher.js.map +1 -0
  304. package/dist/services/sessions/session-analysis-service.d.ts +72 -0
  305. package/dist/services/sessions/session-analysis-service.d.ts.map +1 -0
  306. package/dist/services/sessions/session-analysis-service.js +373 -0
  307. package/dist/services/sessions/session-analysis-service.js.map +1 -0
  308. package/dist/services/sessions/session-branch-service.d.ts +76 -0
  309. package/dist/services/sessions/session-branch-service.d.ts.map +1 -0
  310. package/dist/services/sessions/session-branch-service.js +355 -0
  311. package/dist/services/sessions/session-branch-service.js.map +1 -0
  312. package/dist/services/sessions/session-info-service.d.ts +455 -0
  313. package/dist/services/sessions/session-info-service.d.ts.map +1 -0
  314. package/dist/services/sessions/session-info-service.js +1640 -0
  315. package/dist/services/sessions/session-info-service.js.map +1 -0
  316. package/dist/services/sessions/session-marks-repository.d.ts +78 -0
  317. package/dist/services/sessions/session-marks-repository.d.ts.map +1 -0
  318. package/dist/services/sessions/session-marks-repository.js +263 -0
  319. package/dist/services/sessions/session-marks-repository.js.map +1 -0
  320. package/dist/services/sessions/session-marks-service.d.ts +137 -0
  321. package/dist/services/sessions/session-marks-service.d.ts.map +1 -0
  322. package/dist/services/sessions/session-marks-service.js +562 -0
  323. package/dist/services/sessions/session-marks-service.js.map +1 -0
  324. package/dist/services/sessions/session-review-service.d.ts +98 -0
  325. package/dist/services/sessions/session-review-service.d.ts.map +1 -0
  326. package/dist/services/sessions/session-review-service.js +629 -0
  327. package/dist/services/sessions/session-review-service.js.map +1 -0
  328. package/dist/services/sessions/turn-capture-service.d.ts +83 -0
  329. package/dist/services/sessions/turn-capture-service.d.ts.map +1 -0
  330. package/dist/services/sessions/turn-capture-service.js +477 -0
  331. package/dist/services/sessions/turn-capture-service.js.map +1 -0
  332. package/dist/services/sessions/turn-repository.d.ts +48 -0
  333. package/dist/services/sessions/turn-repository.d.ts.map +1 -0
  334. package/dist/services/sessions/turn-repository.js +103 -0
  335. package/dist/services/sessions/turn-repository.js.map +1 -0
  336. package/dist/services/walkthrough-service.d.ts +226 -0
  337. package/dist/services/walkthrough-service.d.ts.map +1 -0
  338. package/dist/services/walkthrough-service.js +1112 -0
  339. package/dist/services/walkthrough-service.js.map +1 -0
  340. package/dist/services/walkthrough-skill-prompt.d.ts +34 -0
  341. package/dist/services/walkthrough-skill-prompt.d.ts.map +1 -0
  342. package/dist/services/walkthrough-skill-prompt.js +313 -0
  343. package/dist/services/walkthrough-skill-prompt.js.map +1 -0
  344. package/dist/services/web-push-service.d.ts +48 -0
  345. package/dist/services/web-push-service.d.ts.map +1 -0
  346. package/dist/services/web-push-service.js +186 -0
  347. package/dist/services/web-push-service.js.map +1 -0
  348. package/dist/services/working-directories-service.d.ts +19 -0
  349. package/dist/services/working-directories-service.d.ts.map +1 -0
  350. package/dist/services/working-directories-service.js +103 -0
  351. package/dist/services/working-directories-service.js.map +1 -0
  352. package/dist/types/config.d.ts +122 -0
  353. package/dist/types/config.d.ts.map +1 -0
  354. package/dist/types/config.js +21 -0
  355. package/dist/types/config.js.map +1 -0
  356. package/dist/types/express.d.ts +5 -0
  357. package/dist/types/express.d.ts.map +1 -0
  358. package/dist/types/express.js +2 -0
  359. package/dist/types/express.js.map +1 -0
  360. package/dist/types/index.d.ts +400 -0
  361. package/dist/types/index.d.ts.map +1 -0
  362. package/dist/types/index.js +41 -0
  363. package/dist/types/index.js.map +1 -0
  364. package/dist/types/insights.d.ts +176 -0
  365. package/dist/types/insights.d.ts.map +1 -0
  366. package/dist/types/insights.js +23 -0
  367. package/dist/types/insights.js.map +1 -0
  368. package/dist/types/license.d.ts +70 -0
  369. package/dist/types/license.d.ts.map +1 -0
  370. package/dist/types/license.js +5 -0
  371. package/dist/types/license.js.map +1 -0
  372. package/dist/types/router-config.d.ts +13 -0
  373. package/dist/types/router-config.d.ts.map +1 -0
  374. package/dist/types/router-config.js +2 -0
  375. package/dist/types/router-config.js.map +1 -0
  376. package/dist/utils/constants.d.ts +26 -0
  377. package/dist/utils/constants.d.ts.map +1 -0
  378. package/dist/utils/constants.js +28 -0
  379. package/dist/utils/constants.js.map +1 -0
  380. package/dist/utils/machine-id.d.ts +7 -0
  381. package/dist/utils/machine-id.d.ts.map +1 -0
  382. package/dist/utils/machine-id.js +76 -0
  383. package/dist/utils/machine-id.js.map +1 -0
  384. package/dist/utils/server-startup.d.ts +11 -0
  385. package/dist/utils/server-startup.d.ts.map +1 -0
  386. package/dist/utils/server-startup.js +9 -0
  387. package/dist/utils/server-startup.js.map +1 -0
  388. package/dist/utils/update-check.d.ts +13 -0
  389. package/dist/utils/update-check.d.ts.map +1 -0
  390. package/dist/utils/update-check.js +90 -0
  391. package/dist/utils/update-check.js.map +1 -0
  392. package/dist/web/assets/ArchivedCardPrototype-S9ifiasa.js +5 -0
  393. package/dist/web/assets/BannerGallery-B__rJV6F.js +1 -0
  394. package/dist/web/assets/BannerPrototype-DBKP9Uiu.js +52 -0
  395. package/dist/web/assets/CodeHikeExperiment-B8jjWAFy.js +15 -0
  396. package/dist/web/assets/ContextTooltipVariations-DzklAFam.js +1 -0
  397. package/dist/web/assets/FontShowcasePrototype-KIMEWeP2.js +13 -0
  398. package/dist/web/assets/GeometricGallery-DddlWhHK.js +1 -0
  399. package/dist/web/assets/HistoryWalkthroughPrototype-DeniRRdw.js +18 -0
  400. package/dist/web/assets/InlineWalkthroughPrototype-Csd5r_Hk.js +1 -0
  401. package/dist/web/assets/MarkButtonPrototype-CxhxE0RP.js +1 -0
  402. package/dist/web/assets/MenuStylesPrototype-D7neA6YM.js +1 -0
  403. package/dist/web/assets/MomentCardVariations-2GT7GyFn.js +1 -0
  404. package/dist/web/assets/MomentHeaderVariations-DhGEw4XC.js +1 -0
  405. package/dist/web/assets/NarrativeWalkthroughDemo-B5C566fu.js +389 -0
  406. package/dist/web/assets/OutcomeVariations-BrZfsVcs.js +1 -0
  407. package/dist/web/assets/PermissionPatternPickerPrototype-CBOhe2Me.js +1 -0
  408. package/dist/web/assets/PermissionPrototype-BcF-a5an.js +1 -0
  409. package/dist/web/assets/PipelineGallery-BJhyM0rx.js +1 -0
  410. package/dist/web/assets/ScopeHeaderPrototype-GD1HNfaO.js +1 -0
  411. package/dist/web/assets/ScopeHeaderStylesPrototype-aa4uNJJ1.js +1 -0
  412. package/dist/web/assets/ScrollycodingPrototype-CKW1bf72.js +70 -0
  413. package/dist/web/assets/SectionHeaderVariations-DM8vUwfj.js +1 -0
  414. package/dist/web/assets/SemanticGallery-CtQEo7St.js +1 -0
  415. package/dist/web/assets/SessionCardPrototype-CgHCIMHe.js +1 -0
  416. package/dist/web/assets/SessionSidebarVariations-DMQL3Azj.js +3 -0
  417. package/dist/web/assets/SessionStartPrototype-Cwsv01Rr.js +1 -0
  418. package/dist/web/assets/SmartMenuPrototype-Br37Qbs_.js +1 -0
  419. package/dist/web/assets/StyleGallery-rZgrploB.js +1 -0
  420. package/dist/web/assets/TimelineCardPrototype-lzPc5mhe.js +19 -0
  421. package/dist/web/assets/ToolbarPrototype-Dm4BNZra.js +1 -0
  422. package/dist/web/assets/TooltipExperiment-Dy8QzTIP.js +13 -0
  423. package/dist/web/assets/WalkthroughCTAPrototype-uHoovujd.js +1 -0
  424. package/dist/web/assets/WalkthroughHeaderVariations-Do7Di1g1.js +1 -0
  425. package/dist/web/assets/WalkthroughShowcase-sGmRoPoM.js +112 -0
  426. package/dist/web/assets/arrow-right-D46Nx1mC.js +1 -0
  427. package/dist/web/assets/brain-BXIZKtOZ.js +1 -0
  428. package/dist/web/assets/grid-3x3-Cb81B62m.js +1 -0
  429. package/dist/web/assets/main-B1fyog77.js +321 -0
  430. package/dist/web/assets/main-C2PK2Klg.css +1 -0
  431. package/dist/web/assets/semantic-variations-Bd-W7ea2.js +1 -0
  432. package/dist/web/assets/target-Cf92wDTW.js +1 -0
  433. package/dist/web/favicon.png +0 -0
  434. package/dist/web/favicon.svg +22 -0
  435. package/dist/web/icon-192x192.png +0 -0
  436. package/dist/web/icon-512x512.png +0 -0
  437. package/dist/web/index.html +45 -0
  438. package/dist/web/manifest.json +61 -0
  439. package/package.json +192 -0
  440. package/scripts/postinstall.js +60 -0
@@ -0,0 +1,1640 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import Database from 'better-sqlite3';
4
+ import { CONFIG_DIR, CONFIG_DIR_NAME } from '../../utils/constants.js';
5
+ import { createLogger } from '../infrastructure/logger.js';
6
+ import { InsightAuditRepository } from '../insights/insight-audit-repository.js';
7
+ import { TurnRepository } from './turn-repository.js';
8
+ import { SessionMarksRepository } from './session-marks-repository.js';
9
+ /**
10
+ * SessionInfoService manages session information using SQLite backend
11
+ * Stores session metadata including custom names in ~/.claudia/session-info.db
12
+ * Provides fast lookups and updates for session-specific data
13
+ *
14
+ * EXTRACTED REPOSITORIES (prefer using these directly for new code):
15
+ * - auditRepository (InsightAuditRepository) - Insight audit logging
16
+ * - turnRepository (TurnRepository) - Per-turn summaries
17
+ * - marksRepository (SessionMarksRepository) - User-flagged session marks
18
+ *
19
+ * REMAINING SECTIONS (search for "// ==="):
20
+ * 1. Core Session CRUD - getSessionInfo, updateSessionInfo, deleteSession
21
+ * 2. Recommendation Management - getPendingRecommendations, acceptRecommendation
22
+ * 3. Insights Cache - getInsights, setInsights, updateSessionMetadata
23
+ * 4. Insight Auditing - wrapper methods → auditRepository
24
+ * 5. Turn Management - wrapper methods → turnRepository
25
+ * 6+7. Session Marks - wrapper methods → marksRepository
26
+ * 8. Branch Operations - copyTurnsForBranch, copyMarksForBranch
27
+ */
28
+ export class SessionInfoService {
29
+ static instance;
30
+ logger;
31
+ dbPath;
32
+ configDir;
33
+ isInitialized = false;
34
+ db;
35
+ // Extracted repositories (lazy-initialized after db is ready)
36
+ _auditRepository;
37
+ _turnRepository;
38
+ _marksRepository;
39
+ getSessionStmt;
40
+ insertSessionStmt;
41
+ updateSessionStmt;
42
+ deleteSessionStmt;
43
+ getAllStmt;
44
+ countStmt;
45
+ archiveAllStmt;
46
+ setMetadataStmt;
47
+ getMetadataStmt;
48
+ // Insights statements
49
+ getInsightsStmt;
50
+ upsertInsightsStmt;
51
+ getAllInsightsStmt;
52
+ markInsightsStaleStmt;
53
+ getStaleInsightsStmt;
54
+ updateMetadataFieldsStmt;
55
+ // File changes statements
56
+ insertFileChangeStmt;
57
+ getFileChangesStmt;
58
+ getFileChangesByFileStmt;
59
+ constructor(customConfigDir) {
60
+ this.logger = createLogger('SessionInfoService');
61
+ this.initializePaths(customConfigDir);
62
+ }
63
+ static getInstance() {
64
+ if (!SessionInfoService.instance) {
65
+ SessionInfoService.instance = new SessionInfoService();
66
+ }
67
+ return SessionInfoService.instance;
68
+ }
69
+ static resetInstance() {
70
+ if (SessionInfoService.instance) {
71
+ SessionInfoService.instance.isInitialized = false;
72
+ }
73
+ SessionInfoService.instance = null;
74
+ }
75
+ initializePaths(customConfigDir) {
76
+ if (customConfigDir) {
77
+ if (customConfigDir === ':memory:') {
78
+ this.configDir = ':memory:';
79
+ this.dbPath = ':memory:';
80
+ return;
81
+ }
82
+ this.configDir = path.join(customConfigDir, CONFIG_DIR_NAME);
83
+ }
84
+ else {
85
+ this.configDir = CONFIG_DIR;
86
+ }
87
+ this.dbPath = path.join(this.configDir, 'session-info.db');
88
+ this.logger.debug('Initializing paths', {
89
+ configDir: this.configDir,
90
+ dbPath: this.dbPath
91
+ });
92
+ }
93
+ async initialize() {
94
+ if (this.isInitialized) {
95
+ return;
96
+ }
97
+ try {
98
+ if (this.dbPath !== ':memory:' && !fs.existsSync(this.configDir)) {
99
+ fs.mkdirSync(this.configDir, { recursive: true });
100
+ this.logger.debug('Created config directory', { dir: this.configDir });
101
+ }
102
+ this.db = new Database(this.dbPath);
103
+ this.db.pragma('journal_mode = WAL');
104
+ this.db.pragma('busy_timeout = 10000');
105
+ this.db.exec(`
106
+ CREATE TABLE IF NOT EXISTS sessions (
107
+ session_id TEXT PRIMARY KEY,
108
+ custom_name TEXT NOT NULL DEFAULT '',
109
+ created_at TEXT NOT NULL,
110
+ updated_at TEXT NOT NULL,
111
+ version INTEGER NOT NULL,
112
+ pinned INTEGER NOT NULL DEFAULT 0,
113
+ archived INTEGER NOT NULL DEFAULT 0,
114
+ continuation_session_id TEXT NOT NULL DEFAULT '',
115
+ initial_commit_head TEXT NOT NULL DEFAULT '',
116
+ permission_mode TEXT NOT NULL DEFAULT 'default'
117
+ );
118
+ CREATE TABLE IF NOT EXISTS metadata (
119
+ key TEXT PRIMARY KEY,
120
+ value TEXT NOT NULL
121
+ );
122
+ CREATE TABLE IF NOT EXISTS session_insights (
123
+ session_id TEXT PRIMARY KEY,
124
+ description TEXT,
125
+ purposes TEXT NOT NULL DEFAULT '[]',
126
+ milestones TEXT NOT NULL DEFAULT '[]',
127
+ recent_actions TEXT NOT NULL DEFAULT '[]',
128
+ progress_completed INTEGER NOT NULL DEFAULT 0,
129
+ progress_total INTEGER NOT NULL DEFAULT 0,
130
+ outstanding_tasks TEXT NOT NULL DEFAULT '[]',
131
+ completed_tasks TEXT NOT NULL DEFAULT '[]',
132
+ current_task TEXT,
133
+ theme TEXT,
134
+ computed_at TEXT NOT NULL,
135
+ stale INTEGER NOT NULL DEFAULT 0
136
+ );
137
+ CREATE TABLE IF NOT EXISTS session_turns (
138
+ id TEXT PRIMARY KEY,
139
+ session_id TEXT NOT NULL,
140
+ turn_number INTEGER NOT NULL,
141
+ timestamp TEXT NOT NULL,
142
+ headline TEXT NOT NULL,
143
+ actions TEXT NOT NULL DEFAULT '[]',
144
+ tag TEXT NOT NULL,
145
+ icon TEXT NOT NULL,
146
+ exit_code INTEGER,
147
+ termination_reason TEXT NOT NULL DEFAULT 'normal_completion',
148
+ tool_count INTEGER NOT NULL DEFAULT 0,
149
+ incomplete INTEGER NOT NULL DEFAULT 0,
150
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
151
+ );
152
+ CREATE INDEX IF NOT EXISTS idx_turns_session_id ON session_turns(session_id);
153
+ CREATE INDEX IF NOT EXISTS idx_turns_timestamp ON session_turns(timestamp);
154
+
155
+ CREATE TABLE IF NOT EXISTS recommendations (
156
+ id TEXT PRIMARY KEY,
157
+ session_id TEXT NOT NULL,
158
+ category TEXT NOT NULL,
159
+ friction TEXT NOT NULL,
160
+ suggestion TEXT NOT NULL,
161
+ why TEXT NOT NULL,
162
+ status TEXT NOT NULL DEFAULT 'pending',
163
+ created_at TEXT NOT NULL,
164
+ actioned_at TEXT,
165
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
166
+ );
167
+ CREATE INDEX IF NOT EXISTS idx_recommendations_session_id ON recommendations(session_id);
168
+ CREATE INDEX IF NOT EXISTS idx_recommendations_status ON recommendations(status);
169
+
170
+ CREATE TABLE IF NOT EXISTS friction_marks (
171
+ id TEXT PRIMARY KEY,
172
+ session_id TEXT NOT NULL,
173
+ message_id TEXT NOT NULL,
174
+ content_type TEXT NOT NULL,
175
+ tool_name TEXT,
176
+ content_preview TEXT NOT NULL,
177
+ full_content TEXT NOT NULL,
178
+ marked_at TEXT NOT NULL,
179
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
180
+ );
181
+ CREATE INDEX IF NOT EXISTS idx_friction_marks_session ON friction_marks(session_id);
182
+
183
+ -- Session marks v2: supports multiple mark types with turn context
184
+ CREATE TABLE IF NOT EXISTS session_marks (
185
+ id TEXT PRIMARY KEY,
186
+ session_id TEXT NOT NULL,
187
+ message_id TEXT NOT NULL,
188
+ mark_type TEXT NOT NULL DEFAULT 'friction',
189
+ content_type TEXT NOT NULL,
190
+ tool_name TEXT,
191
+ content_preview TEXT NOT NULL,
192
+ turn_number INTEGER,
193
+ turn_context TEXT,
194
+ marked_at TEXT NOT NULL,
195
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
196
+ );
197
+ CREATE INDEX IF NOT EXISTS idx_session_marks_session ON session_marks(session_id);
198
+ CREATE INDEX IF NOT EXISTS idx_session_marks_type ON session_marks(mark_type);
199
+
200
+ -- File changes captured from Edit/Write tool calls for walkthrough generation
201
+ CREATE TABLE IF NOT EXISTS turn_file_changes (
202
+ id TEXT PRIMARY KEY,
203
+ session_id TEXT NOT NULL,
204
+ turn_number INTEGER NOT NULL,
205
+ tool_name TEXT NOT NULL,
206
+ file_path TEXT NOT NULL,
207
+ old_string TEXT,
208
+ new_string TEXT NOT NULL,
209
+ timestamp TEXT NOT NULL,
210
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
211
+ );
212
+ CREATE INDEX IF NOT EXISTS idx_turn_file_changes_session ON turn_file_changes(session_id);
213
+ CREATE INDEX IF NOT EXISTS idx_turn_file_changes_file ON turn_file_changes(file_path);
214
+
215
+ -- Walkthrough scope analysis cache
216
+ CREATE TABLE IF NOT EXISTS walkthrough_scope_cache (
217
+ session_id TEXT PRIMARY KEY,
218
+ turn_count INTEGER NOT NULL,
219
+ generated_at TEXT NOT NULL,
220
+ analysis_json TEXT NOT NULL,
221
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
222
+ );
223
+ `);
224
+ // Migration: Add new columns if they don't exist (for existing databases)
225
+ this.migrateSessionsColumns();
226
+ this.migrateInsightsColumns();
227
+ this.migrateTurnTags();
228
+ this.migrateTurnsColumns();
229
+ this.migrateToSessionMarks();
230
+ this.prepareStatements();
231
+ this.ensureMetadata();
232
+ // Initialize extracted repositories
233
+ this._auditRepository = new InsightAuditRepository(this.db);
234
+ this._turnRepository = new TurnRepository(this.db);
235
+ this._marksRepository = new SessionMarksRepository(this.db, (shortId) => this.resolveSessionId(shortId));
236
+ this.isInitialized = true;
237
+ }
238
+ catch (error) {
239
+ this.logger.error('Failed to initialize session info database', error);
240
+ throw new Error(`Session info database initialization failed: ${error instanceof Error ? error.message : String(error)}`);
241
+ }
242
+ }
243
+ migrateSessionsColumns() {
244
+ // Check if new columns exist on sessions table and add them if not
245
+ try {
246
+ const tableInfo = this.db.pragma('table_info(sessions)');
247
+ const columnNames = tableInfo.map(col => col.name);
248
+ if (!columnNames.includes('identity_image')) {
249
+ this.db.exec("ALTER TABLE sessions ADD COLUMN identity_image TEXT DEFAULT NULL");
250
+ this.logger.info('Added identity_image column to sessions');
251
+ }
252
+ if (!columnNames.includes('last_termination_reason')) {
253
+ this.db.exec("ALTER TABLE sessions ADD COLUMN last_termination_reason TEXT DEFAULT NULL");
254
+ this.logger.info('Added last_termination_reason column to sessions');
255
+ }
256
+ // Branch lineage columns
257
+ if (!columnNames.includes('branched_from_session_id')) {
258
+ this.db.exec("ALTER TABLE sessions ADD COLUMN branched_from_session_id TEXT DEFAULT NULL");
259
+ this.logger.info('Added branched_from_session_id column to sessions');
260
+ }
261
+ if (!columnNames.includes('branched_at_turn')) {
262
+ this.db.exec("ALTER TABLE sessions ADD COLUMN branched_at_turn INTEGER DEFAULT NULL");
263
+ this.logger.info('Added branched_at_turn column to sessions');
264
+ }
265
+ }
266
+ catch (error) {
267
+ this.logger.debug('Sessions migration check failed', { error });
268
+ }
269
+ }
270
+ migrateInsightsColumns() {
271
+ // Check if new columns exist and add them if not
272
+ try {
273
+ const tableInfo = this.db.pragma('table_info(session_insights)');
274
+ const columnNames = tableInfo.map(col => col.name);
275
+ if (!columnNames.includes('purposes')) {
276
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN purposes TEXT NOT NULL DEFAULT '[]'");
277
+ this.logger.info('Added purposes column to session_insights');
278
+ }
279
+ if (!columnNames.includes('milestones')) {
280
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN milestones TEXT NOT NULL DEFAULT '[]'");
281
+ this.logger.info('Added milestones column to session_insights');
282
+ }
283
+ if (!columnNames.includes('recent_actions')) {
284
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN recent_actions TEXT NOT NULL DEFAULT '[]'");
285
+ this.logger.info('Added recent_actions column to session_insights');
286
+ }
287
+ // New ontology columns
288
+ if (!columnNames.includes('context')) {
289
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN context TEXT DEFAULT NULL");
290
+ this.logger.info('Added context column to session_insights');
291
+ }
292
+ if (!columnNames.includes('notable')) {
293
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN notable TEXT NOT NULL DEFAULT '[]'");
294
+ this.logger.info('Added notable column to session_insights');
295
+ }
296
+ if (!columnNames.includes('tags')) {
297
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN tags TEXT DEFAULT NULL");
298
+ this.logger.info('Added tags column to session_insights');
299
+ }
300
+ if (!columnNames.includes('panels')) {
301
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN panels TEXT NOT NULL DEFAULT '[]'");
302
+ this.logger.info('Added panels column to session_insights');
303
+ }
304
+ // Delta detection metadata columns
305
+ if (!columnNames.includes('message_count')) {
306
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN message_count INTEGER DEFAULT NULL");
307
+ this.logger.info('Added message_count column to session_insights');
308
+ }
309
+ if (!columnNames.includes('last_delta_check_at')) {
310
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN last_delta_check_at TEXT DEFAULT NULL");
311
+ this.logger.info('Added last_delta_check_at column to session_insights');
312
+ }
313
+ if (!columnNames.includes('patched_at')) {
314
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN patched_at TEXT DEFAULT NULL");
315
+ this.logger.info('Added patched_at column to session_insights');
316
+ }
317
+ if (!columnNames.includes('current_state')) {
318
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN current_state TEXT DEFAULT NULL");
319
+ this.logger.info('Added current_state column to session_insights');
320
+ }
321
+ if (!columnNames.includes('last_patch_trace_id')) {
322
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN last_patch_trace_id TEXT DEFAULT NULL");
323
+ this.logger.info('Added last_patch_trace_id column to session_insights');
324
+ }
325
+ // V9 accumulative fields
326
+ if (!columnNames.includes('progress_items')) {
327
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN progress_items TEXT NOT NULL DEFAULT '[]'");
328
+ this.logger.info('Added progress_items column to session_insights');
329
+ }
330
+ if (!columnNames.includes('notable_events')) {
331
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN notable_events TEXT NOT NULL DEFAULT '[]'");
332
+ this.logger.info('Added notable_events column to session_insights');
333
+ }
334
+ if (!columnNames.includes('recent_actions_v2')) {
335
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN recent_actions_v2 TEXT NOT NULL DEFAULT '[]'");
336
+ this.logger.info('Added recent_actions_v2 column to session_insights');
337
+ }
338
+ if (!columnNames.includes('progress_summary')) {
339
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN progress_summary TEXT DEFAULT NULL");
340
+ this.logger.info('Added progress_summary column to session_insights');
341
+ }
342
+ if (!columnNames.includes('purpose')) {
343
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN purpose TEXT DEFAULT NULL");
344
+ this.logger.info('Added purpose column to session_insights');
345
+ }
346
+ // Accumulated propositions for post-session analysis
347
+ if (!columnNames.includes('propositions')) {
348
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN propositions TEXT NOT NULL DEFAULT '[]'");
349
+ this.logger.info('Added propositions column to session_insights');
350
+ }
351
+ // Tool metrics columns (Phase 1 of list endpoint optimization)
352
+ // These cache ToolMetrics to avoid re-parsing all messages on every list request
353
+ if (!columnNames.includes('lines_added')) {
354
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN lines_added INTEGER DEFAULT 0");
355
+ this.logger.info('Added lines_added column to session_insights');
356
+ }
357
+ if (!columnNames.includes('lines_removed')) {
358
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN lines_removed INTEGER DEFAULT 0");
359
+ this.logger.info('Added lines_removed column to session_insights');
360
+ }
361
+ if (!columnNames.includes('edit_count')) {
362
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN edit_count INTEGER DEFAULT 0");
363
+ this.logger.info('Added edit_count column to session_insights');
364
+ }
365
+ if (!columnNames.includes('write_count')) {
366
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN write_count INTEGER DEFAULT 0");
367
+ this.logger.info('Added write_count column to session_insights');
368
+ }
369
+ if (!columnNames.includes('metrics_updated_at')) {
370
+ this.db.exec("ALTER TABLE session_insights ADD COLUMN metrics_updated_at TEXT DEFAULT NULL");
371
+ this.logger.info('Added metrics_updated_at column to session_insights');
372
+ }
373
+ }
374
+ catch (error) {
375
+ this.logger.debug('Migration check failed (table may not exist yet)', { error });
376
+ }
377
+ }
378
+ /**
379
+ * Clean turn tags that have trailing emojis from the Haiku bug.
380
+ * Uses metadata key to ensure this only runs once per database.
381
+ */
382
+ migrateTurnTags() {
383
+ try {
384
+ // Check if migration already ran
385
+ const migrated = this.db.prepare('SELECT value FROM metadata WHERE key = ?').get('turn_tags_cleaned');
386
+ if (migrated?.value === 'true') {
387
+ return;
388
+ }
389
+ // Find all turns with emoji in tag field and clean them
390
+ const turnsWithEmoji = this.db.prepare(`
391
+ SELECT id, tag FROM session_turns
392
+ WHERE tag LIKE '%🔍%' OR tag LIKE '%🔧%' OR tag LIKE '%💡%'
393
+ OR tag LIKE '%🏗%' OR tag LIKE '%♻%' OR tag LIKE '%🐛%'
394
+ OR tag LIKE '%💬%' OR tag LIKE '%⚖%' OR tag LIKE '%🔄%' OR tag LIKE '%🚧%'
395
+ `).all();
396
+ if (turnsWithEmoji.length > 0) {
397
+ const updateStmt = this.db.prepare('UPDATE session_turns SET tag = ? WHERE id = ?');
398
+ // Same regex used in turn-capture-service.ts
399
+ const emojiStrippingRegex = /[\p{Extended_Pictographic}\p{Variation_Selector}\s]+$/u;
400
+ for (const turn of turnsWithEmoji) {
401
+ const cleanTag = turn.tag.replace(emojiStrippingRegex, '').trim() || 'discuss';
402
+ updateStmt.run(cleanTag, turn.id);
403
+ }
404
+ this.logger.info(`Cleaned ${turnsWithEmoji.length} turn tags with trailing emojis`);
405
+ }
406
+ // Mark migration as complete
407
+ this.db.prepare('INSERT INTO metadata (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value')
408
+ .run('turn_tags_cleaned', 'true');
409
+ }
410
+ catch (error) {
411
+ this.logger.debug('Turn tag migration failed', { error });
412
+ }
413
+ }
414
+ migrateTurnsColumns() {
415
+ // Add termination_reason column to session_turns if it doesn't exist
416
+ try {
417
+ const tableInfo = this.db.pragma('table_info(session_turns)');
418
+ const columnNames = tableInfo.map(col => col.name);
419
+ if (!columnNames.includes('termination_reason')) {
420
+ this.db.exec("ALTER TABLE session_turns ADD COLUMN termination_reason TEXT NOT NULL DEFAULT 'normal_completion'");
421
+ this.logger.info('Added termination_reason column to session_turns');
422
+ }
423
+ }
424
+ catch (error) {
425
+ this.logger.debug('Session turns migration check failed', { error });
426
+ }
427
+ }
428
+ /**
429
+ * Migrate existing friction_marks to session_marks table.
430
+ * Runs once, tracked via metadata key.
431
+ */
432
+ migrateToSessionMarks() {
433
+ try {
434
+ // Check if migration already ran
435
+ const migrated = this.db.prepare('SELECT value FROM metadata WHERE key = ?').get('session_marks_migrated');
436
+ if (migrated?.value === 'true') {
437
+ return;
438
+ }
439
+ // Check if there are existing friction_marks to migrate
440
+ const count = this.db.prepare('SELECT COUNT(*) as count FROM friction_marks').get();
441
+ if (count.count > 0) {
442
+ this.logger.info('Migrating friction_marks to session_marks', { count: count.count });
443
+ // Copy existing friction marks to new table with mark_type='friction'
444
+ // Note: we don't have turn_context for old marks, they'll work with fallback display
445
+ this.db.exec(`
446
+ INSERT INTO session_marks (id, session_id, message_id, mark_type, content_type, tool_name, content_preview, turn_number, turn_context, marked_at)
447
+ SELECT id, session_id, message_id, 'friction', content_type, tool_name, content_preview, NULL, NULL, marked_at
448
+ FROM friction_marks
449
+ `);
450
+ this.logger.info('Migrated friction_marks to session_marks', { count: count.count });
451
+ }
452
+ // Mark migration as complete
453
+ this.db.prepare('INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)').run('session_marks_migrated', 'true');
454
+ this.logger.info('Session marks migration complete');
455
+ }
456
+ catch (error) {
457
+ this.logger.error('Failed to migrate to session_marks', { error });
458
+ }
459
+ }
460
+ prepareStatements() {
461
+ this.getSessionStmt = this.db.prepare('SELECT * FROM sessions WHERE session_id = ?');
462
+ this.insertSessionStmt = this.db.prepare(`
463
+ INSERT INTO sessions (
464
+ session_id,
465
+ custom_name,
466
+ created_at,
467
+ updated_at,
468
+ version,
469
+ pinned,
470
+ archived,
471
+ continuation_session_id,
472
+ initial_commit_head,
473
+ permission_mode,
474
+ branched_from_session_id,
475
+ branched_at_turn
476
+ ) VALUES (
477
+ @session_id,
478
+ @custom_name,
479
+ @created_at,
480
+ @updated_at,
481
+ @version,
482
+ @pinned,
483
+ @archived,
484
+ @continuation_session_id,
485
+ @initial_commit_head,
486
+ @permission_mode,
487
+ @branched_from_session_id,
488
+ @branched_at_turn
489
+ )
490
+ `);
491
+ this.updateSessionStmt = this.db.prepare(`
492
+ UPDATE sessions SET
493
+ custom_name=@custom_name,
494
+ updated_at=@updated_at,
495
+ pinned=@pinned,
496
+ archived=@archived,
497
+ continuation_session_id=@continuation_session_id,
498
+ initial_commit_head=@initial_commit_head,
499
+ permission_mode=@permission_mode,
500
+ version=@version,
501
+ branched_from_session_id=@branched_from_session_id,
502
+ branched_at_turn=@branched_at_turn
503
+ WHERE session_id=@session_id
504
+ `);
505
+ this.deleteSessionStmt = this.db.prepare('DELETE FROM sessions WHERE session_id = ?');
506
+ this.getAllStmt = this.db.prepare('SELECT * FROM sessions');
507
+ this.countStmt = this.db.prepare('SELECT COUNT(*) as count FROM sessions');
508
+ this.archiveAllStmt = this.db.prepare('UPDATE sessions SET archived=1, updated_at=@updated_at WHERE archived=0');
509
+ this.setMetadataStmt = this.db.prepare('INSERT INTO metadata (key, value) VALUES (@key, @value) ON CONFLICT(key) DO UPDATE SET value=excluded.value');
510
+ this.getMetadataStmt = this.db.prepare('SELECT value FROM metadata WHERE key = ?');
511
+ // Insights statements
512
+ this.getInsightsStmt = this.db.prepare('SELECT * FROM session_insights WHERE session_id = ?');
513
+ this.upsertInsightsStmt = this.db.prepare(`
514
+ INSERT INTO session_insights (
515
+ session_id, description, context, current_state, last_patch_trace_id, purposes, milestones, notable, tags, recent_actions, panels,
516
+ progress_completed, progress_total,
517
+ outstanding_tasks, completed_tasks, current_task, theme, computed_at, stale,
518
+ message_count, last_delta_check_at, patched_at,
519
+ progress_items, notable_events, recent_actions_v2, progress_summary, purpose, propositions
520
+ ) VALUES (
521
+ @session_id, @description, @context, @current_state, @last_patch_trace_id, @purposes, @milestones, @notable, @tags, @recent_actions, @panels,
522
+ @progress_completed, @progress_total,
523
+ @outstanding_tasks, @completed_tasks, @current_task, @theme, @computed_at, @stale,
524
+ @message_count, @last_delta_check_at, @patched_at,
525
+ @progress_items, @notable_events, @recent_actions_v2, @progress_summary, @purpose, @propositions
526
+ ) ON CONFLICT(session_id) DO UPDATE SET
527
+ description=excluded.description,
528
+ context=excluded.context,
529
+ current_state=excluded.current_state,
530
+ last_patch_trace_id=excluded.last_patch_trace_id,
531
+ purposes=excluded.purposes,
532
+ milestones=excluded.milestones,
533
+ notable=excluded.notable,
534
+ tags=excluded.tags,
535
+ recent_actions=excluded.recent_actions,
536
+ panels=excluded.panels,
537
+ progress_completed=excluded.progress_completed,
538
+ progress_total=excluded.progress_total,
539
+ outstanding_tasks=excluded.outstanding_tasks,
540
+ completed_tasks=excluded.completed_tasks,
541
+ current_task=excluded.current_task,
542
+ theme=excluded.theme,
543
+ computed_at=excluded.computed_at,
544
+ stale=excluded.stale,
545
+ message_count=excluded.message_count,
546
+ last_delta_check_at=excluded.last_delta_check_at,
547
+ patched_at=excluded.patched_at,
548
+ progress_items=excluded.progress_items,
549
+ notable_events=excluded.notable_events,
550
+ recent_actions_v2=excluded.recent_actions_v2,
551
+ progress_summary=excluded.progress_summary,
552
+ purpose=excluded.purpose,
553
+ propositions=excluded.propositions
554
+ `);
555
+ this.getAllInsightsStmt = this.db.prepare('SELECT * FROM session_insights');
556
+ this.markInsightsStaleStmt = this.db.prepare('UPDATE session_insights SET stale=1 WHERE session_id = ?');
557
+ this.getStaleInsightsStmt = this.db.prepare('SELECT session_id FROM session_insights WHERE stale = 1');
558
+ // Lightweight metadata update (purpose/theme/tags only)
559
+ this.updateMetadataFieldsStmt = this.db.prepare(`
560
+ UPDATE session_insights SET
561
+ purpose = COALESCE(@purpose, purpose),
562
+ theme = COALESCE(@theme, theme),
563
+ tags = COALESCE(@tags, tags),
564
+ patched_at = @patched_at
565
+ WHERE session_id = @session_id
566
+ `);
567
+ // File changes statements (for walkthrough generation)
568
+ this.insertFileChangeStmt = this.db.prepare(`
569
+ INSERT INTO turn_file_changes (id, session_id, turn_number, tool_name, file_path, old_string, new_string, timestamp)
570
+ VALUES (@id, @session_id, @turn_number, @tool_name, @file_path, @old_string, @new_string, @timestamp)
571
+ `);
572
+ this.getFileChangesStmt = this.db.prepare(`
573
+ SELECT * FROM turn_file_changes WHERE session_id = ? ORDER BY turn_number, id
574
+ `);
575
+ this.getFileChangesByFileStmt = this.db.prepare(`
576
+ SELECT * FROM turn_file_changes WHERE session_id = ? AND file_path = ? ORDER BY turn_number
577
+ `);
578
+ }
579
+ ensureMetadata() {
580
+ const now = new Date().toISOString();
581
+ const schema = this.getMetadataStmt.get('schema_version');
582
+ if (!schema) {
583
+ this.setMetadataStmt.run({ key: 'schema_version', value: '3' });
584
+ this.setMetadataStmt.run({ key: 'created_at', value: now });
585
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
586
+ }
587
+ }
588
+ mapRow(row) {
589
+ return {
590
+ custom_name: row.custom_name,
591
+ created_at: row.created_at,
592
+ updated_at: row.updated_at,
593
+ version: row.version,
594
+ pinned: !!row.pinned,
595
+ archived: !!row.archived,
596
+ continuation_session_id: row.continuation_session_id,
597
+ initial_commit_head: row.initial_commit_head,
598
+ permission_mode: row.permission_mode,
599
+ identity_image: row.identity_image ?? undefined,
600
+ last_termination_reason: row.last_termination_reason ?? undefined,
601
+ branched_from_session_id: row.branched_from_session_id ?? undefined,
602
+ branched_at_turn: row.branched_at_turn ?? undefined
603
+ };
604
+ }
605
+ // ===========================================================================
606
+ // SECTION 1: Core Session CRUD
607
+ // ===========================================================================
608
+ async getSessionInfo(sessionId) {
609
+ try {
610
+ const row = this.getSessionStmt.get(sessionId);
611
+ if (row) {
612
+ return this.mapRow(row);
613
+ }
614
+ const now = new Date().toISOString();
615
+ const defaultSession = {
616
+ custom_name: '',
617
+ created_at: now,
618
+ updated_at: now,
619
+ version: 3,
620
+ pinned: false,
621
+ archived: true, // Sessions discovered outside Claudia start archived
622
+ continuation_session_id: '',
623
+ initial_commit_head: '',
624
+ permission_mode: 'default'
625
+ };
626
+ this.insertSessionStmt.run({
627
+ session_id: sessionId,
628
+ custom_name: '',
629
+ created_at: now,
630
+ updated_at: now,
631
+ version: 3,
632
+ pinned: 0,
633
+ archived: 1, // Sessions discovered outside Claudia start archived
634
+ continuation_session_id: '',
635
+ initial_commit_head: '',
636
+ permission_mode: 'default'
637
+ });
638
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
639
+ return defaultSession;
640
+ }
641
+ catch (error) {
642
+ this.logger.error('Failed to get session info', { sessionId, error });
643
+ const now = new Date().toISOString();
644
+ return {
645
+ custom_name: '',
646
+ created_at: now,
647
+ updated_at: now,
648
+ version: 3,
649
+ pinned: false,
650
+ archived: true, // Sessions discovered outside Claudia start archived
651
+ continuation_session_id: '',
652
+ initial_commit_head: '',
653
+ permission_mode: 'default'
654
+ };
655
+ }
656
+ }
657
+ async updateSessionInfo(sessionId, updates) {
658
+ try {
659
+ const existingRow = this.getSessionStmt.get(sessionId);
660
+ const now = new Date().toISOString();
661
+ if (existingRow) {
662
+ const updatedSession = {
663
+ ...this.mapRow(existingRow),
664
+ ...updates,
665
+ updated_at: now
666
+ };
667
+ this.updateSessionStmt.run({
668
+ session_id: sessionId,
669
+ custom_name: updatedSession.custom_name,
670
+ updated_at: updatedSession.updated_at,
671
+ pinned: updatedSession.pinned ? 1 : 0,
672
+ archived: updatedSession.archived ? 1 : 0,
673
+ continuation_session_id: updatedSession.continuation_session_id,
674
+ initial_commit_head: updatedSession.initial_commit_head,
675
+ permission_mode: updatedSession.permission_mode,
676
+ version: updatedSession.version,
677
+ branched_from_session_id: updatedSession.branched_from_session_id || null,
678
+ branched_at_turn: updatedSession.branched_at_turn || null
679
+ });
680
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
681
+ return updatedSession;
682
+ }
683
+ else {
684
+ const newSession = {
685
+ custom_name: '',
686
+ created_at: now,
687
+ updated_at: now,
688
+ version: 3,
689
+ pinned: false,
690
+ archived: true, // Default archived, but updates can override
691
+ continuation_session_id: '',
692
+ initial_commit_head: '',
693
+ permission_mode: 'default',
694
+ ...updates
695
+ };
696
+ this.logger.info('Inserting new session with branch info', {
697
+ sessionId: sessionId.slice(0, 8),
698
+ branched_from: newSession.branched_from_session_id?.slice(0, 8),
699
+ branched_at_turn: newSession.branched_at_turn,
700
+ updates_keys: Object.keys(updates),
701
+ });
702
+ this.insertSessionStmt.run({
703
+ session_id: sessionId,
704
+ custom_name: newSession.custom_name,
705
+ created_at: newSession.created_at,
706
+ updated_at: newSession.updated_at,
707
+ version: newSession.version,
708
+ pinned: newSession.pinned ? 1 : 0,
709
+ archived: newSession.archived ? 1 : 0,
710
+ continuation_session_id: newSession.continuation_session_id,
711
+ initial_commit_head: newSession.initial_commit_head,
712
+ permission_mode: newSession.permission_mode,
713
+ branched_from_session_id: newSession.branched_from_session_id || null,
714
+ branched_at_turn: newSession.branched_at_turn || null
715
+ });
716
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
717
+ return newSession;
718
+ }
719
+ }
720
+ catch (error) {
721
+ this.logger.error('Failed to update session info', { sessionId, updates, error });
722
+ throw new Error(`Failed to update session info: ${error instanceof Error ? error.message : String(error)}`);
723
+ }
724
+ }
725
+ async updateCustomName(sessionId, customName) {
726
+ await this.updateSessionInfo(sessionId, { custom_name: customName });
727
+ }
728
+ /**
729
+ * Set the identity image for a session. This is a one-time operation
730
+ * that doesn't update the session's updated_at timestamp.
731
+ */
732
+ async setIdentityImage(sessionId, imageData) {
733
+ try {
734
+ const stmt = this.db.prepare('UPDATE sessions SET identity_image = ? WHERE session_id = ?');
735
+ stmt.run(imageData, sessionId);
736
+ this.logger.debug('Identity image set for session', { sessionId });
737
+ }
738
+ catch (error) {
739
+ this.logger.error('Failed to set identity image', { sessionId, error });
740
+ throw new Error(`Failed to set identity image: ${error instanceof Error ? error.message : String(error)}`);
741
+ }
742
+ }
743
+ /**
744
+ * Check if a session already has an identity image.
745
+ */
746
+ async hasIdentityImage(sessionId) {
747
+ try {
748
+ const row = this.getSessionStmt.get(sessionId);
749
+ return row?.identity_image != null;
750
+ }
751
+ catch (error) {
752
+ this.logger.error('Failed to check identity image', { sessionId, error });
753
+ return false;
754
+ }
755
+ }
756
+ /**
757
+ * Update the last termination reason for a session.
758
+ * Called when a turn is captured to persist why the session ended.
759
+ */
760
+ async setLastTerminationReason(sessionId, reason) {
761
+ try {
762
+ const stmt = this.db.prepare('UPDATE sessions SET last_termination_reason = ? WHERE session_id = ?');
763
+ stmt.run(reason, sessionId);
764
+ this.logger.debug('Last termination reason set for session', { sessionId: sessionId.slice(0, 8), reason });
765
+ }
766
+ catch (error) {
767
+ this.logger.error('Failed to set last termination reason', { sessionId, error });
768
+ // Don't throw - this is non-critical metadata
769
+ }
770
+ }
771
+ async deleteSession(sessionId) {
772
+ this.logger.info('Deleting session info', { sessionId });
773
+ try {
774
+ const result = this.deleteSessionStmt.run(sessionId);
775
+ if (result.changes > 0) {
776
+ const now = new Date().toISOString();
777
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
778
+ this.logger.info('Session info deleted successfully', { sessionId });
779
+ }
780
+ else {
781
+ this.logger.debug('Session info not found for deletion', { sessionId });
782
+ }
783
+ }
784
+ catch (error) {
785
+ this.logger.error('Failed to delete session info', { sessionId, error });
786
+ throw new Error(`Failed to delete session info: ${error instanceof Error ? error.message : String(error)}`);
787
+ }
788
+ }
789
+ async getAllSessionInfo() {
790
+ this.logger.debug('Getting all session info');
791
+ try {
792
+ const rows = this.getAllStmt.all();
793
+ const result = {};
794
+ for (const row of rows) {
795
+ result[row.session_id] = this.mapRow(row);
796
+ }
797
+ return result;
798
+ }
799
+ catch (error) {
800
+ this.logger.error('Failed to get all session info', error);
801
+ return {};
802
+ }
803
+ }
804
+ async getStats() {
805
+ try {
806
+ const countRow = this.countStmt.get();
807
+ let dbSize = 0;
808
+ if (this.dbPath !== ':memory:') {
809
+ try {
810
+ const stats = fs.statSync(this.dbPath);
811
+ dbSize = stats.size;
812
+ }
813
+ catch {
814
+ dbSize = 0;
815
+ }
816
+ }
817
+ const lastUpdatedRow = this.getMetadataStmt.get('last_updated');
818
+ return {
819
+ sessionCount: countRow.count,
820
+ dbSize,
821
+ lastUpdated: lastUpdatedRow?.value || new Date().toISOString()
822
+ };
823
+ }
824
+ catch (error) {
825
+ this.logger.error('Failed to get database stats', error);
826
+ return {
827
+ sessionCount: 0,
828
+ dbSize: 0,
829
+ lastUpdated: new Date().toISOString()
830
+ };
831
+ }
832
+ }
833
+ reinitializePaths(customConfigDir) {
834
+ this.initializePaths(customConfigDir);
835
+ }
836
+ getDbPath() {
837
+ return this.dbPath;
838
+ }
839
+ getConfigDir() {
840
+ return this.configDir;
841
+ }
842
+ /**
843
+ * Resolve a short session ID prefix to the full session ID.
844
+ * Returns null if no matching session is found.
845
+ */
846
+ async resolveSessionId(sessionIdPrefix) {
847
+ // If it's already a full UUID (36 chars), return as-is
848
+ if (sessionIdPrefix.length === 36) {
849
+ return sessionIdPrefix;
850
+ }
851
+ try {
852
+ const row = this.db.prepare(`
853
+ SELECT session_id FROM sessions
854
+ WHERE session_id LIKE ?
855
+ LIMIT 1
856
+ `).get(`${sessionIdPrefix}%`);
857
+ return row?.session_id || null;
858
+ }
859
+ catch (error) {
860
+ this.logger.debug('Failed to resolve session ID', { error, prefix: sessionIdPrefix });
861
+ return null;
862
+ }
863
+ }
864
+ // ==========================================================================
865
+ // Recommendation Management
866
+ // ==========================================================================
867
+ /**
868
+ * Save an accepted recommendation to the database
869
+ * Note: observation is stored in the 'friction' column for backwards compatibility
870
+ */
871
+ async acceptRecommendation(recommendation) {
872
+ try {
873
+ // Resolve short session ID to full UUID
874
+ const fullSessionId = await this.resolveSessionId(recommendation.sessionId);
875
+ if (!fullSessionId) {
876
+ throw new Error(`Session not found: ${recommendation.sessionId}`);
877
+ }
878
+ const stmt = this.db.prepare(`
879
+ INSERT OR REPLACE INTO recommendations (id, session_id, category, friction, suggestion, why, status, created_at)
880
+ VALUES (?, ?, ?, ?, ?, ?, 'accepted', ?)
881
+ `);
882
+ stmt.run(recommendation.id, fullSessionId, 'general', // No longer using categories
883
+ recommendation.observation, // Stored in 'friction' column
884
+ recommendation.suggestion, recommendation.why, new Date().toISOString());
885
+ this.logger.debug('Recommendation accepted', { id: recommendation.id, sessionId: fullSessionId });
886
+ }
887
+ catch (error) {
888
+ this.logger.error('Failed to accept recommendation', { error, id: recommendation.id });
889
+ throw error;
890
+ }
891
+ }
892
+ /**
893
+ * Mark a recommendation as dismissed (won't be shown again)
894
+ */
895
+ async dismissRecommendation(recommendationId) {
896
+ try {
897
+ const stmt = this.db.prepare(`
898
+ UPDATE recommendations SET status = 'dismissed', actioned_at = ?
899
+ WHERE id = ?
900
+ `);
901
+ const result = stmt.run(new Date().toISOString(), recommendationId);
902
+ // If it wasn't in the table, insert it as dismissed
903
+ if (result.changes === 0) {
904
+ this.logger.debug('Recommendation not found, cannot dismiss', { id: recommendationId });
905
+ }
906
+ else {
907
+ this.logger.debug('Recommendation dismissed', { id: recommendationId });
908
+ }
909
+ }
910
+ catch (error) {
911
+ this.logger.error('Failed to dismiss recommendation', { error, id: recommendationId });
912
+ throw error;
913
+ }
914
+ }
915
+ /**
916
+ * Mark a recommendation as completed (was actioned)
917
+ */
918
+ async completeRecommendation(recommendationId) {
919
+ try {
920
+ const stmt = this.db.prepare(`
921
+ UPDATE recommendations SET status = 'completed', actioned_at = ?
922
+ WHERE id = ?
923
+ `);
924
+ stmt.run(new Date().toISOString(), recommendationId);
925
+ this.logger.debug('Recommendation completed', { id: recommendationId });
926
+ }
927
+ catch (error) {
928
+ this.logger.error('Failed to complete recommendation', { error, id: recommendationId });
929
+ throw error;
930
+ }
931
+ }
932
+ /**
933
+ * Get all pending (accepted but not completed) recommendations
934
+ */
935
+ async getPendingRecommendations() {
936
+ try {
937
+ const stmt = this.db.prepare(`
938
+ SELECT id, session_id, category, friction, suggestion, why, status, created_at
939
+ FROM recommendations
940
+ WHERE status = 'accepted'
941
+ ORDER BY created_at DESC
942
+ `);
943
+ const rows = stmt.all();
944
+ return rows.map(row => ({
945
+ id: row.id,
946
+ sessionId: row.session_id,
947
+ category: row.category,
948
+ observation: row.friction, // Map DB 'friction' column to 'observation' field
949
+ suggestion: row.suggestion,
950
+ why: row.why,
951
+ status: row.status,
952
+ createdAt: row.created_at,
953
+ }));
954
+ }
955
+ catch (error) {
956
+ this.logger.error('Failed to get pending recommendations', { error });
957
+ return [];
958
+ }
959
+ }
960
+ /**
961
+ * Get recommendations for a specific session
962
+ */
963
+ async getRecommendationsForSession(sessionId) {
964
+ try {
965
+ const stmt = this.db.prepare(`
966
+ SELECT id, category, friction, suggestion, why, status, created_at
967
+ FROM recommendations
968
+ WHERE session_id = ? OR session_id LIKE ?
969
+ ORDER BY created_at DESC
970
+ `);
971
+ const rows = stmt.all(sessionId, `${sessionId}%`);
972
+ return rows.map(row => ({
973
+ id: row.id,
974
+ category: row.category,
975
+ observation: row.friction, // Map DB 'friction' column to 'observation' field
976
+ suggestion: row.suggestion,
977
+ why: row.why,
978
+ status: row.status,
979
+ createdAt: row.created_at,
980
+ }));
981
+ }
982
+ catch (error) {
983
+ this.logger.error('Failed to get recommendations for session', { error, sessionId });
984
+ return [];
985
+ }
986
+ }
987
+ async archiveAllSessions() {
988
+ this.logger.info('Archiving all sessions');
989
+ try {
990
+ const now = new Date().toISOString();
991
+ const transaction = this.db.transaction(() => {
992
+ const info = this.archiveAllStmt.run({ updated_at: now });
993
+ if (info.changes > 0) {
994
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
995
+ }
996
+ return info.changes;
997
+ });
998
+ const archivedCount = transaction();
999
+ this.logger.info('Sessions archived successfully', { archivedCount });
1000
+ return archivedCount;
1001
+ }
1002
+ catch (error) {
1003
+ this.logger.error('Failed to archive all sessions', error);
1004
+ throw new Error(`Failed to archive all sessions: ${error instanceof Error ? error.message : String(error)}`);
1005
+ }
1006
+ }
1007
+ async syncMissingSessions(sessionIds) {
1008
+ try {
1009
+ const now = new Date().toISOString();
1010
+ // Default to archived=1 so synced sessions don't clutter the active list.
1011
+ // Sessions started from the dashboard are explicitly unarchived in conversation.routes.ts
1012
+ const insert = this.db.prepare(`
1013
+ INSERT OR IGNORE INTO sessions (
1014
+ session_id,
1015
+ custom_name,
1016
+ created_at,
1017
+ updated_at,
1018
+ version,
1019
+ pinned,
1020
+ archived,
1021
+ continuation_session_id,
1022
+ initial_commit_head,
1023
+ permission_mode
1024
+ ) VALUES (
1025
+ @session_id,
1026
+ '',
1027
+ @now,
1028
+ @now,
1029
+ 3,
1030
+ 0,
1031
+ 1,
1032
+ '',
1033
+ '',
1034
+ 'default'
1035
+ )
1036
+ `);
1037
+ const transaction = this.db.transaction((ids) => {
1038
+ let inserted = 0;
1039
+ for (const id of ids) {
1040
+ const info = insert.run({ session_id: id, now });
1041
+ if (info.changes > 0)
1042
+ inserted++;
1043
+ }
1044
+ if (inserted > 0) {
1045
+ this.setMetadataStmt.run({ key: 'last_updated', value: now });
1046
+ }
1047
+ return inserted;
1048
+ });
1049
+ return transaction(sessionIds);
1050
+ }
1051
+ catch (error) {
1052
+ this.logger.error('Failed to sync missing sessions', error);
1053
+ throw new Error(`Failed to sync missing sessions: ${error instanceof Error ? error.message : String(error)}`);
1054
+ }
1055
+ }
1056
+ // ==================== INSIGHTS METHODS ====================
1057
+ mapInsightsRow(row) {
1058
+ return {
1059
+ session_id: row.session_id,
1060
+ description: row.description,
1061
+ context: row.context ? JSON.parse(row.context) : null,
1062
+ current_state: row.current_state ? JSON.parse(row.current_state) : null,
1063
+ last_patch_trace_id: row.last_patch_trace_id ?? null,
1064
+ purposes: JSON.parse(row.purposes || '[]'),
1065
+ milestones: JSON.parse(row.milestones || '[]'),
1066
+ notable: JSON.parse(row.notable || '[]'),
1067
+ tags: row.tags ? JSON.parse(row.tags) : null,
1068
+ recent_actions: JSON.parse(row.recent_actions || '[]'),
1069
+ panels: JSON.parse(row.panels || '[]'),
1070
+ progress_completed: row.progress_completed,
1071
+ progress_total: row.progress_total,
1072
+ outstanding_tasks: JSON.parse(row.outstanding_tasks || '[]'),
1073
+ completed_tasks: JSON.parse(row.completed_tasks || '[]'),
1074
+ current_task: row.current_task,
1075
+ theme: row.theme,
1076
+ computed_at: row.computed_at,
1077
+ stale: !!row.stale,
1078
+ message_count: row.message_count ?? undefined,
1079
+ last_delta_check_at: row.last_delta_check_at ?? undefined,
1080
+ patched_at: row.patched_at ?? undefined,
1081
+ // V9 accumulative fields
1082
+ progress_items: JSON.parse(row.progress_items || '[]'),
1083
+ notable_events: JSON.parse(row.notable_events || '[]'),
1084
+ recent_actions_v2: JSON.parse(row.recent_actions_v2 || '[]'),
1085
+ progress_summary: row.progress_summary ? JSON.parse(row.progress_summary) : null,
1086
+ propositions: JSON.parse(row.propositions || '[]'),
1087
+ // Evolving purpose
1088
+ purpose: row.purpose ?? undefined,
1089
+ // Cached tool metrics
1090
+ lines_added: row.lines_added ?? undefined,
1091
+ lines_removed: row.lines_removed ?? undefined,
1092
+ edit_count: row.edit_count ?? undefined,
1093
+ write_count: row.write_count ?? undefined,
1094
+ metrics_updated_at: row.metrics_updated_at ?? undefined
1095
+ };
1096
+ }
1097
+ async getInsights(sessionId) {
1098
+ try {
1099
+ const row = this.getInsightsStmt.get(sessionId);
1100
+ if (row) {
1101
+ return this.mapInsightsRow(row);
1102
+ }
1103
+ return null;
1104
+ }
1105
+ catch (error) {
1106
+ this.logger.error('Failed to get insights', { sessionId, error });
1107
+ return null;
1108
+ }
1109
+ }
1110
+ async setInsights(insights) {
1111
+ try {
1112
+ this.upsertInsightsStmt.run({
1113
+ session_id: insights.session_id,
1114
+ description: insights.description,
1115
+ context: insights.context ? JSON.stringify(insights.context) : null,
1116
+ current_state: insights.current_state ? JSON.stringify(insights.current_state) : null,
1117
+ last_patch_trace_id: insights.last_patch_trace_id ?? null,
1118
+ purposes: JSON.stringify(insights.purposes || []),
1119
+ milestones: JSON.stringify(insights.milestones || []),
1120
+ notable: JSON.stringify(insights.notable || []),
1121
+ tags: insights.tags ? JSON.stringify(insights.tags) : null,
1122
+ recent_actions: JSON.stringify(insights.recent_actions || []),
1123
+ panels: JSON.stringify(insights.panels || []),
1124
+ progress_completed: insights.progress_completed,
1125
+ progress_total: insights.progress_total,
1126
+ outstanding_tasks: JSON.stringify(insights.outstanding_tasks),
1127
+ completed_tasks: JSON.stringify(insights.completed_tasks),
1128
+ current_task: insights.current_task,
1129
+ theme: insights.theme,
1130
+ computed_at: insights.computed_at,
1131
+ stale: insights.stale ? 1 : 0,
1132
+ message_count: insights.message_count ?? null,
1133
+ last_delta_check_at: insights.last_delta_check_at ?? null,
1134
+ patched_at: insights.patched_at ?? null,
1135
+ // V9 accumulative fields
1136
+ progress_items: JSON.stringify(insights.progress_items || []),
1137
+ notable_events: JSON.stringify(insights.notable_events || []),
1138
+ recent_actions_v2: JSON.stringify(insights.recent_actions_v2 || []),
1139
+ progress_summary: insights.progress_summary ? JSON.stringify(insights.progress_summary) : null,
1140
+ propositions: JSON.stringify(insights.propositions || []),
1141
+ // Evolving purpose
1142
+ purpose: insights.purpose ?? null
1143
+ });
1144
+ this.logger.debug('Insights cached', { sessionId: insights.session_id });
1145
+ }
1146
+ catch (error) {
1147
+ this.logger.error('Failed to set insights', { sessionId: insights.session_id, error });
1148
+ throw error;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Lightweight update for session metadata (purpose/theme/tags only).
1153
+ * Called on user message to update session classification without full recompute.
1154
+ * Only updates fields that are provided (null/undefined preserves existing).
1155
+ */
1156
+ async updateSessionMetadata(sessionId, updates) {
1157
+ try {
1158
+ this.updateMetadataFieldsStmt.run({
1159
+ session_id: sessionId,
1160
+ purpose: updates.purpose ?? null,
1161
+ theme: updates.theme ?? null,
1162
+ tags: updates.tags ? JSON.stringify(updates.tags) : null,
1163
+ patched_at: new Date().toISOString()
1164
+ });
1165
+ this.logger.debug('Session metadata updated', {
1166
+ sessionId,
1167
+ fields: Object.keys(updates).filter(k => updates[k] !== undefined)
1168
+ });
1169
+ }
1170
+ catch (error) {
1171
+ this.logger.error('Failed to update session metadata', { sessionId, error });
1172
+ throw error;
1173
+ }
1174
+ }
1175
+ /**
1176
+ * Update tool metrics and message count for a session (list endpoint optimization).
1177
+ * Called to cache metrics so list endpoint doesn't need to re-parse messages.
1178
+ * Also updates message_count for use in metadata-only queries.
1179
+ */
1180
+ async updateToolMetrics(sessionId, metrics, messageCount) {
1181
+ try {
1182
+ const stmt = this.db.prepare(`
1183
+ UPDATE session_insights
1184
+ SET lines_added = ?, lines_removed = ?, edit_count = ?, write_count = ?, metrics_updated_at = ?
1185
+ ${messageCount !== undefined ? ', message_count = ?' : ''}
1186
+ WHERE session_id = ?
1187
+ `);
1188
+ const params = [
1189
+ metrics.linesAdded,
1190
+ metrics.linesRemoved,
1191
+ metrics.editCount,
1192
+ metrics.writeCount,
1193
+ new Date().toISOString(),
1194
+ ...(messageCount !== undefined ? [messageCount] : []),
1195
+ sessionId
1196
+ ];
1197
+ const result = stmt.run(...params);
1198
+ if (result.changes > 0) {
1199
+ this.logger.debug('Tool metrics updated', { sessionId, metrics, messageCount });
1200
+ }
1201
+ }
1202
+ catch (error) {
1203
+ this.logger.error('Failed to update tool metrics', { sessionId, error });
1204
+ // Don't throw - this is a non-critical optimization
1205
+ }
1206
+ }
1207
+ async getAllInsights() {
1208
+ try {
1209
+ const rows = this.getAllInsightsStmt.all();
1210
+ const result = new Map();
1211
+ for (const row of rows) {
1212
+ result.set(row.session_id, this.mapInsightsRow(row));
1213
+ }
1214
+ return result;
1215
+ }
1216
+ catch (error) {
1217
+ this.logger.error('Failed to get all insights', error);
1218
+ return new Map();
1219
+ }
1220
+ }
1221
+ async markInsightsStale(sessionId) {
1222
+ try {
1223
+ this.markInsightsStaleStmt.run(sessionId);
1224
+ }
1225
+ catch (error) {
1226
+ this.logger.error('Failed to mark insights stale', { sessionId, error });
1227
+ }
1228
+ }
1229
+ async getStaleSessionIds() {
1230
+ try {
1231
+ const rows = this.getStaleInsightsStmt.all();
1232
+ return rows.map(r => r.session_id);
1233
+ }
1234
+ catch (error) {
1235
+ this.logger.error('Failed to get stale session IDs', error);
1236
+ return [];
1237
+ }
1238
+ }
1239
+ async getMissingInsightsSessionIds(sessionIds) {
1240
+ try {
1241
+ const existing = await this.getAllInsights();
1242
+ return sessionIds.filter(id => !existing.has(id));
1243
+ }
1244
+ catch (error) {
1245
+ this.logger.error('Failed to get missing insights session IDs', error);
1246
+ return sessionIds;
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Get all non-archived session IDs (for refresh operations)
1251
+ */
1252
+ getNonArchivedSessionIds() {
1253
+ try {
1254
+ const rows = this.db.prepare('SELECT session_id FROM sessions WHERE archived = 0').all();
1255
+ return rows.map(r => r.session_id);
1256
+ }
1257
+ catch (error) {
1258
+ this.logger.error('Failed to get non-archived session IDs', error);
1259
+ return [];
1260
+ }
1261
+ }
1262
+ /**
1263
+ * Fast path for archived sessions - returns session + insights data directly from DB.
1264
+ * This avoids parsing JSONL files which is extremely slow for 1000+ archived sessions.
1265
+ *
1266
+ * @param limit - Maximum number of sessions to return
1267
+ * @param offset - Number of sessions to skip (for pagination)
1268
+ * @returns Array of session data with insights, sorted by updated_at desc
1269
+ */
1270
+ getArchivedSessionsWithInsights(limit, offset) {
1271
+ const startTime = Date.now();
1272
+ try {
1273
+ // Join sessions with insights for archived sessions, sorted by most recent
1274
+ const rows = this.db.prepare(`
1275
+ SELECT
1276
+ s.session_id,
1277
+ s.*,
1278
+ i.purpose, i.description, i.theme, i.current_state, i.progress_summary,
1279
+ i.milestones, i.panels, i.notable_events, i.tags,
1280
+ i.recent_actions_v2, i.progress_items, i.propositions,
1281
+ i.message_count, i.computed_at, i.patched_at, i.stale,
1282
+ i.lines_added, i.lines_removed, i.edit_count, i.write_count, i.metrics_updated_at,
1283
+ i.last_delta_check_at, i.last_patch_trace_id
1284
+ FROM sessions s
1285
+ LEFT JOIN session_insights i ON s.session_id = i.session_id
1286
+ WHERE s.archived = 1
1287
+ ORDER BY s.updated_at DESC
1288
+ LIMIT ? OFFSET ?
1289
+ `).all(limit, offset);
1290
+ const results = rows.map(row => ({
1291
+ sessionId: row.session_id,
1292
+ sessionInfo: this.mapRow(row),
1293
+ insights: row.purpose ? this.mapInsightsRow(row) : null
1294
+ }));
1295
+ this.logger.debug('getArchivedSessionsWithInsights', {
1296
+ limit,
1297
+ offset,
1298
+ returnedCount: results.length,
1299
+ durationMs: Date.now() - startTime
1300
+ });
1301
+ return results;
1302
+ }
1303
+ catch (error) {
1304
+ this.logger.error('Failed to get archived sessions with insights', error);
1305
+ return [];
1306
+ }
1307
+ }
1308
+ /**
1309
+ * Get total count of archived sessions (for pagination)
1310
+ */
1311
+ getArchivedSessionCount() {
1312
+ try {
1313
+ const row = this.db.prepare('SELECT COUNT(*) as count FROM sessions WHERE archived = 1').get();
1314
+ return row.count;
1315
+ }
1316
+ catch (error) {
1317
+ this.logger.error('Failed to get archived session count', error);
1318
+ return 0;
1319
+ }
1320
+ }
1321
+ // ===========================================================================
1322
+ // SECTION 4: Insight Auditing (delegated to InsightAuditRepository)
1323
+ // ===========================================================================
1324
+ /**
1325
+ * Get the audit repository for direct access.
1326
+ * Prefer using the wrapper methods below for compatibility.
1327
+ */
1328
+ get auditRepository() {
1329
+ if (!this._auditRepository) {
1330
+ throw new Error('SessionInfoService not initialized - call initialize() first');
1331
+ }
1332
+ return this._auditRepository;
1333
+ }
1334
+ /**
1335
+ * Audit insight regeneration for debugging (legacy method).
1336
+ * @deprecated Use auditRepository.auditLegacy() directly for new code.
1337
+ */
1338
+ auditRegeneration(params) {
1339
+ this.auditRepository.auditLegacy(params);
1340
+ }
1341
+ /**
1342
+ * Audit event-driven insight operations with full context for debugging.
1343
+ * @deprecated Use auditRepository.auditEvent() directly for new code.
1344
+ */
1345
+ auditEventInsight(params) {
1346
+ this.auditRepository.auditEvent(params);
1347
+ }
1348
+ /**
1349
+ * Get event audit history by trace ID.
1350
+ * @deprecated Use auditRepository.getByTraceId() directly for new code.
1351
+ */
1352
+ getEventAuditByTraceId(traceId) {
1353
+ return this.auditRepository.getByTraceId(traceId);
1354
+ }
1355
+ /**
1356
+ * Get recent event audit history for a session.
1357
+ * @deprecated Use auditRepository.getRecentForSession() directly for new code.
1358
+ */
1359
+ getRecentEventAudit(sessionId, limit = 50) {
1360
+ return this.auditRepository.getRecentForSession(sessionId, limit);
1361
+ }
1362
+ /**
1363
+ * Get all audit history across all sessions.
1364
+ * @deprecated Use auditRepository.getAllLegacyAudit() directly for new code.
1365
+ */
1366
+ getAllAuditHistory(limit = 100) {
1367
+ return this.auditRepository.getAllLegacyAudit(limit);
1368
+ }
1369
+ /**
1370
+ * Get audit history for a session.
1371
+ * @deprecated Use auditRepository.getLegacyForSession() directly for new code.
1372
+ */
1373
+ getAuditHistory(sessionId, limit = 50) {
1374
+ return this.auditRepository.getLegacyForSession(sessionId, limit);
1375
+ }
1376
+ // ===========================================================================
1377
+ // SECTION 5: Turn Management (delegated to TurnRepository)
1378
+ // ===========================================================================
1379
+ /**
1380
+ * Get the turn repository for direct access.
1381
+ */
1382
+ get turnRepository() {
1383
+ if (!this._turnRepository) {
1384
+ throw new Error('SessionInfoService not initialized - call initialize() first');
1385
+ }
1386
+ return this._turnRepository;
1387
+ }
1388
+ /**
1389
+ * Save a turn to the database.
1390
+ * @deprecated Use turnRepository.save() directly for new code.
1391
+ */
1392
+ async saveTurn(turn) {
1393
+ return this.turnRepository.save(turn);
1394
+ }
1395
+ /**
1396
+ * Get all turns for a session, ordered by turn number.
1397
+ * @deprecated Use turnRepository.getForSession() directly for new code.
1398
+ */
1399
+ async getTurns(sessionId) {
1400
+ return this.turnRepository.getForSession(sessionId);
1401
+ }
1402
+ /**
1403
+ * Get turn count for a session (without fetching all data).
1404
+ * @deprecated Use turnRepository.getCount() directly for new code.
1405
+ */
1406
+ async getTurnCount(sessionId) {
1407
+ return this.turnRepository.getCount(sessionId);
1408
+ }
1409
+ // ===========================================================================
1410
+ // SECTION 5b: File Changes (for walkthrough generation)
1411
+ // ===========================================================================
1412
+ /**
1413
+ * Record a file change from an Edit or Write tool call.
1414
+ * Called by TurnCaptureService when processing tool calls.
1415
+ */
1416
+ insertFileChange(change) {
1417
+ const id = `${change.sessionId}-${change.turnNumber}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1418
+ this.insertFileChangeStmt.run({
1419
+ id,
1420
+ session_id: change.sessionId,
1421
+ turn_number: change.turnNumber,
1422
+ tool_name: change.toolName,
1423
+ file_path: change.filePath,
1424
+ old_string: change.oldString,
1425
+ new_string: change.newString,
1426
+ timestamp: change.timestamp,
1427
+ });
1428
+ }
1429
+ /**
1430
+ * Get all file changes for a session, ordered by turn number.
1431
+ */
1432
+ getFileChanges(sessionId) {
1433
+ const rows = this.getFileChangesStmt.all(sessionId);
1434
+ return rows.map(row => ({
1435
+ id: row.id,
1436
+ sessionId: row.session_id,
1437
+ turnNumber: row.turn_number,
1438
+ toolName: row.tool_name,
1439
+ filePath: row.file_path,
1440
+ oldString: row.old_string,
1441
+ newString: row.new_string,
1442
+ timestamp: row.timestamp,
1443
+ }));
1444
+ }
1445
+ /**
1446
+ * Get file changes for a specific file in a session.
1447
+ */
1448
+ getFileChangesByFile(sessionId, filePath) {
1449
+ const rows = this.getFileChangesByFileStmt.all(sessionId, filePath);
1450
+ return rows.map(row => ({
1451
+ id: row.id,
1452
+ turnNumber: row.turn_number,
1453
+ toolName: row.tool_name,
1454
+ oldString: row.old_string,
1455
+ newString: row.new_string,
1456
+ timestamp: row.timestamp,
1457
+ }));
1458
+ }
1459
+ // ===========================================================================
1460
+ // SECTION 6+7: Session Marks (delegated to SessionMarksRepository)
1461
+ // ===========================================================================
1462
+ /**
1463
+ * Get the marks repository for direct access.
1464
+ */
1465
+ get marksRepository() {
1466
+ if (!this._marksRepository) {
1467
+ throw new Error('SessionInfoService not initialized - call initialize() first');
1468
+ }
1469
+ return this._marksRepository;
1470
+ }
1471
+ // --- Friction Marks (v1 legacy) ---
1472
+ /** @deprecated Use marksRepository.addFrictionMark() directly */
1473
+ async addFrictionMark(mark) {
1474
+ return this.marksRepository.addFrictionMark(mark);
1475
+ }
1476
+ /** @deprecated Use marksRepository.getFrictionMarksForSession() directly */
1477
+ async getFrictionMarksForSession(sessionId) {
1478
+ return this.marksRepository.getFrictionMarksForSession(sessionId);
1479
+ }
1480
+ /** @deprecated Use marksRepository.deleteFrictionMark() directly */
1481
+ async deleteFrictionMark(id) {
1482
+ await this.marksRepository.deleteFrictionMark(id);
1483
+ }
1484
+ /** @deprecated Use marksRepository.isFrictionMarked() directly */
1485
+ async isFrictionMarked(sessionId, messageId) {
1486
+ return this.marksRepository.isFrictionMarked(sessionId, messageId);
1487
+ }
1488
+ // --- Session Marks v2 ---
1489
+ /** @deprecated Use marksRepository.addSessionMark() directly */
1490
+ async addSessionMark(mark) {
1491
+ return this.marksRepository.addSessionMark(mark);
1492
+ }
1493
+ /** @deprecated Use marksRepository.getSessionMarks() directly */
1494
+ async getSessionMarks(sessionId) {
1495
+ return this.marksRepository.getSessionMarks(sessionId);
1496
+ }
1497
+ /** @deprecated Use marksRepository.deleteSessionMark() directly */
1498
+ async deleteSessionMark(id) {
1499
+ await this.marksRepository.deleteSessionMark(id);
1500
+ }
1501
+ /** @deprecated Use marksRepository.isSessionMarked() directly */
1502
+ async isSessionMarked(sessionId, messageId) {
1503
+ return this.marksRepository.isSessionMarked(sessionId, messageId);
1504
+ }
1505
+ /** @deprecated Use marksRepository.getMarksWithoutTurnContext() directly */
1506
+ async getMarksWithoutTurnContext(sessionId) {
1507
+ return this.marksRepository.getMarksWithoutTurnContext(sessionId);
1508
+ }
1509
+ /** @deprecated Use marksRepository.updateMarkTurnContext() directly */
1510
+ async updateMarkTurnContext(markId, turnNumber, turnContext) {
1511
+ return this.marksRepository.updateMarkTurnContext(markId, turnNumber, turnContext);
1512
+ }
1513
+ // ===========================================================================
1514
+ // SECTION 8: Branch Operations
1515
+ // ===========================================================================
1516
+ /**
1517
+ * Get all sessions that branched from this parent session.
1518
+ * Returns the child session IDs and the turn numbers they branched at.
1519
+ */
1520
+ getBranchChildren(parentSessionId) {
1521
+ try {
1522
+ const rows = this.db.prepare(`
1523
+ SELECT session_id, branched_at_turn
1524
+ FROM sessions
1525
+ WHERE branched_from_session_id = ?
1526
+ ORDER BY branched_at_turn ASC
1527
+ `).all(parentSessionId);
1528
+ return rows
1529
+ .filter(r => r.branched_at_turn != null)
1530
+ .map(r => ({
1531
+ childSessionId: r.session_id,
1532
+ atTurn: r.branched_at_turn,
1533
+ }));
1534
+ }
1535
+ catch (error) {
1536
+ this.logger.debug('Failed to get branch children', { error, parentSessionId });
1537
+ return [];
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Copy session_turns from source session to new branch session.
1542
+ * Only copies turns with turn_number <= afterTurn.
1543
+ * Generates new IDs for the copied turns.
1544
+ * @returns Number of turns copied
1545
+ */
1546
+ copyTurnsForBranch(sourceSessionId, newSessionId, afterTurn) {
1547
+ try {
1548
+ // Get turns to copy
1549
+ const turns = this.db.prepare(`
1550
+ SELECT turn_number, timestamp, headline, actions, tag, icon, exit_code, tool_count, incomplete, termination_reason
1551
+ FROM session_turns
1552
+ WHERE session_id = ? AND turn_number <= ?
1553
+ `).all(sourceSessionId, afterTurn);
1554
+ if (turns.length === 0) {
1555
+ return 0;
1556
+ }
1557
+ // Insert with new session ID and new IDs
1558
+ const insertStmt = this.db.prepare(`
1559
+ INSERT INTO session_turns (id, session_id, turn_number, timestamp, headline, actions, tag, icon, exit_code, tool_count, incomplete, termination_reason)
1560
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1561
+ `);
1562
+ for (const turn of turns) {
1563
+ const newId = `${newSessionId}-turn-${turn.turn_number}`;
1564
+ insertStmt.run(newId, newSessionId, turn.turn_number, turn.timestamp, turn.headline, turn.actions, turn.tag, turn.icon, turn.exit_code, turn.tool_count, turn.incomplete, turn.termination_reason);
1565
+ }
1566
+ this.logger.debug('Copied turns for branch', {
1567
+ sourceSession: sourceSessionId.slice(0, 8),
1568
+ newSession: newSessionId.slice(0, 8),
1569
+ count: turns.length,
1570
+ });
1571
+ return turns.length;
1572
+ }
1573
+ catch (error) {
1574
+ this.logger.error('Failed to copy turns for branch', { error, sourceSessionId, newSessionId });
1575
+ throw error;
1576
+ }
1577
+ }
1578
+ /**
1579
+ * Copy session_marks from source session to new branch session.
1580
+ * Only copies marks with turn_number <= afterTurn (or null turn_number).
1581
+ * Generates new IDs for the copied marks.
1582
+ * @returns Number of marks copied
1583
+ */
1584
+ copyMarksForBranch(sourceSessionId, newSessionId, afterTurn) {
1585
+ return this.marksRepository.copyMarksForBranch(sourceSessionId, newSessionId, afterTurn);
1586
+ }
1587
+ // ===========================================================================
1588
+ // SECTION 9: Walkthrough Scope Cache
1589
+ // ===========================================================================
1590
+ /**
1591
+ * Get cached walkthrough scope analysis.
1592
+ * Returns null if not cached.
1593
+ */
1594
+ getWalkthroughScopeCache(sessionId) {
1595
+ try {
1596
+ const row = this.db.prepare(`
1597
+ SELECT turn_count, generated_at, analysis_json
1598
+ FROM walkthrough_scope_cache
1599
+ WHERE session_id = ?
1600
+ `).get(sessionId);
1601
+ if (!row)
1602
+ return null;
1603
+ return {
1604
+ turnCount: row.turn_count,
1605
+ generatedAt: row.generated_at,
1606
+ analysis: JSON.parse(row.analysis_json),
1607
+ };
1608
+ }
1609
+ catch (error) {
1610
+ this.logger.error('Failed to get walkthrough scope cache', { error, sessionId });
1611
+ return null;
1612
+ }
1613
+ }
1614
+ /**
1615
+ * Save walkthrough scope analysis to cache.
1616
+ */
1617
+ setWalkthroughScopeCache(sessionId, turnCount, analysis) {
1618
+ try {
1619
+ this.db.prepare(`
1620
+ INSERT OR REPLACE INTO walkthrough_scope_cache (session_id, turn_count, generated_at, analysis_json)
1621
+ VALUES (?, ?, ?, ?)
1622
+ `).run(sessionId, turnCount, new Date().toISOString(), JSON.stringify(analysis));
1623
+ }
1624
+ catch (error) {
1625
+ this.logger.error('Failed to set walkthrough scope cache', { error, sessionId });
1626
+ }
1627
+ }
1628
+ /**
1629
+ * Delete walkthrough scope cache for a session.
1630
+ */
1631
+ deleteWalkthroughScopeCache(sessionId) {
1632
+ try {
1633
+ this.db.prepare('DELETE FROM walkthrough_scope_cache WHERE session_id = ?').run(sessionId);
1634
+ }
1635
+ catch (error) {
1636
+ this.logger.error('Failed to delete walkthrough scope cache', { error, sessionId });
1637
+ }
1638
+ }
1639
+ }
1640
+ //# sourceMappingURL=session-info-service.js.map