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