claudia-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +109 -0
- package/dist/cli-parser.d.ts +11 -0
- package/dist/cli-parser.d.ts.map +1 -0
- package/dist/cli-parser.js +57 -0
- package/dist/cli-parser.js.map +1 -0
- package/dist/cui-server.d.ts +69 -0
- package/dist/cui-server.d.ts.map +1 -0
- package/dist/cui-server.js +705 -0
- package/dist/cui-server.js.map +1 -0
- package/dist/mcp-server/claudia-tools.d.ts +15 -0
- package/dist/mcp-server/claudia-tools.d.ts.map +1 -0
- package/dist/mcp-server/claudia-tools.js +366 -0
- package/dist/mcp-server/claudia-tools.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +176 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +18 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +136 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors-setup.d.ts +7 -0
- package/dist/middleware/cors-setup.d.ts.map +1 -0
- package/dist/middleware/cors-setup.js +8 -0
- package/dist/middleware/cors-setup.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +4 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +27 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/query-parser.d.ts +11 -0
- package/dist/middleware/query-parser.d.ts.map +1 -0
- package/dist/middleware/query-parser.js +68 -0
- package/dist/middleware/query-parser.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +4 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +29 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/process-daemon/index.d.ts +14 -0
- package/dist/process-daemon/index.d.ts.map +1 -0
- package/dist/process-daemon/index.js +51 -0
- package/dist/process-daemon/index.js.map +1 -0
- package/dist/process-daemon/process-daemon.d.ts +78 -0
- package/dist/process-daemon/process-daemon.d.ts.map +1 -0
- package/dist/process-daemon/process-daemon.js +568 -0
- package/dist/process-daemon/process-daemon.js.map +1 -0
- package/dist/process-daemon/process-manager-client.d.ts +108 -0
- package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
- package/dist/process-daemon/process-manager-client.js +314 -0
- package/dist/process-daemon/process-manager-client.js.map +1 -0
- package/dist/process-daemon/process-manager-interface.d.ts +47 -0
- package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
- package/dist/process-daemon/process-manager-interface.js +8 -0
- package/dist/process-daemon/process-manager-interface.js.map +1 -0
- package/dist/process-daemon/test-daemon.d.ts +12 -0
- package/dist/process-daemon/test-daemon.d.ts.map +1 -0
- package/dist/process-daemon/test-daemon.js +81 -0
- package/dist/process-daemon/test-daemon.js.map +1 -0
- package/dist/process-daemon/types.d.ts +85 -0
- package/dist/process-daemon/types.d.ts.map +1 -0
- package/dist/process-daemon/types.js +8 -0
- package/dist/process-daemon/types.js.map +1 -0
- package/dist/routes/claudia.routes.d.ts +10 -0
- package/dist/routes/claudia.routes.d.ts.map +1 -0
- package/dist/routes/claudia.routes.js +123 -0
- package/dist/routes/claudia.routes.js.map +1 -0
- package/dist/routes/config.routes.d.ts +4 -0
- package/dist/routes/config.routes.d.ts.map +1 -0
- package/dist/routes/config.routes.js +27 -0
- package/dist/routes/config.routes.js.map +1 -0
- package/dist/routes/conversation.routes.d.ts +8 -0
- package/dist/routes/conversation.routes.d.ts.map +1 -0
- package/dist/routes/conversation.routes.js +870 -0
- package/dist/routes/conversation.routes.js.map +1 -0
- package/dist/routes/filesystem.routes.d.ts +4 -0
- package/dist/routes/filesystem.routes.d.ts.map +1 -0
- package/dist/routes/filesystem.routes.js +86 -0
- package/dist/routes/filesystem.routes.js.map +1 -0
- package/dist/routes/gemini.routes.d.ts +4 -0
- package/dist/routes/gemini.routes.d.ts.map +1 -0
- package/dist/routes/gemini.routes.js +93 -0
- package/dist/routes/gemini.routes.js.map +1 -0
- package/dist/routes/insights.routes.d.ts +17 -0
- package/dist/routes/insights.routes.d.ts.map +1 -0
- package/dist/routes/insights.routes.js +417 -0
- package/dist/routes/insights.routes.js.map +1 -0
- package/dist/routes/license.routes.d.ts +3 -0
- package/dist/routes/license.routes.d.ts.map +1 -0
- package/dist/routes/license.routes.js +111 -0
- package/dist/routes/license.routes.js.map +1 -0
- package/dist/routes/log.routes.d.ts +3 -0
- package/dist/routes/log.routes.d.ts.map +1 -0
- package/dist/routes/log.routes.js +65 -0
- package/dist/routes/log.routes.js.map +1 -0
- package/dist/routes/notifications.routes.d.ts +4 -0
- package/dist/routes/notifications.routes.d.ts.map +1 -0
- package/dist/routes/notifications.routes.js +71 -0
- package/dist/routes/notifications.routes.js.map +1 -0
- package/dist/routes/permission.routes.d.ts +4 -0
- package/dist/routes/permission.routes.d.ts.map +1 -0
- package/dist/routes/permission.routes.js +116 -0
- package/dist/routes/permission.routes.js.map +1 -0
- package/dist/routes/question.routes.d.ts +8 -0
- package/dist/routes/question.routes.d.ts.map +1 -0
- package/dist/routes/question.routes.js +82 -0
- package/dist/routes/question.routes.js.map +1 -0
- package/dist/routes/streaming.routes.d.ts +4 -0
- package/dist/routes/streaming.routes.d.ts.map +1 -0
- package/dist/routes/streaming.routes.js +28 -0
- package/dist/routes/streaming.routes.js.map +1 -0
- package/dist/routes/system.routes.d.ts +5 -0
- package/dist/routes/system.routes.d.ts.map +1 -0
- package/dist/routes/system.routes.js +103 -0
- package/dist/routes/system.routes.js.map +1 -0
- package/dist/routes/working-directories.routes.d.ts +4 -0
- package/dist/routes/working-directories.routes.d.ts.map +1 -0
- package/dist/routes/working-directories.routes.js +25 -0
- package/dist/routes/working-directories.routes.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +34 -0
- package/dist/server.js.map +1 -0
- package/dist/services/ToolMetricsService.d.ts +53 -0
- package/dist/services/ToolMetricsService.d.ts.map +1 -0
- package/dist/services/ToolMetricsService.js +230 -0
- package/dist/services/ToolMetricsService.js.map +1 -0
- package/dist/services/anthropic-service.d.ts +186 -0
- package/dist/services/anthropic-service.d.ts.map +1 -0
- package/dist/services/anthropic-service.js +1132 -0
- package/dist/services/anthropic-service.js.map +1 -0
- package/dist/services/claude-history-reader.d.ts +126 -0
- package/dist/services/claude-history-reader.d.ts.map +1 -0
- package/dist/services/claude-history-reader.js +717 -0
- package/dist/services/claude-history-reader.js.map +1 -0
- package/dist/services/claude-process-manager.d.ts +108 -0
- package/dist/services/claude-process-manager.d.ts.map +1 -0
- package/dist/services/claude-process-manager.js +909 -0
- package/dist/services/claude-process-manager.js.map +1 -0
- package/dist/services/claude-router-service.d.ts +19 -0
- package/dist/services/claude-router-service.d.ts.map +1 -0
- package/dist/services/claude-router-service.js +160 -0
- package/dist/services/claude-router-service.js.map +1 -0
- package/dist/services/claudia-service.d.ts +77 -0
- package/dist/services/claudia-service.d.ts.map +1 -0
- package/dist/services/claudia-service.js +194 -0
- package/dist/services/claudia-service.js.map +1 -0
- package/dist/services/commands-service.d.ts +18 -0
- package/dist/services/commands-service.d.ts.map +1 -0
- package/dist/services/commands-service.js +76 -0
- package/dist/services/commands-service.js.map +1 -0
- package/dist/services/config-service.d.ts +68 -0
- package/dist/services/config-service.d.ts.map +1 -0
- package/dist/services/config-service.js +429 -0
- package/dist/services/config-service.js.map +1 -0
- package/dist/services/conversation-cache.d.ts +86 -0
- package/dist/services/conversation-cache.d.ts.map +1 -0
- package/dist/services/conversation-cache.js +235 -0
- package/dist/services/conversation-cache.js.map +1 -0
- package/dist/services/conversation-status-manager.d.ts +98 -0
- package/dist/services/conversation-status-manager.d.ts.map +1 -0
- package/dist/services/conversation-status-manager.js +295 -0
- package/dist/services/conversation-status-manager.js.map +1 -0
- package/dist/services/cost-tracker.d.ts +87 -0
- package/dist/services/cost-tracker.d.ts.map +1 -0
- package/dist/services/cost-tracker.js +335 -0
- package/dist/services/cost-tracker.js.map +1 -0
- package/dist/services/file-system-service.d.ts +61 -0
- package/dist/services/file-system-service.d.ts.map +1 -0
- package/dist/services/file-system-service.js +348 -0
- package/dist/services/file-system-service.js.map +1 -0
- package/dist/services/gemini-service.d.ts +72 -0
- package/dist/services/gemini-service.d.ts.map +1 -0
- package/dist/services/gemini-service.js +431 -0
- package/dist/services/gemini-service.js.map +1 -0
- package/dist/services/insight-queue.d.ts +99 -0
- package/dist/services/insight-queue.d.ts.map +1 -0
- package/dist/services/insight-queue.js +277 -0
- package/dist/services/insight-queue.js.map +1 -0
- package/dist/services/insights-service.d.ts +102 -0
- package/dist/services/insights-service.d.ts.map +1 -0
- package/dist/services/insights-service.js +1152 -0
- package/dist/services/insights-service.js.map +1 -0
- package/dist/services/json-lines-parser.d.ts +19 -0
- package/dist/services/json-lines-parser.d.ts.map +1 -0
- package/dist/services/json-lines-parser.js +56 -0
- package/dist/services/json-lines-parser.js.map +1 -0
- package/dist/services/license-service.d.ts +69 -0
- package/dist/services/license-service.d.ts.map +1 -0
- package/dist/services/license-service.js +330 -0
- package/dist/services/license-service.js.map +1 -0
- package/dist/services/log-formatter.d.ts +5 -0
- package/dist/services/log-formatter.d.ts.map +1 -0
- package/dist/services/log-formatter.js +77 -0
- package/dist/services/log-formatter.js.map +1 -0
- package/dist/services/log-stream-buffer.d.ts +11 -0
- package/dist/services/log-stream-buffer.d.ts.map +1 -0
- package/dist/services/log-stream-buffer.js +36 -0
- package/dist/services/log-stream-buffer.js.map +1 -0
- package/dist/services/logger.d.ts +71 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +215 -0
- package/dist/services/logger.js.map +1 -0
- package/dist/services/mcp-config-generator.d.ts +32 -0
- package/dist/services/mcp-config-generator.d.ts.map +1 -0
- package/dist/services/mcp-config-generator.js +126 -0
- package/dist/services/mcp-config-generator.js.map +1 -0
- package/dist/services/message-filter.d.ts +22 -0
- package/dist/services/message-filter.d.ts.map +1 -0
- package/dist/services/message-filter.js +57 -0
- package/dist/services/message-filter.js.map +1 -0
- package/dist/services/notification-service.d.ts +45 -0
- package/dist/services/notification-service.d.ts.map +1 -0
- package/dist/services/notification-service.js +184 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/permission-tracker.d.ts +67 -0
- package/dist/services/permission-tracker.d.ts.map +1 -0
- package/dist/services/permission-tracker.js +161 -0
- package/dist/services/permission-tracker.js.map +1 -0
- package/dist/services/process-manager-factory.d.ts +81 -0
- package/dist/services/process-manager-factory.d.ts.map +1 -0
- package/dist/services/process-manager-factory.js +211 -0
- package/dist/services/process-manager-factory.js.map +1 -0
- package/dist/services/question-tracker.d.ts +47 -0
- package/dist/services/question-tracker.d.ts.map +1 -0
- package/dist/services/question-tracker.js +105 -0
- package/dist/services/question-tracker.js.map +1 -0
- package/dist/services/session-activity-watcher.d.ts +33 -0
- package/dist/services/session-activity-watcher.d.ts.map +1 -0
- package/dist/services/session-activity-watcher.js +194 -0
- package/dist/services/session-activity-watcher.js.map +1 -0
- package/dist/services/session-info-service.d.ts +228 -0
- package/dist/services/session-info-service.d.ts.map +1 -0
- package/dist/services/session-info-service.js +920 -0
- package/dist/services/session-info-service.js.map +1 -0
- package/dist/services/session-insights-service.d.ts +119 -0
- package/dist/services/session-insights-service.d.ts.map +1 -0
- package/dist/services/session-insights-service.js +889 -0
- package/dist/services/session-insights-service.js.map +1 -0
- package/dist/services/stream-manager.d.ts +62 -0
- package/dist/services/stream-manager.d.ts.map +1 -0
- package/dist/services/stream-manager.js +239 -0
- package/dist/services/stream-manager.js.map +1 -0
- package/dist/services/web-push-service.d.ts +48 -0
- package/dist/services/web-push-service.d.ts.map +1 -0
- package/dist/services/web-push-service.js +186 -0
- package/dist/services/web-push-service.js.map +1 -0
- package/dist/services/working-directories-service.d.ts +19 -0
- package/dist/services/working-directories-service.d.ts.map +1 -0
- package/dist/services/working-directories-service.js +103 -0
- package/dist/services/working-directories-service.js.map +1 -0
- package/dist/types/config.d.ts +111 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +14 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/express.d.ts +5 -0
- package/dist/types/express.d.ts.map +1 -0
- package/dist/types/express.js +2 -0
- package/dist/types/express.js.map +1 -0
- package/dist/types/index.d.ts +325 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/insights.d.ts +99 -0
- package/dist/types/insights.d.ts.map +1 -0
- package/dist/types/insights.js +7 -0
- package/dist/types/insights.js.map +1 -0
- package/dist/types/license.d.ts +70 -0
- package/dist/types/license.d.ts.map +1 -0
- package/dist/types/license.js +5 -0
- package/dist/types/license.js.map +1 -0
- package/dist/types/router-config.d.ts +13 -0
- package/dist/types/router-config.d.ts.map +1 -0
- package/dist/types/router-config.js +2 -0
- package/dist/types/router-config.js.map +1 -0
- package/dist/utils/constants.d.ts +26 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/machine-id.d.ts +7 -0
- package/dist/utils/machine-id.d.ts.map +1 -0
- package/dist/utils/machine-id.js +76 -0
- package/dist/utils/machine-id.js.map +1 -0
- package/dist/utils/server-startup.d.ts +13 -0
- package/dist/utils/server-startup.d.ts.map +1 -0
- package/dist/utils/server-startup.js +20 -0
- package/dist/utils/server-startup.js.map +1 -0
- package/dist/web/assets/main-DAc2rjJ2.css +1 -0
- package/dist/web/assets/main-DvlZ02mT.js +137 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/favicon.svg +22 -0
- package/dist/web/icon-192x192.png +0 -0
- package/dist/web/icon-512x512.png +0 -0
- package/dist/web/index.html +36 -0
- package/dist/web/manifest.json +61 -0
- package/package.json +174 -0
- package/scripts/postinstall.js +30 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { CUIError } from '../types/index.js';
|
|
3
|
+
import { createLogger } from '../services/logger.js';
|
|
4
|
+
import { SessionInsightsService } from '../services/session-insights-service.js';
|
|
5
|
+
import { getInsightsService } from '../services/insights-service.js';
|
|
6
|
+
export function createConversationRoutes(processManager, historyReader, statusTracker, sessionInfoService, conversationStatusManager, toolMetricsService) {
|
|
7
|
+
const router = Router();
|
|
8
|
+
const logger = createLogger('ConversationRoutes');
|
|
9
|
+
const insightsService = new SessionInsightsService(historyReader, sessionInfoService);
|
|
10
|
+
// Track sessions currently being started to enforce limits accurately
|
|
11
|
+
const sessionsBeingStarted = new Set();
|
|
12
|
+
// Start new conversation (also handles resume if resumedSessionId is provided)
|
|
13
|
+
router.post('/start', async (req, res, next) => {
|
|
14
|
+
const requestId = req.requestId;
|
|
15
|
+
const isResume = !!req.body.resumedSessionId;
|
|
16
|
+
// DEBUG TRACE - prominent logging
|
|
17
|
+
logger.info('[TRACE] POST /start received', {
|
|
18
|
+
requestId,
|
|
19
|
+
isResume,
|
|
20
|
+
resumedSessionId: req.body.resumedSessionId,
|
|
21
|
+
workingDirectory: req.body.workingDirectory,
|
|
22
|
+
promptPreview: req.body.initialPrompt?.substring(0, 80)
|
|
23
|
+
});
|
|
24
|
+
logger.debug('Start conversation request', {
|
|
25
|
+
requestId,
|
|
26
|
+
isResume,
|
|
27
|
+
resumedSessionId: req.body.resumedSessionId,
|
|
28
|
+
body: {
|
|
29
|
+
...req.body,
|
|
30
|
+
initialPrompt: req.body.initialPrompt ? `${req.body.initialPrompt.substring(0, 50)}...` : undefined
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
// Validate required fields
|
|
35
|
+
// For resume, workingDirectory can be fetched from the original session
|
|
36
|
+
if (!req.body.workingDirectory && !req.body.resumedSessionId) {
|
|
37
|
+
throw new CUIError('MISSING_WORKING_DIRECTORY', 'workingDirectory is required for new conversations', 400);
|
|
38
|
+
}
|
|
39
|
+
if (!req.body.initialPrompt) {
|
|
40
|
+
throw new CUIError('MISSING_INITIAL_PROMPT', 'initialPrompt is required', 400);
|
|
41
|
+
}
|
|
42
|
+
// Check license limits - enforce on ALL sessions (new and resumed)
|
|
43
|
+
// This prevents the workaround of starting multiple quick sessions then resuming them all
|
|
44
|
+
// TEMPORARILY DISABLED FOR DEVELOPMENT
|
|
45
|
+
/*
|
|
46
|
+
const limits = await licenseService.getLimits();
|
|
47
|
+
|
|
48
|
+
if (limits.maxSessions !== null) {
|
|
49
|
+
// Count both active sessions AND sessions currently being started
|
|
50
|
+
const activeSessions = conversationStatusManager.getActiveSessionIds();
|
|
51
|
+
const totalSessions = activeSessions.length + sessionsBeingStarted.size;
|
|
52
|
+
|
|
53
|
+
// Check if we're at the session limit
|
|
54
|
+
if (totalSessions >= limits.maxSessions) {
|
|
55
|
+
logger.info('Session limit reached', {
|
|
56
|
+
requestId,
|
|
57
|
+
activeCount: activeSessions.length,
|
|
58
|
+
startingCount: sessionsBeingStarted.size,
|
|
59
|
+
totalCount: totalSessions,
|
|
60
|
+
limit: limits.maxSessions,
|
|
61
|
+
tier: (await licenseService.validate()).tier,
|
|
62
|
+
isResume: !!req.body.resumedSessionId
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
throw new CUIError(
|
|
66
|
+
'SESSION_LIMIT_REACHED',
|
|
67
|
+
`Session limit reached. Free tier allows ${limits.maxSessions} concurrent session${limits.maxSessions === 1 ? '' : 's'}. Upgrade to Pro for unlimited sessions.`,
|
|
68
|
+
403
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Mark this request as starting a session (for both new and resumed)
|
|
73
|
+
if (requestId) {
|
|
74
|
+
sessionsBeingStarted.add(requestId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
*/
|
|
78
|
+
// Validate permissionMode if provided
|
|
79
|
+
if (req.body.permissionMode) {
|
|
80
|
+
const validModes = ['acceptEdits', 'bypassPermissions', 'default', 'plan'];
|
|
81
|
+
if (!validModes.includes(req.body.permissionMode)) {
|
|
82
|
+
throw new CUIError('INVALID_PERMISSION_MODE', `permissionMode must be one of: ${validModes.join(', ')}`, 400);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// For resume operations, fetch working directory and permission mode in parallel
|
|
86
|
+
// (messages are fetched after process spawn to avoid blocking startup)
|
|
87
|
+
let effectiveWorkingDirectory = req.body.workingDirectory;
|
|
88
|
+
let inheritedPermissionMode;
|
|
89
|
+
if (req.body.resumedSessionId) {
|
|
90
|
+
// Parallel fetch of working directory and session info
|
|
91
|
+
const [fetchedDir, sessionInfo] = await Promise.all([
|
|
92
|
+
!effectiveWorkingDirectory
|
|
93
|
+
? historyReader.getConversationWorkingDirectory(req.body.resumedSessionId)
|
|
94
|
+
: Promise.resolve(effectiveWorkingDirectory),
|
|
95
|
+
!req.body.permissionMode
|
|
96
|
+
? sessionInfoService.getSessionInfo(req.body.resumedSessionId).catch(() => null)
|
|
97
|
+
: Promise.resolve(null)
|
|
98
|
+
]);
|
|
99
|
+
if (!effectiveWorkingDirectory) {
|
|
100
|
+
if (!fetchedDir) {
|
|
101
|
+
throw new CUIError('CONVERSATION_NOT_FOUND', `Could not find working directory for session ${req.body.resumedSessionId}`, 404);
|
|
102
|
+
}
|
|
103
|
+
effectiveWorkingDirectory = fetchedDir;
|
|
104
|
+
logger.debug('Fetched working directory for resume', {
|
|
105
|
+
requestId,
|
|
106
|
+
resumedSessionId: req.body.resumedSessionId,
|
|
107
|
+
workingDirectory: effectiveWorkingDirectory
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (sessionInfo && !req.body.permissionMode) {
|
|
111
|
+
inheritedPermissionMode = sessionInfo.permission_mode;
|
|
112
|
+
logger.debug('Retrieved permission mode from session info', {
|
|
113
|
+
requestId,
|
|
114
|
+
originalSessionId: req.body.resumedSessionId,
|
|
115
|
+
permissionMode: inheritedPermissionMode
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Start process immediately - don't wait for message fetch
|
|
120
|
+
const conversationConfig = {
|
|
121
|
+
...req.body,
|
|
122
|
+
workingDirectory: effectiveWorkingDirectory,
|
|
123
|
+
permissionMode: req.body.permissionMode || inheritedPermissionMode
|
|
124
|
+
};
|
|
125
|
+
// Start the process and fetch previous messages in parallel
|
|
126
|
+
// Messages are only needed for optimistic UI, not for Claude CLI (which uses --resume)
|
|
127
|
+
const [spawnResult, previousMessages] = await Promise.all([
|
|
128
|
+
processManager.startConversation(conversationConfig),
|
|
129
|
+
req.body.resumedSessionId
|
|
130
|
+
? historyReader.fetchConversationDirect(req.body.resumedSessionId)
|
|
131
|
+
.then(({ messages }) => messages)
|
|
132
|
+
.catch((error) => {
|
|
133
|
+
logger.warn('Failed to fetch previous session messages', {
|
|
134
|
+
requestId,
|
|
135
|
+
originalSessionId: req.body.resumedSessionId,
|
|
136
|
+
error: error instanceof Error ? error.message : String(error)
|
|
137
|
+
});
|
|
138
|
+
return [];
|
|
139
|
+
})
|
|
140
|
+
: Promise.resolve([])
|
|
141
|
+
]);
|
|
142
|
+
const { streamingId, systemInit } = spawnResult;
|
|
143
|
+
// DEBUG TRACE - after process started
|
|
144
|
+
logger.info('[TRACE] Claude process started', {
|
|
145
|
+
streamingId,
|
|
146
|
+
newSessionId: systemInit.session_id,
|
|
147
|
+
model: systemInit.model,
|
|
148
|
+
cwd: systemInit.cwd
|
|
149
|
+
});
|
|
150
|
+
// Register the session with conversation status manager immediately (optimistic UI)
|
|
151
|
+
try {
|
|
152
|
+
conversationStatusManager.registerActiveSession(streamingId, systemInit.session_id, {
|
|
153
|
+
initialPrompt: req.body.initialPrompt,
|
|
154
|
+
workingDirectory: systemInit.cwd,
|
|
155
|
+
model: systemInit.model,
|
|
156
|
+
inheritedMessages: previousMessages.length > 0 ? previousMessages : undefined
|
|
157
|
+
});
|
|
158
|
+
logger.debug('Registered active session with status manager', {
|
|
159
|
+
requestId,
|
|
160
|
+
newSessionId: systemInit.session_id,
|
|
161
|
+
streamingId,
|
|
162
|
+
isResumed: !!req.body.resumedSessionId,
|
|
163
|
+
inheritedMessageCount: previousMessages.length
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.warn('Failed to register session with status manager', {
|
|
168
|
+
requestId,
|
|
169
|
+
error: error instanceof Error ? error.message : String(error)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Note: continuation_session_id tracking removed
|
|
173
|
+
// Resume operations continue in the same session (no forking), so there's no
|
|
174
|
+
// continuation to track. This prevents self-referential continuation_session_id
|
|
175
|
+
// entries that were caused by the UI expecting new session IDs (commit aa64a4b bug)
|
|
176
|
+
// Store permission mode in session info if provided
|
|
177
|
+
if (conversationConfig.permissionMode) {
|
|
178
|
+
try {
|
|
179
|
+
await sessionInfoService.updateSessionInfo(systemInit.session_id, {
|
|
180
|
+
permission_mode: conversationConfig.permissionMode
|
|
181
|
+
});
|
|
182
|
+
logger.debug('Stored permission mode in session info', {
|
|
183
|
+
sessionId: systemInit.session_id,
|
|
184
|
+
permissionMode: conversationConfig.permissionMode
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
logger.warn('Failed to store permission mode in session info', {
|
|
189
|
+
sessionId: systemInit.session_id,
|
|
190
|
+
permissionMode: conversationConfig.permissionMode,
|
|
191
|
+
error: error instanceof Error ? error.message : String(error)
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
logger.debug('Conversation started successfully', {
|
|
196
|
+
requestId,
|
|
197
|
+
isResume,
|
|
198
|
+
resumedSessionId: req.body.resumedSessionId,
|
|
199
|
+
streamingId,
|
|
200
|
+
sessionId: systemInit.session_id,
|
|
201
|
+
model: systemInit.model,
|
|
202
|
+
cwd: systemInit.cwd,
|
|
203
|
+
previousMessageCount: previousMessages.length
|
|
204
|
+
});
|
|
205
|
+
// Clean up session tracking
|
|
206
|
+
if (requestId) {
|
|
207
|
+
sessionsBeingStarted.delete(requestId);
|
|
208
|
+
}
|
|
209
|
+
res.json({
|
|
210
|
+
streamingId,
|
|
211
|
+
streamUrl: `/api/stream/${streamingId}`,
|
|
212
|
+
// System init fields
|
|
213
|
+
sessionId: systemInit.session_id,
|
|
214
|
+
cwd: systemInit.cwd,
|
|
215
|
+
tools: systemInit.tools,
|
|
216
|
+
mcpServers: systemInit.mcp_servers,
|
|
217
|
+
model: systemInit.model,
|
|
218
|
+
permissionMode: systemInit.permissionMode,
|
|
219
|
+
apiKeySource: systemInit.apiKeySource
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
// Clean up session tracking on error
|
|
224
|
+
if (requestId) {
|
|
225
|
+
sessionsBeingStarted.delete(requestId);
|
|
226
|
+
}
|
|
227
|
+
logger.debug('Start conversation failed', {
|
|
228
|
+
requestId,
|
|
229
|
+
isResume,
|
|
230
|
+
error: error instanceof Error ? error.message : String(error)
|
|
231
|
+
});
|
|
232
|
+
next(error);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// List conversations
|
|
236
|
+
router.get('/', async (req, res, next) => {
|
|
237
|
+
const requestId = req.requestId;
|
|
238
|
+
logger.debug('List conversations request', {
|
|
239
|
+
requestId,
|
|
240
|
+
query: req.query
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
const result = await historyReader.listConversations(req.query);
|
|
244
|
+
// Update status for each conversation based on active streams
|
|
245
|
+
const conversationsWithStatus = result.conversations.map(conversation => {
|
|
246
|
+
const status = statusTracker.getConversationStatus(conversation.sessionId);
|
|
247
|
+
const baseConversation = {
|
|
248
|
+
...conversation,
|
|
249
|
+
status
|
|
250
|
+
};
|
|
251
|
+
// Add toolMetrics if available
|
|
252
|
+
const metrics = toolMetricsService.getMetrics(conversation.sessionId);
|
|
253
|
+
if (metrics) {
|
|
254
|
+
baseConversation.toolMetrics = metrics;
|
|
255
|
+
}
|
|
256
|
+
// Add streamingId if conversation is ongoing
|
|
257
|
+
if (status === 'ongoing') {
|
|
258
|
+
const streamingId = statusTracker.getStreamingId(conversation.sessionId);
|
|
259
|
+
if (streamingId) {
|
|
260
|
+
return { ...baseConversation, streamingId };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return baseConversation;
|
|
264
|
+
});
|
|
265
|
+
// Get all active sessions and add optimistic conversations for those not in history
|
|
266
|
+
// But only when NOT viewing archive (active sessions shouldn't appear in archive view)
|
|
267
|
+
const existingSessionIds = new Set(conversationsWithStatus.map(c => c.sessionId));
|
|
268
|
+
const isArchiveView = req.query.archived === true;
|
|
269
|
+
const conversationsNotInHistory = isArchiveView
|
|
270
|
+
? []
|
|
271
|
+
: conversationStatusManager.getConversationsNotInHistory(existingSessionIds);
|
|
272
|
+
// Combine history conversations with active ones not in history
|
|
273
|
+
const allConversations = [...conversationsWithStatus, ...conversationsNotInHistory];
|
|
274
|
+
// Ensure session info entries exist for all conversations
|
|
275
|
+
try {
|
|
276
|
+
await sessionInfoService.syncMissingSessions(allConversations.map(c => c.sessionId));
|
|
277
|
+
}
|
|
278
|
+
catch (syncError) {
|
|
279
|
+
logger.debug('Failed to sync session info', {
|
|
280
|
+
requestId,
|
|
281
|
+
error: syncError instanceof Error ? syncError.message : String(syncError)
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Fetch cached insights for all sessions in one DB call
|
|
285
|
+
const sessionIds = allConversations.map(c => c.sessionId);
|
|
286
|
+
let cachedInsights = new Map();
|
|
287
|
+
try {
|
|
288
|
+
cachedInsights = await insightsService.getCachedInsightsForSessions(sessionIds);
|
|
289
|
+
// If we have sessions without cached insights, compute them in background
|
|
290
|
+
const missingSessions = sessionIds.filter(id => !cachedInsights.has(id));
|
|
291
|
+
if (missingSessions.length > 0) {
|
|
292
|
+
logger.debug('Computing missing insights in background', { count: missingSessions.length });
|
|
293
|
+
// Fire and forget - don't block the response
|
|
294
|
+
insightsService.computeMissingInsights(missingSessions, 3).catch(err => logger.debug('Background insights computation failed', { error: err }));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (insightsError) {
|
|
298
|
+
logger.debug('Failed to fetch cached insights', {
|
|
299
|
+
requestId,
|
|
300
|
+
error: insightsError instanceof Error ? insightsError.message : String(insightsError)
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Attach insights to each conversation
|
|
304
|
+
const conversationsWithInsights = allConversations.map(conv => ({
|
|
305
|
+
...conv,
|
|
306
|
+
insights: cachedInsights.get(conv.sessionId) || undefined
|
|
307
|
+
}));
|
|
308
|
+
logger.debug('Conversations listed successfully', {
|
|
309
|
+
requestId,
|
|
310
|
+
conversationCount: conversationsWithInsights.length,
|
|
311
|
+
historyConversations: conversationsWithStatus.length,
|
|
312
|
+
conversationsNotInHistory: conversationsNotInHistory.length,
|
|
313
|
+
totalFound: result.total,
|
|
314
|
+
activeConversations: conversationsWithInsights.filter(c => c.status === 'ongoing').length,
|
|
315
|
+
withInsights: cachedInsights.size
|
|
316
|
+
});
|
|
317
|
+
res.json({
|
|
318
|
+
conversations: conversationsWithInsights,
|
|
319
|
+
total: result.total + conversationsNotInHistory.length // Update total to include conversations not in history
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
logger.debug('List conversations failed', {
|
|
324
|
+
requestId,
|
|
325
|
+
error: error instanceof Error ? error.message : String(error)
|
|
326
|
+
});
|
|
327
|
+
next(error);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
// SSE endpoint for real-time activity updates
|
|
331
|
+
// Clients subscribe to receive instant updates when files change
|
|
332
|
+
// IMPORTANT: This must be defined BEFORE /:sessionId or Express will match "activity-stream" as a sessionId
|
|
333
|
+
router.get('/activity-stream', (req, res) => {
|
|
334
|
+
const requestId = req.requestId;
|
|
335
|
+
logger.debug('Activity stream client connected', { requestId });
|
|
336
|
+
// Configure SSE headers
|
|
337
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
338
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
339
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
340
|
+
res.setHeader('Connection', 'keep-alive');
|
|
341
|
+
// Flush headers immediately so client knows connection is established
|
|
342
|
+
res.flushHeaders();
|
|
343
|
+
// Import and get the watcher
|
|
344
|
+
import('../services/session-activity-watcher.js').then(async ({ getSessionActivityWatcher }) => {
|
|
345
|
+
const watcher = getSessionActivityWatcher();
|
|
346
|
+
// Start the watcher if not already running
|
|
347
|
+
watcher.start();
|
|
348
|
+
// Send initial connection confirmation
|
|
349
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: Date.now() })}\n\n`);
|
|
350
|
+
// Seed initial state: send action history for all recent sessions
|
|
351
|
+
try {
|
|
352
|
+
const { conversations } = await historyReader.listConversations({ limit: 50, sortBy: 'updated', order: 'desc', archived: false });
|
|
353
|
+
const recentSessions = conversations.map(c => c.sessionId);
|
|
354
|
+
for (const sessionId of recentSessions) {
|
|
355
|
+
try {
|
|
356
|
+
const { messages } = await historyReader.fetchConversationDirect(sessionId);
|
|
357
|
+
if (messages.length === 0)
|
|
358
|
+
continue;
|
|
359
|
+
const recentActions = insightsService.extractRecentActions(messages, 14);
|
|
360
|
+
if (recentActions.length > 0) {
|
|
361
|
+
const initialUpdate = {
|
|
362
|
+
type: 'activity',
|
|
363
|
+
sessionId,
|
|
364
|
+
recentActions,
|
|
365
|
+
timestamp: Date.now()
|
|
366
|
+
};
|
|
367
|
+
res.write(`data: ${JSON.stringify(initialUpdate)}\n\n`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
// Skip sessions with errors
|
|
372
|
+
logger.debug('Failed to seed initial state for session', { sessionId: sessionId.slice(0, 8), err });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
logger.warn('Failed to seed initial activity state', { err });
|
|
378
|
+
}
|
|
379
|
+
// Seed insights timestamps so reconnecting clients can detect missed updates
|
|
380
|
+
try {
|
|
381
|
+
const sessionIds = (await historyReader.listConversations({ limit: 50, sortBy: 'updated', order: 'desc', archived: false })).conversations.map(c => c.sessionId);
|
|
382
|
+
const cachedInsights = await insightsService.getCachedInsightsForSessions(sessionIds);
|
|
383
|
+
const insightsTimestamps = {};
|
|
384
|
+
for (const [sessionId, insights] of cachedInsights.entries()) {
|
|
385
|
+
if (insights.computedAt) {
|
|
386
|
+
insightsTimestamps[sessionId] = insights.computedAt;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (Object.keys(insightsTimestamps).length > 0) {
|
|
390
|
+
res.write(`data: ${JSON.stringify({ type: 'insights-status', timestamps: insightsTimestamps })}\n\n`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
logger.warn('Failed to seed insights timestamps', { err });
|
|
395
|
+
}
|
|
396
|
+
// Forward activity updates to this client
|
|
397
|
+
const onActivity = (update) => {
|
|
398
|
+
res.write(`data: ${JSON.stringify({ type: 'activity', ...update })}\n\n`);
|
|
399
|
+
};
|
|
400
|
+
// Forward insights updates to this client
|
|
401
|
+
const onInsights = (update) => {
|
|
402
|
+
// Rename 'type' to 'insightType' to avoid collision with SSE event type
|
|
403
|
+
const { type: insightType, ...rest } = update;
|
|
404
|
+
res.write(`data: ${JSON.stringify({ type: 'insights', insightType, ...rest })}\n\n`);
|
|
405
|
+
};
|
|
406
|
+
watcher.on('activity', onActivity);
|
|
407
|
+
watcher.on('insights', onInsights);
|
|
408
|
+
// Clean up on disconnect
|
|
409
|
+
req.on('close', () => {
|
|
410
|
+
logger.debug('Activity stream client disconnected', { requestId });
|
|
411
|
+
watcher.off('activity', onActivity);
|
|
412
|
+
watcher.off('insights', onInsights);
|
|
413
|
+
});
|
|
414
|
+
// Keep-alive ping every 30 seconds
|
|
415
|
+
const keepAlive = setInterval(() => {
|
|
416
|
+
res.write(`data: ${JSON.stringify({ type: 'ping', timestamp: Date.now() })}\n\n`);
|
|
417
|
+
}, 30000);
|
|
418
|
+
req.on('close', () => {
|
|
419
|
+
clearInterval(keepAlive);
|
|
420
|
+
});
|
|
421
|
+
}).catch((error) => {
|
|
422
|
+
logger.error('Activity stream failed to initialize watcher', { requestId, error });
|
|
423
|
+
res.write(`data: ${JSON.stringify({ type: 'error', error: 'Failed to initialize activity watcher' })}\n\n`);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
// Get conversation details
|
|
427
|
+
// Supports pagination via query params:
|
|
428
|
+
// ?limit=N - return only the last N messages (default: all)
|
|
429
|
+
// ?before=messageId - return messages before this message ID (for loading older messages)
|
|
430
|
+
router.get('/:sessionId', async (req, res, next) => {
|
|
431
|
+
const requestId = req.requestId;
|
|
432
|
+
const { sessionId } = req.params;
|
|
433
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : undefined;
|
|
434
|
+
const before = req.query.before;
|
|
435
|
+
const startTime = Date.now();
|
|
436
|
+
logger.debug('Get conversation details request', {
|
|
437
|
+
requestId,
|
|
438
|
+
sessionId,
|
|
439
|
+
limit,
|
|
440
|
+
before
|
|
441
|
+
});
|
|
442
|
+
try {
|
|
443
|
+
// First try to fetch from history using fast direct lookup
|
|
444
|
+
try {
|
|
445
|
+
const { messages: allMessages, metadata } = await historyReader.fetchConversationDirect(req.params.sessionId);
|
|
446
|
+
const fetchTime = Date.now() - startTime;
|
|
447
|
+
logger.debug('Conversation fetch timing (direct)', {
|
|
448
|
+
requestId,
|
|
449
|
+
sessionId,
|
|
450
|
+
fetchMs: fetchTime,
|
|
451
|
+
messageCount: allMessages.length
|
|
452
|
+
});
|
|
453
|
+
// Apply pagination if requested
|
|
454
|
+
let messages = allMessages;
|
|
455
|
+
let hasMore = false;
|
|
456
|
+
let oldestMessageId;
|
|
457
|
+
const totalMessages = allMessages.length;
|
|
458
|
+
if (limit || before) {
|
|
459
|
+
// If 'before' is specified, find that message and take messages before it
|
|
460
|
+
let endIndex = allMessages.length;
|
|
461
|
+
if (before) {
|
|
462
|
+
const beforeIndex = allMessages.findIndex(m => m.uuid === before);
|
|
463
|
+
if (beforeIndex > 0) {
|
|
464
|
+
endIndex = beforeIndex;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Apply limit - take the last N messages up to endIndex
|
|
468
|
+
const effectiveLimit = limit || 50;
|
|
469
|
+
const startIndex = Math.max(0, endIndex - effectiveLimit);
|
|
470
|
+
messages = allMessages.slice(startIndex, endIndex);
|
|
471
|
+
hasMore = startIndex > 0;
|
|
472
|
+
oldestMessageId = messages.length > 0 ? messages[0].uuid : undefined;
|
|
473
|
+
}
|
|
474
|
+
const response = {
|
|
475
|
+
messages,
|
|
476
|
+
summary: metadata.summary,
|
|
477
|
+
projectPath: metadata.projectPath,
|
|
478
|
+
metadata: {
|
|
479
|
+
totalDuration: metadata.totalDuration,
|
|
480
|
+
model: metadata.model
|
|
481
|
+
},
|
|
482
|
+
totalMessages,
|
|
483
|
+
hasMore,
|
|
484
|
+
oldestMessageId
|
|
485
|
+
};
|
|
486
|
+
// Add toolMetrics if available
|
|
487
|
+
const metrics = toolMetricsService.getMetrics(req.params.sessionId);
|
|
488
|
+
if (metrics) {
|
|
489
|
+
response.toolMetrics = metrics;
|
|
490
|
+
}
|
|
491
|
+
// Check if this historical session is also currently active
|
|
492
|
+
// First check CUI-spawned sessions (have streamingId)
|
|
493
|
+
const activeStreamingId = conversationStatusManager.getStreamingId(sessionId);
|
|
494
|
+
if (activeStreamingId) {
|
|
495
|
+
response.status = 'ongoing';
|
|
496
|
+
response.streamingId = activeStreamingId;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
// Also check for CLI sessions via process detection
|
|
500
|
+
const hasActiveProcess = await historyReader.isSessionFileInUse(sessionId);
|
|
501
|
+
response.status = hasActiveProcess ? 'ongoing' : 'completed';
|
|
502
|
+
}
|
|
503
|
+
logger.debug('Conversation details retrieved from history', {
|
|
504
|
+
requestId,
|
|
505
|
+
sessionId,
|
|
506
|
+
messageCount: response.messages.length,
|
|
507
|
+
totalMessages,
|
|
508
|
+
hasMore,
|
|
509
|
+
hasSummary: !!response.summary,
|
|
510
|
+
projectPath: response.projectPath
|
|
511
|
+
});
|
|
512
|
+
res.json(response);
|
|
513
|
+
}
|
|
514
|
+
catch (historyError) {
|
|
515
|
+
// If not found in history, check if it's an active session
|
|
516
|
+
if (historyError instanceof CUIError && historyError.code === 'CONVERSATION_NOT_FOUND') {
|
|
517
|
+
const activeDetails = conversationStatusManager.getActiveConversationDetails(sessionId);
|
|
518
|
+
if (activeDetails) {
|
|
519
|
+
logger.debug('Conversation details created for active session', {
|
|
520
|
+
requestId,
|
|
521
|
+
sessionId,
|
|
522
|
+
projectPath: activeDetails.projectPath
|
|
523
|
+
});
|
|
524
|
+
res.json(activeDetails);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Not found in history and not active
|
|
528
|
+
throw historyError;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
// Other errors, re-throw
|
|
533
|
+
throw historyError;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
logger.debug('Get conversation details failed', {
|
|
539
|
+
requestId,
|
|
540
|
+
sessionId,
|
|
541
|
+
error: error instanceof Error ? error.message : String(error)
|
|
542
|
+
});
|
|
543
|
+
next(error);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
// Stop conversation
|
|
547
|
+
router.post('/:streamingId/stop', async (req, res, next) => {
|
|
548
|
+
const requestId = req.requestId;
|
|
549
|
+
const { streamingId } = req.params;
|
|
550
|
+
logger.debug('Stop conversation request', {
|
|
551
|
+
requestId,
|
|
552
|
+
streamingId
|
|
553
|
+
});
|
|
554
|
+
try {
|
|
555
|
+
const success = await processManager.stopConversation(streamingId);
|
|
556
|
+
logger.debug('Stop conversation result', {
|
|
557
|
+
requestId,
|
|
558
|
+
streamingId,
|
|
559
|
+
success
|
|
560
|
+
});
|
|
561
|
+
res.json({ success });
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
logger.debug('Stop conversation failed', {
|
|
565
|
+
requestId,
|
|
566
|
+
streamingId,
|
|
567
|
+
error: error instanceof Error ? error.message : String(error)
|
|
568
|
+
});
|
|
569
|
+
next(error);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
// Stop session by sessionId (looks up streamingId internally)
|
|
573
|
+
router.post('/session/:sessionId/stop', async (req, res, next) => {
|
|
574
|
+
const requestId = req.requestId;
|
|
575
|
+
const { sessionId } = req.params;
|
|
576
|
+
logger.debug('Stop session by sessionId request', {
|
|
577
|
+
requestId,
|
|
578
|
+
sessionId
|
|
579
|
+
});
|
|
580
|
+
try {
|
|
581
|
+
// Look up the streamingId for this session
|
|
582
|
+
const streamingId = statusTracker.getStreamingId(sessionId);
|
|
583
|
+
if (!streamingId) {
|
|
584
|
+
logger.debug('No active streamingId found for session', {
|
|
585
|
+
requestId,
|
|
586
|
+
sessionId
|
|
587
|
+
});
|
|
588
|
+
res.json({ success: false, error: 'Session not active or streamingId not found' });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const success = await processManager.stopConversation(streamingId);
|
|
592
|
+
logger.debug('Stop session result', {
|
|
593
|
+
requestId,
|
|
594
|
+
sessionId,
|
|
595
|
+
streamingId,
|
|
596
|
+
success
|
|
597
|
+
});
|
|
598
|
+
res.json({ success });
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
logger.debug('Stop session failed', {
|
|
602
|
+
requestId,
|
|
603
|
+
sessionId,
|
|
604
|
+
error: error instanceof Error ? error.message : String(error)
|
|
605
|
+
});
|
|
606
|
+
next(error);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
// Force kill session by sessionId (immediate SIGKILL)
|
|
610
|
+
router.post('/session/:sessionId/force-kill', async (req, res, next) => {
|
|
611
|
+
const requestId = req.requestId;
|
|
612
|
+
const { sessionId } = req.params;
|
|
613
|
+
logger.info('Force kill session request', {
|
|
614
|
+
requestId,
|
|
615
|
+
sessionId
|
|
616
|
+
});
|
|
617
|
+
try {
|
|
618
|
+
const streamingId = statusTracker.getStreamingId(sessionId);
|
|
619
|
+
if (!streamingId) {
|
|
620
|
+
logger.debug('No active streamingId found for session', {
|
|
621
|
+
requestId,
|
|
622
|
+
sessionId
|
|
623
|
+
});
|
|
624
|
+
res.json({ success: false, error: 'Session not active or streamingId not found' });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const success = processManager.forceKillConversation(streamingId);
|
|
628
|
+
logger.info('Force kill session result', {
|
|
629
|
+
requestId,
|
|
630
|
+
sessionId,
|
|
631
|
+
streamingId,
|
|
632
|
+
success
|
|
633
|
+
});
|
|
634
|
+
res.json({ success });
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
logger.error('Force kill session failed', {
|
|
638
|
+
requestId,
|
|
639
|
+
sessionId,
|
|
640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
641
|
+
});
|
|
642
|
+
next(error);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
// Inject message into running session (send to stdin)
|
|
646
|
+
router.post('/session/:sessionId/inject', async (req, res, next) => {
|
|
647
|
+
const requestId = req.requestId;
|
|
648
|
+
const { sessionId } = req.params;
|
|
649
|
+
const { message } = req.body;
|
|
650
|
+
logger.info('Inject message request', {
|
|
651
|
+
requestId,
|
|
652
|
+
sessionId,
|
|
653
|
+
messageLength: message?.length
|
|
654
|
+
});
|
|
655
|
+
try {
|
|
656
|
+
if (!message) {
|
|
657
|
+
res.status(400).json({ success: false, error: 'message is required' });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const streamingId = statusTracker.getStreamingId(sessionId);
|
|
661
|
+
if (!streamingId) {
|
|
662
|
+
logger.debug('No active streamingId found for session', {
|
|
663
|
+
requestId,
|
|
664
|
+
sessionId
|
|
665
|
+
});
|
|
666
|
+
res.json({ success: false, error: 'Session not active or streamingId not found' });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// Format as a user message for Claude's stdin protocol
|
|
670
|
+
const stdinMessage = JSON.stringify({
|
|
671
|
+
type: 'user',
|
|
672
|
+
message: {
|
|
673
|
+
role: 'user',
|
|
674
|
+
content: message
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
const success = processManager.sendStdinMessage(streamingId, stdinMessage);
|
|
678
|
+
logger.info('Inject message result', {
|
|
679
|
+
requestId,
|
|
680
|
+
sessionId,
|
|
681
|
+
streamingId,
|
|
682
|
+
success
|
|
683
|
+
});
|
|
684
|
+
res.json({ success });
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
logger.error('Inject message failed', {
|
|
688
|
+
requestId,
|
|
689
|
+
sessionId,
|
|
690
|
+
error: error instanceof Error ? error.message : String(error)
|
|
691
|
+
});
|
|
692
|
+
next(error);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
// Rename session (update custom name)
|
|
696
|
+
router.put('/:sessionId/rename', async (req, res, next) => {
|
|
697
|
+
const requestId = req.requestId;
|
|
698
|
+
const { sessionId } = req.params;
|
|
699
|
+
const { customName } = req.body;
|
|
700
|
+
logger.debug('Rename session request', {
|
|
701
|
+
requestId,
|
|
702
|
+
sessionId,
|
|
703
|
+
customName
|
|
704
|
+
});
|
|
705
|
+
try {
|
|
706
|
+
// Validate required fields
|
|
707
|
+
if (!sessionId || !sessionId.trim()) {
|
|
708
|
+
throw new CUIError('MISSING_SESSION_ID', 'sessionId is required', 400);
|
|
709
|
+
}
|
|
710
|
+
if (customName === undefined || customName === null) {
|
|
711
|
+
throw new CUIError('MISSING_CUSTOM_NAME', 'customName is required', 400);
|
|
712
|
+
}
|
|
713
|
+
// Validate custom name length (reasonable limit)
|
|
714
|
+
if (customName.length > 200) {
|
|
715
|
+
throw new CUIError('CUSTOM_NAME_TOO_LONG', 'customName must be 200 characters or less', 400);
|
|
716
|
+
}
|
|
717
|
+
// Check if session exists by trying to fetch it
|
|
718
|
+
try {
|
|
719
|
+
await historyReader.fetchConversationDirect(sessionId);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
throw new CUIError('CONVERSATION_NOT_FOUND', 'Conversation not found', 404);
|
|
723
|
+
}
|
|
724
|
+
// Update custom name
|
|
725
|
+
await sessionInfoService.updateCustomName(sessionId, customName.trim());
|
|
726
|
+
logger.info('Session renamed successfully', {
|
|
727
|
+
requestId,
|
|
728
|
+
sessionId,
|
|
729
|
+
customName: customName.trim()
|
|
730
|
+
});
|
|
731
|
+
res.json({
|
|
732
|
+
success: true,
|
|
733
|
+
sessionId,
|
|
734
|
+
customName: customName.trim()
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
logger.debug('Rename session failed', {
|
|
739
|
+
requestId,
|
|
740
|
+
sessionId,
|
|
741
|
+
customName,
|
|
742
|
+
error: error instanceof Error ? error.message : String(error)
|
|
743
|
+
});
|
|
744
|
+
next(error);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
// Update session info (replaces rename endpoint)
|
|
748
|
+
router.put('/:sessionId/update', async (req, res, next) => {
|
|
749
|
+
const requestId = req.requestId;
|
|
750
|
+
const { sessionId } = req.params;
|
|
751
|
+
const updates = req.body;
|
|
752
|
+
logger.debug('Update session request', {
|
|
753
|
+
requestId,
|
|
754
|
+
sessionId,
|
|
755
|
+
updates
|
|
756
|
+
});
|
|
757
|
+
try {
|
|
758
|
+
// Validate sessionId
|
|
759
|
+
if (!sessionId || sessionId.trim() === '') {
|
|
760
|
+
logger.debug('Invalid session ID', { requestId, sessionId });
|
|
761
|
+
return res.status(400).json({
|
|
762
|
+
success: false,
|
|
763
|
+
sessionId: '',
|
|
764
|
+
updatedFields: {},
|
|
765
|
+
error: 'Session ID is required'
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
// Validate fields if provided
|
|
769
|
+
if (updates.customName !== undefined && updates.customName.length > 200) {
|
|
770
|
+
logger.debug('Custom name too long', { requestId, length: updates.customName.length });
|
|
771
|
+
return res.status(400).json({
|
|
772
|
+
success: false,
|
|
773
|
+
sessionId,
|
|
774
|
+
updatedFields: {},
|
|
775
|
+
error: 'Custom name must be 200 characters or less'
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
// Prepare updates object - map camelCase to snake_case
|
|
779
|
+
const sessionUpdates = {};
|
|
780
|
+
if (updates.customName !== undefined)
|
|
781
|
+
sessionUpdates.custom_name = updates.customName.trim();
|
|
782
|
+
if (updates.pinned !== undefined)
|
|
783
|
+
sessionUpdates.pinned = updates.pinned;
|
|
784
|
+
if (updates.archived !== undefined)
|
|
785
|
+
sessionUpdates.archived = updates.archived;
|
|
786
|
+
if (updates.continuationSessionId !== undefined)
|
|
787
|
+
sessionUpdates.continuation_session_id = updates.continuationSessionId;
|
|
788
|
+
if (updates.initialCommitHead !== undefined)
|
|
789
|
+
sessionUpdates.initial_commit_head = updates.initialCommitHead;
|
|
790
|
+
if (updates.permissionMode !== undefined) {
|
|
791
|
+
// Validate permission mode
|
|
792
|
+
const validModes = ['acceptEdits', 'bypassPermissions', 'default', 'plan'];
|
|
793
|
+
if (!validModes.includes(updates.permissionMode)) {
|
|
794
|
+
logger.debug('Invalid permission mode', { requestId, permissionMode: updates.permissionMode });
|
|
795
|
+
return res.status(400).json({
|
|
796
|
+
success: false,
|
|
797
|
+
sessionId,
|
|
798
|
+
updatedFields: {},
|
|
799
|
+
error: `Permission mode must be one of: ${validModes.join(', ')}`
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
sessionUpdates.permission_mode = updates.permissionMode;
|
|
803
|
+
}
|
|
804
|
+
// Update session info
|
|
805
|
+
const updatedFields = await sessionInfoService.updateSessionInfo(sessionId, sessionUpdates);
|
|
806
|
+
// If session was archived, clean up insights service tracking
|
|
807
|
+
if (updates.archived === true) {
|
|
808
|
+
const insightsService = getInsightsService();
|
|
809
|
+
insightsService.cleanupSession(sessionId);
|
|
810
|
+
logger.debug('Cleaned up insights tracking for archived session', {
|
|
811
|
+
requestId,
|
|
812
|
+
sessionId
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// Log without huge base64 image data
|
|
816
|
+
const logFields = { ...updatedFields };
|
|
817
|
+
if (logFields.identity_image && logFields.identity_image.length > 100) {
|
|
818
|
+
logFields.identity_image = `[base64 ${logFields.identity_image.length} chars]`;
|
|
819
|
+
}
|
|
820
|
+
logger.info('Session updated successfully', {
|
|
821
|
+
requestId,
|
|
822
|
+
sessionId,
|
|
823
|
+
updatedFields: logFields
|
|
824
|
+
});
|
|
825
|
+
res.json({
|
|
826
|
+
success: true,
|
|
827
|
+
sessionId,
|
|
828
|
+
updatedFields
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
logger.debug('Update session failed', {
|
|
833
|
+
requestId,
|
|
834
|
+
sessionId,
|
|
835
|
+
updates,
|
|
836
|
+
error: error instanceof Error ? error.message : String(error)
|
|
837
|
+
});
|
|
838
|
+
next(error);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
// Archive all sessions
|
|
842
|
+
router.post('/archive-all', async (req, res, next) => {
|
|
843
|
+
const requestId = req.requestId;
|
|
844
|
+
logger.debug('Archive all sessions request', {
|
|
845
|
+
requestId
|
|
846
|
+
});
|
|
847
|
+
try {
|
|
848
|
+
// Archive all sessions
|
|
849
|
+
const archivedCount = await sessionInfoService.archiveAllSessions();
|
|
850
|
+
logger.info('All sessions archived successfully', {
|
|
851
|
+
requestId,
|
|
852
|
+
archivedCount
|
|
853
|
+
});
|
|
854
|
+
res.json({
|
|
855
|
+
success: true,
|
|
856
|
+
archivedCount,
|
|
857
|
+
message: `Successfully archived ${archivedCount} session${archivedCount !== 1 ? 's' : ''}`
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
logger.debug('Archive all sessions failed', {
|
|
862
|
+
requestId,
|
|
863
|
+
error: error instanceof Error ? error.message : String(error)
|
|
864
|
+
});
|
|
865
|
+
next(error);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
return router;
|
|
869
|
+
}
|
|
870
|
+
//# sourceMappingURL=conversation.routes.js.map
|