claudia-orchestrator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +109 -0
  3. package/dist/cli-parser.d.ts +11 -0
  4. package/dist/cli-parser.d.ts.map +1 -0
  5. package/dist/cli-parser.js +57 -0
  6. package/dist/cli-parser.js.map +1 -0
  7. package/dist/cui-server.d.ts +69 -0
  8. package/dist/cui-server.d.ts.map +1 -0
  9. package/dist/cui-server.js +705 -0
  10. package/dist/cui-server.js.map +1 -0
  11. package/dist/mcp-server/claudia-tools.d.ts +15 -0
  12. package/dist/mcp-server/claudia-tools.d.ts.map +1 -0
  13. package/dist/mcp-server/claudia-tools.js +366 -0
  14. package/dist/mcp-server/claudia-tools.js.map +1 -0
  15. package/dist/mcp-server/index.d.ts +3 -0
  16. package/dist/mcp-server/index.d.ts.map +1 -0
  17. package/dist/mcp-server/index.js +176 -0
  18. package/dist/mcp-server/index.js.map +1 -0
  19. package/dist/middleware/auth.d.ts +18 -0
  20. package/dist/middleware/auth.d.ts.map +1 -0
  21. package/dist/middleware/auth.js +136 -0
  22. package/dist/middleware/auth.js.map +1 -0
  23. package/dist/middleware/cors-setup.d.ts +7 -0
  24. package/dist/middleware/cors-setup.d.ts.map +1 -0
  25. package/dist/middleware/cors-setup.js +8 -0
  26. package/dist/middleware/cors-setup.js.map +1 -0
  27. package/dist/middleware/error-handler.d.ts +4 -0
  28. package/dist/middleware/error-handler.d.ts.map +1 -0
  29. package/dist/middleware/error-handler.js +27 -0
  30. package/dist/middleware/error-handler.js.map +1 -0
  31. package/dist/middleware/query-parser.d.ts +11 -0
  32. package/dist/middleware/query-parser.d.ts.map +1 -0
  33. package/dist/middleware/query-parser.js +68 -0
  34. package/dist/middleware/query-parser.js.map +1 -0
  35. package/dist/middleware/request-logger.d.ts +4 -0
  36. package/dist/middleware/request-logger.d.ts.map +1 -0
  37. package/dist/middleware/request-logger.js +29 -0
  38. package/dist/middleware/request-logger.js.map +1 -0
  39. package/dist/process-daemon/index.d.ts +14 -0
  40. package/dist/process-daemon/index.d.ts.map +1 -0
  41. package/dist/process-daemon/index.js +51 -0
  42. package/dist/process-daemon/index.js.map +1 -0
  43. package/dist/process-daemon/process-daemon.d.ts +78 -0
  44. package/dist/process-daemon/process-daemon.d.ts.map +1 -0
  45. package/dist/process-daemon/process-daemon.js +568 -0
  46. package/dist/process-daemon/process-daemon.js.map +1 -0
  47. package/dist/process-daemon/process-manager-client.d.ts +108 -0
  48. package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
  49. package/dist/process-daemon/process-manager-client.js +314 -0
  50. package/dist/process-daemon/process-manager-client.js.map +1 -0
  51. package/dist/process-daemon/process-manager-interface.d.ts +47 -0
  52. package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
  53. package/dist/process-daemon/process-manager-interface.js +8 -0
  54. package/dist/process-daemon/process-manager-interface.js.map +1 -0
  55. package/dist/process-daemon/test-daemon.d.ts +12 -0
  56. package/dist/process-daemon/test-daemon.d.ts.map +1 -0
  57. package/dist/process-daemon/test-daemon.js +81 -0
  58. package/dist/process-daemon/test-daemon.js.map +1 -0
  59. package/dist/process-daemon/types.d.ts +85 -0
  60. package/dist/process-daemon/types.d.ts.map +1 -0
  61. package/dist/process-daemon/types.js +8 -0
  62. package/dist/process-daemon/types.js.map +1 -0
  63. package/dist/routes/claudia.routes.d.ts +10 -0
  64. package/dist/routes/claudia.routes.d.ts.map +1 -0
  65. package/dist/routes/claudia.routes.js +123 -0
  66. package/dist/routes/claudia.routes.js.map +1 -0
  67. package/dist/routes/config.routes.d.ts +4 -0
  68. package/dist/routes/config.routes.d.ts.map +1 -0
  69. package/dist/routes/config.routes.js +27 -0
  70. package/dist/routes/config.routes.js.map +1 -0
  71. package/dist/routes/conversation.routes.d.ts +8 -0
  72. package/dist/routes/conversation.routes.d.ts.map +1 -0
  73. package/dist/routes/conversation.routes.js +870 -0
  74. package/dist/routes/conversation.routes.js.map +1 -0
  75. package/dist/routes/filesystem.routes.d.ts +4 -0
  76. package/dist/routes/filesystem.routes.d.ts.map +1 -0
  77. package/dist/routes/filesystem.routes.js +86 -0
  78. package/dist/routes/filesystem.routes.js.map +1 -0
  79. package/dist/routes/gemini.routes.d.ts +4 -0
  80. package/dist/routes/gemini.routes.d.ts.map +1 -0
  81. package/dist/routes/gemini.routes.js +93 -0
  82. package/dist/routes/gemini.routes.js.map +1 -0
  83. package/dist/routes/insights.routes.d.ts +17 -0
  84. package/dist/routes/insights.routes.d.ts.map +1 -0
  85. package/dist/routes/insights.routes.js +417 -0
  86. package/dist/routes/insights.routes.js.map +1 -0
  87. package/dist/routes/license.routes.d.ts +3 -0
  88. package/dist/routes/license.routes.d.ts.map +1 -0
  89. package/dist/routes/license.routes.js +111 -0
  90. package/dist/routes/license.routes.js.map +1 -0
  91. package/dist/routes/log.routes.d.ts +3 -0
  92. package/dist/routes/log.routes.d.ts.map +1 -0
  93. package/dist/routes/log.routes.js +65 -0
  94. package/dist/routes/log.routes.js.map +1 -0
  95. package/dist/routes/notifications.routes.d.ts +4 -0
  96. package/dist/routes/notifications.routes.d.ts.map +1 -0
  97. package/dist/routes/notifications.routes.js +71 -0
  98. package/dist/routes/notifications.routes.js.map +1 -0
  99. package/dist/routes/permission.routes.d.ts +4 -0
  100. package/dist/routes/permission.routes.d.ts.map +1 -0
  101. package/dist/routes/permission.routes.js +116 -0
  102. package/dist/routes/permission.routes.js.map +1 -0
  103. package/dist/routes/question.routes.d.ts +8 -0
  104. package/dist/routes/question.routes.d.ts.map +1 -0
  105. package/dist/routes/question.routes.js +82 -0
  106. package/dist/routes/question.routes.js.map +1 -0
  107. package/dist/routes/streaming.routes.d.ts +4 -0
  108. package/dist/routes/streaming.routes.d.ts.map +1 -0
  109. package/dist/routes/streaming.routes.js +28 -0
  110. package/dist/routes/streaming.routes.js.map +1 -0
  111. package/dist/routes/system.routes.d.ts +5 -0
  112. package/dist/routes/system.routes.d.ts.map +1 -0
  113. package/dist/routes/system.routes.js +103 -0
  114. package/dist/routes/system.routes.js.map +1 -0
  115. package/dist/routes/working-directories.routes.d.ts +4 -0
  116. package/dist/routes/working-directories.routes.d.ts.map +1 -0
  117. package/dist/routes/working-directories.routes.js +25 -0
  118. package/dist/routes/working-directories.routes.js.map +1 -0
  119. package/dist/server.d.ts +3 -0
  120. package/dist/server.d.ts.map +1 -0
  121. package/dist/server.js +34 -0
  122. package/dist/server.js.map +1 -0
  123. package/dist/services/ToolMetricsService.d.ts +53 -0
  124. package/dist/services/ToolMetricsService.d.ts.map +1 -0
  125. package/dist/services/ToolMetricsService.js +230 -0
  126. package/dist/services/ToolMetricsService.js.map +1 -0
  127. package/dist/services/anthropic-service.d.ts +186 -0
  128. package/dist/services/anthropic-service.d.ts.map +1 -0
  129. package/dist/services/anthropic-service.js +1132 -0
  130. package/dist/services/anthropic-service.js.map +1 -0
  131. package/dist/services/claude-history-reader.d.ts +126 -0
  132. package/dist/services/claude-history-reader.d.ts.map +1 -0
  133. package/dist/services/claude-history-reader.js +717 -0
  134. package/dist/services/claude-history-reader.js.map +1 -0
  135. package/dist/services/claude-process-manager.d.ts +108 -0
  136. package/dist/services/claude-process-manager.d.ts.map +1 -0
  137. package/dist/services/claude-process-manager.js +909 -0
  138. package/dist/services/claude-process-manager.js.map +1 -0
  139. package/dist/services/claude-router-service.d.ts +19 -0
  140. package/dist/services/claude-router-service.d.ts.map +1 -0
  141. package/dist/services/claude-router-service.js +160 -0
  142. package/dist/services/claude-router-service.js.map +1 -0
  143. package/dist/services/claudia-service.d.ts +77 -0
  144. package/dist/services/claudia-service.d.ts.map +1 -0
  145. package/dist/services/claudia-service.js +194 -0
  146. package/dist/services/claudia-service.js.map +1 -0
  147. package/dist/services/commands-service.d.ts +18 -0
  148. package/dist/services/commands-service.d.ts.map +1 -0
  149. package/dist/services/commands-service.js +76 -0
  150. package/dist/services/commands-service.js.map +1 -0
  151. package/dist/services/config-service.d.ts +68 -0
  152. package/dist/services/config-service.d.ts.map +1 -0
  153. package/dist/services/config-service.js +429 -0
  154. package/dist/services/config-service.js.map +1 -0
  155. package/dist/services/conversation-cache.d.ts +86 -0
  156. package/dist/services/conversation-cache.d.ts.map +1 -0
  157. package/dist/services/conversation-cache.js +235 -0
  158. package/dist/services/conversation-cache.js.map +1 -0
  159. package/dist/services/conversation-status-manager.d.ts +98 -0
  160. package/dist/services/conversation-status-manager.d.ts.map +1 -0
  161. package/dist/services/conversation-status-manager.js +295 -0
  162. package/dist/services/conversation-status-manager.js.map +1 -0
  163. package/dist/services/cost-tracker.d.ts +87 -0
  164. package/dist/services/cost-tracker.d.ts.map +1 -0
  165. package/dist/services/cost-tracker.js +335 -0
  166. package/dist/services/cost-tracker.js.map +1 -0
  167. package/dist/services/file-system-service.d.ts +61 -0
  168. package/dist/services/file-system-service.d.ts.map +1 -0
  169. package/dist/services/file-system-service.js +348 -0
  170. package/dist/services/file-system-service.js.map +1 -0
  171. package/dist/services/gemini-service.d.ts +72 -0
  172. package/dist/services/gemini-service.d.ts.map +1 -0
  173. package/dist/services/gemini-service.js +431 -0
  174. package/dist/services/gemini-service.js.map +1 -0
  175. package/dist/services/insight-queue.d.ts +99 -0
  176. package/dist/services/insight-queue.d.ts.map +1 -0
  177. package/dist/services/insight-queue.js +277 -0
  178. package/dist/services/insight-queue.js.map +1 -0
  179. package/dist/services/insights-service.d.ts +102 -0
  180. package/dist/services/insights-service.d.ts.map +1 -0
  181. package/dist/services/insights-service.js +1152 -0
  182. package/dist/services/insights-service.js.map +1 -0
  183. package/dist/services/json-lines-parser.d.ts +19 -0
  184. package/dist/services/json-lines-parser.d.ts.map +1 -0
  185. package/dist/services/json-lines-parser.js +56 -0
  186. package/dist/services/json-lines-parser.js.map +1 -0
  187. package/dist/services/license-service.d.ts +69 -0
  188. package/dist/services/license-service.d.ts.map +1 -0
  189. package/dist/services/license-service.js +330 -0
  190. package/dist/services/license-service.js.map +1 -0
  191. package/dist/services/log-formatter.d.ts +5 -0
  192. package/dist/services/log-formatter.d.ts.map +1 -0
  193. package/dist/services/log-formatter.js +77 -0
  194. package/dist/services/log-formatter.js.map +1 -0
  195. package/dist/services/log-stream-buffer.d.ts +11 -0
  196. package/dist/services/log-stream-buffer.d.ts.map +1 -0
  197. package/dist/services/log-stream-buffer.js +36 -0
  198. package/dist/services/log-stream-buffer.js.map +1 -0
  199. package/dist/services/logger.d.ts +71 -0
  200. package/dist/services/logger.d.ts.map +1 -0
  201. package/dist/services/logger.js +215 -0
  202. package/dist/services/logger.js.map +1 -0
  203. package/dist/services/mcp-config-generator.d.ts +32 -0
  204. package/dist/services/mcp-config-generator.d.ts.map +1 -0
  205. package/dist/services/mcp-config-generator.js +126 -0
  206. package/dist/services/mcp-config-generator.js.map +1 -0
  207. package/dist/services/message-filter.d.ts +22 -0
  208. package/dist/services/message-filter.d.ts.map +1 -0
  209. package/dist/services/message-filter.js +57 -0
  210. package/dist/services/message-filter.js.map +1 -0
  211. package/dist/services/notification-service.d.ts +45 -0
  212. package/dist/services/notification-service.d.ts.map +1 -0
  213. package/dist/services/notification-service.js +184 -0
  214. package/dist/services/notification-service.js.map +1 -0
  215. package/dist/services/permission-tracker.d.ts +67 -0
  216. package/dist/services/permission-tracker.d.ts.map +1 -0
  217. package/dist/services/permission-tracker.js +161 -0
  218. package/dist/services/permission-tracker.js.map +1 -0
  219. package/dist/services/process-manager-factory.d.ts +81 -0
  220. package/dist/services/process-manager-factory.d.ts.map +1 -0
  221. package/dist/services/process-manager-factory.js +211 -0
  222. package/dist/services/process-manager-factory.js.map +1 -0
  223. package/dist/services/question-tracker.d.ts +47 -0
  224. package/dist/services/question-tracker.d.ts.map +1 -0
  225. package/dist/services/question-tracker.js +105 -0
  226. package/dist/services/question-tracker.js.map +1 -0
  227. package/dist/services/session-activity-watcher.d.ts +33 -0
  228. package/dist/services/session-activity-watcher.d.ts.map +1 -0
  229. package/dist/services/session-activity-watcher.js +194 -0
  230. package/dist/services/session-activity-watcher.js.map +1 -0
  231. package/dist/services/session-info-service.d.ts +228 -0
  232. package/dist/services/session-info-service.d.ts.map +1 -0
  233. package/dist/services/session-info-service.js +920 -0
  234. package/dist/services/session-info-service.js.map +1 -0
  235. package/dist/services/session-insights-service.d.ts +119 -0
  236. package/dist/services/session-insights-service.d.ts.map +1 -0
  237. package/dist/services/session-insights-service.js +889 -0
  238. package/dist/services/session-insights-service.js.map +1 -0
  239. package/dist/services/stream-manager.d.ts +62 -0
  240. package/dist/services/stream-manager.d.ts.map +1 -0
  241. package/dist/services/stream-manager.js +239 -0
  242. package/dist/services/stream-manager.js.map +1 -0
  243. package/dist/services/web-push-service.d.ts +48 -0
  244. package/dist/services/web-push-service.d.ts.map +1 -0
  245. package/dist/services/web-push-service.js +186 -0
  246. package/dist/services/web-push-service.js.map +1 -0
  247. package/dist/services/working-directories-service.d.ts +19 -0
  248. package/dist/services/working-directories-service.d.ts.map +1 -0
  249. package/dist/services/working-directories-service.js +103 -0
  250. package/dist/services/working-directories-service.js.map +1 -0
  251. package/dist/types/config.d.ts +111 -0
  252. package/dist/types/config.d.ts.map +1 -0
  253. package/dist/types/config.js +14 -0
  254. package/dist/types/config.js.map +1 -0
  255. package/dist/types/express.d.ts +5 -0
  256. package/dist/types/express.d.ts.map +1 -0
  257. package/dist/types/express.js +2 -0
  258. package/dist/types/express.js.map +1 -0
  259. package/dist/types/index.d.ts +325 -0
  260. package/dist/types/index.d.ts.map +1 -0
  261. package/dist/types/index.js +18 -0
  262. package/dist/types/index.js.map +1 -0
  263. package/dist/types/insights.d.ts +99 -0
  264. package/dist/types/insights.d.ts.map +1 -0
  265. package/dist/types/insights.js +7 -0
  266. package/dist/types/insights.js.map +1 -0
  267. package/dist/types/license.d.ts +70 -0
  268. package/dist/types/license.d.ts.map +1 -0
  269. package/dist/types/license.js +5 -0
  270. package/dist/types/license.js.map +1 -0
  271. package/dist/types/router-config.d.ts +13 -0
  272. package/dist/types/router-config.d.ts.map +1 -0
  273. package/dist/types/router-config.js +2 -0
  274. package/dist/types/router-config.js.map +1 -0
  275. package/dist/utils/constants.d.ts +26 -0
  276. package/dist/utils/constants.d.ts.map +1 -0
  277. package/dist/utils/constants.js +28 -0
  278. package/dist/utils/constants.js.map +1 -0
  279. package/dist/utils/machine-id.d.ts +7 -0
  280. package/dist/utils/machine-id.d.ts.map +1 -0
  281. package/dist/utils/machine-id.js +76 -0
  282. package/dist/utils/machine-id.js.map +1 -0
  283. package/dist/utils/server-startup.d.ts +13 -0
  284. package/dist/utils/server-startup.d.ts.map +1 -0
  285. package/dist/utils/server-startup.js +20 -0
  286. package/dist/utils/server-startup.js.map +1 -0
  287. package/dist/web/assets/main-DAc2rjJ2.css +1 -0
  288. package/dist/web/assets/main-DvlZ02mT.js +137 -0
  289. package/dist/web/favicon.png +0 -0
  290. package/dist/web/favicon.svg +22 -0
  291. package/dist/web/icon-192x192.png +0 -0
  292. package/dist/web/icon-512x512.png +0 -0
  293. package/dist/web/index.html +36 -0
  294. package/dist/web/manifest.json +61 -0
  295. package/package.json +174 -0
  296. package/scripts/postinstall.js +30 -0
@@ -0,0 +1,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