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