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,1152 @@
1
+ /**
2
+ * InsightsService - Unified event-driven insights system.
3
+ *
4
+ * This consolidates InsightsEventCoordinator + InsightsEventHandler into a single service.
5
+ * It handles:
6
+ * 1. Watching for session activity (file changes in ~/.claude/projects)
7
+ * 2. Tracking session state (action counts, timing, etc.)
8
+ * 3. Triggering insight generation and patching via InsightQueue
9
+ * 4. Executing LLM operations (generation, quick check, patch)
10
+ *
11
+ * Architecture V2 Features:
12
+ * - InsightQueue for serialization (eliminates race conditions)
13
+ * - Haiku-heavy patches (cheap quick checks, targeted updates)
14
+ * - Ownership model for REFRESH (deterministic field ownership)
15
+ */
16
+ import { EventEmitter } from 'events';
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import * as os from 'os';
20
+ import * as crypto from 'crypto';
21
+ import { createLogger } from './logger.js';
22
+ import { ClaudeHistoryReader } from './claude-history-reader.js';
23
+ import { SessionInfoService } from './session-info-service.js';
24
+ import { SessionInsightsService } from './session-insights-service.js';
25
+ import { anthropicService } from './anthropic-service.js';
26
+ import { getSessionActivityWatcher } from './session-activity-watcher.js';
27
+ import { getInsightQueue } from './insight-queue.js';
28
+ // ============================================================================
29
+ // Constants
30
+ // ============================================================================
31
+ const INITIAL_GENERATION_THRESHOLD = 15; // Generate initial insights after 15 actions
32
+ const FULL_REGEN_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes (reduced - Haiku patches handle most updates)
33
+ const DEBOUNCE_MS = 150; // Debounce file changes
34
+ const COMPLETION_CHECK_DELAY_MS = 5000; // Wait 5 seconds after assistant response
35
+ const QUICK_CHECK_DEBOUNCE_MS = 5000; // Wait 5s of quiet before running quick check
36
+ const QUICK_CHECK_STALENESS_THRESHOLD_MS = 30 * 1000; // Force check if >30s since last check
37
+ const MAX_MILESTONES = 12; // Cap milestones to prevent endless growth
38
+ // ============================================================================
39
+ // Helpers
40
+ // ============================================================================
41
+ /**
42
+ * Generate a short trace ID for correlating events through the system.
43
+ */
44
+ function generateTraceId(sessionId) {
45
+ const sessionPrefix = sessionId.slice(0, 8);
46
+ const timestampHex = Date.now().toString(16);
47
+ const random = crypto.randomBytes(2).toString('hex');
48
+ return `${sessionPrefix}-${timestampHex}-${random}`;
49
+ }
50
+ /**
51
+ * Check if insights are missing critical content fields.
52
+ *
53
+ * Only checks fields that are truly essential for the dashboard to be useful.
54
+ * Notable, progress_total, and recent_actions are OPTIONAL - sessions may not
55
+ * have significant events, todos, or enough activity yet.
56
+ */
57
+ function hasMissingCriticalFields(insights) {
58
+ if (!insights)
59
+ return true;
60
+ // Mission is critical - we need to know what the session is about
61
+ if (!insights.context || !insights.context.mission)
62
+ return true;
63
+ // Panels are critical - they provide the main dashboard content
64
+ if (!insights.panels || insights.panels.length === 0)
65
+ return true;
66
+ // Milestones are critical - they show progress/intent
67
+ if (!insights.milestones || insights.milestones.length === 0)
68
+ return true;
69
+ // NOT critical (removed):
70
+ // - notable: may be empty if no significant events occurred
71
+ // - progress_total: 0 if no TodoWrite used (common)
72
+ // - recent_actions: may be empty early in session
73
+ return false;
74
+ }
75
+ // ============================================================================
76
+ // InsightsService
77
+ // ============================================================================
78
+ export class InsightsService extends EventEmitter {
79
+ logger;
80
+ historyReader;
81
+ sessionInfoService;
82
+ insightsComputeService;
83
+ projectsDir;
84
+ // File watching
85
+ watchers = new Map();
86
+ debounceTimers = new Map();
87
+ isWatching = false;
88
+ // Session state tracking
89
+ sessionStates = new Map();
90
+ lastKnownMessageCounts = new Map();
91
+ // Duplicate prevention
92
+ pendingGenerations = new Set();
93
+ pendingQuickChecks = new Set();
94
+ // Queue integration - store events for deferred execution
95
+ pendingActionEvents = new Map();
96
+ pendingGenerateEvents = new Map();
97
+ constructor() {
98
+ super();
99
+ this.logger = createLogger('InsightsService');
100
+ this.historyReader = new ClaudeHistoryReader();
101
+ this.sessionInfoService = SessionInfoService.getInstance();
102
+ this.insightsComputeService = new SessionInsightsService(this.historyReader, this.sessionInfoService);
103
+ this.projectsDir = path.join(os.homedir(), '.claude', 'projects');
104
+ }
105
+ // ==========================================================================
106
+ // Lifecycle
107
+ // ==========================================================================
108
+ /**
109
+ * Initialize the insights service. Call once at startup.
110
+ */
111
+ initialize() {
112
+ // Set up the insight queue executor
113
+ const queue = getInsightQueue();
114
+ queue.setExecutor(this.executeQueueOperation.bind(this));
115
+ // Start watching for session activity
116
+ this.startWatching();
117
+ this.logger.info('Insights service initialized');
118
+ }
119
+ /**
120
+ * Stop the insights service. Call at shutdown.
121
+ */
122
+ stop() {
123
+ this.logger.info('Stopping insights service');
124
+ // Stop file watchers
125
+ for (const watcher of this.watchers.values()) {
126
+ watcher.close();
127
+ }
128
+ this.watchers.clear();
129
+ // Clear debounce timers
130
+ for (const timer of this.debounceTimers.values()) {
131
+ clearTimeout(timer);
132
+ }
133
+ this.debounceTimers.clear();
134
+ // Clear session timers
135
+ for (const state of this.sessionStates.values()) {
136
+ if (state.regenTimerId)
137
+ clearTimeout(state.regenTimerId);
138
+ if (state.completionCheckTimer)
139
+ clearTimeout(state.completionCheckTimer);
140
+ if (state.quickCheckDebounceTimer)
141
+ clearTimeout(state.quickCheckDebounceTimer);
142
+ }
143
+ this.sessionStates.clear();
144
+ this.isWatching = false;
145
+ }
146
+ // ==========================================================================
147
+ // File Watching
148
+ // ==========================================================================
149
+ startWatching() {
150
+ if (this.isWatching)
151
+ return;
152
+ this.logger.info('Starting file watchers', { projectsDir: this.projectsDir });
153
+ try {
154
+ this.watchDirectory(this.projectsDir);
155
+ if (fs.existsSync(this.projectsDir)) {
156
+ const projects = fs.readdirSync(this.projectsDir);
157
+ for (const project of projects) {
158
+ const projectPath = path.join(this.projectsDir, project);
159
+ if (fs.statSync(projectPath).isDirectory()) {
160
+ this.watchProjectDirectory(projectPath);
161
+ }
162
+ }
163
+ }
164
+ this.isWatching = true;
165
+ this.logger.info('File watchers started', { watcherCount: this.watchers.size });
166
+ }
167
+ catch (error) {
168
+ this.logger.error('Failed to start file watchers', error);
169
+ }
170
+ }
171
+ watchDirectory(dirPath) {
172
+ if (this.watchers.has(dirPath))
173
+ return;
174
+ try {
175
+ const watcher = fs.watch(dirPath, { persistent: false }, (eventType, filename) => {
176
+ if (!filename)
177
+ return;
178
+ const fullPath = path.join(dirPath, filename);
179
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
180
+ this.watchProjectDirectory(fullPath);
181
+ }
182
+ });
183
+ this.watchers.set(dirPath, watcher);
184
+ }
185
+ catch (error) {
186
+ this.logger.warn('Failed to watch directory', { dirPath, error });
187
+ }
188
+ }
189
+ watchProjectDirectory(projectPath) {
190
+ if (this.watchers.has(projectPath))
191
+ return;
192
+ try {
193
+ const watcher = fs.watch(projectPath, { persistent: false }, (eventType, filename) => {
194
+ if (!filename || !filename.endsWith('.jsonl'))
195
+ return;
196
+ const filePath = path.join(projectPath, filename);
197
+ this.handleFileChange(filePath, filename);
198
+ });
199
+ this.watchers.set(projectPath, watcher);
200
+ }
201
+ catch (error) {
202
+ this.logger.warn('Failed to watch project directory', { projectPath, error });
203
+ }
204
+ }
205
+ handleFileChange(filePath, filename) {
206
+ const sessionId = filename.replace('.jsonl', '');
207
+ // Debounce rapid changes
208
+ const existingTimer = this.debounceTimers.get(sessionId);
209
+ if (existingTimer) {
210
+ clearTimeout(existingTimer);
211
+ }
212
+ this.debounceTimers.set(sessionId, setTimeout(async () => {
213
+ this.debounceTimers.delete(sessionId);
214
+ await this.processSessionUpdate(sessionId);
215
+ }, DEBOUNCE_MS));
216
+ }
217
+ // ==========================================================================
218
+ // Session Update Processing
219
+ // ==========================================================================
220
+ async processSessionUpdate(sessionId) {
221
+ try {
222
+ const { messages } = await this.historyReader.fetchConversationDirect(sessionId);
223
+ let previousCount = this.lastKnownMessageCounts.get(sessionId) || 0;
224
+ const currentCount = messages.length;
225
+ // COLD START FIX: If this is first time seeing this session,
226
+ // check if cached insights exist. If so, skip entire history.
227
+ if (previousCount === 0) {
228
+ const state = this.sessionStates.get(sessionId);
229
+ if (!state) {
230
+ const cachedInsights = await this.sessionInfoService.getInsights(sessionId);
231
+ if (cachedInsights && cachedInsights.computed_at) {
232
+ previousCount = currentCount;
233
+ this.lastKnownMessageCounts.set(sessionId, currentCount);
234
+ this.logger.info('Cold start: skipping history for session with existing insights', {
235
+ sessionId: sessionId.slice(0, 8),
236
+ messageCount: currentCount,
237
+ computed_at: cachedInsights.computed_at
238
+ });
239
+ return;
240
+ }
241
+ }
242
+ }
243
+ // No new messages
244
+ if (currentCount <= previousCount) {
245
+ return;
246
+ }
247
+ this.lastKnownMessageCounts.set(sessionId, currentCount);
248
+ // Extract new actions from new messages
249
+ const newMessages = messages.slice(previousCount);
250
+ const newActions = this.extractActions(newMessages);
251
+ if (newActions.length === 0) {
252
+ return;
253
+ }
254
+ // Update session state
255
+ let state = this.sessionStates.get(sessionId);
256
+ if (!state) {
257
+ const cachedInsights = await this.sessionInfoService.getInsights(sessionId);
258
+ const hasExistingInsights = cachedInsights && cachedInsights.computed_at;
259
+ state = {
260
+ actionCount: 0,
261
+ lastActionAt: 0,
262
+ lastCheckAt: Date.now(),
263
+ insightsGeneratedAt: hasExistingInsights ? Date.now() : undefined
264
+ };
265
+ this.sessionStates.set(sessionId, state);
266
+ if (hasExistingInsights) {
267
+ this.logger.debug('Session state initialized with existing insights', {
268
+ sessionId: sessionId.slice(0, 8),
269
+ computed_at: cachedInsights.computed_at
270
+ });
271
+ }
272
+ }
273
+ const previousActionCount = state.actionCount;
274
+ state.actionCount += newActions.length;
275
+ state.lastActionAt = Date.now();
276
+ this.logger.debug('Session activity detected', {
277
+ sessionId: sessionId.slice(0, 8),
278
+ newActions: newActions.length,
279
+ totalActions: state.actionCount,
280
+ hasInsights: !!state.insightsGeneratedAt
281
+ });
282
+ // Check if we should trigger initial generation
283
+ const shouldGenerateInitial = !state.insightsGeneratedAt &&
284
+ previousActionCount < INITIAL_GENERATION_THRESHOLD &&
285
+ state.actionCount >= INITIAL_GENERATION_THRESHOLD;
286
+ if (shouldGenerateInitial) {
287
+ if (this.pendingGenerations.has(sessionId)) {
288
+ this.logger.debug('Skipping generation - already in progress', {
289
+ sessionId: sessionId.slice(0, 8)
290
+ });
291
+ return;
292
+ }
293
+ this.logger.info('Triggering insights generation', {
294
+ sessionId: sessionId.slice(0, 8),
295
+ actionCount: state.actionCount,
296
+ reason: 'initial'
297
+ });
298
+ this.enqueueGenerate({ sessionId, reason: 'initial', actionCount: state.actionCount });
299
+ return;
300
+ }
301
+ // Background regen every 100 actions (Haiku patches handle incremental updates)
302
+ if (state.insightsGeneratedAt && state.actionCount % 100 === 0 && state.actionCount > 0) {
303
+ if (!this.pendingGenerations.has(sessionId)) {
304
+ this.logger.debug('Triggering background regeneration', {
305
+ sessionId: sessionId.slice(0, 8),
306
+ actionCount: state.actionCount
307
+ });
308
+ this.enqueueGenerate({ sessionId, reason: 'background', actionCount: state.actionCount });
309
+ }
310
+ }
311
+ // Force generation if insights exist but are missing critical content
312
+ // Check only every 100 actions - Haiku patches should fill gaps in most cases
313
+ // Also enforce 10-minute cooldown to prevent rapid regeneration
314
+ const MISSING_FIELDS_CHECK_INTERVAL = 100;
315
+ const REGENERATION_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
316
+ let shouldForceRegenDueToMissingFields = false;
317
+ if (state.insightsGeneratedAt && state.actionCount % MISSING_FIELDS_CHECK_INTERVAL === 0) {
318
+ const cachedInsights = await this.sessionInfoService.getInsights(sessionId);
319
+ const hasMissing = hasMissingCriticalFields(cachedInsights);
320
+ // Check cooldown - don't regenerate if we regenerated recently
321
+ const timeSinceLastGen = Date.now() - state.insightsGeneratedAt;
322
+ const cooldownActive = timeSinceLastGen < REGENERATION_COOLDOWN_MS;
323
+ if (hasMissing && cooldownActive) {
324
+ this.logger.debug('Skipping missing_fields regen due to cooldown', {
325
+ sessionId: sessionId.slice(0, 8),
326
+ timeSinceLastGenMs: timeSinceLastGen,
327
+ cooldownMs: REGENERATION_COOLDOWN_MS
328
+ });
329
+ }
330
+ shouldForceRegenDueToMissingFields = hasMissing && !cooldownActive;
331
+ }
332
+ if (shouldForceRegenDueToMissingFields) {
333
+ if (this.pendingGenerations.has(sessionId)) {
334
+ return;
335
+ }
336
+ this.logger.info('Triggering insights generation', {
337
+ sessionId: sessionId.slice(0, 8),
338
+ actionCount: state.actionCount,
339
+ reason: 'missing_fields'
340
+ });
341
+ this.enqueueGenerate({ sessionId, reason: 'missing_fields', actionCount: state.actionCount });
342
+ return;
343
+ }
344
+ // If we have insights, trigger action event for patch checking
345
+ if (state.insightsGeneratedAt) {
346
+ this.handleSessionAction(sessionId, state, newActions);
347
+ }
348
+ }
349
+ catch (error) {
350
+ this.logger.debug('Failed to process session update', {
351
+ sessionId: sessionId.slice(0, 8),
352
+ error
353
+ });
354
+ }
355
+ }
356
+ /**
357
+ * Handle action event - determine if/how to trigger patch checking
358
+ */
359
+ handleSessionAction(sessionId, state, newActions) {
360
+ const hasOnlyTextResponse = newActions.length === 1 && newActions[0].type === 'text';
361
+ const hasToolUse = newActions.some(a => a.type === 'tool_use');
362
+ if (hasOnlyTextResponse && !hasToolUse) {
363
+ // This might be a completion message - set up delayed check
364
+ if (state.completionCheckTimer) {
365
+ clearTimeout(state.completionCheckTimer);
366
+ }
367
+ state.lastResponseTimestamp = Date.now();
368
+ state.completionCheckTimer = setTimeout(() => {
369
+ const traceId = generateTraceId(sessionId);
370
+ this.logger.debug('Completion detected - forcing patch', {
371
+ traceId,
372
+ sessionId: sessionId.slice(0, 8),
373
+ });
374
+ this.enqueueAction({
375
+ sessionId,
376
+ actionCount: state.actionCount,
377
+ newActions,
378
+ timestamp: Date.now(),
379
+ forcePatch: true,
380
+ traceId
381
+ });
382
+ state.completionCheckTimer = undefined;
383
+ }, COMPLETION_CHECK_DELAY_MS);
384
+ }
385
+ else {
386
+ // Has tool use or user message - debounced action event
387
+ if (state.completionCheckTimer) {
388
+ clearTimeout(state.completionCheckTimer);
389
+ state.completionCheckTimer = undefined;
390
+ }
391
+ const hasUserMessage = newActions.some(a => a.type === 'user_message');
392
+ const timeSinceLastCheck = Date.now() - state.lastCheckAt;
393
+ const isStale = timeSinceLastCheck > QUICK_CHECK_STALENESS_THRESHOLD_MS;
394
+ if (hasUserMessage || isStale) {
395
+ // Immediate emit
396
+ if (state.quickCheckDebounceTimer) {
397
+ clearTimeout(state.quickCheckDebounceTimer);
398
+ state.quickCheckDebounceTimer = undefined;
399
+ }
400
+ const traceId = generateTraceId(sessionId);
401
+ this.enqueueAction({
402
+ sessionId,
403
+ actionCount: state.actionCount,
404
+ newActions,
405
+ timestamp: Date.now(),
406
+ forcePatch: hasUserMessage,
407
+ traceId
408
+ });
409
+ }
410
+ else {
411
+ // DEBOUNCE: Wait for quiet period
412
+ if (state.quickCheckDebounceTimer) {
413
+ clearTimeout(state.quickCheckDebounceTimer);
414
+ }
415
+ const capturedActions = [...newActions];
416
+ const capturedActionCount = state.actionCount;
417
+ state.quickCheckDebounceTimer = setTimeout(() => {
418
+ const traceId = generateTraceId(sessionId);
419
+ this.enqueueAction({
420
+ sessionId,
421
+ actionCount: capturedActionCount,
422
+ newActions: capturedActions,
423
+ timestamp: Date.now(),
424
+ forcePatch: false,
425
+ traceId
426
+ });
427
+ state.quickCheckDebounceTimer = undefined;
428
+ }, QUICK_CHECK_DEBOUNCE_MS);
429
+ }
430
+ }
431
+ }
432
+ // ==========================================================================
433
+ // Action Extraction
434
+ // ==========================================================================
435
+ extractActions(messages) {
436
+ const actions = [];
437
+ const pendingToolUses = new Map();
438
+ for (const msg of messages) {
439
+ const content = msg.message?.content;
440
+ if (!Array.isArray(content))
441
+ continue;
442
+ if (msg.type === 'user') {
443
+ for (const block of content) {
444
+ if (typeof block !== 'object' || block === null || !('type' in block))
445
+ continue;
446
+ if (block.type === 'tool_result' && 'tool_use_id' in block) {
447
+ const toolUseId = block.tool_use_id;
448
+ const pending = pendingToolUses.get(toolUseId);
449
+ if (pending) {
450
+ let resultContent = '';
451
+ if (typeof block.content === 'string') {
452
+ resultContent = block.content;
453
+ }
454
+ else if (Array.isArray(block.content)) {
455
+ resultContent = block.content
456
+ .filter((b) => b.type === 'text' && b.text)
457
+ .map((b) => b.text)
458
+ .join('\n');
459
+ }
460
+ if (actions[pending.index]) {
461
+ actions[pending.index].output = resultContent.slice(0, 200);
462
+ }
463
+ pendingToolUses.delete(toolUseId);
464
+ }
465
+ continue;
466
+ }
467
+ if (block.type === 'text' && 'text' in block) {
468
+ const fullText = block.text.trim();
469
+ if (fullText.length > 0) {
470
+ actions.push({ type: 'user_message', text: fullText });
471
+ }
472
+ }
473
+ }
474
+ continue;
475
+ }
476
+ if (msg.type === 'assistant') {
477
+ for (const block of content) {
478
+ if (typeof block !== 'object' || block === null || !('type' in block))
479
+ continue;
480
+ if (block.type === 'tool_use' && 'name' in block && 'id' in block) {
481
+ const toolName = block.name;
482
+ const toolId = block.id;
483
+ const input = block.input;
484
+ let inputSummary = '';
485
+ if (input) {
486
+ switch (toolName) {
487
+ case 'Read':
488
+ case 'Edit':
489
+ case 'Write':
490
+ inputSummary = this.extractFilename(input.file_path);
491
+ break;
492
+ case 'Grep':
493
+ inputSummary = `pattern: "${input.pattern?.slice(0, 40)}"`;
494
+ break;
495
+ case 'Glob':
496
+ inputSummary = `pattern: ${input.pattern?.slice(0, 40)}`;
497
+ break;
498
+ case 'Bash': {
499
+ const desc = input.description;
500
+ const cmd = input.command;
501
+ inputSummary = desc || cmd?.slice(0, 80) || '';
502
+ break;
503
+ }
504
+ case 'Task':
505
+ inputSummary = input.description?.slice(0, 60) || '';
506
+ break;
507
+ default: {
508
+ const firstKey = Object.keys(input)[0];
509
+ if (firstKey && input[firstKey]) {
510
+ const val = input[firstKey];
511
+ inputSummary = typeof val === 'string' ? val.slice(0, 60) : JSON.stringify(val).slice(0, 60);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ const actionIndex = actions.length;
517
+ actions.push({ type: 'tool_use', name: toolName, input: inputSummary });
518
+ pendingToolUses.set(toolId, { name: toolName, input: inputSummary, index: actionIndex });
519
+ }
520
+ else if (block.type === 'text' && 'text' in block) {
521
+ const fullText = block.text.trim();
522
+ if (fullText.length > 0) {
523
+ actions.push({ type: 'text', text: fullText });
524
+ }
525
+ }
526
+ }
527
+ }
528
+ }
529
+ return actions;
530
+ }
531
+ extractFilename(filePath) {
532
+ if (!filePath)
533
+ return '';
534
+ const parts = filePath.split('/');
535
+ return parts[parts.length - 1] || filePath.slice(-40);
536
+ }
537
+ // ==========================================================================
538
+ // Queue Integration
539
+ // ==========================================================================
540
+ enqueueGenerate(event) {
541
+ const { sessionId, reason } = event;
542
+ this.pendingGenerateEvents.set(sessionId, event);
543
+ const opType = (reason === 'background' || reason === 'scheduled') ? 'REFRESH' : 'GENERATE';
544
+ const priority = (reason === 'initial' || reason === 'missing_fields') ? 'high' : 'low';
545
+ getInsightQueue().enqueue({
546
+ type: opType,
547
+ sessionId,
548
+ priority,
549
+ trigger: this.mapReasonToTrigger(reason),
550
+ });
551
+ }
552
+ enqueueAction(event) {
553
+ const { sessionId, newActions, traceId } = event;
554
+ this.pendingActionEvents.set(sessionId, event);
555
+ const hasUserMessage = newActions.some(a => a.type === 'user_message');
556
+ const trigger = hasUserMessage ? 'user_message' : 'tool_use';
557
+ const priority = hasUserMessage ? 'high' : 'normal';
558
+ getInsightQueue().enqueue({
559
+ type: 'PATCH',
560
+ sessionId,
561
+ priority,
562
+ trigger,
563
+ traceId,
564
+ });
565
+ }
566
+ mapReasonToTrigger(reason) {
567
+ switch (reason) {
568
+ case 'initial': return 'initial';
569
+ case 'scheduled': return 'scheduled';
570
+ case 'manual': return 'manual';
571
+ case 'missing_fields': return 'missing_fields';
572
+ case 'background': return 'scheduled';
573
+ default: return 'manual';
574
+ }
575
+ }
576
+ async executeQueueOperation(op) {
577
+ const { type, sessionId, trigger, traceId } = op;
578
+ this.logger.debug('Executing queued operation', {
579
+ type,
580
+ sessionId: sessionId.slice(0, 8),
581
+ trigger,
582
+ traceId,
583
+ });
584
+ switch (type) {
585
+ case 'GENERATE': {
586
+ const event = this.pendingGenerateEvents.get(sessionId);
587
+ if (event) {
588
+ this.pendingGenerateEvents.delete(sessionId);
589
+ await this.executeGenerate(event);
590
+ }
591
+ break;
592
+ }
593
+ case 'PATCH': {
594
+ const event = this.pendingActionEvents.get(sessionId);
595
+ if (event) {
596
+ this.pendingActionEvents.delete(sessionId);
597
+ await this.executeAction(event);
598
+ }
599
+ break;
600
+ }
601
+ case 'REFRESH': {
602
+ const event = this.pendingGenerateEvents.get(sessionId);
603
+ if (event) {
604
+ this.pendingGenerateEvents.delete(sessionId);
605
+ await this.executeGenerate(event);
606
+ }
607
+ else {
608
+ await this.executeGenerate({ sessionId, reason: 'scheduled', actionCount: 0 });
609
+ }
610
+ break;
611
+ }
612
+ }
613
+ }
614
+ // ==========================================================================
615
+ // LLM Operations - Generation
616
+ // ==========================================================================
617
+ async executeGenerate(event) {
618
+ const { sessionId, reason, actionCount } = event;
619
+ if (this.pendingGenerations.has(sessionId)) {
620
+ return;
621
+ }
622
+ this.pendingGenerations.add(sessionId);
623
+ try {
624
+ this.logger.debug('Generating insights', {
625
+ sessionId: sessionId.slice(0, 8),
626
+ reason,
627
+ actionCount
628
+ });
629
+ const startTime = Date.now();
630
+ const freshInsights = await this.insightsComputeService.computeInsights(sessionId);
631
+ const duration = Date.now() - startTime;
632
+ let finalInsights = freshInsights;
633
+ // For REFRESH, apply ownership model
634
+ if (reason === 'background' || reason === 'scheduled') {
635
+ try {
636
+ const cachedInsights = await this.insightsComputeService.getInsights(sessionId);
637
+ if (cachedInsights) {
638
+ finalInsights = this.applyOwnershipModel(cachedInsights, freshInsights);
639
+ this.logger.debug('[REFRESH] Ownership model applied', {
640
+ sessionId: sessionId.slice(0, 8),
641
+ finalMission: finalInsights.context?.mission?.slice(0, 50),
642
+ finalMilestoneCount: finalInsights.milestones?.length || 0,
643
+ });
644
+ }
645
+ }
646
+ catch (mergeError) {
647
+ this.logger.warn('[REFRESH] Ownership apply failed, using fresh insights', {
648
+ sessionId: sessionId.slice(0, 8),
649
+ error: mergeError instanceof Error ? mergeError.message : String(mergeError)
650
+ });
651
+ }
652
+ }
653
+ await this.insightsComputeService.cacheInsights(sessionId, finalInsights);
654
+ this.markInsightsGenerated(sessionId);
655
+ this.logger.info('Insights generated successfully', {
656
+ sessionId: sessionId.slice(0, 8),
657
+ reason,
658
+ durationMs: duration,
659
+ mission: finalInsights.context?.mission?.slice(0, 50),
660
+ });
661
+ await new Promise(resolve => setTimeout(resolve, 100));
662
+ await getSessionActivityWatcher().emitInsightsUpdate(sessionId, 'generated');
663
+ }
664
+ catch (error) {
665
+ this.logger.error('Failed to generate insights', {
666
+ sessionId: sessionId.slice(0, 8),
667
+ reason,
668
+ error: error instanceof Error ? error.message : String(error)
669
+ });
670
+ }
671
+ finally {
672
+ this.pendingGenerations.delete(sessionId);
673
+ }
674
+ }
675
+ /**
676
+ * Apply deterministic ownership model for REFRESH operations.
677
+ * - Haiku owns (keep cached): currentState, milestones, recentActions
678
+ * - Opus owns (use fresh): context, panels, tags, theme
679
+ * - Notable: accumulates (merge, don't replace)
680
+ */
681
+ applyOwnershipModel(cached, fresh) {
682
+ const mergedNotable = this.mergeNotableEvents(cached.notable || [], fresh.notable || []);
683
+ return {
684
+ ...fresh,
685
+ context: fresh.context,
686
+ panels: fresh.panels,
687
+ tags: fresh.tags,
688
+ theme: fresh.theme,
689
+ currentState: cached.currentState,
690
+ milestones: cached.milestones,
691
+ recentActions: cached.recentActions,
692
+ notable: mergedNotable,
693
+ purposes: fresh.purposes?.length ? fresh.purposes : cached.purposes,
694
+ description: fresh.description || cached.description,
695
+ progress: fresh.progress || cached.progress,
696
+ recentlyCompleted: fresh.recentlyCompleted?.length ? fresh.recentlyCompleted : cached.recentlyCompleted,
697
+ outstanding: fresh.outstanding?.length ? fresh.outstanding : cached.outstanding,
698
+ currentTask: fresh.currentTask || cached.currentTask,
699
+ computedAt: fresh.computedAt,
700
+ patchedAt: cached.patchedAt,
701
+ };
702
+ }
703
+ mergeNotableEvents(existing, fresh) {
704
+ const existingSignatures = new Set(existing.map(e => (e.text || e.description || '').toLowerCase().trim()));
705
+ const merged = [...existing];
706
+ for (const freshEvent of fresh) {
707
+ const signature = (freshEvent.text || freshEvent.description || '').toLowerCase().trim();
708
+ if (signature && !existingSignatures.has(signature)) {
709
+ merged.push(freshEvent);
710
+ existingSignatures.add(signature);
711
+ }
712
+ }
713
+ const MAX_NOTABLE = 20;
714
+ if (merged.length > MAX_NOTABLE) {
715
+ return merged.slice(-MAX_NOTABLE);
716
+ }
717
+ return merged;
718
+ }
719
+ // ==========================================================================
720
+ // LLM Operations - Patching
721
+ // ==========================================================================
722
+ async executeAction(event) {
723
+ const { sessionId, newActions, actionCount, forcePatch, traceId } = event;
724
+ const trigger = newActions.some(a => a.type === 'user_message') ? 'user_message'
725
+ : newActions.some(a => a.type === 'tool_use') ? 'tool_use'
726
+ : 'completion';
727
+ if (this.pendingQuickChecks.has(sessionId)) {
728
+ this.logger.debug('Skipping check - already in progress', { traceId, sessionId: sessionId.slice(0, 8) });
729
+ SessionInfoService.getInstance().auditEventInsight({
730
+ traceId,
731
+ sessionId,
732
+ eventType: 'skip',
733
+ trigger,
734
+ actionContent: newActions.map(a => a.type === 'tool_use' ? `tool:${a.name}` : `${a.type}:${(a.text || '').slice(0, 50)}`),
735
+ skippedReason: 'already_in_progress'
736
+ });
737
+ return;
738
+ }
739
+ this.pendingQuickChecks.add(sessionId);
740
+ const startTime = Date.now();
741
+ try {
742
+ const cached = await SessionInfoService.getInstance().getInsights(sessionId);
743
+ if (!cached) {
744
+ this.logger.debug('No cached insights for check', { traceId, sessionId: sessionId.slice(0, 8) });
745
+ SessionInfoService.getInstance().auditEventInsight({
746
+ traceId,
747
+ sessionId,
748
+ eventType: 'skip',
749
+ trigger,
750
+ actionContent: newActions.map(a => a.type === 'tool_use' ? `tool:${a.name}` : `${a.type}:${(a.text || '').slice(0, 50)}`),
751
+ skippedReason: 'no_cached_insights'
752
+ });
753
+ return;
754
+ }
755
+ const beforeState = {
756
+ mission: cached.context?.mission,
757
+ currentState: cached.current_state?.content,
758
+ milestones: cached.milestones?.map(m => `${m.label}:${m.status}`),
759
+ panels: cached.panels?.map(p => p.label || p.title || 'unnamed')
760
+ };
761
+ const recentActivity = newActions.map(a => {
762
+ if (a.type === 'tool_use') {
763
+ let content = `Tool: ${a.name}`;
764
+ if (a.input)
765
+ content += ` (${a.input})`;
766
+ if (a.output)
767
+ content += ` → ${a.output.slice(0, 100)}`;
768
+ return { type: a.type, content };
769
+ }
770
+ return { type: a.type, content: a.text || '' };
771
+ });
772
+ const actionContent = newActions.map(a => {
773
+ if (a.type === 'tool_use') {
774
+ let content = `tool:${a.name}`;
775
+ if (a.input)
776
+ content += `(${a.input.slice(0, 30)})`;
777
+ return content;
778
+ }
779
+ return `${a.type}:${(a.text || '').slice(0, 50)}`;
780
+ });
781
+ if (forcePatch) {
782
+ await this.doPatch(sessionId, cached, recentActivity, traceId, beforeState, actionContent, trigger);
783
+ return;
784
+ }
785
+ // Normal flow: quick check with Haiku first
786
+ const recentActionsText = newActions.map(a => {
787
+ if (a.type === 'tool_use') {
788
+ let actionText = `Tool: ${a.name}`;
789
+ if (a.input)
790
+ actionText += ` (${a.input})`;
791
+ if (a.output)
792
+ actionText += ` → ${a.output.slice(0, 100)}`;
793
+ return actionText;
794
+ }
795
+ else if (a.type === 'user_message') {
796
+ return `User: ${a.text}`;
797
+ }
798
+ else {
799
+ return `Response: ${a.text}`;
800
+ }
801
+ });
802
+ const quickCheckStartTime = Date.now();
803
+ const quickCheckResult = await this.doQuickCheck(cached, recentActionsText, traceId);
804
+ const quickCheckDurationMs = Date.now() - quickCheckStartTime;
805
+ this.markQuickCheckPerformed(sessionId);
806
+ if (quickCheckResult.needsPatch) {
807
+ SessionInfoService.getInstance().auditEventInsight({
808
+ traceId,
809
+ sessionId,
810
+ eventType: 'quick_check',
811
+ trigger,
812
+ actionContent,
813
+ beforeState,
814
+ llmResponse: `needsPatch=true: ${quickCheckResult.reason}`,
815
+ durationMs: quickCheckDurationMs
816
+ });
817
+ await this.doPatch(sessionId, cached, recentActivity, traceId, beforeState, actionContent, trigger);
818
+ }
819
+ else {
820
+ this.logger.debug('Quick check: no patch needed', {
821
+ traceId,
822
+ sessionId: sessionId.slice(0, 8),
823
+ reason: quickCheckResult.reason,
824
+ });
825
+ SessionInfoService.getInstance().auditEventInsight({
826
+ traceId,
827
+ sessionId,
828
+ eventType: 'quick_check',
829
+ trigger,
830
+ actionContent,
831
+ beforeState,
832
+ llmResponse: `needsPatch=false: ${quickCheckResult.reason}`,
833
+ durationMs: quickCheckDurationMs,
834
+ skippedReason: 'haiku_said_no_patch_needed'
835
+ });
836
+ }
837
+ }
838
+ catch (error) {
839
+ this.logger.error('Failed to handle action event', {
840
+ traceId,
841
+ sessionId: sessionId.slice(0, 8),
842
+ error: error instanceof Error ? error.message : String(error),
843
+ });
844
+ }
845
+ finally {
846
+ this.pendingQuickChecks.delete(sessionId);
847
+ }
848
+ }
849
+ async doQuickCheck(cached, recentActions, traceId) {
850
+ const mission = cached.context?.mission || 'Unknown';
851
+ const milestones = (cached.milestones || []);
852
+ const panels = (cached.panels || []);
853
+ return await anthropicService.quickCheckInsightsStale({ mission, milestones, panels }, recentActions);
854
+ }
855
+ async doPatch(sessionId, cached, recentActivity, traceId, beforeState, actionContent, trigger) {
856
+ const patchStartTime = Date.now();
857
+ const currentInsights = {
858
+ mission: cached.context?.mission || cached.description || 'Unknown',
859
+ description: cached.description || '',
860
+ theme: cached.theme || 'unknown',
861
+ currentState: cached.current_state || null,
862
+ milestones: (cached.milestones || []),
863
+ panels: (cached.panels || []),
864
+ notable: (cached.notable || []).map(n => ({ text: n.description || n.text || '', icon: n.icon })),
865
+ recentActions: (cached.recent_actions || []),
866
+ tags: cached.tags ? [...(cached.tags.workType || []), ...(cached.tags.domain || [])] : [],
867
+ previousPropositions: (cached.propositions || [])
868
+ };
869
+ const result = await anthropicService.generateInsightsPatch(currentInsights, recentActivity);
870
+ const llmDurationMs = Date.now() - patchStartTime;
871
+ const patchKeys = Object.keys(result.patches);
872
+ if (patchKeys.length === 0) {
873
+ this.logger.debug('Sonnet generated no patches', { traceId, sessionId: sessionId.slice(0, 8) });
874
+ if (trigger) {
875
+ SessionInfoService.getInstance().auditEventInsight({
876
+ traceId,
877
+ sessionId,
878
+ eventType: 'patch',
879
+ trigger,
880
+ actionContent,
881
+ beforeState,
882
+ llmResponse: `no_patches: ${result.reason}`,
883
+ patchedFields: [],
884
+ durationMs: llmDurationMs
885
+ });
886
+ }
887
+ return;
888
+ }
889
+ await this.applyPatches(sessionId, cached, result.patches, result.propositions, traceId);
890
+ this.markInsightsPatched(sessionId);
891
+ const totalDurationMs = Date.now() - patchStartTime;
892
+ this.logger.debug('Patches applied', {
893
+ traceId,
894
+ sessionId: sessionId.slice(0, 8),
895
+ patchedFields: patchKeys,
896
+ });
897
+ const updatedCached = await SessionInfoService.getInstance().getInsights(sessionId);
898
+ const afterState = updatedCached ? {
899
+ mission: updatedCached.context?.mission,
900
+ currentState: updatedCached.current_state?.content,
901
+ milestones: updatedCached.milestones?.map(m => `${m.label}:${m.status}`),
902
+ panels: updatedCached.panels?.map(p => p.label || p.title || 'unnamed')
903
+ } : undefined;
904
+ if (trigger) {
905
+ SessionInfoService.getInstance().auditEventInsight({
906
+ traceId,
907
+ sessionId,
908
+ eventType: 'patch',
909
+ trigger,
910
+ actionContent,
911
+ beforeState,
912
+ afterState,
913
+ llmResponse: result.reason,
914
+ patchedFields: patchKeys,
915
+ durationMs: totalDurationMs
916
+ });
917
+ }
918
+ await new Promise(resolve => setTimeout(resolve, 100));
919
+ await getSessionActivityWatcher().emitInsightsUpdate(sessionId, 'patched');
920
+ }
921
+ async applyPatches(sessionId, cached, patches, propositions, traceId) {
922
+ const updated = { ...cached };
923
+ updated.propositions = propositions;
924
+ if (patches.mission && updated.context) {
925
+ updated.context = { ...updated.context, mission: patches.mission };
926
+ }
927
+ if (patches.description) {
928
+ updated.description = patches.description;
929
+ }
930
+ if (patches.theme) {
931
+ updated.theme = patches.theme;
932
+ }
933
+ if (patches.currentState) {
934
+ updated.current_state = patches.currentState;
935
+ updated.last_patch_trace_id = traceId;
936
+ }
937
+ if (patches.recentActions) {
938
+ updated.recent_actions = patches.recentActions;
939
+ }
940
+ if (patches.milestones && Array.isArray(updated.milestones)) {
941
+ const milestones = [...updated.milestones];
942
+ // Track existing labels for deduplication (case-insensitive)
943
+ const existingLabels = new Set(milestones.map(m => m.label.toLowerCase().trim()));
944
+ for (const patch of patches.milestones) {
945
+ if (patch.action === 'update') {
946
+ const idx = milestones.findIndex(m => m.label === patch.label);
947
+ if (idx >= 0 && patch.status) {
948
+ milestones[idx] = { ...milestones[idx], status: patch.status };
949
+ }
950
+ }
951
+ else if (patch.action === 'add' && patch.status) {
952
+ // Deduplicate: skip if label already exists
953
+ const normalizedLabel = patch.label.toLowerCase().trim();
954
+ if (!existingLabels.has(normalizedLabel)) {
955
+ milestones.push({ label: patch.label, status: patch.status });
956
+ existingLabels.add(normalizedLabel);
957
+ }
958
+ }
959
+ }
960
+ // Cap milestones to prevent endless growth (keep most recent)
961
+ if (milestones.length > MAX_MILESTONES) {
962
+ // Keep the first few done items and the most recent items
963
+ const doneItems = milestones.filter(m => m.status === 'done').slice(0, 3);
964
+ const nonDoneItems = milestones.filter(m => m.status !== 'done');
965
+ const recentItems = milestones.slice(-(MAX_MILESTONES - doneItems.length));
966
+ // Merge: keep done items that aren't in recent, plus recent
967
+ const mergedSet = new Set(recentItems.map(m => m.label));
968
+ const uniqueDone = doneItems.filter(m => !mergedSet.has(m.label));
969
+ updated.milestones = [...uniqueDone, ...recentItems].slice(-MAX_MILESTONES);
970
+ }
971
+ else {
972
+ updated.milestones = milestones;
973
+ }
974
+ }
975
+ if (patches.panels && Array.isArray(updated.panels)) {
976
+ const panels = [...updated.panels];
977
+ for (const patch of patches.panels) {
978
+ if (patch.action === 'remove') {
979
+ const idx = panels.findIndex(p => (p.label && p.label === patch.label) ||
980
+ (p.title && p.title === patch.title));
981
+ if (idx >= 0) {
982
+ panels.splice(idx, 1);
983
+ }
984
+ }
985
+ else if (patch.action === 'update') {
986
+ const idx = panels.findIndex(p => (p.label && p.label === patch.label) ||
987
+ (p.title && p.title === patch.title));
988
+ if (idx >= 0) {
989
+ if (patch.value !== undefined) {
990
+ panels[idx] = { ...panels[idx], value: patch.value };
991
+ }
992
+ else if (patch.items !== undefined) {
993
+ panels[idx] = { ...panels[idx], items: patch.items };
994
+ }
995
+ }
996
+ }
997
+ else if (patch.action === 'add') {
998
+ if (panels.length >= 4) {
999
+ this.logger.warn('Panel limit reached - skipping add', {
1000
+ traceId,
1001
+ sessionId: sessionId.slice(0, 8),
1002
+ });
1003
+ continue;
1004
+ }
1005
+ panels.push({
1006
+ label: patch.label,
1007
+ icon: patch.icon,
1008
+ color: patch.color,
1009
+ value: patch.value
1010
+ });
1011
+ }
1012
+ }
1013
+ if (panels.length > 4) {
1014
+ panels.splice(4);
1015
+ }
1016
+ updated.panels = panels;
1017
+ }
1018
+ if (patches.notable && Array.isArray(updated.notable)) {
1019
+ const notable = [...updated.notable];
1020
+ for (const patch of patches.notable) {
1021
+ if (patch.action === 'add') {
1022
+ notable.push({
1023
+ type: patch.type || 'event',
1024
+ description: patch.text,
1025
+ text: patch.text,
1026
+ icon: patch.icon
1027
+ });
1028
+ }
1029
+ }
1030
+ updated.notable = notable;
1031
+ }
1032
+ updated.patched_at = new Date().toISOString();
1033
+ await SessionInfoService.getInstance().setInsights(updated);
1034
+ }
1035
+ // ==========================================================================
1036
+ // Session State Management
1037
+ // ==========================================================================
1038
+ /**
1039
+ * Called when insights have been generated for a session
1040
+ */
1041
+ markInsightsGenerated(sessionId) {
1042
+ let state = this.sessionStates.get(sessionId);
1043
+ if (!state) {
1044
+ state = {
1045
+ actionCount: 0,
1046
+ lastActionAt: Date.now(),
1047
+ lastCheckAt: Date.now()
1048
+ };
1049
+ this.sessionStates.set(sessionId, state);
1050
+ }
1051
+ state.insightsGeneratedAt = Date.now();
1052
+ state.insightsPatchedAt = undefined;
1053
+ this.startRegenTimer(sessionId, state);
1054
+ this.logger.info('Insights generated, starting regen timer', {
1055
+ sessionId: sessionId.slice(0, 8),
1056
+ regenAt: new Date(Date.now() + FULL_REGEN_INTERVAL_MS).toISOString()
1057
+ });
1058
+ }
1059
+ markInsightsPatched(sessionId) {
1060
+ const state = this.sessionStates.get(sessionId);
1061
+ if (state) {
1062
+ state.insightsPatchedAt = Date.now();
1063
+ state.lastCheckAt = Date.now();
1064
+ }
1065
+ }
1066
+ markQuickCheckPerformed(sessionId) {
1067
+ const state = this.sessionStates.get(sessionId);
1068
+ if (state) {
1069
+ state.lastCheckAt = Date.now();
1070
+ }
1071
+ }
1072
+ startRegenTimer(sessionId, state) {
1073
+ if (state.regenTimerId) {
1074
+ clearTimeout(state.regenTimerId);
1075
+ }
1076
+ state.regenTimerId = setTimeout(() => {
1077
+ const timeSinceLastAction = Date.now() - state.lastActionAt;
1078
+ if (timeSinceLastAction > 5 * 60 * 1000) {
1079
+ this.logger.info('Session idle - cleaning up tracking', {
1080
+ sessionId: sessionId.slice(0, 8),
1081
+ idleMinutes: Math.round(timeSinceLastAction / 60000)
1082
+ });
1083
+ this.cleanupSession(sessionId);
1084
+ return;
1085
+ }
1086
+ this.logger.info('Triggering scheduled full regeneration', {
1087
+ sessionId: sessionId.slice(0, 8),
1088
+ });
1089
+ this.enqueueGenerate({ sessionId, reason: 'scheduled', actionCount: state.actionCount });
1090
+ }, FULL_REGEN_INTERVAL_MS);
1091
+ }
1092
+ /**
1093
+ * Clean up tracking for a session (call when archived or idle)
1094
+ */
1095
+ cleanupSession(sessionId) {
1096
+ const state = this.sessionStates.get(sessionId);
1097
+ if (!state) {
1098
+ return;
1099
+ }
1100
+ this.logger.info('Cleaning up session tracking', {
1101
+ sessionId: sessionId.slice(0, 8),
1102
+ actionCount: state.actionCount,
1103
+ });
1104
+ if (state.regenTimerId)
1105
+ clearTimeout(state.regenTimerId);
1106
+ if (state.completionCheckTimer)
1107
+ clearTimeout(state.completionCheckTimer);
1108
+ if (state.quickCheckDebounceTimer)
1109
+ clearTimeout(state.quickCheckDebounceTimer);
1110
+ const debounceTimer = this.debounceTimers.get(sessionId);
1111
+ if (debounceTimer) {
1112
+ clearTimeout(debounceTimer);
1113
+ this.debounceTimers.delete(sessionId);
1114
+ }
1115
+ const projectPath = path.join(this.projectsDir, sessionId);
1116
+ const watcher = this.watchers.get(projectPath);
1117
+ if (watcher) {
1118
+ watcher.close();
1119
+ this.watchers.delete(projectPath);
1120
+ }
1121
+ this.sessionStates.delete(sessionId);
1122
+ this.lastKnownMessageCounts.delete(sessionId);
1123
+ this.logger.debug('Session cleanup complete', {
1124
+ sessionId: sessionId.slice(0, 8),
1125
+ remainingTrackedSessions: this.sessionStates.size
1126
+ });
1127
+ }
1128
+ /**
1129
+ * Get the current state for a session (for debugging/UI)
1130
+ */
1131
+ getSessionState(sessionId) {
1132
+ return this.sessionStates.get(sessionId);
1133
+ }
1134
+ }
1135
+ // ============================================================================
1136
+ // Singleton
1137
+ // ============================================================================
1138
+ let instance = null;
1139
+ export function getInsightsService() {
1140
+ if (!instance) {
1141
+ instance = new InsightsService();
1142
+ }
1143
+ return instance;
1144
+ }
1145
+ /**
1146
+ * Initialize the insights service. Call once at server startup.
1147
+ */
1148
+ export function initializeInsightsService() {
1149
+ const service = getInsightsService();
1150
+ service.initialize();
1151
+ }
1152
+ //# sourceMappingURL=insights-service.js.map