claudia-orchestrator 0.1.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 (296) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +109 -0
  3. package/dist/cli-parser.d.ts +11 -0
  4. package/dist/cli-parser.d.ts.map +1 -0
  5. package/dist/cli-parser.js +57 -0
  6. package/dist/cli-parser.js.map +1 -0
  7. package/dist/cui-server.d.ts +69 -0
  8. package/dist/cui-server.d.ts.map +1 -0
  9. package/dist/cui-server.js +705 -0
  10. package/dist/cui-server.js.map +1 -0
  11. package/dist/mcp-server/claudia-tools.d.ts +15 -0
  12. package/dist/mcp-server/claudia-tools.d.ts.map +1 -0
  13. package/dist/mcp-server/claudia-tools.js +366 -0
  14. package/dist/mcp-server/claudia-tools.js.map +1 -0
  15. package/dist/mcp-server/index.d.ts +3 -0
  16. package/dist/mcp-server/index.d.ts.map +1 -0
  17. package/dist/mcp-server/index.js +176 -0
  18. package/dist/mcp-server/index.js.map +1 -0
  19. package/dist/middleware/auth.d.ts +18 -0
  20. package/dist/middleware/auth.d.ts.map +1 -0
  21. package/dist/middleware/auth.js +136 -0
  22. package/dist/middleware/auth.js.map +1 -0
  23. package/dist/middleware/cors-setup.d.ts +7 -0
  24. package/dist/middleware/cors-setup.d.ts.map +1 -0
  25. package/dist/middleware/cors-setup.js +8 -0
  26. package/dist/middleware/cors-setup.js.map +1 -0
  27. package/dist/middleware/error-handler.d.ts +4 -0
  28. package/dist/middleware/error-handler.d.ts.map +1 -0
  29. package/dist/middleware/error-handler.js +27 -0
  30. package/dist/middleware/error-handler.js.map +1 -0
  31. package/dist/middleware/query-parser.d.ts +11 -0
  32. package/dist/middleware/query-parser.d.ts.map +1 -0
  33. package/dist/middleware/query-parser.js +68 -0
  34. package/dist/middleware/query-parser.js.map +1 -0
  35. package/dist/middleware/request-logger.d.ts +4 -0
  36. package/dist/middleware/request-logger.d.ts.map +1 -0
  37. package/dist/middleware/request-logger.js +29 -0
  38. package/dist/middleware/request-logger.js.map +1 -0
  39. package/dist/process-daemon/index.d.ts +14 -0
  40. package/dist/process-daemon/index.d.ts.map +1 -0
  41. package/dist/process-daemon/index.js +51 -0
  42. package/dist/process-daemon/index.js.map +1 -0
  43. package/dist/process-daemon/process-daemon.d.ts +78 -0
  44. package/dist/process-daemon/process-daemon.d.ts.map +1 -0
  45. package/dist/process-daemon/process-daemon.js +568 -0
  46. package/dist/process-daemon/process-daemon.js.map +1 -0
  47. package/dist/process-daemon/process-manager-client.d.ts +108 -0
  48. package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
  49. package/dist/process-daemon/process-manager-client.js +314 -0
  50. package/dist/process-daemon/process-manager-client.js.map +1 -0
  51. package/dist/process-daemon/process-manager-interface.d.ts +47 -0
  52. package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
  53. package/dist/process-daemon/process-manager-interface.js +8 -0
  54. package/dist/process-daemon/process-manager-interface.js.map +1 -0
  55. package/dist/process-daemon/test-daemon.d.ts +12 -0
  56. package/dist/process-daemon/test-daemon.d.ts.map +1 -0
  57. package/dist/process-daemon/test-daemon.js +81 -0
  58. package/dist/process-daemon/test-daemon.js.map +1 -0
  59. package/dist/process-daemon/types.d.ts +85 -0
  60. package/dist/process-daemon/types.d.ts.map +1 -0
  61. package/dist/process-daemon/types.js +8 -0
  62. package/dist/process-daemon/types.js.map +1 -0
  63. package/dist/routes/claudia.routes.d.ts +10 -0
  64. package/dist/routes/claudia.routes.d.ts.map +1 -0
  65. package/dist/routes/claudia.routes.js +123 -0
  66. package/dist/routes/claudia.routes.js.map +1 -0
  67. package/dist/routes/config.routes.d.ts +4 -0
  68. package/dist/routes/config.routes.d.ts.map +1 -0
  69. package/dist/routes/config.routes.js +27 -0
  70. package/dist/routes/config.routes.js.map +1 -0
  71. package/dist/routes/conversation.routes.d.ts +8 -0
  72. package/dist/routes/conversation.routes.d.ts.map +1 -0
  73. package/dist/routes/conversation.routes.js +870 -0
  74. package/dist/routes/conversation.routes.js.map +1 -0
  75. package/dist/routes/filesystem.routes.d.ts +4 -0
  76. package/dist/routes/filesystem.routes.d.ts.map +1 -0
  77. package/dist/routes/filesystem.routes.js +86 -0
  78. package/dist/routes/filesystem.routes.js.map +1 -0
  79. package/dist/routes/gemini.routes.d.ts +4 -0
  80. package/dist/routes/gemini.routes.d.ts.map +1 -0
  81. package/dist/routes/gemini.routes.js +93 -0
  82. package/dist/routes/gemini.routes.js.map +1 -0
  83. package/dist/routes/insights.routes.d.ts +17 -0
  84. package/dist/routes/insights.routes.d.ts.map +1 -0
  85. package/dist/routes/insights.routes.js +417 -0
  86. package/dist/routes/insights.routes.js.map +1 -0
  87. package/dist/routes/license.routes.d.ts +3 -0
  88. package/dist/routes/license.routes.d.ts.map +1 -0
  89. package/dist/routes/license.routes.js +111 -0
  90. package/dist/routes/license.routes.js.map +1 -0
  91. package/dist/routes/log.routes.d.ts +3 -0
  92. package/dist/routes/log.routes.d.ts.map +1 -0
  93. package/dist/routes/log.routes.js +65 -0
  94. package/dist/routes/log.routes.js.map +1 -0
  95. package/dist/routes/notifications.routes.d.ts +4 -0
  96. package/dist/routes/notifications.routes.d.ts.map +1 -0
  97. package/dist/routes/notifications.routes.js +71 -0
  98. package/dist/routes/notifications.routes.js.map +1 -0
  99. package/dist/routes/permission.routes.d.ts +4 -0
  100. package/dist/routes/permission.routes.d.ts.map +1 -0
  101. package/dist/routes/permission.routes.js +116 -0
  102. package/dist/routes/permission.routes.js.map +1 -0
  103. package/dist/routes/question.routes.d.ts +8 -0
  104. package/dist/routes/question.routes.d.ts.map +1 -0
  105. package/dist/routes/question.routes.js +82 -0
  106. package/dist/routes/question.routes.js.map +1 -0
  107. package/dist/routes/streaming.routes.d.ts +4 -0
  108. package/dist/routes/streaming.routes.d.ts.map +1 -0
  109. package/dist/routes/streaming.routes.js +28 -0
  110. package/dist/routes/streaming.routes.js.map +1 -0
  111. package/dist/routes/system.routes.d.ts +5 -0
  112. package/dist/routes/system.routes.d.ts.map +1 -0
  113. package/dist/routes/system.routes.js +103 -0
  114. package/dist/routes/system.routes.js.map +1 -0
  115. package/dist/routes/working-directories.routes.d.ts +4 -0
  116. package/dist/routes/working-directories.routes.d.ts.map +1 -0
  117. package/dist/routes/working-directories.routes.js +25 -0
  118. package/dist/routes/working-directories.routes.js.map +1 -0
  119. package/dist/server.d.ts +3 -0
  120. package/dist/server.d.ts.map +1 -0
  121. package/dist/server.js +34 -0
  122. package/dist/server.js.map +1 -0
  123. package/dist/services/ToolMetricsService.d.ts +53 -0
  124. package/dist/services/ToolMetricsService.d.ts.map +1 -0
  125. package/dist/services/ToolMetricsService.js +230 -0
  126. package/dist/services/ToolMetricsService.js.map +1 -0
  127. package/dist/services/anthropic-service.d.ts +186 -0
  128. package/dist/services/anthropic-service.d.ts.map +1 -0
  129. package/dist/services/anthropic-service.js +1132 -0
  130. package/dist/services/anthropic-service.js.map +1 -0
  131. package/dist/services/claude-history-reader.d.ts +126 -0
  132. package/dist/services/claude-history-reader.d.ts.map +1 -0
  133. package/dist/services/claude-history-reader.js +717 -0
  134. package/dist/services/claude-history-reader.js.map +1 -0
  135. package/dist/services/claude-process-manager.d.ts +108 -0
  136. package/dist/services/claude-process-manager.d.ts.map +1 -0
  137. package/dist/services/claude-process-manager.js +909 -0
  138. package/dist/services/claude-process-manager.js.map +1 -0
  139. package/dist/services/claude-router-service.d.ts +19 -0
  140. package/dist/services/claude-router-service.d.ts.map +1 -0
  141. package/dist/services/claude-router-service.js +160 -0
  142. package/dist/services/claude-router-service.js.map +1 -0
  143. package/dist/services/claudia-service.d.ts +77 -0
  144. package/dist/services/claudia-service.d.ts.map +1 -0
  145. package/dist/services/claudia-service.js +194 -0
  146. package/dist/services/claudia-service.js.map +1 -0
  147. package/dist/services/commands-service.d.ts +18 -0
  148. package/dist/services/commands-service.d.ts.map +1 -0
  149. package/dist/services/commands-service.js +76 -0
  150. package/dist/services/commands-service.js.map +1 -0
  151. package/dist/services/config-service.d.ts +68 -0
  152. package/dist/services/config-service.d.ts.map +1 -0
  153. package/dist/services/config-service.js +429 -0
  154. package/dist/services/config-service.js.map +1 -0
  155. package/dist/services/conversation-cache.d.ts +86 -0
  156. package/dist/services/conversation-cache.d.ts.map +1 -0
  157. package/dist/services/conversation-cache.js +235 -0
  158. package/dist/services/conversation-cache.js.map +1 -0
  159. package/dist/services/conversation-status-manager.d.ts +98 -0
  160. package/dist/services/conversation-status-manager.d.ts.map +1 -0
  161. package/dist/services/conversation-status-manager.js +295 -0
  162. package/dist/services/conversation-status-manager.js.map +1 -0
  163. package/dist/services/cost-tracker.d.ts +87 -0
  164. package/dist/services/cost-tracker.d.ts.map +1 -0
  165. package/dist/services/cost-tracker.js +335 -0
  166. package/dist/services/cost-tracker.js.map +1 -0
  167. package/dist/services/file-system-service.d.ts +61 -0
  168. package/dist/services/file-system-service.d.ts.map +1 -0
  169. package/dist/services/file-system-service.js +348 -0
  170. package/dist/services/file-system-service.js.map +1 -0
  171. package/dist/services/gemini-service.d.ts +72 -0
  172. package/dist/services/gemini-service.d.ts.map +1 -0
  173. package/dist/services/gemini-service.js +431 -0
  174. package/dist/services/gemini-service.js.map +1 -0
  175. package/dist/services/insight-queue.d.ts +99 -0
  176. package/dist/services/insight-queue.d.ts.map +1 -0
  177. package/dist/services/insight-queue.js +277 -0
  178. package/dist/services/insight-queue.js.map +1 -0
  179. package/dist/services/insights-service.d.ts +102 -0
  180. package/dist/services/insights-service.d.ts.map +1 -0
  181. package/dist/services/insights-service.js +1152 -0
  182. package/dist/services/insights-service.js.map +1 -0
  183. package/dist/services/json-lines-parser.d.ts +19 -0
  184. package/dist/services/json-lines-parser.d.ts.map +1 -0
  185. package/dist/services/json-lines-parser.js +56 -0
  186. package/dist/services/json-lines-parser.js.map +1 -0
  187. package/dist/services/license-service.d.ts +69 -0
  188. package/dist/services/license-service.d.ts.map +1 -0
  189. package/dist/services/license-service.js +330 -0
  190. package/dist/services/license-service.js.map +1 -0
  191. package/dist/services/log-formatter.d.ts +5 -0
  192. package/dist/services/log-formatter.d.ts.map +1 -0
  193. package/dist/services/log-formatter.js +77 -0
  194. package/dist/services/log-formatter.js.map +1 -0
  195. package/dist/services/log-stream-buffer.d.ts +11 -0
  196. package/dist/services/log-stream-buffer.d.ts.map +1 -0
  197. package/dist/services/log-stream-buffer.js +36 -0
  198. package/dist/services/log-stream-buffer.js.map +1 -0
  199. package/dist/services/logger.d.ts +71 -0
  200. package/dist/services/logger.d.ts.map +1 -0
  201. package/dist/services/logger.js +215 -0
  202. package/dist/services/logger.js.map +1 -0
  203. package/dist/services/mcp-config-generator.d.ts +32 -0
  204. package/dist/services/mcp-config-generator.d.ts.map +1 -0
  205. package/dist/services/mcp-config-generator.js +126 -0
  206. package/dist/services/mcp-config-generator.js.map +1 -0
  207. package/dist/services/message-filter.d.ts +22 -0
  208. package/dist/services/message-filter.d.ts.map +1 -0
  209. package/dist/services/message-filter.js +57 -0
  210. package/dist/services/message-filter.js.map +1 -0
  211. package/dist/services/notification-service.d.ts +45 -0
  212. package/dist/services/notification-service.d.ts.map +1 -0
  213. package/dist/services/notification-service.js +184 -0
  214. package/dist/services/notification-service.js.map +1 -0
  215. package/dist/services/permission-tracker.d.ts +67 -0
  216. package/dist/services/permission-tracker.d.ts.map +1 -0
  217. package/dist/services/permission-tracker.js +161 -0
  218. package/dist/services/permission-tracker.js.map +1 -0
  219. package/dist/services/process-manager-factory.d.ts +81 -0
  220. package/dist/services/process-manager-factory.d.ts.map +1 -0
  221. package/dist/services/process-manager-factory.js +211 -0
  222. package/dist/services/process-manager-factory.js.map +1 -0
  223. package/dist/services/question-tracker.d.ts +47 -0
  224. package/dist/services/question-tracker.d.ts.map +1 -0
  225. package/dist/services/question-tracker.js +105 -0
  226. package/dist/services/question-tracker.js.map +1 -0
  227. package/dist/services/session-activity-watcher.d.ts +33 -0
  228. package/dist/services/session-activity-watcher.d.ts.map +1 -0
  229. package/dist/services/session-activity-watcher.js +194 -0
  230. package/dist/services/session-activity-watcher.js.map +1 -0
  231. package/dist/services/session-info-service.d.ts +228 -0
  232. package/dist/services/session-info-service.d.ts.map +1 -0
  233. package/dist/services/session-info-service.js +920 -0
  234. package/dist/services/session-info-service.js.map +1 -0
  235. package/dist/services/session-insights-service.d.ts +119 -0
  236. package/dist/services/session-insights-service.d.ts.map +1 -0
  237. package/dist/services/session-insights-service.js +889 -0
  238. package/dist/services/session-insights-service.js.map +1 -0
  239. package/dist/services/stream-manager.d.ts +62 -0
  240. package/dist/services/stream-manager.d.ts.map +1 -0
  241. package/dist/services/stream-manager.js +239 -0
  242. package/dist/services/stream-manager.js.map +1 -0
  243. package/dist/services/web-push-service.d.ts +48 -0
  244. package/dist/services/web-push-service.d.ts.map +1 -0
  245. package/dist/services/web-push-service.js +186 -0
  246. package/dist/services/web-push-service.js.map +1 -0
  247. package/dist/services/working-directories-service.d.ts +19 -0
  248. package/dist/services/working-directories-service.d.ts.map +1 -0
  249. package/dist/services/working-directories-service.js +103 -0
  250. package/dist/services/working-directories-service.js.map +1 -0
  251. package/dist/types/config.d.ts +111 -0
  252. package/dist/types/config.d.ts.map +1 -0
  253. package/dist/types/config.js +14 -0
  254. package/dist/types/config.js.map +1 -0
  255. package/dist/types/express.d.ts +5 -0
  256. package/dist/types/express.d.ts.map +1 -0
  257. package/dist/types/express.js +2 -0
  258. package/dist/types/express.js.map +1 -0
  259. package/dist/types/index.d.ts +325 -0
  260. package/dist/types/index.d.ts.map +1 -0
  261. package/dist/types/index.js +18 -0
  262. package/dist/types/index.js.map +1 -0
  263. package/dist/types/insights.d.ts +99 -0
  264. package/dist/types/insights.d.ts.map +1 -0
  265. package/dist/types/insights.js +7 -0
  266. package/dist/types/insights.js.map +1 -0
  267. package/dist/types/license.d.ts +70 -0
  268. package/dist/types/license.d.ts.map +1 -0
  269. package/dist/types/license.js +5 -0
  270. package/dist/types/license.js.map +1 -0
  271. package/dist/types/router-config.d.ts +13 -0
  272. package/dist/types/router-config.d.ts.map +1 -0
  273. package/dist/types/router-config.js +2 -0
  274. package/dist/types/router-config.js.map +1 -0
  275. package/dist/utils/constants.d.ts +26 -0
  276. package/dist/utils/constants.d.ts.map +1 -0
  277. package/dist/utils/constants.js +28 -0
  278. package/dist/utils/constants.js.map +1 -0
  279. package/dist/utils/machine-id.d.ts +7 -0
  280. package/dist/utils/machine-id.d.ts.map +1 -0
  281. package/dist/utils/machine-id.js +76 -0
  282. package/dist/utils/machine-id.js.map +1 -0
  283. package/dist/utils/server-startup.d.ts +13 -0
  284. package/dist/utils/server-startup.d.ts.map +1 -0
  285. package/dist/utils/server-startup.js +20 -0
  286. package/dist/utils/server-startup.js.map +1 -0
  287. package/dist/web/assets/main-DAc2rjJ2.css +1 -0
  288. package/dist/web/assets/main-DvlZ02mT.js +137 -0
  289. package/dist/web/favicon.png +0 -0
  290. package/dist/web/favicon.svg +22 -0
  291. package/dist/web/icon-192x192.png +0 -0
  292. package/dist/web/icon-512x512.png +0 -0
  293. package/dist/web/index.html +36 -0
  294. package/dist/web/manifest.json +61 -0
  295. package/package.json +174 -0
  296. package/scripts/postinstall.js +30 -0
@@ -0,0 +1,889 @@
1
+ import crypto from 'crypto';
2
+ import { createLogger } from './logger.js';
3
+ import { anthropicService } from './anthropic-service.js';
4
+ import { SessionInfoService } from './session-info-service.js';
5
+ import { geminiService } from './gemini-service.js';
6
+ import { getSessionActivityWatcher } from './session-activity-watcher.js';
7
+ /**
8
+ * Generate a trace ID for correlating recompute events through the system.
9
+ * Format: <session-prefix>-<timestamp-hex>-<random>
10
+ */
11
+ function generateTraceId(sessionId) {
12
+ const sessionPrefix = sessionId.slice(0, 8);
13
+ const timestampHex = Date.now().toString(16);
14
+ const random = crypto.randomBytes(2).toString('hex');
15
+ return `${sessionPrefix}-${timestampHex}-${random}`;
16
+ }
17
+ /**
18
+ * Build a state snapshot for auditing insight changes.
19
+ * Captures all fields relevant to dashboard display.
20
+ */
21
+ function buildStateSnapshot(cache, insights) {
22
+ if (insights) {
23
+ return {
24
+ mission: insights.context?.mission || undefined,
25
+ description: insights.description || undefined,
26
+ currentState: insights.currentState?.content || undefined,
27
+ theme: insights.theme || undefined,
28
+ milestones: insights.milestones?.map(m => `${m.label}:${m.status}`) || [],
29
+ panels: insights.panels?.map(p => p.label || p.title || 'unnamed') || [],
30
+ notableCount: insights.notable?.length || 0,
31
+ };
32
+ }
33
+ if (cache) {
34
+ return {
35
+ mission: cache.context?.mission || undefined,
36
+ description: cache.description || undefined,
37
+ currentState: cache.current_state?.content || undefined,
38
+ theme: cache.theme || undefined,
39
+ milestones: cache.milestones?.map((m) => `${m.label}:${m.status}`) || [],
40
+ panels: cache.panels?.map((p) => p.label || p.title || 'unnamed') || [],
41
+ notableCount: cache.notable?.length || 0,
42
+ };
43
+ }
44
+ return {};
45
+ }
46
+ export class SessionInsightsService {
47
+ logger;
48
+ historyReader;
49
+ sessionInfoService;
50
+ constructor(historyReader, sessionInfoService) {
51
+ this.logger = createLogger('SessionInsightsService');
52
+ this.historyReader = historyReader;
53
+ this.sessionInfoService = sessionInfoService || SessionInfoService.getInstance();
54
+ }
55
+ /**
56
+ * Check if a session is automated (e.g., Claudia sessions).
57
+ * Automated sessions should skip expensive AI insight generation.
58
+ */
59
+ async isAutomatedSession(sessionId) {
60
+ try {
61
+ const { metadata } = await this.historyReader.fetchConversationDirect(sessionId);
62
+ return metadata.projectPath?.toLowerCase().includes('/claudia') || false;
63
+ }
64
+ catch {
65
+ // If we can't determine, assume not automated
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Extract the final todo state from conversation messages
71
+ */
72
+ extractTodoState(messages) {
73
+ let lastTodoState = null;
74
+ for (const msg of messages) {
75
+ const content = msg.message?.content;
76
+ if (!Array.isArray(content))
77
+ continue;
78
+ for (const block of content) {
79
+ if (typeof block === 'object' &&
80
+ block !== null &&
81
+ 'type' in block &&
82
+ block.type === 'tool_use' &&
83
+ 'name' in block &&
84
+ block.name === 'TodoWrite' &&
85
+ 'input' in block &&
86
+ typeof block.input === 'object' &&
87
+ block.input !== null &&
88
+ 'todos' in block.input &&
89
+ Array.isArray(block.input.todos)) {
90
+ lastTodoState = block.input.todos;
91
+ }
92
+ }
93
+ }
94
+ return lastTodoState;
95
+ }
96
+ /**
97
+ * Compute progress from todo state
98
+ */
99
+ computeProgress(todos) {
100
+ const completed = todos.filter(t => t.status === 'completed').length;
101
+ const inProgress = todos.filter(t => t.status === 'in_progress').length;
102
+ const pending = todos.filter(t => t.status === 'pending').length;
103
+ const total = todos.length;
104
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
105
+ return { completed, inProgress, pending, total, percent };
106
+ }
107
+ /**
108
+ * Extract the last N actions from recent messages (tool uses, user messages, assistant text)
109
+ * Returns an array of {tool, timestamp} for mini action log display
110
+ */
111
+ extractRecentActions(messages, limit = 14) {
112
+ const actions = [];
113
+ // Walk backwards through messages
114
+ for (let i = messages.length - 1; i >= 0 && actions.length < limit; i--) {
115
+ const msg = messages[i];
116
+ const content = msg.message?.content;
117
+ const timestamp = msg.timestamp ? new Date(msg.timestamp).getTime() : Date.now();
118
+ // User messages - handle both string and array content
119
+ if (msg.type === 'user') {
120
+ // String content (most common for typed user prompts)
121
+ if (typeof content === 'string' && content.trim().length > 0) {
122
+ actions.push({
123
+ tool: 'User',
124
+ timestamp
125
+ });
126
+ if (actions.length >= limit)
127
+ break;
128
+ continue;
129
+ }
130
+ // Array content - look for text blocks
131
+ if (Array.isArray(content)) {
132
+ for (const block of content) {
133
+ if (typeof block === 'object' &&
134
+ block !== null &&
135
+ 'type' in block &&
136
+ block.type === 'text' &&
137
+ 'text' in block &&
138
+ typeof block.text === 'string' &&
139
+ block.text.trim().length > 0) {
140
+ actions.push({
141
+ tool: 'User',
142
+ timestamp
143
+ });
144
+ // Only count one user action per message
145
+ break;
146
+ }
147
+ }
148
+ if (actions.length >= limit)
149
+ break;
150
+ }
151
+ continue;
152
+ }
153
+ // Skip non-array content for assistant messages
154
+ if (!Array.isArray(content))
155
+ continue;
156
+ // Assistant messages - both tool uses and text responses
157
+ if (msg.type === 'assistant') {
158
+ let hasToolUse = false;
159
+ let hasText = false;
160
+ // First pass: check what types we have
161
+ for (const block of content) {
162
+ if (typeof block === 'object' && block !== null && 'type' in block) {
163
+ if (block.type === 'tool_use')
164
+ hasToolUse = true;
165
+ if (block.type === 'text')
166
+ hasText = true;
167
+ }
168
+ }
169
+ // Second pass: add actions
170
+ for (const block of content) {
171
+ if (typeof block !== 'object' || block === null || !('type' in block))
172
+ continue;
173
+ if (block.type === 'tool_use' && 'name' in block && typeof block.name === 'string') {
174
+ actions.push({
175
+ tool: block.name,
176
+ timestamp
177
+ });
178
+ if (actions.length >= limit)
179
+ break;
180
+ }
181
+ else if (block.type === 'text' && 'text' in block && typeof block.text === 'string') {
182
+ // Only add text response if there's no tool use in this message
183
+ if (!hasToolUse && block.text.trim().length > 0) {
184
+ actions.push({
185
+ tool: 'Response',
186
+ timestamp
187
+ });
188
+ // Only count one text response per message
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ if (actions.length >= limit)
194
+ break;
195
+ }
196
+ }
197
+ // Reverse to get chronological order (oldest first, newest last)
198
+ return actions.reverse();
199
+ }
200
+ /**
201
+ * Extract user prompts from conversation
202
+ */
203
+ extractUserPrompts(messages) {
204
+ const prompts = [];
205
+ for (const msg of messages) {
206
+ if (msg.type !== 'user')
207
+ continue;
208
+ const content = msg.message?.content;
209
+ if (typeof content === 'string' && content.trim()) {
210
+ prompts.push(content.trim());
211
+ }
212
+ else if (Array.isArray(content)) {
213
+ for (const block of content) {
214
+ if (typeof block === 'object' &&
215
+ block !== null &&
216
+ 'type' in block &&
217
+ block.type === 'text' &&
218
+ 'text' in block &&
219
+ typeof block.text === 'string') {
220
+ prompts.push(block.text.trim());
221
+ }
222
+ }
223
+ }
224
+ }
225
+ return prompts.filter(p => p.length > 0 && !p.startsWith('['));
226
+ }
227
+ /**
228
+ * Extract recent assistant text responses (last N messages)
229
+ * Used to capture outcomes and progress for insight generation
230
+ */
231
+ extractRecentAssistantText(messages, limit = 3) {
232
+ const texts = [];
233
+ // Work backwards from the end to get most recent first
234
+ for (let i = messages.length - 1; i >= 0 && texts.length < limit; i--) {
235
+ const msg = messages[i];
236
+ if (msg.type !== 'assistant')
237
+ continue;
238
+ const content = msg.message?.content;
239
+ if (Array.isArray(content)) {
240
+ for (const block of content) {
241
+ if (typeof block === 'object' &&
242
+ block !== null &&
243
+ 'type' in block &&
244
+ block.type === 'text' &&
245
+ 'text' in block &&
246
+ typeof block.text === 'string' &&
247
+ block.text.trim()) {
248
+ texts.push(block.text.trim());
249
+ break; // Only one text block per message
250
+ }
251
+ }
252
+ }
253
+ }
254
+ return texts;
255
+ }
256
+ /**
257
+ * Format a tool call into a human-readable action description
258
+ */
259
+ formatToolAction(toolName, input) {
260
+ switch (toolName) {
261
+ case 'Read':
262
+ return `Reading ${this.extractFilename(input.file_path)}`;
263
+ case 'Edit':
264
+ return `Editing ${this.extractFilename(input.file_path)}`;
265
+ case 'Write':
266
+ return `Writing ${this.extractFilename(input.file_path)}`;
267
+ case 'Grep':
268
+ return `Searching: "${input.pattern?.slice(0, 40)}"`;
269
+ case 'Glob':
270
+ return `Finding: ${input.pattern?.slice(0, 40)}`;
271
+ case 'Bash': {
272
+ const description = input.description;
273
+ if (description) {
274
+ return description.slice(0, 50);
275
+ }
276
+ const command = input.command || '';
277
+ return command.slice(0, 50) || 'Running command';
278
+ }
279
+ case 'Task':
280
+ return input.description?.slice(0, 50) || 'Delegating task';
281
+ case 'TodoWrite': {
282
+ const todos = input.todos;
283
+ if (todos && todos.length > 0) {
284
+ const inProgress = todos.find(t => t.status === 'in_progress');
285
+ if (inProgress?.content) {
286
+ return inProgress.content.slice(0, 50);
287
+ }
288
+ }
289
+ return 'Updating todo list';
290
+ }
291
+ default:
292
+ return toolName;
293
+ }
294
+ }
295
+ /**
296
+ * Extract filename from a path for display
297
+ */
298
+ extractFilename(path) {
299
+ if (!path)
300
+ return 'file';
301
+ const parts = path.split('/');
302
+ return parts[parts.length - 1] || 'file';
303
+ }
304
+ /**
305
+ * Infer session theme from tool usage patterns
306
+ */
307
+ inferTheme(messages) {
308
+ const toolCounts = {};
309
+ for (const msg of messages) {
310
+ const content = msg.message?.content;
311
+ if (!Array.isArray(content))
312
+ continue;
313
+ for (const block of content) {
314
+ if (typeof block === 'object' &&
315
+ block !== null &&
316
+ 'type' in block &&
317
+ block.type === 'tool_use' &&
318
+ 'name' in block &&
319
+ typeof block.name === 'string') {
320
+ toolCounts[block.name] = (toolCounts[block.name] || 0) + 1;
321
+ }
322
+ }
323
+ }
324
+ const readTools = (toolCounts['Read'] || 0) + (toolCounts['Grep'] || 0) + (toolCounts['Glob'] || 0);
325
+ const writeTools = (toolCounts['Edit'] || 0) + (toolCounts['Write'] || 0);
326
+ const bashTools = toolCounts['Bash'] || 0;
327
+ const taskTools = toolCounts['Task'] || 0;
328
+ // Heuristics for theme detection
329
+ if (writeTools > readTools && writeTools > 5) {
330
+ return 'implementation';
331
+ }
332
+ if (bashTools > writeTools && bashTools > 5) {
333
+ return 'debugging';
334
+ }
335
+ if (readTools > writeTools * 2 && readTools > 10) {
336
+ return 'research';
337
+ }
338
+ if (taskTools > 3) {
339
+ return 'research'; // Lots of subagents = exploration
340
+ }
341
+ return null;
342
+ }
343
+ /**
344
+ * Generate rich structured session insights using Opus 4.5
345
+ */
346
+ async generateStructuredInsights(messages) {
347
+ // Check if Anthropic is configured
348
+ if (!anthropicService.isConfigured()) {
349
+ this.logger.debug('Anthropic service not configured, skipping insight extraction');
350
+ return { context: null, currentState: null, milestones: [], notable: [], tags: null, recentActions: [], panels: [], theme: null };
351
+ }
352
+ try {
353
+ // Get user prompts for context
354
+ const userPrompts = this.extractUserPrompts(messages);
355
+ if (userPrompts.length === 0) {
356
+ return { context: null, currentState: null, milestones: [], notable: [], tags: null, recentActions: [], panels: [], theme: null };
357
+ }
358
+ // Get todo items for context
359
+ const todos = this.extractTodoState(messages);
360
+ const todoContext = todos
361
+ ? `\nTask list:\n${todos.map(t => `- [${t.status}] ${t.content}`).join('\n')}`
362
+ : '';
363
+ // Get recent assistant responses for outcome/progress context
364
+ const recentAssistantText = this.extractRecentAssistantText(messages, 3);
365
+ const assistantContext = recentAssistantText.length > 0
366
+ ? `\nRecent assistant responses:\n${recentAssistantText.map(t => `- "${t}"`).join('\n')}`
367
+ : '';
368
+ // Build conversation context - no truncation, models have plenty of context
369
+ const conversationText = `User requests (chronological):
370
+ ${userPrompts.slice(-12).map(p => `- "${p}"`).join('\n')}
371
+ ${todoContext}${assistantContext}`;
372
+ const result = await anthropicService.extractSessionInsights(conversationText);
373
+ return {
374
+ context: result.context,
375
+ currentState: result.currentState || null,
376
+ milestones: result.milestones,
377
+ notable: result.notable || [],
378
+ tags: result.tags,
379
+ recentActions: result.recentActions,
380
+ panels: result.panels || [],
381
+ theme: result.theme || null
382
+ };
383
+ }
384
+ catch (error) {
385
+ this.logger.error('Failed to generate structured insights - re-throwing to preserve existing cache', { error });
386
+ // Re-throw so callers can decide whether to use cached data or handle the error
387
+ // Previously this returned empty data which would overwrite good cached insights
388
+ throw error;
389
+ }
390
+ }
391
+ /**
392
+ * Get full insights for a session (checks cache first)
393
+ * For automated sessions, returns quick insights without AI generation.
394
+ *
395
+ * NON-BLOCKING BEHAVIOR:
396
+ * - If cache is fresh → return it immediately
397
+ * - If cache is stale → return stale data immediately, trigger background regeneration
398
+ * - If no cache → generate synchronously (first time only)
399
+ */
400
+ async getInsights(sessionId) {
401
+ this.logger.info('[GET INSIGHTS] Called', {
402
+ sessionId: sessionId.slice(0, 8),
403
+ timestamp: new Date().toISOString()
404
+ });
405
+ // Check cache first
406
+ const cached = await this.sessionInfoService.getInsights(sessionId);
407
+ this.logger.info('[GET INSIGHTS] Cache lookup result', {
408
+ sessionId: sessionId.slice(0, 8),
409
+ hasCached: !!cached,
410
+ isStale: cached?.stale || false,
411
+ cachedMission: cached?.context ? (typeof cached.context === 'string' ? JSON.parse(cached.context).mission : cached.context.mission)?.slice(0, 50) : null
412
+ });
413
+ if (cached && !cached.stale) {
414
+ this.logger.info('[GET INSIGHTS] Returning cached (not stale)', {
415
+ sessionId: sessionId.slice(0, 8)
416
+ });
417
+ return this.cachedToSessionInsights(cached);
418
+ }
419
+ // For automated sessions, skip AI generation entirely
420
+ const isAutomated = await this.isAutomatedSession(sessionId);
421
+ if (isAutomated) {
422
+ this.logger.debug('Skipping AI insights for automated session', { sessionId });
423
+ return this.getInsightsQuick(sessionId);
424
+ }
425
+ // NON-BLOCKING: If we have stale cache, return it immediately and regenerate in background
426
+ if (cached && cached.stale) {
427
+ this.logger.info('[GET INSIGHTS] Returning stale cache, triggering background regeneration', {
428
+ sessionId: sessionId.slice(0, 8)
429
+ });
430
+ // Fire off background regeneration (don't await)
431
+ this.regenerateInsightsInBackground(sessionId, cached).catch(err => this.logger.debug('Background regeneration failed', { sessionId, error: err }));
432
+ // Return stale data immediately for fast UX
433
+ return this.cachedToSessionInsights(cached);
434
+ }
435
+ // No cache at all - must generate synchronously (first time for this session)
436
+ this.logger.warn('[GET INSIGHTS] No cache - triggering synchronous generation', {
437
+ sessionId: sessionId.slice(0, 8)
438
+ });
439
+ const traceId = generateTraceId(sessionId);
440
+ const startTime = Date.now();
441
+ const beforeState = buildStateSnapshot(cached);
442
+ const insights = await this.computeInsights(sessionId);
443
+ // Cache the result and audit the recompute
444
+ const afterState = buildStateSnapshot(null, insights);
445
+ const changedFields = [];
446
+ if (beforeState.mission !== afterState.mission)
447
+ changedFields.push('mission');
448
+ if (beforeState.description !== afterState.description)
449
+ changedFields.push('description');
450
+ if (beforeState.currentState !== afterState.currentState)
451
+ changedFields.push('currentState');
452
+ if (beforeState.theme !== afterState.theme)
453
+ changedFields.push('theme');
454
+ if (JSON.stringify(beforeState.milestones) !== JSON.stringify(afterState.milestones))
455
+ changedFields.push('milestones');
456
+ if (JSON.stringify(beforeState.panels) !== JSON.stringify(afterState.panels))
457
+ changedFields.push('panels');
458
+ const durationMs = Date.now() - startTime;
459
+ // Audit the recompute
460
+ this.sessionInfoService.auditEventInsight({
461
+ traceId,
462
+ sessionId,
463
+ eventType: 'recompute',
464
+ trigger: 'api_request',
465
+ beforeState,
466
+ afterState,
467
+ patchedFields: changedFields,
468
+ durationMs,
469
+ llmResponse: `Opus returned: context=${!!insights.context}, description=${!!insights.description}, milestones=${insights.milestones.length}, panels=${insights.panels.length}`,
470
+ });
471
+ // Cache the result (don't await - fire and forget)
472
+ this.cacheInsights(sessionId, insights).catch(err => this.logger.debug('Failed to cache insights', { sessionId, error: err }));
473
+ return insights;
474
+ }
475
+ /**
476
+ * Regenerate insights in the background (for stale cache refresh).
477
+ * This runs async and updates the cache when done.
478
+ */
479
+ async regenerateInsightsInBackground(sessionId, existingCache) {
480
+ const traceId = generateTraceId(sessionId);
481
+ const startTime = Date.now();
482
+ const beforeState = buildStateSnapshot(existingCache);
483
+ this.logger.info('[BACKGROUND REGEN] Starting', {
484
+ traceId,
485
+ sessionId: sessionId.slice(0, 8)
486
+ });
487
+ try {
488
+ const insights = await this.computeInsights(sessionId);
489
+ // Cache the result and audit the recompute
490
+ const afterState = buildStateSnapshot(null, insights);
491
+ const changedFields = [];
492
+ if (beforeState.mission !== afterState.mission)
493
+ changedFields.push('mission');
494
+ if (beforeState.description !== afterState.description)
495
+ changedFields.push('description');
496
+ if (beforeState.currentState !== afterState.currentState)
497
+ changedFields.push('currentState');
498
+ if (beforeState.theme !== afterState.theme)
499
+ changedFields.push('theme');
500
+ if (JSON.stringify(beforeState.milestones) !== JSON.stringify(afterState.milestones))
501
+ changedFields.push('milestones');
502
+ if (JSON.stringify(beforeState.panels) !== JSON.stringify(afterState.panels))
503
+ changedFields.push('panels');
504
+ const durationMs = Date.now() - startTime;
505
+ // Audit the recompute
506
+ this.sessionInfoService.auditEventInsight({
507
+ traceId,
508
+ sessionId,
509
+ eventType: 'recompute',
510
+ trigger: 'background_stale_refresh',
511
+ beforeState,
512
+ afterState,
513
+ patchedFields: changedFields,
514
+ durationMs,
515
+ llmResponse: `Opus returned: context=${!!insights.context}, description=${!!insights.description}, milestones=${insights.milestones.length}, panels=${insights.panels.length}`,
516
+ });
517
+ await this.cacheInsights(sessionId, insights);
518
+ this.logger.info('[BACKGROUND REGEN] Completed', {
519
+ traceId,
520
+ sessionId: sessionId.slice(0, 8),
521
+ durationMs,
522
+ changedFields
523
+ });
524
+ }
525
+ catch (error) {
526
+ this.logger.warn('[BACKGROUND REGEN] Failed - existing cache preserved', {
527
+ traceId,
528
+ sessionId: sessionId.slice(0, 8),
529
+ error
530
+ });
531
+ // Don't throw - this is a background operation
532
+ // IMPORTANT: We intentionally don't cache anything on failure to preserve existing good data
533
+ }
534
+ }
535
+ /**
536
+ * Compute insights from conversation (bypasses cache)
537
+ */
538
+ async computeInsights(sessionId) {
539
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
540
+ const messageCount = messages.length;
541
+ // Extract todo state
542
+ const todos = this.extractTodoState(messages);
543
+ // Compute progress
544
+ const progress = todos ? this.computeProgress(todos) : null;
545
+ // Get recently completed (last 3)
546
+ const recentlyCompleted = todos
547
+ ? todos
548
+ .filter(t => t.status === 'completed')
549
+ .slice(-3)
550
+ .map(t => t.content)
551
+ : [];
552
+ // Get outstanding tasks
553
+ const outstanding = todos
554
+ ? todos
555
+ .filter(t => t.status === 'pending' || t.status === 'in_progress')
556
+ .map(t => t.content)
557
+ : [];
558
+ // Get current task (in_progress)
559
+ const currentTask = todos
560
+ ? todos.find(t => t.status === 'in_progress')?.content || null
561
+ : null;
562
+ // Generate rich structured insights using Opus
563
+ const structured = await this.generateStructuredInsights(messages);
564
+ // Use Opus theme, fallback to inferred
565
+ const theme = structured.theme || this.inferTheme(messages);
566
+ // Generate brief description for title (backward compat)
567
+ const description = structured.context?.mission || null;
568
+ // Build purposes array from context for backward compatibility
569
+ const purposes = structured.context
570
+ ? [structured.context.mission]
571
+ : [];
572
+ return {
573
+ sessionId,
574
+ context: structured.context,
575
+ currentState: structured.currentState,
576
+ milestones: structured.milestones,
577
+ notable: structured.notable,
578
+ tags: structured.tags,
579
+ recentActions: structured.recentActions,
580
+ panels: structured.panels || [],
581
+ theme,
582
+ purposes,
583
+ description,
584
+ progress,
585
+ recentlyCompleted,
586
+ outstanding,
587
+ currentTask,
588
+ messageCount
589
+ };
590
+ }
591
+ /**
592
+ * Cache insights to database
593
+ */
594
+ async cacheInsights(sessionId, insights) {
595
+ const cached = {
596
+ session_id: sessionId,
597
+ description: insights.description,
598
+ context: insights.context,
599
+ current_state: insights.currentState || null,
600
+ last_patch_trace_id: null,
601
+ purposes: insights.purposes,
602
+ milestones: insights.milestones, // Supports both old and V8 format
603
+ notable: insights.notable, // Supports both old and V8 format
604
+ tags: insights.tags,
605
+ recent_actions: insights.recentActions,
606
+ panels: insights.panels || [], // Supports both old and V8 format
607
+ progress_completed: insights.progress?.completed || 0,
608
+ progress_total: insights.progress?.total || 0,
609
+ outstanding_tasks: insights.outstanding,
610
+ completed_tasks: insights.recentlyCompleted,
611
+ current_task: insights.currentTask,
612
+ theme: insights.theme,
613
+ computed_at: new Date().toISOString(),
614
+ stale: false,
615
+ message_count: insights.messageCount
616
+ };
617
+ await this.sessionInfoService.setInsights(cached);
618
+ // Generate identity image if session doesn't have one yet (fire and forget)
619
+ this.maybeGenerateIdentityImage(sessionId, insights).catch(err => this.logger.debug('Failed to generate identity image', { sessionId, error: err }));
620
+ }
621
+ /**
622
+ * Generate identity image for a session if it doesn't already have one.
623
+ * This is called after insights are cached, and runs in the background.
624
+ */
625
+ async maybeGenerateIdentityImage(sessionId, insights) {
626
+ // Check if session already has an identity image
627
+ const hasImage = await this.sessionInfoService.hasIdentityImage(sessionId);
628
+ if (hasImage) {
629
+ return;
630
+ }
631
+ // Skip automated sessions (claudia) - only generate for manual sessions
632
+ try {
633
+ const { metadata } = await this.historyReader.fetchConversationDirect(sessionId);
634
+ if (metadata.projectPath?.includes('/claudia')) {
635
+ this.logger.debug('Skipping identity image - automated session', { sessionId, projectPath: metadata.projectPath });
636
+ return;
637
+ }
638
+ }
639
+ catch {
640
+ // If we can't fetch metadata, skip image generation
641
+ this.logger.debug('Skipping identity image - could not fetch metadata', { sessionId });
642
+ return;
643
+ }
644
+ // Need context to generate meaningful image
645
+ if (!insights.context?.mission) {
646
+ this.logger.debug('Skipping identity image - no mission context', { sessionId });
647
+ return;
648
+ }
649
+ const imageContext = {
650
+ mission: insights.context.mission,
651
+ project: insights.context.project,
652
+ theme: insights.theme ?? undefined,
653
+ workTypes: insights.tags?.workType
654
+ };
655
+ this.logger.info('Generating identity image for session', {
656
+ sessionId,
657
+ imageContext
658
+ });
659
+ try {
660
+ const result = await geminiService.generateSessionImage(imageContext);
661
+ await this.sessionInfoService.setIdentityImage(sessionId, result.imageData);
662
+ this.logger.info('Identity image generated and saved', { sessionId });
663
+ // Emit a follow-up SSE event so clients can update their cache with the new image
664
+ // This solves the race condition where the initial insights SSE is sent before
665
+ // the identity image is generated
666
+ try {
667
+ await getSessionActivityWatcher().emitInsightsUpdate(sessionId, 'patched');
668
+ this.logger.debug('Emitted SSE update for new identity image', { sessionId });
669
+ }
670
+ catch (sseError) {
671
+ this.logger.debug('Failed to emit SSE for identity image', { sessionId, error: sseError });
672
+ }
673
+ }
674
+ catch (error) {
675
+ this.logger.warn('Failed to generate identity image', { sessionId, error });
676
+ // Don't throw - this is a non-critical background operation
677
+ }
678
+ }
679
+ /**
680
+ * Convert cached insights to SessionInsights format
681
+ */
682
+ cachedToSessionInsights(cached) {
683
+ return {
684
+ sessionId: cached.session_id,
685
+ context: cached.context,
686
+ currentState: cached.current_state,
687
+ lastPatchTraceId: cached.last_patch_trace_id,
688
+ milestones: (cached.milestones || []),
689
+ notable: (cached.notable || []),
690
+ tags: cached.tags,
691
+ recentActions: cached.recent_actions || [],
692
+ panels: (cached.panels || []),
693
+ purposes: cached.purposes || [],
694
+ description: cached.description,
695
+ progress: cached.progress_total > 0 ? {
696
+ completed: cached.progress_completed,
697
+ inProgress: 0,
698
+ pending: cached.progress_total - cached.progress_completed,
699
+ total: cached.progress_total,
700
+ percent: Math.round((cached.progress_completed / cached.progress_total) * 100)
701
+ } : null,
702
+ recentlyCompleted: cached.completed_tasks,
703
+ outstanding: cached.outstanding_tasks,
704
+ currentTask: cached.current_task,
705
+ theme: cached.theme,
706
+ computedAt: cached.computed_at,
707
+ patchedAt: cached.patched_at
708
+ };
709
+ }
710
+ /**
711
+ * Get cached insights for multiple sessions (for list view)
712
+ */
713
+ async getCachedInsightsForSessions(sessionIds) {
714
+ const allCached = await this.sessionInfoService.getAllInsights();
715
+ const result = new Map();
716
+ for (const id of sessionIds) {
717
+ const cached = allCached.get(id);
718
+ if (cached) {
719
+ result.set(id, this.cachedToSessionInsights(cached));
720
+ }
721
+ }
722
+ return result;
723
+ }
724
+ /**
725
+ * Compute and cache insights for sessions that don't have cached insights
726
+ */
727
+ async computeMissingInsights(sessionIds, maxConcurrent = 3) {
728
+ const missing = await this.sessionInfoService.getMissingInsightsSessionIds(sessionIds);
729
+ if (missing.length === 0) {
730
+ return 0;
731
+ }
732
+ this.logger.info('Computing missing insights', { count: missing.length });
733
+ // Process in batches to avoid overwhelming the system
734
+ let computed = 0;
735
+ for (let i = 0; i < missing.length; i += maxConcurrent) {
736
+ const batch = missing.slice(i, i + maxConcurrent);
737
+ await Promise.all(batch.map(async (sessionId) => {
738
+ try {
739
+ const insights = await this.computeInsights(sessionId);
740
+ await this.cacheInsights(sessionId, insights);
741
+ computed++;
742
+ }
743
+ catch (error) {
744
+ this.logger.debug('Failed to compute insights for session', { sessionId, error });
745
+ }
746
+ }));
747
+ }
748
+ return computed;
749
+ }
750
+ /**
751
+ * Recompute insights for stale sessions (force refresh)
752
+ * Preserves accumulated milestones/notable from existing cache if new result is empty
753
+ */
754
+ async recomputeStaleInsights(sessionIds, maxConcurrent = 3) {
755
+ if (sessionIds.length === 0) {
756
+ return 0;
757
+ }
758
+ this.logger.info('Recomputing stale insights', { count: sessionIds.length });
759
+ // Process in batches to avoid overwhelming the system
760
+ let computed = 0;
761
+ for (let i = 0; i < sessionIds.length; i += maxConcurrent) {
762
+ const batch = sessionIds.slice(i, i + maxConcurrent);
763
+ await Promise.all(batch.map(async (sessionId) => {
764
+ const traceId = generateTraceId(sessionId);
765
+ const startTime = Date.now();
766
+ try {
767
+ // Get existing cache BEFORE regenerating - we may need to preserve accumulated state
768
+ const existingCache = await this.sessionInfoService.getInsights(sessionId);
769
+ const beforeState = buildStateSnapshot(existingCache);
770
+ this.logger.info('Computing insights for session', { traceId, sessionId });
771
+ const insights = await this.computeInsights(sessionId);
772
+ // Preserve accumulated milestones/notable if new result came back empty
773
+ // This handles cases where the LLM regeneration doesn't capture the full history
774
+ if (existingCache) {
775
+ const existingMilestones = existingCache.milestones || [];
776
+ const existingNotable = existingCache.notable || [];
777
+ // If new milestones are empty but we had some before, preserve them
778
+ if (insights.milestones.length === 0 && existingMilestones.length > 0) {
779
+ this.logger.info('Preserving accumulated milestones from cache', {
780
+ traceId,
781
+ sessionId,
782
+ preservedCount: existingMilestones.length
783
+ });
784
+ insights.milestones = existingMilestones;
785
+ }
786
+ // If new notable events are empty but we had some before, preserve them
787
+ if (insights.notable.length === 0 && existingNotable.length > 0) {
788
+ this.logger.info('Preserving accumulated notable events from cache', {
789
+ traceId,
790
+ sessionId,
791
+ preservedCount: existingNotable.length
792
+ });
793
+ insights.notable = existingNotable;
794
+ }
795
+ }
796
+ await this.cacheInsights(sessionId, insights);
797
+ computed++;
798
+ // Build after state and determine what changed
799
+ const afterState = buildStateSnapshot(null, insights);
800
+ const changedFields = [];
801
+ if (beforeState.mission !== afterState.mission)
802
+ changedFields.push('mission');
803
+ if (beforeState.description !== afterState.description)
804
+ changedFields.push('description');
805
+ if (beforeState.currentState !== afterState.currentState)
806
+ changedFields.push('currentState');
807
+ if (beforeState.theme !== afterState.theme)
808
+ changedFields.push('theme');
809
+ if (JSON.stringify(beforeState.milestones) !== JSON.stringify(afterState.milestones))
810
+ changedFields.push('milestones');
811
+ if (JSON.stringify(beforeState.panels) !== JSON.stringify(afterState.panels))
812
+ changedFields.push('panels');
813
+ const durationMs = Date.now() - startTime;
814
+ // Audit the recompute with full before/after state
815
+ this.sessionInfoService.auditEventInsight({
816
+ traceId,
817
+ sessionId,
818
+ eventType: 'recompute',
819
+ trigger: 'stale_refresh',
820
+ beforeState,
821
+ afterState,
822
+ patchedFields: changedFields,
823
+ durationMs,
824
+ llmResponse: `Opus returned: context=${!!insights.context}, description=${!!insights.description}, milestones=${insights.milestones.length}, panels=${insights.panels.length}`,
825
+ });
826
+ this.logger.info('Insights computed successfully', {
827
+ traceId,
828
+ sessionId,
829
+ milestones: insights.milestones.length,
830
+ notable: insights.notable.length,
831
+ changedFields,
832
+ durationMs,
833
+ });
834
+ }
835
+ catch (error) {
836
+ this.logger.warn('Failed to compute insights for session', { traceId, sessionId, error });
837
+ }
838
+ }));
839
+ }
840
+ return computed;
841
+ }
842
+ /**
843
+ * Get insights without AI generation (faster, for list views)
844
+ */
845
+ async getInsightsQuick(sessionId) {
846
+ // Check cache first - return cached AI insights if available
847
+ const cached = await this.sessionInfoService.getInsights(sessionId);
848
+ if (cached) {
849
+ return this.cachedToSessionInsights(cached);
850
+ }
851
+ // No cached insights - fall back to local extraction (no AI call)
852
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
853
+ const todos = this.extractTodoState(messages);
854
+ const progress = todos ? this.computeProgress(todos) : null;
855
+ const recentlyCompleted = todos
856
+ ? todos
857
+ .filter(t => t.status === 'completed')
858
+ .slice(-3)
859
+ .map(t => t.content)
860
+ : [];
861
+ const outstanding = todos
862
+ ? todos
863
+ .filter(t => t.status === 'pending' || t.status === 'in_progress')
864
+ .map(t => t.content)
865
+ : [];
866
+ const currentTask = todos
867
+ ? todos.find(t => t.status === 'in_progress')?.content || null
868
+ : null;
869
+ const theme = this.inferTheme(messages);
870
+ return {
871
+ sessionId,
872
+ context: null,
873
+ currentState: null,
874
+ milestones: [],
875
+ notable: [],
876
+ tags: null,
877
+ recentActions: [],
878
+ panels: [],
879
+ purposes: [],
880
+ description: null, // Skip AI generation for quick mode
881
+ progress,
882
+ recentlyCompleted,
883
+ outstanding,
884
+ currentTask,
885
+ theme
886
+ };
887
+ }
888
+ }
889
+ //# sourceMappingURL=session-insights-service.js.map