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.
- package/LICENSE +201 -0
- package/README.md +109 -0
- package/dist/cli-parser.d.ts +11 -0
- package/dist/cli-parser.d.ts.map +1 -0
- package/dist/cli-parser.js +57 -0
- package/dist/cli-parser.js.map +1 -0
- package/dist/cui-server.d.ts +69 -0
- package/dist/cui-server.d.ts.map +1 -0
- package/dist/cui-server.js +705 -0
- package/dist/cui-server.js.map +1 -0
- package/dist/mcp-server/claudia-tools.d.ts +15 -0
- package/dist/mcp-server/claudia-tools.d.ts.map +1 -0
- package/dist/mcp-server/claudia-tools.js +366 -0
- package/dist/mcp-server/claudia-tools.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +176 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +18 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +136 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors-setup.d.ts +7 -0
- package/dist/middleware/cors-setup.d.ts.map +1 -0
- package/dist/middleware/cors-setup.js +8 -0
- package/dist/middleware/cors-setup.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +4 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +27 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/query-parser.d.ts +11 -0
- package/dist/middleware/query-parser.d.ts.map +1 -0
- package/dist/middleware/query-parser.js +68 -0
- package/dist/middleware/query-parser.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +4 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +29 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/process-daemon/index.d.ts +14 -0
- package/dist/process-daemon/index.d.ts.map +1 -0
- package/dist/process-daemon/index.js +51 -0
- package/dist/process-daemon/index.js.map +1 -0
- package/dist/process-daemon/process-daemon.d.ts +78 -0
- package/dist/process-daemon/process-daemon.d.ts.map +1 -0
- package/dist/process-daemon/process-daemon.js +568 -0
- package/dist/process-daemon/process-daemon.js.map +1 -0
- package/dist/process-daemon/process-manager-client.d.ts +108 -0
- package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
- package/dist/process-daemon/process-manager-client.js +314 -0
- package/dist/process-daemon/process-manager-client.js.map +1 -0
- package/dist/process-daemon/process-manager-interface.d.ts +47 -0
- package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
- package/dist/process-daemon/process-manager-interface.js +8 -0
- package/dist/process-daemon/process-manager-interface.js.map +1 -0
- package/dist/process-daemon/test-daemon.d.ts +12 -0
- package/dist/process-daemon/test-daemon.d.ts.map +1 -0
- package/dist/process-daemon/test-daemon.js +81 -0
- package/dist/process-daemon/test-daemon.js.map +1 -0
- package/dist/process-daemon/types.d.ts +85 -0
- package/dist/process-daemon/types.d.ts.map +1 -0
- package/dist/process-daemon/types.js +8 -0
- package/dist/process-daemon/types.js.map +1 -0
- package/dist/routes/claudia.routes.d.ts +10 -0
- package/dist/routes/claudia.routes.d.ts.map +1 -0
- package/dist/routes/claudia.routes.js +123 -0
- package/dist/routes/claudia.routes.js.map +1 -0
- package/dist/routes/config.routes.d.ts +4 -0
- package/dist/routes/config.routes.d.ts.map +1 -0
- package/dist/routes/config.routes.js +27 -0
- package/dist/routes/config.routes.js.map +1 -0
- package/dist/routes/conversation.routes.d.ts +8 -0
- package/dist/routes/conversation.routes.d.ts.map +1 -0
- package/dist/routes/conversation.routes.js +870 -0
- package/dist/routes/conversation.routes.js.map +1 -0
- package/dist/routes/filesystem.routes.d.ts +4 -0
- package/dist/routes/filesystem.routes.d.ts.map +1 -0
- package/dist/routes/filesystem.routes.js +86 -0
- package/dist/routes/filesystem.routes.js.map +1 -0
- package/dist/routes/gemini.routes.d.ts +4 -0
- package/dist/routes/gemini.routes.d.ts.map +1 -0
- package/dist/routes/gemini.routes.js +93 -0
- package/dist/routes/gemini.routes.js.map +1 -0
- package/dist/routes/insights.routes.d.ts +17 -0
- package/dist/routes/insights.routes.d.ts.map +1 -0
- package/dist/routes/insights.routes.js +417 -0
- package/dist/routes/insights.routes.js.map +1 -0
- package/dist/routes/license.routes.d.ts +3 -0
- package/dist/routes/license.routes.d.ts.map +1 -0
- package/dist/routes/license.routes.js +111 -0
- package/dist/routes/license.routes.js.map +1 -0
- package/dist/routes/log.routes.d.ts +3 -0
- package/dist/routes/log.routes.d.ts.map +1 -0
- package/dist/routes/log.routes.js +65 -0
- package/dist/routes/log.routes.js.map +1 -0
- package/dist/routes/notifications.routes.d.ts +4 -0
- package/dist/routes/notifications.routes.d.ts.map +1 -0
- package/dist/routes/notifications.routes.js +71 -0
- package/dist/routes/notifications.routes.js.map +1 -0
- package/dist/routes/permission.routes.d.ts +4 -0
- package/dist/routes/permission.routes.d.ts.map +1 -0
- package/dist/routes/permission.routes.js +116 -0
- package/dist/routes/permission.routes.js.map +1 -0
- package/dist/routes/question.routes.d.ts +8 -0
- package/dist/routes/question.routes.d.ts.map +1 -0
- package/dist/routes/question.routes.js +82 -0
- package/dist/routes/question.routes.js.map +1 -0
- package/dist/routes/streaming.routes.d.ts +4 -0
- package/dist/routes/streaming.routes.d.ts.map +1 -0
- package/dist/routes/streaming.routes.js +28 -0
- package/dist/routes/streaming.routes.js.map +1 -0
- package/dist/routes/system.routes.d.ts +5 -0
- package/dist/routes/system.routes.d.ts.map +1 -0
- package/dist/routes/system.routes.js +103 -0
- package/dist/routes/system.routes.js.map +1 -0
- package/dist/routes/working-directories.routes.d.ts +4 -0
- package/dist/routes/working-directories.routes.d.ts.map +1 -0
- package/dist/routes/working-directories.routes.js +25 -0
- package/dist/routes/working-directories.routes.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +34 -0
- package/dist/server.js.map +1 -0
- package/dist/services/ToolMetricsService.d.ts +53 -0
- package/dist/services/ToolMetricsService.d.ts.map +1 -0
- package/dist/services/ToolMetricsService.js +230 -0
- package/dist/services/ToolMetricsService.js.map +1 -0
- package/dist/services/anthropic-service.d.ts +186 -0
- package/dist/services/anthropic-service.d.ts.map +1 -0
- package/dist/services/anthropic-service.js +1132 -0
- package/dist/services/anthropic-service.js.map +1 -0
- package/dist/services/claude-history-reader.d.ts +126 -0
- package/dist/services/claude-history-reader.d.ts.map +1 -0
- package/dist/services/claude-history-reader.js +717 -0
- package/dist/services/claude-history-reader.js.map +1 -0
- package/dist/services/claude-process-manager.d.ts +108 -0
- package/dist/services/claude-process-manager.d.ts.map +1 -0
- package/dist/services/claude-process-manager.js +909 -0
- package/dist/services/claude-process-manager.js.map +1 -0
- package/dist/services/claude-router-service.d.ts +19 -0
- package/dist/services/claude-router-service.d.ts.map +1 -0
- package/dist/services/claude-router-service.js +160 -0
- package/dist/services/claude-router-service.js.map +1 -0
- package/dist/services/claudia-service.d.ts +77 -0
- package/dist/services/claudia-service.d.ts.map +1 -0
- package/dist/services/claudia-service.js +194 -0
- package/dist/services/claudia-service.js.map +1 -0
- package/dist/services/commands-service.d.ts +18 -0
- package/dist/services/commands-service.d.ts.map +1 -0
- package/dist/services/commands-service.js +76 -0
- package/dist/services/commands-service.js.map +1 -0
- package/dist/services/config-service.d.ts +68 -0
- package/dist/services/config-service.d.ts.map +1 -0
- package/dist/services/config-service.js +429 -0
- package/dist/services/config-service.js.map +1 -0
- package/dist/services/conversation-cache.d.ts +86 -0
- package/dist/services/conversation-cache.d.ts.map +1 -0
- package/dist/services/conversation-cache.js +235 -0
- package/dist/services/conversation-cache.js.map +1 -0
- package/dist/services/conversation-status-manager.d.ts +98 -0
- package/dist/services/conversation-status-manager.d.ts.map +1 -0
- package/dist/services/conversation-status-manager.js +295 -0
- package/dist/services/conversation-status-manager.js.map +1 -0
- package/dist/services/cost-tracker.d.ts +87 -0
- package/dist/services/cost-tracker.d.ts.map +1 -0
- package/dist/services/cost-tracker.js +335 -0
- package/dist/services/cost-tracker.js.map +1 -0
- package/dist/services/file-system-service.d.ts +61 -0
- package/dist/services/file-system-service.d.ts.map +1 -0
- package/dist/services/file-system-service.js +348 -0
- package/dist/services/file-system-service.js.map +1 -0
- package/dist/services/gemini-service.d.ts +72 -0
- package/dist/services/gemini-service.d.ts.map +1 -0
- package/dist/services/gemini-service.js +431 -0
- package/dist/services/gemini-service.js.map +1 -0
- package/dist/services/insight-queue.d.ts +99 -0
- package/dist/services/insight-queue.d.ts.map +1 -0
- package/dist/services/insight-queue.js +277 -0
- package/dist/services/insight-queue.js.map +1 -0
- package/dist/services/insights-service.d.ts +102 -0
- package/dist/services/insights-service.d.ts.map +1 -0
- package/dist/services/insights-service.js +1152 -0
- package/dist/services/insights-service.js.map +1 -0
- package/dist/services/json-lines-parser.d.ts +19 -0
- package/dist/services/json-lines-parser.d.ts.map +1 -0
- package/dist/services/json-lines-parser.js +56 -0
- package/dist/services/json-lines-parser.js.map +1 -0
- package/dist/services/license-service.d.ts +69 -0
- package/dist/services/license-service.d.ts.map +1 -0
- package/dist/services/license-service.js +330 -0
- package/dist/services/license-service.js.map +1 -0
- package/dist/services/log-formatter.d.ts +5 -0
- package/dist/services/log-formatter.d.ts.map +1 -0
- package/dist/services/log-formatter.js +77 -0
- package/dist/services/log-formatter.js.map +1 -0
- package/dist/services/log-stream-buffer.d.ts +11 -0
- package/dist/services/log-stream-buffer.d.ts.map +1 -0
- package/dist/services/log-stream-buffer.js +36 -0
- package/dist/services/log-stream-buffer.js.map +1 -0
- package/dist/services/logger.d.ts +71 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +215 -0
- package/dist/services/logger.js.map +1 -0
- package/dist/services/mcp-config-generator.d.ts +32 -0
- package/dist/services/mcp-config-generator.d.ts.map +1 -0
- package/dist/services/mcp-config-generator.js +126 -0
- package/dist/services/mcp-config-generator.js.map +1 -0
- package/dist/services/message-filter.d.ts +22 -0
- package/dist/services/message-filter.d.ts.map +1 -0
- package/dist/services/message-filter.js +57 -0
- package/dist/services/message-filter.js.map +1 -0
- package/dist/services/notification-service.d.ts +45 -0
- package/dist/services/notification-service.d.ts.map +1 -0
- package/dist/services/notification-service.js +184 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/permission-tracker.d.ts +67 -0
- package/dist/services/permission-tracker.d.ts.map +1 -0
- package/dist/services/permission-tracker.js +161 -0
- package/dist/services/permission-tracker.js.map +1 -0
- package/dist/services/process-manager-factory.d.ts +81 -0
- package/dist/services/process-manager-factory.d.ts.map +1 -0
- package/dist/services/process-manager-factory.js +211 -0
- package/dist/services/process-manager-factory.js.map +1 -0
- package/dist/services/question-tracker.d.ts +47 -0
- package/dist/services/question-tracker.d.ts.map +1 -0
- package/dist/services/question-tracker.js +105 -0
- package/dist/services/question-tracker.js.map +1 -0
- package/dist/services/session-activity-watcher.d.ts +33 -0
- package/dist/services/session-activity-watcher.d.ts.map +1 -0
- package/dist/services/session-activity-watcher.js +194 -0
- package/dist/services/session-activity-watcher.js.map +1 -0
- package/dist/services/session-info-service.d.ts +228 -0
- package/dist/services/session-info-service.d.ts.map +1 -0
- package/dist/services/session-info-service.js +920 -0
- package/dist/services/session-info-service.js.map +1 -0
- package/dist/services/session-insights-service.d.ts +119 -0
- package/dist/services/session-insights-service.d.ts.map +1 -0
- package/dist/services/session-insights-service.js +889 -0
- package/dist/services/session-insights-service.js.map +1 -0
- package/dist/services/stream-manager.d.ts +62 -0
- package/dist/services/stream-manager.d.ts.map +1 -0
- package/dist/services/stream-manager.js +239 -0
- package/dist/services/stream-manager.js.map +1 -0
- package/dist/services/web-push-service.d.ts +48 -0
- package/dist/services/web-push-service.d.ts.map +1 -0
- package/dist/services/web-push-service.js +186 -0
- package/dist/services/web-push-service.js.map +1 -0
- package/dist/services/working-directories-service.d.ts +19 -0
- package/dist/services/working-directories-service.d.ts.map +1 -0
- package/dist/services/working-directories-service.js +103 -0
- package/dist/services/working-directories-service.js.map +1 -0
- package/dist/types/config.d.ts +111 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +14 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/express.d.ts +5 -0
- package/dist/types/express.d.ts.map +1 -0
- package/dist/types/express.js +2 -0
- package/dist/types/express.js.map +1 -0
- package/dist/types/index.d.ts +325 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/insights.d.ts +99 -0
- package/dist/types/insights.d.ts.map +1 -0
- package/dist/types/insights.js +7 -0
- package/dist/types/insights.js.map +1 -0
- package/dist/types/license.d.ts +70 -0
- package/dist/types/license.d.ts.map +1 -0
- package/dist/types/license.js +5 -0
- package/dist/types/license.js.map +1 -0
- package/dist/types/router-config.d.ts +13 -0
- package/dist/types/router-config.d.ts.map +1 -0
- package/dist/types/router-config.js +2 -0
- package/dist/types/router-config.js.map +1 -0
- package/dist/utils/constants.d.ts +26 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/machine-id.d.ts +7 -0
- package/dist/utils/machine-id.d.ts.map +1 -0
- package/dist/utils/machine-id.js +76 -0
- package/dist/utils/machine-id.js.map +1 -0
- package/dist/utils/server-startup.d.ts +13 -0
- package/dist/utils/server-startup.d.ts.map +1 -0
- package/dist/utils/server-startup.js +20 -0
- package/dist/utils/server-startup.js.map +1 -0
- package/dist/web/assets/main-DAc2rjJ2.css +1 -0
- package/dist/web/assets/main-DvlZ02mT.js +137 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/favicon.svg +22 -0
- package/dist/web/icon-192x192.png +0 -0
- package/dist/web/icon-512x512.png +0 -0
- package/dist/web/index.html +36 -0
- package/dist/web/manifest.json +61 -0
- package/package.json +174 -0
- 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
|