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,1678 @@
1
+ /**
2
+ * InsightsCoordinator - Unified event-driven insights system.
3
+ *
4
+ * ⚠️ LARGE FILE WARNING: ~2000 lines. Navigate using section markers (search "// ====").
5
+ *
6
+ * SECTIONS:
7
+ * - Types (~line 40) - ActionEvent, GenerateEvent, etc.
8
+ * - Constants (~line 100) - Timing constants, model settings
9
+ * - Helpers (~line 130) - generateTraceId, etc.
10
+ * - InsightsCoordinator class (~line 150) - Main coordinator
11
+ * - Singleton (~line 1950) - getInstance, initialization
12
+ *
13
+ * KEY METHODS:
14
+ * - handleAction() - Process new tool/text actions
15
+ * - handleGenerate() - Initial insight generation
16
+ * - handleRefresh() - Re-fetch from cache or regenerate
17
+ * - doPatch() - Execute Haiku quick-check → Sonnet patch flow
18
+ * - doRecompute() - Full Opus regeneration
19
+ *
20
+ * This is the COORDINATOR that orchestrates insight generation. It does NOT compute
21
+ * insights itself - that's InsightsComputer's job. This service handles:
22
+ * 1. Watching for session activity (file changes in ~/.claude/projects)
23
+ * 2. Tracking session state (action counts, timing, etc.)
24
+ * 3. Triggering insight generation and patching via InsightQueue
25
+ * 4. Executing LLM operations (generation, quick check, patch)
26
+ *
27
+ * Architecture V2 Features:
28
+ * - InsightQueue for serialization (eliminates race conditions)
29
+ * - Haiku-heavy patches (cheap quick checks, targeted updates)
30
+ * - Ownership model for REFRESH (deterministic field ownership)
31
+ *
32
+ * Related services:
33
+ * - InsightsComputer (insights-computer.ts) - builds prompts, calls LLMs
34
+ * - InsightQueue (insight-queue.ts) - serializes operations per session
35
+ * - InsightsEventLog (insights-event-log.ts) - audit logging
36
+ */
37
+ import { EventEmitter } from 'events';
38
+ import * as fs from 'fs';
39
+ import * as path from 'path';
40
+ import * as os from 'os';
41
+ import * as crypto from 'crypto';
42
+ import { createLogger } from '../infrastructure/logger.js';
43
+ import { ClaudeHistoryReader } from '../sessions/claude-history-reader.js';
44
+ import { SessionInfoService } from '../sessions/session-info-service.js';
45
+ import { InsightsComputer } from '../insights/insights-computer.js';
46
+ import { anthropicService } from '../insights/anthropic-service.js';
47
+ import { getSessionActivityWatcher } from '../sessions/session-activity-watcher.js';
48
+ import { getInsightQueue } from '../insights/insight-queue.js';
49
+ import { getInsightsEventLog } from '../insights/insights-event-log.js';
50
+ // ============================================================================
51
+ // Constants
52
+ // ============================================================================
53
+ const INITIAL_GENERATION_THRESHOLD = 20; // Generate initial insights after 20 actions
54
+ const FULL_REGEN_INTERVAL_MS = 0; // DISABLED - patches handle incremental updates, no need for periodic full regen
55
+ const DEBOUNCE_MS = 150; // Debounce file changes
56
+ const COMPLETION_CHECK_DELAY_MS = 5000; // Wait 5 seconds after assistant response
57
+ const QUICK_CHECK_DEBOUNCE_MS = 15000; // Wait 15s of quiet before running quick check
58
+ const QUICK_CHECK_STALENESS_THRESHOLD_MS = 120 * 1000; // Force check if >120s since last check
59
+ // ============================================================================
60
+ // Helpers
61
+ // ============================================================================
62
+ /**
63
+ * Generate a short trace ID for correlating events through the system.
64
+ */
65
+ function generateTraceId(sessionId) {
66
+ const sessionPrefix = sessionId.slice(0, 8);
67
+ const timestampHex = Date.now().toString(16);
68
+ const random = crypto.randomBytes(2).toString('hex');
69
+ return `${sessionPrefix}-${timestampHex}-${random}`;
70
+ }
71
+ function hasMissingCriticalFields(insights) {
72
+ if (!insights)
73
+ return true;
74
+ // Mission is critical - we need to know what the session is about
75
+ if (!insights.context || !insights.context.mission)
76
+ return true;
77
+ return false;
78
+ }
79
+ // ============================================================================
80
+ // InsightsCoordinator
81
+ // ============================================================================
82
+ export class InsightsCoordinator extends EventEmitter {
83
+ logger;
84
+ historyReader;
85
+ sessionInfoService;
86
+ insightsComputer;
87
+ projectsDir;
88
+ // File watching
89
+ watchers = new Map();
90
+ debounceTimers = new Map();
91
+ isWatching = false;
92
+ // Session state tracking
93
+ sessionStates = new Map();
94
+ lastKnownMessageCounts = new Map();
95
+ // Duplicate prevention
96
+ pendingGenerations = new Set();
97
+ pendingQuickChecks = new Set();
98
+ // Queue integration - store events for deferred execution
99
+ pendingActionEvents = new Map();
100
+ pendingGenerateEvents = new Map();
101
+ // Event log for observability
102
+ eventLog = getInsightsEventLog();
103
+ constructor() {
104
+ super();
105
+ this.logger = createLogger('InsightsCoordinator');
106
+ this.historyReader = new ClaudeHistoryReader();
107
+ this.sessionInfoService = SessionInfoService.getInstance();
108
+ this.insightsComputer = new InsightsComputer(this.historyReader, this.sessionInfoService);
109
+ this.projectsDir = path.join(os.homedir(), '.claude', 'projects');
110
+ }
111
+ // ==========================================================================
112
+ // Lifecycle
113
+ // ==========================================================================
114
+ /**
115
+ * Initialize the insights service. Call once at startup.
116
+ */
117
+ initialize() {
118
+ // Set up the insight queue executor
119
+ const queue = getInsightQueue();
120
+ queue.setExecutor(this.executeQueueOperation.bind(this));
121
+ // Start watching for session activity
122
+ this.startWatching();
123
+ // Reconcile any sessions that changed while server was down
124
+ // Run async - don't block initialization
125
+ this.reconcileStaleInsights().catch(error => {
126
+ this.logger.error('Failed to reconcile stale insights on startup', error);
127
+ });
128
+ this.logger.info('Insights service initialized');
129
+ }
130
+ /**
131
+ * Stop the insights service. Call at shutdown.
132
+ */
133
+ stop() {
134
+ this.logger.info('Stopping insights service');
135
+ // Stop file watchers
136
+ for (const watcher of this.watchers.values()) {
137
+ watcher.close();
138
+ }
139
+ this.watchers.clear();
140
+ // Clear debounce timers
141
+ for (const timer of this.debounceTimers.values()) {
142
+ clearTimeout(timer);
143
+ }
144
+ this.debounceTimers.clear();
145
+ // Clear session timers
146
+ for (const state of this.sessionStates.values()) {
147
+ if (state.regenTimerId)
148
+ clearTimeout(state.regenTimerId);
149
+ if (state.completionCheckTimer)
150
+ clearTimeout(state.completionCheckTimer);
151
+ if (state.quickCheckDebounceTimer)
152
+ clearTimeout(state.quickCheckDebounceTimer);
153
+ }
154
+ this.sessionStates.clear();
155
+ this.isWatching = false;
156
+ }
157
+ /**
158
+ * Reconcile stale insights on startup.
159
+ *
160
+ * Finds sessions where the file mtime > patched_at, meaning the session
161
+ * had activity that wasn't captured (e.g., during server restart).
162
+ * Queues forced patches to bring insights up to date.
163
+ */
164
+ async reconcileStaleInsights() {
165
+ const startTime = Date.now();
166
+ try {
167
+ // Get all sessions with insights
168
+ const allInsights = await this.sessionInfoService.getAllInsights();
169
+ if (allInsights.size === 0) {
170
+ this.logger.debug('No insights to reconcile');
171
+ return;
172
+ }
173
+ // Get session IDs that have insights
174
+ const sessionIds = Array.from(allInsights.keys());
175
+ // Get file mtimes for all sessions
176
+ const mtimes = await this.historyReader.getSessionFileMtimes(sessionIds);
177
+ // Find sessions where file is newer than patched_at
178
+ const staleSessions = [];
179
+ for (const [sessionId, insights] of allInsights.entries()) {
180
+ const mtime = mtimes.get(sessionId);
181
+ if (!mtime)
182
+ continue; // File doesn't exist
183
+ const patchedAt = insights.patched_at
184
+ ? new Date(insights.patched_at).getTime()
185
+ : insights.computed_at
186
+ ? new Date(insights.computed_at).getTime()
187
+ : 0;
188
+ // If file was modified after last patch, it's stale
189
+ // Add 1 second buffer to avoid false positives from timing differences
190
+ if (mtime > patchedAt + 1000) {
191
+ staleSessions.push({
192
+ sessionId,
193
+ patchedAt,
194
+ mtime,
195
+ gap: Math.round((mtime - patchedAt) / 1000)
196
+ });
197
+ }
198
+ }
199
+ if (staleSessions.length === 0) {
200
+ this.logger.info('Startup reconciliation: all insights up to date', {
201
+ checkedCount: sessionIds.length,
202
+ durationMs: Date.now() - startTime
203
+ });
204
+ return;
205
+ }
206
+ this.logger.info('Startup reconciliation: found stale sessions', {
207
+ staleCount: staleSessions.length,
208
+ totalChecked: sessionIds.length,
209
+ staleSessions: staleSessions.slice(0, 10).map(s => ({
210
+ sessionId: s.sessionId.slice(0, 8),
211
+ gapSeconds: s.gap
212
+ }))
213
+ });
214
+ // Queue forced patches for stale sessions
215
+ // Limit to most recently modified to avoid overwhelming the queue on first startup
216
+ const sortedStale = staleSessions.sort((a, b) => b.mtime - a.mtime);
217
+ const toReconcile = sortedStale.slice(0, 20); // Max 20 sessions per startup
218
+ for (const { sessionId } of toReconcile) {
219
+ await this.triggerReconciliationPatch(sessionId);
220
+ }
221
+ this.logger.info('Startup reconciliation: queued patches', {
222
+ queuedCount: toReconcile.length,
223
+ skippedCount: staleSessions.length - toReconcile.length,
224
+ durationMs: Date.now() - startTime
225
+ });
226
+ }
227
+ catch (error) {
228
+ this.logger.error('Startup reconciliation failed', error);
229
+ }
230
+ }
231
+ /**
232
+ * Trigger a forced patch for a session during reconciliation.
233
+ * Reads recent messages and queues a force-patch (skips Haiku quick-check).
234
+ */
235
+ async triggerReconciliationPatch(sessionId) {
236
+ try {
237
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
238
+ if (!messages || messages.length === 0)
239
+ return;
240
+ // Get the last 20 messages for context (or fewer if session is short)
241
+ const recentMessages = messages.slice(-20);
242
+ const newActions = this.extractActions(recentMessages);
243
+ if (newActions.length === 0) {
244
+ // If no actions extractable, create a synthetic one
245
+ newActions.push({ type: 'text', text: '[Reconciliation - catching up on missed activity]' });
246
+ }
247
+ const traceId = generateTraceId(sessionId);
248
+ this.logger.debug('Triggering reconciliation patch', {
249
+ traceId,
250
+ sessionId: sessionId.slice(0, 8),
251
+ messageCount: messages.length,
252
+ actionCount: newActions.length
253
+ });
254
+ // Initialize session state if needed
255
+ if (!this.sessionStates.has(sessionId)) {
256
+ this.sessionStates.set(sessionId, {
257
+ actionCount: 0,
258
+ lastActionAt: Date.now(),
259
+ lastCheckAt: Date.now(),
260
+ actionsSinceLastPatch: []
261
+ });
262
+ }
263
+ const state = this.sessionStates.get(sessionId);
264
+ state.actionsSinceLastPatch = newActions;
265
+ // Queue a forced patch (bypasses Haiku quick-check)
266
+ this.enqueueAction({
267
+ sessionId,
268
+ actionCount: newActions.length,
269
+ messageCount: messages.length,
270
+ newActions,
271
+ timestamp: Date.now(),
272
+ forcePatch: true,
273
+ traceId
274
+ });
275
+ }
276
+ catch (error) {
277
+ this.logger.warn('Failed to trigger reconciliation patch', {
278
+ sessionId: sessionId.slice(0, 8),
279
+ error
280
+ });
281
+ }
282
+ }
283
+ // ==========================================================================
284
+ // File Watching
285
+ // ==========================================================================
286
+ startWatching() {
287
+ if (this.isWatching)
288
+ return;
289
+ this.logger.info('Starting file watchers', { projectsDir: this.projectsDir });
290
+ try {
291
+ this.watchDirectory(this.projectsDir);
292
+ if (fs.existsSync(this.projectsDir)) {
293
+ const projects = fs.readdirSync(this.projectsDir);
294
+ for (const project of projects) {
295
+ const projectPath = path.join(this.projectsDir, project);
296
+ if (fs.statSync(projectPath).isDirectory()) {
297
+ this.watchProjectDirectory(projectPath);
298
+ }
299
+ }
300
+ }
301
+ this.isWatching = true;
302
+ this.logger.info('File watchers started', { watcherCount: this.watchers.size });
303
+ }
304
+ catch (error) {
305
+ this.logger.error('Failed to start file watchers', error);
306
+ }
307
+ }
308
+ watchDirectory(dirPath) {
309
+ if (this.watchers.has(dirPath))
310
+ return;
311
+ try {
312
+ const watcher = fs.watch(dirPath, { persistent: false }, (eventType, filename) => {
313
+ if (!filename)
314
+ return;
315
+ const fullPath = path.join(dirPath, filename);
316
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
317
+ this.watchProjectDirectory(fullPath);
318
+ }
319
+ });
320
+ this.watchers.set(dirPath, watcher);
321
+ }
322
+ catch (error) {
323
+ this.logger.warn('Failed to watch directory', { dirPath, error });
324
+ }
325
+ }
326
+ watchProjectDirectory(projectPath) {
327
+ if (this.watchers.has(projectPath))
328
+ return;
329
+ try {
330
+ const watcher = fs.watch(projectPath, { persistent: false }, (eventType, filename) => {
331
+ if (!filename || !filename.endsWith('.jsonl'))
332
+ return;
333
+ const filePath = path.join(projectPath, filename);
334
+ this.handleFileChange(filePath, filename);
335
+ });
336
+ this.watchers.set(projectPath, watcher);
337
+ }
338
+ catch (error) {
339
+ this.logger.warn('Failed to watch project directory', { projectPath, error });
340
+ }
341
+ }
342
+ handleFileChange(filePath, filename) {
343
+ const sessionId = filename.replace('.jsonl', '');
344
+ // Debounce rapid changes
345
+ const existingTimer = this.debounceTimers.get(sessionId);
346
+ if (existingTimer) {
347
+ clearTimeout(existingTimer);
348
+ }
349
+ this.debounceTimers.set(sessionId, setTimeout(async () => {
350
+ this.debounceTimers.delete(sessionId);
351
+ await this.processSessionUpdate(sessionId);
352
+ }, DEBOUNCE_MS));
353
+ }
354
+ // ==========================================================================
355
+ // Session Update Processing
356
+ // ==========================================================================
357
+ async processSessionUpdate(sessionId) {
358
+ try {
359
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
360
+ let previousCount = this.lastKnownMessageCounts.get(sessionId) || 0;
361
+ const currentCount = messages.length;
362
+ // COLD START FIX: If this is first time seeing this session,
363
+ // check if cached insights exist. If so, use cached message_count as baseline
364
+ // to avoid re-processing old messages while still processing new ones.
365
+ if (previousCount === 0) {
366
+ const state = this.sessionStates.get(sessionId);
367
+ if (!state) {
368
+ const cachedInsights = await this.sessionInfoService.getInsights(sessionId);
369
+ if (cachedInsights && cachedInsights.computed_at) {
370
+ // Use cached message count as baseline, not current count
371
+ // This allows processing of new messages that arrived after the cache
372
+ const cachedMsgCount = cachedInsights.message_count || 0;
373
+ previousCount = cachedMsgCount;
374
+ this.lastKnownMessageCounts.set(sessionId, cachedMsgCount);
375
+ this.logger.info('Cold start: resuming from cached message count', {
376
+ sessionId: sessionId.slice(0, 8),
377
+ cachedMessageCount: cachedMsgCount,
378
+ currentMessageCount: currentCount,
379
+ newMessages: currentCount - cachedMsgCount,
380
+ computed_at: cachedInsights.computed_at
381
+ });
382
+ // Don't return - continue to process any new messages
383
+ }
384
+ }
385
+ }
386
+ // No new messages
387
+ if (currentCount <= previousCount) {
388
+ return;
389
+ }
390
+ this.lastKnownMessageCounts.set(sessionId, currentCount);
391
+ // Extract new actions from new messages
392
+ const newMessages = messages.slice(previousCount);
393
+ const newActions = this.extractActions(newMessages);
394
+ if (newActions.length === 0) {
395
+ return;
396
+ }
397
+ // Update session state
398
+ let state = this.sessionStates.get(sessionId);
399
+ let shouldForceRegenDueToStaleCache = false;
400
+ if (!state) {
401
+ const cachedInsights = await this.sessionInfoService.getInsights(sessionId);
402
+ const hasExistingInsights = cachedInsights && cachedInsights.computed_at;
403
+ // Check for stale/incomplete cache that needs regeneration
404
+ if (hasExistingInsights) {
405
+ const hasMissing = hasMissingCriticalFields(cachedInsights);
406
+ // If critical fields are missing, regenerate regardless of message count
407
+ // This catches sessions that were never properly generated
408
+ if (hasMissing) {
409
+ this.logger.info('Incomplete cache detected - triggering regeneration', {
410
+ sessionId: sessionId.slice(0, 8),
411
+ cachedMsgCount: cachedInsights.message_count,
412
+ currentMsgCount: currentCount,
413
+ missingCriticalFields: true
414
+ });
415
+ shouldForceRegenDueToStaleCache = true;
416
+ }
417
+ // Also check for very early caching (<=5 messages) with significant new activity
418
+ else if (cachedInsights.message_count && cachedInsights.message_count <= 5) {
419
+ const messageCountDisparity = currentCount - cachedInsights.message_count;
420
+ if (messageCountDisparity >= 20) {
421
+ this.logger.info('Early cache with significant new activity - triggering regeneration', {
422
+ sessionId: sessionId.slice(0, 8),
423
+ cachedMsgCount: cachedInsights.message_count,
424
+ currentMsgCount: currentCount,
425
+ disparity: messageCountDisparity
426
+ });
427
+ shouldForceRegenDueToStaleCache = true;
428
+ }
429
+ }
430
+ }
431
+ state = {
432
+ actionCount: 0,
433
+ lastActionAt: 0,
434
+ lastCheckAt: Date.now(),
435
+ insightsGeneratedAt: hasExistingInsights ? Date.now() : undefined,
436
+ actionsSinceLastPatch: []
437
+ };
438
+ this.sessionStates.set(sessionId, state);
439
+ if (hasExistingInsights && !shouldForceRegenDueToStaleCache) {
440
+ this.logger.debug('Session state initialized with existing insights', {
441
+ sessionId: sessionId.slice(0, 8),
442
+ computed_at: cachedInsights.computed_at
443
+ });
444
+ }
445
+ }
446
+ const previousActionCount = state.actionCount;
447
+ state.actionCount += newActions.length;
448
+ state.lastActionAt = Date.now();
449
+ // Accumulate actions for the next patch - ensures we have full context
450
+ // (user message, Claude response, tool uses) not just the latest action
451
+ state.actionsSinceLastPatch = state.actionsSinceLastPatch || [];
452
+ state.actionsSinceLastPatch.push(...newActions);
453
+ // Cap at 50 to prevent memory bloat in very long sessions
454
+ if (state.actionsSinceLastPatch.length > 50) {
455
+ state.actionsSinceLastPatch = state.actionsSinceLastPatch.slice(-50);
456
+ }
457
+ this.logger.debug('Session activity detected', {
458
+ sessionId: sessionId.slice(0, 8),
459
+ newActions: newActions.length,
460
+ totalActions: state.actionCount,
461
+ hasInsights: !!state.insightsGeneratedAt
462
+ });
463
+ // Check if we should trigger initial generation
464
+ const shouldGenerateInitial = !state.insightsGeneratedAt &&
465
+ previousActionCount < INITIAL_GENERATION_THRESHOLD &&
466
+ state.actionCount >= INITIAL_GENERATION_THRESHOLD;
467
+ if (shouldGenerateInitial || shouldForceRegenDueToStaleCache) {
468
+ if (this.pendingGenerations.has(sessionId)) {
469
+ this.logger.debug('Skipping generation - already in progress', {
470
+ sessionId: sessionId.slice(0, 8)
471
+ });
472
+ return;
473
+ }
474
+ const reason = shouldForceRegenDueToStaleCache ? 'stale_cache' : 'initial';
475
+ this.logger.info('Triggering insights generation', {
476
+ sessionId: sessionId.slice(0, 8),
477
+ actionCount: state.actionCount,
478
+ reason
479
+ });
480
+ this.enqueueGenerate({ sessionId, reason, actionCount: state.actionCount });
481
+ return;
482
+ }
483
+ // Force generation if insights exist but are missing critical content
484
+ // Check only every 100 actions - Haiku patches should fill gaps in most cases
485
+ // Also enforce 10-minute cooldown to prevent rapid regeneration
486
+ const MISSING_FIELDS_CHECK_INTERVAL = 100;
487
+ const REGENERATION_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
488
+ let shouldForceRegenDueToMissingFields = false;
489
+ if (state.insightsGeneratedAt && state.actionCount % MISSING_FIELDS_CHECK_INTERVAL === 0) {
490
+ const cachedInsights = await this.sessionInfoService.getInsights(sessionId);
491
+ const hasMissing = hasMissingCriticalFields(cachedInsights);
492
+ // Check cooldown - don't regenerate if we regenerated recently
493
+ const timeSinceLastGen = Date.now() - state.insightsGeneratedAt;
494
+ const cooldownActive = timeSinceLastGen < REGENERATION_COOLDOWN_MS;
495
+ if (hasMissing && cooldownActive) {
496
+ this.logger.debug('Skipping missing_fields regen due to cooldown', {
497
+ sessionId: sessionId.slice(0, 8),
498
+ timeSinceLastGenMs: timeSinceLastGen,
499
+ cooldownMs: REGENERATION_COOLDOWN_MS
500
+ });
501
+ }
502
+ shouldForceRegenDueToMissingFields = hasMissing && !cooldownActive;
503
+ }
504
+ if (shouldForceRegenDueToMissingFields) {
505
+ if (this.pendingGenerations.has(sessionId)) {
506
+ return;
507
+ }
508
+ this.logger.info('Triggering insights generation', {
509
+ sessionId: sessionId.slice(0, 8),
510
+ actionCount: state.actionCount,
511
+ reason: 'missing_fields'
512
+ });
513
+ this.enqueueGenerate({ sessionId, reason: 'missing_fields', actionCount: state.actionCount });
514
+ return;
515
+ }
516
+ // If we have insights, trigger action event for patch checking
517
+ if (state.insightsGeneratedAt) {
518
+ this.handleSessionAction(sessionId, state, newActions, currentCount);
519
+ }
520
+ }
521
+ catch (error) {
522
+ this.logger.debug('Failed to process session update', {
523
+ sessionId: sessionId.slice(0, 8),
524
+ error
525
+ });
526
+ }
527
+ }
528
+ /**
529
+ * Handle action event - determine if/how to trigger patch checking
530
+ */
531
+ handleSessionAction(sessionId, state, newActions, messageCount) {
532
+ const hasUserMessage = newActions.some(a => a.type === 'user_message');
533
+ const hasToolUse = newActions.some(a => a.type === 'tool_use');
534
+ const lastAction = newActions[newActions.length - 1];
535
+ const endsWithText = lastAction?.type === 'text';
536
+ // User message: immediate patch to update current state
537
+ // This gives instant feedback when user starts a new interaction
538
+ if (hasUserMessage) {
539
+ if (state.completionCheckTimer) {
540
+ clearTimeout(state.completionCheckTimer);
541
+ state.completionCheckTimer = undefined;
542
+ }
543
+ if (state.quickCheckDebounceTimer) {
544
+ clearTimeout(state.quickCheckDebounceTimer);
545
+ state.quickCheckDebounceTimer = undefined;
546
+ }
547
+ const traceId = generateTraceId(sessionId);
548
+ this.logger.debug('User message - immediate patch', {
549
+ traceId,
550
+ sessionId: sessionId.slice(0, 8),
551
+ });
552
+ // Extract the user message text for currentWork summarization
553
+ const userMessageAction = newActions.find(a => a.type === 'user_message');
554
+ const userMessageText = userMessageAction?.text || '';
555
+ // Trigger currentWork summarization (fire and forget - don't block patching)
556
+ if (userMessageText) {
557
+ this.triggerCurrentWorkSummarization(sessionId, userMessageText, traceId).catch(err => {
558
+ this.logger.debug('CurrentWork summarization failed', {
559
+ traceId,
560
+ sessionId: sessionId.slice(0, 8),
561
+ error: err instanceof Error ? err.message : String(err)
562
+ });
563
+ });
564
+ }
565
+ // Log trigger event
566
+ this.eventLog.trigger({
567
+ traceId,
568
+ sessionId,
569
+ source: 'user_message',
570
+ actionCount: state.actionCount,
571
+ messageCount,
572
+ newActions: newActions.map(a => a.type + (a.name ? `:${a.name}` : '')),
573
+ });
574
+ this.enqueueAction({
575
+ sessionId,
576
+ actionCount: state.actionCount,
577
+ messageCount,
578
+ newActions,
579
+ timestamp: Date.now(),
580
+ forcePatch: true,
581
+ traceId
582
+ });
583
+ return;
584
+ }
585
+ // Batch ends with text (no pending tool use): likely completion
586
+ // Set up delayed check - if no more activity, patch after 5s
587
+ if (endsWithText && !hasToolUse) {
588
+ if (state.completionCheckTimer) {
589
+ clearTimeout(state.completionCheckTimer);
590
+ }
591
+ state.lastResponseTimestamp = Date.now();
592
+ const capturedMessageCount = messageCount;
593
+ state.completionCheckTimer = setTimeout(() => {
594
+ const traceId = generateTraceId(sessionId);
595
+ this.logger.debug('Completion detected - forcing patch', {
596
+ traceId,
597
+ sessionId: sessionId.slice(0, 8),
598
+ });
599
+ // Log completion trigger
600
+ this.eventLog.trigger({
601
+ traceId,
602
+ sessionId,
603
+ source: 'completion',
604
+ actionCount: state.actionCount,
605
+ messageCount: capturedMessageCount,
606
+ newActions: newActions.map(a => a.type + (a.name ? `:${a.name}` : '')),
607
+ });
608
+ this.enqueueAction({
609
+ sessionId,
610
+ actionCount: state.actionCount,
611
+ messageCount: capturedMessageCount,
612
+ newActions,
613
+ timestamp: Date.now(),
614
+ forcePatch: true,
615
+ traceId
616
+ });
617
+ state.completionCheckTimer = undefined;
618
+ }, COMPLETION_CHECK_DELAY_MS);
619
+ return;
620
+ }
621
+ // Tool use in progress - debounced check
622
+ // Clear any pending completion timer since we're still active
623
+ if (state.completionCheckTimer) {
624
+ clearTimeout(state.completionCheckTimer);
625
+ state.completionCheckTimer = undefined;
626
+ }
627
+ const timeSinceLastCheck = Date.now() - state.lastCheckAt;
628
+ const isStale = timeSinceLastCheck > QUICK_CHECK_STALENESS_THRESHOLD_MS;
629
+ if (isStale) {
630
+ // Force check if stale
631
+ if (state.quickCheckDebounceTimer) {
632
+ clearTimeout(state.quickCheckDebounceTimer);
633
+ state.quickCheckDebounceTimer = undefined;
634
+ }
635
+ const traceId = generateTraceId(sessionId);
636
+ // Log tool_use trigger (stale)
637
+ this.eventLog.trigger({
638
+ traceId,
639
+ sessionId,
640
+ source: 'tool_use',
641
+ actionCount: state.actionCount,
642
+ messageCount,
643
+ newActions: newActions.map(a => a.type + (a.name ? `:${a.name}` : '')),
644
+ });
645
+ this.enqueueAction({
646
+ sessionId,
647
+ actionCount: state.actionCount,
648
+ messageCount,
649
+ newActions,
650
+ timestamp: Date.now(),
651
+ forcePatch: false,
652
+ traceId
653
+ });
654
+ }
655
+ else {
656
+ // Debounce: wait for quiet period before checking
657
+ if (state.quickCheckDebounceTimer) {
658
+ clearTimeout(state.quickCheckDebounceTimer);
659
+ }
660
+ const capturedActions = [...newActions];
661
+ const capturedActionCount = state.actionCount;
662
+ const capturedMessageCount = messageCount;
663
+ state.quickCheckDebounceTimer = setTimeout(() => {
664
+ const traceId = generateTraceId(sessionId);
665
+ // Log debounced tool_use trigger
666
+ this.eventLog.trigger({
667
+ traceId,
668
+ sessionId,
669
+ source: 'tool_use',
670
+ actionCount: capturedActionCount,
671
+ messageCount: capturedMessageCount,
672
+ newActions: capturedActions.map(a => a.type + (a.name ? `:${a.name}` : '')),
673
+ });
674
+ this.enqueueAction({
675
+ sessionId,
676
+ actionCount: capturedActionCount,
677
+ messageCount: capturedMessageCount,
678
+ newActions: capturedActions,
679
+ timestamp: Date.now(),
680
+ forcePatch: false,
681
+ traceId
682
+ });
683
+ state.quickCheckDebounceTimer = undefined;
684
+ }, QUICK_CHECK_DEBOUNCE_MS);
685
+ }
686
+ }
687
+ // ==========================================================================
688
+ // Action Extraction
689
+ // ==========================================================================
690
+ extractActions(messages) {
691
+ const actions = [];
692
+ const pendingToolUses = new Map();
693
+ for (const msg of messages) {
694
+ const content = msg.message?.content;
695
+ // Extract timestamp from message for temporal context
696
+ const msgTimestamp = msg.timestamp;
697
+ // Handle user messages - content can be string or array
698
+ if (msg.type === 'user') {
699
+ // Handle string content (common for user input)
700
+ if (typeof content === 'string') {
701
+ const trimmed = content.trim();
702
+ // Skip meta/system messages
703
+ if (trimmed.length > 0 && !trimmed.startsWith('<') && !trimmed.includes('DO NOT respond to these messages')) {
704
+ actions.push({ type: 'user_message', text: trimmed, timestamp: msgTimestamp });
705
+ }
706
+ continue;
707
+ }
708
+ // Handle array content (tool results, etc.)
709
+ if (!Array.isArray(content))
710
+ continue;
711
+ for (const block of content) {
712
+ if (typeof block !== 'object' || block === null || !('type' in block))
713
+ continue;
714
+ if (block.type === 'tool_result' && 'tool_use_id' in block) {
715
+ const toolUseId = block.tool_use_id;
716
+ const pending = pendingToolUses.get(toolUseId);
717
+ if (pending) {
718
+ let resultContent = '';
719
+ if (typeof block.content === 'string') {
720
+ resultContent = block.content;
721
+ }
722
+ else if (Array.isArray(block.content)) {
723
+ resultContent = block.content
724
+ .filter((b) => b.type === 'text' && b.text)
725
+ .map((b) => b.text)
726
+ .join('\n');
727
+ }
728
+ if (actions[pending.index]) {
729
+ actions[pending.index].output = resultContent.slice(0, 200);
730
+ }
731
+ pendingToolUses.delete(toolUseId);
732
+ }
733
+ continue;
734
+ }
735
+ if (block.type === 'text' && 'text' in block) {
736
+ const fullText = block.text.trim();
737
+ if (fullText.length > 0) {
738
+ actions.push({ type: 'user_message', text: fullText, timestamp: msgTimestamp });
739
+ }
740
+ }
741
+ }
742
+ continue;
743
+ }
744
+ // For assistant messages, content must be an array
745
+ if (!Array.isArray(content))
746
+ continue;
747
+ if (msg.type === 'assistant') {
748
+ for (const block of content) {
749
+ if (typeof block !== 'object' || block === null || !('type' in block))
750
+ continue;
751
+ if (block.type === 'tool_use' && 'name' in block && 'id' in block) {
752
+ const toolName = block.name;
753
+ const toolId = block.id;
754
+ const input = block.input;
755
+ let inputSummary = '';
756
+ if (input) {
757
+ switch (toolName) {
758
+ case 'Read':
759
+ case 'Edit':
760
+ case 'Write':
761
+ inputSummary = this.extractFilename(input.file_path);
762
+ break;
763
+ case 'Grep':
764
+ inputSummary = `pattern: "${input.pattern?.slice(0, 40)}"`;
765
+ break;
766
+ case 'Glob':
767
+ inputSummary = `pattern: ${input.pattern?.slice(0, 40)}`;
768
+ break;
769
+ case 'Bash': {
770
+ const desc = input.description;
771
+ const cmd = input.command;
772
+ inputSummary = desc || cmd?.slice(0, 80) || '';
773
+ break;
774
+ }
775
+ case 'Task':
776
+ inputSummary = input.description?.slice(0, 60) || '';
777
+ break;
778
+ default: {
779
+ const firstKey = Object.keys(input)[0];
780
+ if (firstKey && input[firstKey]) {
781
+ const val = input[firstKey];
782
+ inputSummary = typeof val === 'string' ? val.slice(0, 60) : JSON.stringify(val).slice(0, 60);
783
+ }
784
+ }
785
+ }
786
+ }
787
+ const actionIndex = actions.length;
788
+ actions.push({ type: 'tool_use', name: toolName, input: inputSummary, timestamp: msgTimestamp });
789
+ pendingToolUses.set(toolId, { name: toolName, input: inputSummary, index: actionIndex });
790
+ }
791
+ else if (block.type === 'text' && 'text' in block) {
792
+ const fullText = block.text.trim();
793
+ if (fullText.length > 0) {
794
+ actions.push({ type: 'text', text: fullText, timestamp: msgTimestamp });
795
+ }
796
+ }
797
+ }
798
+ }
799
+ }
800
+ return actions;
801
+ }
802
+ extractFilename(filePath) {
803
+ if (!filePath)
804
+ return '';
805
+ const parts = filePath.split('/');
806
+ return parts[parts.length - 1] || filePath.slice(-40);
807
+ }
808
+ // ==========================================================================
809
+ // Queue Integration
810
+ // ==========================================================================
811
+ enqueueGenerate(event) {
812
+ const { sessionId, reason } = event;
813
+ this.pendingGenerateEvents.set(sessionId, event);
814
+ const opType = (reason === 'background' || reason === 'scheduled') ? 'REFRESH' : 'GENERATE';
815
+ const priority = (reason === 'initial' || reason === 'missing_fields') ? 'high' : 'low';
816
+ getInsightQueue().enqueue({
817
+ type: opType,
818
+ sessionId,
819
+ priority,
820
+ trigger: this.mapReasonToTrigger(reason),
821
+ });
822
+ }
823
+ enqueueAction(event) {
824
+ const { sessionId, newActions, traceId } = event;
825
+ this.pendingActionEvents.set(sessionId, event);
826
+ const hasUserMessage = newActions.some(a => a.type === 'user_message');
827
+ const trigger = hasUserMessage ? 'user_message' : 'tool_use';
828
+ const priority = hasUserMessage ? 'high' : 'normal';
829
+ getInsightQueue().enqueue({
830
+ type: 'PATCH',
831
+ sessionId,
832
+ priority,
833
+ trigger,
834
+ traceId,
835
+ });
836
+ }
837
+ mapReasonToTrigger(reason) {
838
+ switch (reason) {
839
+ case 'initial': return 'initial';
840
+ case 'scheduled': return 'scheduled';
841
+ case 'manual': return 'manual';
842
+ case 'missing_fields': return 'missing_fields';
843
+ case 'background': return 'scheduled';
844
+ default: return 'manual';
845
+ }
846
+ }
847
+ async executeQueueOperation(op) {
848
+ const { type, sessionId, trigger, traceId } = op;
849
+ this.logger.debug('Executing queued operation', {
850
+ type,
851
+ sessionId: sessionId.slice(0, 8),
852
+ trigger,
853
+ traceId,
854
+ });
855
+ switch (type) {
856
+ case 'GENERATE': {
857
+ const event = this.pendingGenerateEvents.get(sessionId);
858
+ if (event) {
859
+ this.pendingGenerateEvents.delete(sessionId);
860
+ await this.executeGenerate(event);
861
+ }
862
+ break;
863
+ }
864
+ case 'PATCH': {
865
+ const event = this.pendingActionEvents.get(sessionId);
866
+ if (event) {
867
+ this.pendingActionEvents.delete(sessionId);
868
+ await this.executeAction(event);
869
+ }
870
+ break;
871
+ }
872
+ case 'REFRESH': {
873
+ const event = this.pendingGenerateEvents.get(sessionId);
874
+ if (event) {
875
+ this.pendingGenerateEvents.delete(sessionId);
876
+ await this.executeGenerate(event);
877
+ }
878
+ else {
879
+ await this.executeGenerate({ sessionId, reason: 'scheduled', actionCount: 0 });
880
+ }
881
+ break;
882
+ }
883
+ }
884
+ }
885
+ // ==========================================================================
886
+ // LLM Operations - Generation
887
+ // ==========================================================================
888
+ async executeGenerate(event) {
889
+ const { sessionId, reason, actionCount } = event;
890
+ if (this.pendingGenerations.has(sessionId)) {
891
+ return;
892
+ }
893
+ this.pendingGenerations.add(sessionId);
894
+ try {
895
+ this.logger.debug('Generating insights', {
896
+ sessionId: sessionId.slice(0, 8),
897
+ reason,
898
+ actionCount
899
+ });
900
+ const startTime = Date.now();
901
+ const freshInsights = await this.insightsComputer.computeInsights(sessionId);
902
+ const duration = Date.now() - startTime;
903
+ let finalInsights = freshInsights;
904
+ // For REFRESH, apply ownership model
905
+ if (reason === 'background' || reason === 'scheduled') {
906
+ try {
907
+ const cachedInsights = await this.insightsComputer.getInsights(sessionId);
908
+ if (cachedInsights) {
909
+ finalInsights = this.applyOwnershipModel(cachedInsights, freshInsights);
910
+ this.logger.debug('[REFRESH] Ownership model applied', {
911
+ sessionId: sessionId.slice(0, 8),
912
+ finalMission: finalInsights.context?.mission?.slice(0, 50),
913
+ finalMilestoneCount: finalInsights.milestones?.length || 0,
914
+ });
915
+ }
916
+ }
917
+ catch (mergeError) {
918
+ this.logger.warn('[REFRESH] Ownership apply failed, using fresh insights', {
919
+ sessionId: sessionId.slice(0, 8),
920
+ error: mergeError instanceof Error ? mergeError.message : String(mergeError)
921
+ });
922
+ }
923
+ }
924
+ await this.insightsComputer.cacheInsights(sessionId, finalInsights);
925
+ this.markInsightsGenerated(sessionId);
926
+ // Log generate completion
927
+ this.eventLog.generate({
928
+ traceId: generateTraceId(sessionId),
929
+ sessionId,
930
+ afterState: {
931
+ mission: finalInsights.context?.mission?.slice(0, 100),
932
+ milestones: finalInsights.milestones?.slice(0, 3).map(m => m.label?.slice(0, 50)),
933
+ notableCount: finalInsights.notable?.length,
934
+ },
935
+ durationMs: duration,
936
+ });
937
+ this.logger.info('Insights generated successfully', {
938
+ sessionId: sessionId.slice(0, 8),
939
+ reason,
940
+ durationMs: duration,
941
+ mission: finalInsights.context?.mission?.slice(0, 50),
942
+ });
943
+ await new Promise(resolve => setTimeout(resolve, 100));
944
+ await getSessionActivityWatcher().emitInsightsUpdate(sessionId, 'generated');
945
+ }
946
+ catch (error) {
947
+ this.logger.error('Failed to generate insights', {
948
+ sessionId: sessionId.slice(0, 8),
949
+ reason,
950
+ error: error instanceof Error ? error.message : String(error)
951
+ });
952
+ }
953
+ finally {
954
+ this.pendingGenerations.delete(sessionId);
955
+ }
956
+ }
957
+ /**
958
+ * Apply deterministic ownership model for REFRESH operations.
959
+ * - Opus owns (use fresh): context, theme, tags
960
+ * - Haiku owns (keep cached): purpose (set via fast patch)
961
+ */
962
+ applyOwnershipModel(cached, fresh) {
963
+ return {
964
+ ...fresh,
965
+ // Opus owns these (use fresh)
966
+ context: fresh.context,
967
+ theme: fresh.theme,
968
+ tags: fresh.tags,
969
+ // Haiku owns purpose (set via fast patch, not full compute)
970
+ purpose: cached.purpose,
971
+ // Preserve timestamps
972
+ computedAt: fresh.computedAt,
973
+ patchedAt: cached.patchedAt,
974
+ };
975
+ }
976
+ // ==========================================================================
977
+ // LLM Operations - Patching
978
+ // ==========================================================================
979
+ async executeAction(event) {
980
+ const { sessionId, newActions, actionCount: _actionCount, messageCount, forcePatch, traceId } = event;
981
+ const trigger = newActions.some(a => a.type === 'user_message') ? 'user_message'
982
+ : newActions.some(a => a.type === 'tool_use') ? 'tool_use'
983
+ : 'completion';
984
+ if (this.pendingQuickChecks.has(sessionId)) {
985
+ this.logger.debug('Skipping check - already in progress', { traceId, sessionId: sessionId.slice(0, 8) });
986
+ SessionInfoService.getInstance().auditEventInsight({
987
+ traceId,
988
+ sessionId,
989
+ eventType: 'skip',
990
+ trigger,
991
+ actionContent: newActions.map(a => a.type === 'tool_use' ? `tool:${a.name}` : `${a.type}:${(a.text || '').slice(0, 50)}`),
992
+ skippedReason: 'already_in_progress'
993
+ });
994
+ return;
995
+ }
996
+ this.pendingQuickChecks.add(sessionId);
997
+ const _startTime = Date.now();
998
+ try {
999
+ const cached = await SessionInfoService.getInstance().getInsights(sessionId);
1000
+ if (!cached) {
1001
+ this.logger.debug('No cached insights for check', { traceId, sessionId: sessionId.slice(0, 8) });
1002
+ SessionInfoService.getInstance().auditEventInsight({
1003
+ traceId,
1004
+ sessionId,
1005
+ eventType: 'skip',
1006
+ trigger,
1007
+ actionContent: newActions.map(a => a.type === 'tool_use' ? `tool:${a.name}` : `${a.type}:${(a.text || '').slice(0, 50)}`),
1008
+ skippedReason: 'no_cached_insights'
1009
+ });
1010
+ return;
1011
+ }
1012
+ const beforeState = {
1013
+ mission: cached.context?.mission,
1014
+ milestones: cached.milestones?.map(m => `${m.label}:${m.status}`),
1015
+ };
1016
+ // Use accumulated actions since last patch for richer context
1017
+ // This ensures we have user message + Claude response + tool uses, not just the latest action
1018
+ const state = this.sessionStates.get(sessionId);
1019
+ const actionsForPatch = state?.actionsSinceLastPatch?.length ? state.actionsSinceLastPatch : newActions;
1020
+ this.logger.debug('Building recentActivity for patch', {
1021
+ sessionId: sessionId.slice(0, 8),
1022
+ accumulatedActions: state?.actionsSinceLastPatch?.length || 0,
1023
+ newActions: newActions.length,
1024
+ usingAccumulated: actionsForPatch === state?.actionsSinceLastPatch
1025
+ });
1026
+ const recentActivity = actionsForPatch.map(a => {
1027
+ if (a.type === 'tool_use') {
1028
+ let content = `Tool: ${a.name}`;
1029
+ if (a.input)
1030
+ content += ` (${a.input})`;
1031
+ if (a.output)
1032
+ content += ` → ${a.output.slice(0, 100)}`;
1033
+ return { type: a.type, content, timestamp: a.timestamp };
1034
+ }
1035
+ return { type: a.type, content: a.text || '', timestamp: a.timestamp };
1036
+ });
1037
+ const actionContent = actionsForPatch.map(a => {
1038
+ if (a.type === 'tool_use') {
1039
+ let content = `tool:${a.name}`;
1040
+ if (a.input)
1041
+ content += `(${a.input.slice(0, 30)})`;
1042
+ return content;
1043
+ }
1044
+ return `${a.type}:${(a.text || '').slice(0, 50)}`;
1045
+ });
1046
+ if (forcePatch) {
1047
+ await this.doPatch(sessionId, cached, recentActivity, traceId, messageCount, beforeState, actionContent, trigger);
1048
+ return;
1049
+ }
1050
+ // Normal flow: quick check with Haiku first
1051
+ const recentActionsText = newActions.map(a => {
1052
+ if (a.type === 'tool_use') {
1053
+ let actionText = `Tool: ${a.name}`;
1054
+ if (a.input)
1055
+ actionText += ` (${a.input})`;
1056
+ if (a.output)
1057
+ actionText += ` → ${a.output.slice(0, 100)}`;
1058
+ return actionText;
1059
+ }
1060
+ else if (a.type === 'user_message') {
1061
+ return `User: ${a.text}`;
1062
+ }
1063
+ else {
1064
+ return `Response: ${a.text}`;
1065
+ }
1066
+ });
1067
+ const quickCheckStartTime = Date.now();
1068
+ const quickCheckResult = await this.doQuickCheck(cached, recentActionsText, traceId, sessionId);
1069
+ const quickCheckDurationMs = Date.now() - quickCheckStartTime;
1070
+ this.markQuickCheckPerformed(sessionId);
1071
+ if (quickCheckResult.needsPatch) {
1072
+ // Log quick check result
1073
+ this.eventLog.quickCheck({
1074
+ traceId,
1075
+ sessionId,
1076
+ result: 'patch_needed',
1077
+ reason: quickCheckResult.reason,
1078
+ durationMs: quickCheckDurationMs,
1079
+ });
1080
+ SessionInfoService.getInstance().auditEventInsight({
1081
+ traceId,
1082
+ sessionId,
1083
+ eventType: 'quick_check',
1084
+ trigger,
1085
+ actionContent,
1086
+ beforeState,
1087
+ llmResponse: `needsPatch=true: ${quickCheckResult.reason}`,
1088
+ durationMs: quickCheckDurationMs
1089
+ });
1090
+ await this.doPatch(sessionId, cached, recentActivity, traceId, messageCount, beforeState, actionContent, trigger);
1091
+ }
1092
+ else {
1093
+ this.logger.debug('Quick check: no patch needed', {
1094
+ traceId,
1095
+ sessionId: sessionId.slice(0, 8),
1096
+ reason: quickCheckResult.reason,
1097
+ });
1098
+ // Log quick check skip
1099
+ this.eventLog.quickCheck({
1100
+ traceId,
1101
+ sessionId,
1102
+ result: 'no_patch',
1103
+ reason: quickCheckResult.reason,
1104
+ durationMs: quickCheckDurationMs,
1105
+ });
1106
+ this.eventLog.skip({
1107
+ traceId,
1108
+ sessionId,
1109
+ reason: 'haiku_said_no',
1110
+ trigger: trigger,
1111
+ });
1112
+ SessionInfoService.getInstance().auditEventInsight({
1113
+ traceId,
1114
+ sessionId,
1115
+ eventType: 'quick_check',
1116
+ trigger,
1117
+ actionContent,
1118
+ beforeState,
1119
+ llmResponse: `needsPatch=false: ${quickCheckResult.reason}`,
1120
+ durationMs: quickCheckDurationMs,
1121
+ skippedReason: 'haiku_said_no_patch_needed'
1122
+ });
1123
+ }
1124
+ }
1125
+ catch (error) {
1126
+ this.logger.error('Failed to handle action event', {
1127
+ traceId,
1128
+ sessionId: sessionId.slice(0, 8),
1129
+ error: error instanceof Error ? error.message : String(error),
1130
+ });
1131
+ }
1132
+ finally {
1133
+ this.pendingQuickChecks.delete(sessionId);
1134
+ }
1135
+ }
1136
+ async doQuickCheck(cached, recentActions, _traceId, sessionId) {
1137
+ const mission = cached.context?.mission || 'Unknown';
1138
+ const milestones = (cached.milestones || []);
1139
+ return await anthropicService.quickCheckInsightsStale({ mission, milestones }, recentActions, sessionId);
1140
+ }
1141
+ async doPatch(sessionId, cached, recentActivity, traceId, messageCount, beforeState, actionContent, trigger) {
1142
+ const patchStartTime = Date.now();
1143
+ // === FRESH FETCH: Re-sample messages at patch execution time ===
1144
+ // This fixes the race condition where assistant responses arrive after
1145
+ // the patch was queued but before it executes. By fetching fresh here,
1146
+ // we ensure we always have the most current conversation state.
1147
+ let freshRecentActivity = recentActivity;
1148
+ let freshMessageCount = messageCount;
1149
+ let freshActionContent = actionContent;
1150
+ try {
1151
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
1152
+ freshMessageCount = messages.length;
1153
+ // Get messages since last patch (or last 20 if no patched_at)
1154
+ const lastPatchTime = cached.patched_at ? new Date(cached.patched_at).getTime() : 0;
1155
+ const newMessages = lastPatchTime > 0
1156
+ ? messages.filter(m => new Date(m.timestamp).getTime() > lastPatchTime)
1157
+ : messages.slice(-20); // Fallback: last 20 messages
1158
+ if (newMessages.length > 0) {
1159
+ const freshActions = this.extractActions(newMessages);
1160
+ if (freshActions.length > 0) {
1161
+ // Convert to recentActivity format
1162
+ freshRecentActivity = freshActions.map(a => {
1163
+ if (a.type === 'tool_use') {
1164
+ let content = `Tool: ${a.name}`;
1165
+ if (a.input)
1166
+ content += ` (${a.input})`;
1167
+ if (a.output)
1168
+ content += ` → ${a.output.slice(0, 100)}`;
1169
+ return { type: a.type, content, timestamp: a.timestamp };
1170
+ }
1171
+ return { type: a.type, content: a.text || '', timestamp: a.timestamp };
1172
+ });
1173
+ // Update actionContent for audit logging
1174
+ freshActionContent = freshActions.map(a => {
1175
+ if (a.type === 'tool_use') {
1176
+ let content = `tool:${a.name}`;
1177
+ if (a.input)
1178
+ content += `(${a.input.slice(0, 30)})`;
1179
+ return content;
1180
+ }
1181
+ return `${a.type}:${(a.text || '').slice(0, 50)}`;
1182
+ });
1183
+ this.logger.info('[PATCH TRACE] Fresh fetch found new activity', {
1184
+ traceId,
1185
+ sessionId: sessionId.slice(0, 8),
1186
+ originalMessageCount: messageCount,
1187
+ freshMessageCount,
1188
+ newMessagesSinceLastPatch: newMessages.length,
1189
+ freshActionCount: freshActions.length,
1190
+ });
1191
+ }
1192
+ }
1193
+ }
1194
+ catch (fetchError) {
1195
+ // If fresh fetch fails, fall back to the original data
1196
+ this.logger.warn('[PATCH TRACE] Fresh fetch failed, using original data', {
1197
+ traceId,
1198
+ sessionId: sessionId.slice(0, 8),
1199
+ error: fetchError instanceof Error ? fetchError.message : String(fetchError),
1200
+ });
1201
+ }
1202
+ // Use fresh data from this point forward
1203
+ recentActivity = freshRecentActivity;
1204
+ messageCount = freshMessageCount;
1205
+ actionContent = freshActionContent;
1206
+ // Use progressItems (V9 with timestamps) for better staleness detection
1207
+ const progressItems = (cached.progress_items || []);
1208
+ // Convert to milestone format but include updatedAt for temporal context
1209
+ const milestonesWithTime = progressItems.map(p => ({
1210
+ label: p.label,
1211
+ status: p.status,
1212
+ updatedAt: p.updatedAt
1213
+ }));
1214
+ const currentInsights = {
1215
+ mission: cached.context?.mission || cached.description || 'Unknown',
1216
+ description: cached.description || '',
1217
+ theme: cached.theme || 'unknown',
1218
+ milestones: milestonesWithTime,
1219
+ notable: (cached.notable || []).map(n => ({ text: n.description || n.text || '', icon: n.icon })),
1220
+ recentActions: (cached.recent_actions || []),
1221
+ previousPropositions: (cached.propositions || []),
1222
+ purpose: cached.purpose || undefined,
1223
+ tags: cached.tags || undefined
1224
+ };
1225
+ const result = await anthropicService.generateInsightsPatch(currentInsights, recentActivity, true, sessionId);
1226
+ const llmDurationMs = Date.now() - patchStartTime;
1227
+ this.logger.info('[PATCH TRACE] LLM returned', { traceId, sessionId: sessionId.slice(0, 8), reason: result.reason?.slice(0, 100) });
1228
+ const patchKeys = Object.keys(result.patches);
1229
+ if (patchKeys.length === 0) {
1230
+ this.logger.warn('[PATCH TRACE] No patches returned - early exit (no SSE will be emitted)', {
1231
+ traceId,
1232
+ sessionId: sessionId.slice(0, 8),
1233
+ reason: result.reason,
1234
+ propositionsCount: result.propositions?.length || 0
1235
+ });
1236
+ if (trigger) {
1237
+ SessionInfoService.getInstance().auditEventInsight({
1238
+ traceId,
1239
+ sessionId,
1240
+ eventType: 'patch',
1241
+ trigger,
1242
+ actionContent,
1243
+ beforeState,
1244
+ llmResponse: `no_patches: ${result.reason}`,
1245
+ patchedFields: [],
1246
+ durationMs: llmDurationMs
1247
+ });
1248
+ }
1249
+ return;
1250
+ }
1251
+ this.logger.info('[PATCH TRACE] About to apply patches', { traceId, sessionId: sessionId.slice(0, 8), patchKeys });
1252
+ await this.applyPatches(sessionId, cached, result.patches, result.propositions, traceId, messageCount);
1253
+ this.logger.info('[PATCH TRACE] Patches applied successfully', { traceId, sessionId: sessionId.slice(0, 8) });
1254
+ this.markInsightsPatched(sessionId);
1255
+ const totalDurationMs = Date.now() - patchStartTime;
1256
+ this.logger.debug('Patches applied', {
1257
+ traceId,
1258
+ sessionId: sessionId.slice(0, 8),
1259
+ patchedFields: patchKeys,
1260
+ });
1261
+ const updatedCached = await SessionInfoService.getInstance().getInsights(sessionId);
1262
+ const afterState = updatedCached ? {
1263
+ mission: updatedCached.context?.mission,
1264
+ milestones: updatedCached.milestones?.map(m => `${m.label}:${m.status}`),
1265
+ } : undefined;
1266
+ if (trigger) {
1267
+ SessionInfoService.getInstance().auditEventInsight({
1268
+ traceId,
1269
+ sessionId,
1270
+ eventType: 'patch',
1271
+ trigger,
1272
+ actionContent,
1273
+ beforeState,
1274
+ afterState,
1275
+ llmResponse: result.reason,
1276
+ patchedFields: patchKeys,
1277
+ durationMs: totalDurationMs
1278
+ });
1279
+ }
1280
+ await new Promise(resolve => setTimeout(resolve, 100));
1281
+ this.logger.info('[SSE TRACE] About to emit insights update', {
1282
+ traceId,
1283
+ sessionId: sessionId.slice(0, 8),
1284
+ type: 'patched'
1285
+ });
1286
+ await getSessionActivityWatcher().emitInsightsUpdate(sessionId, 'patched', traceId);
1287
+ this.logger.info('[SSE TRACE] Emitted insights update', {
1288
+ traceId,
1289
+ sessionId: sessionId.slice(0, 8),
1290
+ type: 'patched'
1291
+ });
1292
+ }
1293
+ async applyPatches(sessionId, cached, patches, propositions, traceId, messageCount) {
1294
+ const updated = { ...cached };
1295
+ // Accumulate propositions with timestamps (don't overwrite)
1296
+ const now = new Date().toISOString();
1297
+ const existingPropositions = updated.propositions || [];
1298
+ const newPropositions = propositions.map(text => ({ text, addedAt: now }));
1299
+ updated.propositions = [...existingPropositions, ...newPropositions];
1300
+ // CRITICAL: Update message_count to prevent staleness drift
1301
+ updated.message_count = messageCount;
1302
+ updated.last_patch_trace_id = traceId;
1303
+ if (patches.mission && updated.context) {
1304
+ updated.context = { ...updated.context, mission: patches.mission };
1305
+ }
1306
+ if (patches.description) {
1307
+ updated.description = patches.description;
1308
+ }
1309
+ if (patches.theme) {
1310
+ updated.theme = patches.theme;
1311
+ }
1312
+ if (patches.purpose) {
1313
+ updated.purpose = patches.purpose;
1314
+ }
1315
+ if (patches.tags) {
1316
+ // Merge with existing tags, preserving domain which isn't re-evaluated
1317
+ updated.tags = {
1318
+ workType: patches.tags.workType || updated.tags?.workType || ['exploring'],
1319
+ collaboration: (patches.tags.collaboration || updated.tags?.collaboration || 'iterative'),
1320
+ complexity: (patches.tags.complexity || updated.tags?.complexity || 'routine'),
1321
+ domain: updated.tags?.domain || [] // Preserve domain - not re-evaluated in fast patch
1322
+ };
1323
+ }
1324
+ updated.patched_at = new Date().toISOString();
1325
+ await SessionInfoService.getInstance().setInsights(updated);
1326
+ // Try to generate identity image if we now have mission context (fire and forget)
1327
+ // This handles the case where initial insights were generated before mission was extracted
1328
+ if (updated.context?.mission) {
1329
+ const insightsForImage = {
1330
+ sessionId,
1331
+ context: updated.context,
1332
+ theme: updated.theme,
1333
+ tags: updated.tags,
1334
+ };
1335
+ this.insightsComputer.maybeGenerateIdentityImage(sessionId, insightsForImage).catch(err => {
1336
+ this.logger.debug('Failed to generate identity image after patch', {
1337
+ sessionId,
1338
+ error: err instanceof Error ? err.message : String(err)
1339
+ });
1340
+ });
1341
+ }
1342
+ // Log patch completion
1343
+ const patchedFields = Object.keys(patches).filter(k => patches[k] !== undefined);
1344
+ this.eventLog.patch({
1345
+ traceId,
1346
+ sessionId,
1347
+ beforeState: {
1348
+ mission: cached.context?.mission?.slice(0, 100),
1349
+ },
1350
+ afterState: {
1351
+ mission: updated.context?.mission?.slice(0, 100),
1352
+ },
1353
+ patchedFields,
1354
+ durationMs: 0, // Duration tracked at higher level
1355
+ });
1356
+ }
1357
+ // ==========================================================================
1358
+ // Session State Management
1359
+ // ==========================================================================
1360
+ /**
1361
+ * Called when insights have been generated for a session
1362
+ */
1363
+ markInsightsGenerated(sessionId) {
1364
+ let state = this.sessionStates.get(sessionId);
1365
+ if (!state) {
1366
+ state = {
1367
+ actionCount: 0,
1368
+ lastActionAt: Date.now(),
1369
+ lastCheckAt: Date.now(),
1370
+ actionsSinceLastPatch: []
1371
+ };
1372
+ this.sessionStates.set(sessionId, state);
1373
+ }
1374
+ state.insightsGeneratedAt = Date.now();
1375
+ state.insightsPatchedAt = undefined;
1376
+ this.startRegenTimer(sessionId, state);
1377
+ this.logger.info('Insights generated, starting regen timer', {
1378
+ sessionId: sessionId.slice(0, 8),
1379
+ regenAt: new Date(Date.now() + FULL_REGEN_INTERVAL_MS).toISOString()
1380
+ });
1381
+ }
1382
+ markInsightsPatched(sessionId) {
1383
+ const state = this.sessionStates.get(sessionId);
1384
+ if (state) {
1385
+ state.insightsPatchedAt = Date.now();
1386
+ state.lastCheckAt = Date.now();
1387
+ // Clear the accumulated actions buffer after successful patch
1388
+ state.actionsSinceLastPatch = [];
1389
+ }
1390
+ }
1391
+ markQuickCheckPerformed(sessionId) {
1392
+ const state = this.sessionStates.get(sessionId);
1393
+ if (state) {
1394
+ state.lastCheckAt = Date.now();
1395
+ }
1396
+ }
1397
+ startRegenTimer(sessionId, state) {
1398
+ if (state.regenTimerId) {
1399
+ clearTimeout(state.regenTimerId);
1400
+ }
1401
+ // Skip if scheduled regeneration is disabled
1402
+ if (FULL_REGEN_INTERVAL_MS <= 0) {
1403
+ return;
1404
+ }
1405
+ state.regenTimerId = setTimeout(() => {
1406
+ const timeSinceLastAction = Date.now() - state.lastActionAt;
1407
+ if (timeSinceLastAction > 5 * 60 * 1000) {
1408
+ this.logger.info('Session idle - cleaning up tracking', {
1409
+ sessionId: sessionId.slice(0, 8),
1410
+ idleMinutes: Math.round(timeSinceLastAction / 60000)
1411
+ });
1412
+ this.cleanupSession(sessionId);
1413
+ return;
1414
+ }
1415
+ this.logger.info('Triggering scheduled full regeneration', {
1416
+ sessionId: sessionId.slice(0, 8),
1417
+ });
1418
+ this.enqueueGenerate({ sessionId, reason: 'scheduled', actionCount: state.actionCount });
1419
+ }, FULL_REGEN_INTERVAL_MS);
1420
+ }
1421
+ /**
1422
+ * Clean up tracking for a session (call when archived or idle)
1423
+ */
1424
+ cleanupSession(sessionId) {
1425
+ const state = this.sessionStates.get(sessionId);
1426
+ if (!state) {
1427
+ return;
1428
+ }
1429
+ this.logger.info('Cleaning up session tracking', {
1430
+ sessionId: sessionId.slice(0, 8),
1431
+ actionCount: state.actionCount,
1432
+ });
1433
+ if (state.regenTimerId)
1434
+ clearTimeout(state.regenTimerId);
1435
+ if (state.completionCheckTimer)
1436
+ clearTimeout(state.completionCheckTimer);
1437
+ if (state.quickCheckDebounceTimer)
1438
+ clearTimeout(state.quickCheckDebounceTimer);
1439
+ const debounceTimer = this.debounceTimers.get(sessionId);
1440
+ if (debounceTimer) {
1441
+ clearTimeout(debounceTimer);
1442
+ this.debounceTimers.delete(sessionId);
1443
+ }
1444
+ const projectPath = path.join(this.projectsDir, sessionId);
1445
+ const watcher = this.watchers.get(projectPath);
1446
+ if (watcher) {
1447
+ watcher.close();
1448
+ this.watchers.delete(projectPath);
1449
+ }
1450
+ this.sessionStates.delete(sessionId);
1451
+ this.lastKnownMessageCounts.delete(sessionId);
1452
+ this.logger.debug('Session cleanup complete', {
1453
+ sessionId: sessionId.slice(0, 8),
1454
+ remainingTrackedSessions: this.sessionStates.size
1455
+ });
1456
+ }
1457
+ /**
1458
+ * Get the current state for a session (for debugging/UI)
1459
+ */
1460
+ getSessionState(sessionId) {
1461
+ return this.sessionStates.get(sessionId);
1462
+ }
1463
+ /**
1464
+ * Handle session ended - immediately fire any pending timers.
1465
+ * This ensures insights update right when the session ends, not 5s later.
1466
+ *
1467
+ * IMPORTANT: We must fire the file debounce timer FIRST to ensure
1468
+ * actionsSinceLastPatch contains the final response before we patch.
1469
+ */
1470
+ async handleSessionEnded(sessionId) {
1471
+ const state = this.sessionStates.get(sessionId);
1472
+ if (!state) {
1473
+ this.logger.debug('Session ended but no state tracked', {
1474
+ sessionId: sessionId.slice(0, 8)
1475
+ });
1476
+ return;
1477
+ }
1478
+ // CRITICAL: Fire pending file debounce timer FIRST
1479
+ // The file watcher has a 150ms debounce. If the process exits within that window,
1480
+ // the final response won't be in actionsSinceLastPatch yet. We need to process
1481
+ // the file update before expediting the completion patch.
1482
+ const debounceTimer = this.debounceTimers.get(sessionId);
1483
+ if (debounceTimer) {
1484
+ clearTimeout(debounceTimer);
1485
+ this.debounceTimers.delete(sessionId);
1486
+ const actionCountBefore = state.actionsSinceLastPatch?.length || 0;
1487
+ this.logger.warn('Session ended with pending file debounce - processing now to capture final response', {
1488
+ sessionId: sessionId.slice(0, 8),
1489
+ actionsSinceLastPatchBefore: actionCountBefore
1490
+ });
1491
+ // Process the session update synchronously to capture final actions
1492
+ await this.processSessionUpdate(sessionId);
1493
+ const actionCountAfter = state.actionsSinceLastPatch?.length || 0;
1494
+ this.logger.warn('File debounce processed - actions captured', {
1495
+ sessionId: sessionId.slice(0, 8),
1496
+ actionsBefore: actionCountBefore,
1497
+ actionsAfter: actionCountAfter,
1498
+ newActionsCaptured: actionCountAfter - actionCountBefore
1499
+ });
1500
+ }
1501
+ // If there's a pending completion timer, fire it immediately
1502
+ if (state.completionCheckTimer) {
1503
+ clearTimeout(state.completionCheckTimer);
1504
+ state.completionCheckTimer = undefined;
1505
+ const traceId = generateTraceId(sessionId);
1506
+ this.logger.info('Session ended - expediting completion patch', {
1507
+ traceId,
1508
+ sessionId: sessionId.slice(0, 8),
1509
+ });
1510
+ // Log the trigger
1511
+ this.eventLog.trigger({
1512
+ traceId,
1513
+ sessionId,
1514
+ source: 'completion',
1515
+ actionCount: state.actionCount,
1516
+ messageCount: this.lastKnownMessageCounts.get(sessionId) || 0,
1517
+ newActions: ['session_ended'],
1518
+ });
1519
+ // Trigger immediate patch
1520
+ this.enqueueAction({
1521
+ sessionId,
1522
+ actionCount: state.actionCount,
1523
+ messageCount: this.lastKnownMessageCounts.get(sessionId) || 0,
1524
+ newActions: [{ type: 'text', text: '[Session ended]' }],
1525
+ timestamp: Date.now(),
1526
+ forcePatch: true,
1527
+ traceId
1528
+ });
1529
+ }
1530
+ // Also fire any pending quick check debounce timer
1531
+ if (state.quickCheckDebounceTimer) {
1532
+ clearTimeout(state.quickCheckDebounceTimer);
1533
+ state.quickCheckDebounceTimer = undefined;
1534
+ // The debounce timer callback captured its own data, so we just
1535
+ // trigger a fresh patch here if there wasn't a completion timer
1536
+ if (!state.completionCheckTimer) {
1537
+ const traceId = generateTraceId(sessionId);
1538
+ this.logger.info('Session ended - expediting debounced check', {
1539
+ traceId,
1540
+ sessionId: sessionId.slice(0, 8),
1541
+ });
1542
+ this.enqueueAction({
1543
+ sessionId,
1544
+ actionCount: state.actionCount,
1545
+ messageCount: this.lastKnownMessageCounts.get(sessionId) || 0,
1546
+ newActions: [{ type: 'text', text: '[Session ended]' }],
1547
+ timestamp: Date.now(),
1548
+ forcePatch: true,
1549
+ traceId
1550
+ });
1551
+ }
1552
+ }
1553
+ }
1554
+ /**
1555
+ * Trigger currentWork summarization and metadata evaluation when user sends a message.
1556
+ *
1557
+ * This gives the sidebar "Currently:" indicator something to show immediately,
1558
+ * and evaluates theme/purpose/tags based on what the user is asking for.
1559
+ * We provide recent conversation context so even "do it" can be understood.
1560
+ */
1561
+ async triggerCurrentWorkSummarization(sessionId, userMessageText, traceId) {
1562
+ const startTime = Date.now();
1563
+ try {
1564
+ // Fetch recent conversation for context (last 4 exchanges max)
1565
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
1566
+ // Build recent context from conversation (excluding the new user message we just got)
1567
+ // Take last 8 messages (roughly 4 exchanges of user+assistant)
1568
+ const recentMessages = messages.slice(-8);
1569
+ const recentContext = [];
1570
+ for (const msg of recentMessages) {
1571
+ const type = msg.type === 'user' ? 'user' : 'assistant';
1572
+ // Extract text from message.content (Anthropic format)
1573
+ let content = '';
1574
+ if (msg.message && 'content' in msg.message) {
1575
+ const msgContent = msg.message.content;
1576
+ if (typeof msgContent === 'string') {
1577
+ content = msgContent;
1578
+ }
1579
+ else if (Array.isArray(msgContent)) {
1580
+ // Find first text block
1581
+ const textBlock = msgContent.find((block) => typeof block === 'object' && block !== null && 'type' in block && block.type === 'text');
1582
+ content = textBlock?.text || '';
1583
+ }
1584
+ }
1585
+ // Truncate long messages but keep enough to understand context
1586
+ if (content) {
1587
+ recentContext.push({ type, content: content.slice(0, 800) });
1588
+ }
1589
+ }
1590
+ // Try to get cached insights for mission/purpose/theme/tags
1591
+ let cachedInsights;
1592
+ try {
1593
+ const cached = await this.insightsComputer.getCachedInsightsForSessions([sessionId]);
1594
+ cachedInsights = cached.get(sessionId);
1595
+ }
1596
+ catch {
1597
+ // No cached insights, that's fine
1598
+ }
1599
+ const sessionMission = cachedInsights?.context?.mission;
1600
+ // Run currentWork summarization and metadata evaluation in parallel
1601
+ const [currentWorkResult, metadataResult] = await Promise.all([
1602
+ // 1. Summarize what user is asking for (for "Currently:" display)
1603
+ anthropicService.summarizeCurrentWork(userMessageText, recentContext, sessionMission, sessionId),
1604
+ // 2. Evaluate session metadata (purpose/theme/tags)
1605
+ anthropicService.evaluateSessionMetadata(userMessageText, recentContext, {
1606
+ mission: sessionMission,
1607
+ purpose: cachedInsights?.purpose || undefined,
1608
+ theme: cachedInsights?.theme || undefined,
1609
+ tags: cachedInsights?.tags || undefined,
1610
+ }, sessionId),
1611
+ ]);
1612
+ const durationMs = Date.now() - startTime;
1613
+ // Emit currentWork via SSE if we got a result
1614
+ if (currentWorkResult) {
1615
+ this.logger.info('CurrentWork summarization complete', {
1616
+ traceId,
1617
+ sessionId: sessionId.slice(0, 8),
1618
+ durationMs,
1619
+ summary: currentWorkResult.summary.slice(0, 50),
1620
+ });
1621
+ const watcher = getSessionActivityWatcher();
1622
+ watcher.emitCurrentWork({
1623
+ sessionId,
1624
+ summary: currentWorkResult.summary,
1625
+ userMessage: userMessageText.slice(0, 200),
1626
+ startedAt: new Date().toISOString(),
1627
+ });
1628
+ }
1629
+ // Persist metadata updates if we got any
1630
+ if (metadataResult) {
1631
+ this.logger.info('Session metadata updated on user message', {
1632
+ traceId,
1633
+ sessionId: sessionId.slice(0, 8),
1634
+ theme: metadataResult.theme,
1635
+ purpose: metadataResult.purpose?.slice(0, 30),
1636
+ });
1637
+ await this.sessionInfoService.updateSessionMetadata(sessionId, {
1638
+ purpose: metadataResult.purpose,
1639
+ theme: metadataResult.theme,
1640
+ tags: metadataResult.tags,
1641
+ });
1642
+ // Emit SSE so UI updates in real-time
1643
+ const watcher = getSessionActivityWatcher();
1644
+ await watcher.emitInsightsUpdate(sessionId, 'patched', traceId);
1645
+ }
1646
+ }
1647
+ catch (error) {
1648
+ this.logger.debug('CurrentWork/metadata processing error', {
1649
+ traceId,
1650
+ sessionId: sessionId.slice(0, 8),
1651
+ error: error instanceof Error ? error.message : String(error),
1652
+ });
1653
+ }
1654
+ }
1655
+ }
1656
+ // ============================================================================
1657
+ // Singleton
1658
+ // ============================================================================
1659
+ let instance = null;
1660
+ export function getInsightsCoordinator() {
1661
+ if (!instance) {
1662
+ instance = new InsightsCoordinator();
1663
+ }
1664
+ return instance;
1665
+ }
1666
+ /**
1667
+ * Initialize the insights coordinator. Call once at server startup.
1668
+ */
1669
+ export function initializeInsightsCoordinator() {
1670
+ const service = getInsightsCoordinator();
1671
+ service.initialize();
1672
+ }
1673
+ // Legacy aliases for backward compatibility during migration
1674
+ /** @deprecated Use getInsightsCoordinator instead */
1675
+ export const getInsightsService = getInsightsCoordinator;
1676
+ /** @deprecated Use initializeInsightsCoordinator instead */
1677
+ export const initializeInsightsService = initializeInsightsCoordinator;
1678
+ //# sourceMappingURL=insights-coordinator.js.map