@xalia/agent 0.6.9 → 0.6.11

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 (199) hide show
  1. package/README.md +11 -0
  2. package/dist/agent/src/agent/agent.js +77 -18
  3. package/dist/agent/src/agent/agentUtils.js +3 -2
  4. package/dist/agent/src/agent/documentSummarizer.js +126 -0
  5. package/dist/agent/src/agent/dummyLLM.js +25 -22
  6. package/dist/agent/src/agent/imageGenLLM.js +22 -19
  7. package/dist/agent/src/agent/llm.js +1 -1
  8. package/dist/agent/src/agent/openAILLM.js +15 -12
  9. package/dist/agent/src/agent/openAILLMStreaming.js +68 -37
  10. package/dist/agent/src/agent/repeatLLM.js +16 -7
  11. package/dist/agent/src/agent/tokenCounter.js +390 -0
  12. package/dist/agent/src/agent/tokenCounter.test.js +206 -0
  13. package/dist/agent/src/agent/toolSettings.js +17 -0
  14. package/dist/agent/src/agent/tools/calculatorTool.js +45 -0
  15. package/dist/agent/src/agent/tools/contentExtractors/pdfToText.js +55 -0
  16. package/dist/agent/src/agent/tools/datetimeTool.js +38 -0
  17. package/dist/agent/src/agent/tools/fileManager/fileManagerTool.js +156 -0
  18. package/dist/agent/src/agent/tools/fileManager/index.js +31 -0
  19. package/dist/agent/src/agent/tools/fileManager/memoryFileManager.js +102 -0
  20. package/dist/agent/src/{chat/data → agent/tools/fileManager}/mimeTypes.js +3 -1
  21. package/dist/agent/src/agent/tools/fileManager/prompt.js +33 -0
  22. package/dist/agent/src/{chat/data/dbSessionFileModels.js → agent/tools/fileManager/types.js} +7 -0
  23. package/dist/agent/src/agent/tools/index.js +64 -0
  24. package/dist/agent/src/agent/tools/openUrlTool.js +57 -0
  25. package/dist/agent/src/agent/tools/renderTool.js +89 -0
  26. package/dist/agent/src/agent/tools/utils.js +61 -0
  27. package/dist/agent/src/{chat/utils/search.js → agent/tools/webSearch.js} +1 -2
  28. package/dist/agent/src/agent/tools/webSearchTool.js +40 -0
  29. package/dist/agent/src/chat/client/chatClient.js +28 -0
  30. package/dist/agent/src/chat/client/index.js +4 -1
  31. package/dist/agent/src/chat/client/sessionClient.js +28 -2
  32. package/dist/agent/src/chat/constants.js +8 -0
  33. package/dist/agent/src/chat/data/dbSessionFiles.js +11 -6
  34. package/dist/agent/src/chat/protocol/messages.js +5 -0
  35. package/dist/agent/src/chat/server/chatContextManager.js +45 -25
  36. package/dist/agent/src/chat/server/conversation.js +3 -0
  37. package/dist/agent/src/chat/server/imageGeneratorTools.js +20 -8
  38. package/dist/agent/src/chat/server/openAIRouterLLM.js +0 -3
  39. package/dist/agent/src/chat/server/openSession.js +218 -55
  40. package/dist/agent/src/chat/server/promptRefiner.js +86 -0
  41. package/dist/agent/src/chat/server/server.js +5 -1
  42. package/dist/agent/src/chat/server/sessionFileManager.js +22 -221
  43. package/dist/agent/src/chat/server/sessionRegistry.js +87 -0
  44. package/dist/agent/src/chat/server/titleGenerator.js +112 -0
  45. package/dist/agent/src/chat/server/titleGenerator.test.js +113 -0
  46. package/dist/agent/src/chat/server/tools.js +63 -287
  47. package/dist/agent/src/chat/utils/approvalManager.js +6 -3
  48. package/dist/agent/src/chat/utils/multiAsyncQueue.js +3 -0
  49. package/dist/agent/src/test/agent.test.js +16 -17
  50. package/dist/agent/src/test/chatContextManager.test.js +15 -3
  51. package/dist/agent/src/test/dbMcpServerConfigs.test.js +4 -4
  52. package/dist/agent/src/test/dbSessionFiles.test.js +17 -17
  53. package/dist/agent/src/test/testTools.js +6 -1
  54. package/dist/agent/src/test/tools.test.js +27 -9
  55. package/dist/agent/src/tool/agentChat.js +5 -2
  56. package/dist/agent/src/tool/chatMain.js +34 -7
  57. package/dist/agent/src/tool/commandPrompt.js +2 -2
  58. package/dist/agent/src/tool/files.js +7 -8
  59. package/package.json +8 -2
  60. package/.env.development +0 -1
  61. package/.prettierrc.json +0 -11
  62. package/dist/agent/src/agent/tools.js +0 -44
  63. package/eslint.config.mjs +0 -38
  64. package/scripts/chat_server +0 -8
  65. package/scripts/git_message +0 -31
  66. package/scripts/git_wip +0 -21
  67. package/scripts/pr_message +0 -18
  68. package/scripts/pr_review +0 -16
  69. package/scripts/setup_chat +0 -90
  70. package/scripts/shutdown_chat_server +0 -42
  71. package/scripts/start_chat_server +0 -24
  72. package/scripts/sudomcp_import +0 -23
  73. package/scripts/test_chat +0 -308
  74. package/src/agent/agent.ts +0 -624
  75. package/src/agent/agentUtils.ts +0 -285
  76. package/src/agent/compressingContextManager.ts +0 -129
  77. package/src/agent/context.ts +0 -265
  78. package/src/agent/contextWithWorkspace.ts +0 -162
  79. package/src/agent/dummyLLM.ts +0 -126
  80. package/src/agent/iAgentEventHandler.ts +0 -64
  81. package/src/agent/imageGenLLM.ts +0 -97
  82. package/src/agent/imageGenerator.ts +0 -45
  83. package/src/agent/iplatform.ts +0 -18
  84. package/src/agent/llm.ts +0 -74
  85. package/src/agent/mcpServerManager.ts +0 -541
  86. package/src/agent/nullAgentEventHandler.ts +0 -26
  87. package/src/agent/nullPlatform.ts +0 -13
  88. package/src/agent/openAI.ts +0 -123
  89. package/src/agent/openAILLM.ts +0 -95
  90. package/src/agent/openAILLMStreaming.ts +0 -609
  91. package/src/agent/promptProvider.ts +0 -87
  92. package/src/agent/repeatLLM.ts +0 -50
  93. package/src/agent/sudoMcpServerManager.ts +0 -361
  94. package/src/agent/tokenAuth.ts +0 -50
  95. package/src/agent/tools.ts +0 -57
  96. package/src/chat/client/chatClient.ts +0 -922
  97. package/src/chat/client/connection.test.ts +0 -241
  98. package/src/chat/client/connection.ts +0 -286
  99. package/src/chat/client/constants.ts +0 -1
  100. package/src/chat/client/index.ts +0 -18
  101. package/src/chat/client/interfaces.ts +0 -34
  102. package/src/chat/client/sessionClient.ts +0 -537
  103. package/src/chat/client/sessionFiles.ts +0 -142
  104. package/src/chat/client/teamManager.ts +0 -29
  105. package/src/chat/data/apiKeyManager.ts +0 -76
  106. package/src/chat/data/dataModels.ts +0 -101
  107. package/src/chat/data/database.ts +0 -997
  108. package/src/chat/data/dbMcpServerConfigs.ts +0 -59
  109. package/src/chat/data/dbSessionFileModels.ts +0 -113
  110. package/src/chat/data/dbSessionFiles.ts +0 -99
  111. package/src/chat/data/dbSessionMessages.ts +0 -102
  112. package/src/chat/data/mimeTypes.ts +0 -58
  113. package/src/chat/protocol/connectionMessages.ts +0 -49
  114. package/src/chat/protocol/constants.ts +0 -55
  115. package/src/chat/protocol/errors.ts +0 -16
  116. package/src/chat/protocol/messages.ts +0 -846
  117. package/src/chat/server/README.md +0 -127
  118. package/src/chat/server/chatContextManager.ts +0 -639
  119. package/src/chat/server/connectionManager.test.ts +0 -246
  120. package/src/chat/server/connectionManager.ts +0 -506
  121. package/src/chat/server/conversation.ts +0 -316
  122. package/src/chat/server/errorUtils.ts +0 -28
  123. package/src/chat/server/imageGeneratorTools.ts +0 -160
  124. package/src/chat/server/openAIRouterLLM.ts +0 -171
  125. package/src/chat/server/openSession.ts +0 -1689
  126. package/src/chat/server/openSessionMessageSender.ts +0 -4
  127. package/src/chat/server/server.ts +0 -175
  128. package/src/chat/server/sessionFileManager.ts +0 -422
  129. package/src/chat/server/sessionRegistry.test.ts +0 -137
  130. package/src/chat/server/sessionRegistry.ts +0 -1425
  131. package/src/chat/server/test-utils/mockFactories.ts +0 -422
  132. package/src/chat/server/tools.ts +0 -397
  133. package/src/chat/utils/agentSessionMap.ts +0 -76
  134. package/src/chat/utils/approvalManager.ts +0 -183
  135. package/src/chat/utils/asyncLock.ts +0 -43
  136. package/src/chat/utils/asyncQueue.ts +0 -62
  137. package/src/chat/utils/htmlToText.ts +0 -61
  138. package/src/chat/utils/multiAsyncQueue.ts +0 -62
  139. package/src/chat/utils/responseAwaiter.ts +0 -181
  140. package/src/chat/utils/search.ts +0 -139
  141. package/src/chat/utils/userResolver.ts +0 -48
  142. package/src/chat/utils/websocket.ts +0 -16
  143. package/src/index.ts +0 -0
  144. package/src/test/agent.test.ts +0 -590
  145. package/src/test/approvalManager.test.ts +0 -141
  146. package/src/test/chatContextManager.test.ts +0 -527
  147. package/src/test/clientServerConnection.test.ts +0 -205
  148. package/src/test/compressingContextManager.test.ts +0 -77
  149. package/src/test/context.test.ts +0 -150
  150. package/src/test/contextTestTools.ts +0 -95
  151. package/src/test/conversation.test.ts +0 -109
  152. package/src/test/db.test.ts +0 -363
  153. package/src/test/dbMcpServerConfigs.test.ts +0 -112
  154. package/src/test/dbSessionFiles.test.ts +0 -258
  155. package/src/test/dbSessionMessages.test.ts +0 -85
  156. package/src/test/dbTestTools.ts +0 -157
  157. package/src/test/imageLoad.test.ts +0 -15
  158. package/src/test/mcpServerManager.test.ts +0 -114
  159. package/src/test/multiAsyncQueue.test.ts +0 -183
  160. package/src/test/openaiStreaming.test.ts +0 -177
  161. package/src/test/prompt.test.ts +0 -27
  162. package/src/test/promptProvider.test.ts +0 -33
  163. package/src/test/responseAwaiter.test.ts +0 -103
  164. package/src/test/sudoMcpServerManager.test.ts +0 -63
  165. package/src/test/testTools.ts +0 -171
  166. package/src/test/tools.test.ts +0 -39
  167. package/src/tool/agentChat.ts +0 -194
  168. package/src/tool/agentMain.ts +0 -180
  169. package/src/tool/chatMain.ts +0 -594
  170. package/src/tool/commandPrompt.ts +0 -264
  171. package/src/tool/files.ts +0 -84
  172. package/src/tool/main.ts +0 -25
  173. package/src/tool/nodePlatform.ts +0 -73
  174. package/src/tool/options.ts +0 -144
  175. package/src/tool/prompt.ts +0 -101
  176. package/test_data/background_test_profile.json +0 -6
  177. package/test_data/background_test_script.json +0 -11
  178. package/test_data/dummyllm_script_crash.json +0 -32
  179. package/test_data/dummyllm_script_image_gen.json +0 -19
  180. package/test_data/dummyllm_script_image_gen_fe.json +0 -29
  181. package/test_data/dummyllm_script_invoke_image_gen_tool.json +0 -37
  182. package/test_data/dummyllm_script_render_tool.json +0 -29
  183. package/test_data/dummyllm_script_simplecalc.json +0 -28
  184. package/test_data/dummyllm_script_test_auto_approve.json +0 -81
  185. package/test_data/dummyllm_script_test_simplecalc_addition.json +0 -29
  186. package/test_data/frog.png +0 -0
  187. package/test_data/frog.png.b64 +0 -1
  188. package/test_data/git_message_profile.json +0 -4
  189. package/test_data/git_wip_system.txt +0 -5
  190. package/test_data/image_gen_test_profile.json +0 -5
  191. package/test_data/pr_message_profile.json +0 -4
  192. package/test_data/pr_review_profile.json +0 -4
  193. package/test_data/prompt_simplecalc.txt +0 -1
  194. package/test_data/simplecalc_profile.json +0 -4
  195. package/test_data/sudomcp_import_profile.json +0 -4
  196. package/test_data/test_script_profile.json +0 -8
  197. package/tsconfig.json +0 -13
  198. package/vitest.config.ts +0 -39
  199. /package/dist/agent/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.js +0 -0
@@ -1,1425 +0,0 @@
1
- import { v4 as uuidv4 } from "uuid";
2
-
3
- import {
4
- AgentProfile,
5
- getLogger,
6
- SavedAgentProfile,
7
- DEFAULT_AGENT_PROFILE_NAME,
8
- DEFAULT_AGENT_PROFILE,
9
- } from "@xalia/xmcp/sdk";
10
-
11
- import {
12
- ClientSessionMessage,
13
- ClientToServer,
14
- ServerControlSessionList,
15
- ClientControlMessage,
16
- isClientControlMessage,
17
- ClientControlGetSessionList,
18
- ClientControlSessionCreate,
19
- ClientControlSessionJoin,
20
- ServerControlError,
21
- ClientControlSessionDelete,
22
- ServerControlSessionDeleted,
23
- ClientControlTeamCreate,
24
- ServerControlTeamCreated,
25
- ClientControlAddTeamUser,
26
- ClientControlRemoveTeamUser,
27
- ServerToClient,
28
- ClientControlAgentProfileCreate,
29
- ClientControlAgentProfileDelete,
30
- ClientControlAddCustomMcpServer,
31
- ClientControlRemoveCustomMcpServer,
32
- } from "../protocol/messages";
33
- import { GUEST_TOKEN_PREFIX, OpenSession } from "./openSession";
34
- import {
35
- CustomMcpServerDescriptor,
36
- SessionCreateData,
37
- SessionData,
38
- SessionDescriptor,
39
- TeamInfo,
40
- TeamParticipant,
41
- } from "../data/dataModels";
42
- import { Database, UserData } from "../data/database";
43
- import { resolveUserIdentifier } from "../utils/userResolver";
44
- import { ChatFatalError } from "../protocol/errors";
45
- import { IMessageProcessor, IUserConnectionManager } from "./connectionManager";
46
- import { getErrorString } from "./errorUtils";
47
- import { ApiKeyManager } from "../data/apiKeyManager";
48
-
49
- const logger = getLogger();
50
-
51
- export type GuestUser = UserData & { guest_for_session: string };
52
-
53
- // server_name => url
54
- type CustomMcpServersForUser = Map<string, CustomMcpServerDescriptor>;
55
-
56
- export class CustomMcpServerManager {
57
- private perUser: Map<string, CustomMcpServersForUser>;
58
-
59
- constructor(_db: Database) {
60
- this.perUser = new Map();
61
- }
62
-
63
- getForUser(user_uuid: string): Record<string, CustomMcpServerDescriptor> {
64
- const servers = this.perUser.get(user_uuid);
65
- if (!servers) {
66
- return {};
67
- }
68
-
69
- return Object.fromEntries(servers.entries());
70
- }
71
-
72
- async addForUser(
73
- user_uuid: string,
74
- server_name: string,
75
- description: string,
76
- url: string
77
- ): Promise<void> {
78
- // TODO: persistence to DB
79
-
80
- const userServers = this.getUserServers(user_uuid);
81
- if (userServers.has(server_name)) {
82
- throw new Error(`server ${server_name} already added`);
83
- }
84
-
85
- userServers.set(server_name, { name: server_name, description, url });
86
-
87
- return Promise.resolve();
88
- }
89
-
90
- async removeForUser(user_uuid: string, server_name: string): Promise<void> {
91
- const userServers = this.getUserServers(user_uuid);
92
- if (userServers.has(server_name)) {
93
- userServers.delete(server_name);
94
- }
95
- return Promise.resolve();
96
- }
97
-
98
- private getUserServers(user_uuid: string): CustomMcpServersForUser {
99
- let userServers = this.perUser.get(user_uuid);
100
- if (!userServers) {
101
- userServers = new Map();
102
- this.perUser.set(user_uuid, userServers);
103
- }
104
-
105
- return userServers;
106
- }
107
- }
108
-
109
- export class SessionRegistry implements IMessageProcessor<ClientToServer> {
110
- // In memory session-user/user-session mappings
111
- // Note: this mappings ONLY trackes online users and will
112
- // be cleaned up when the user disconnects.
113
- // sessionId -> userIds
114
- private sessionUsers: Map<string, Set<string>> = new Map();
115
- // userId -> sessionIds
116
- private userSessions: Map<string, Set<string>> = new Map();
117
-
118
- // Session instances
119
- // sessionId -> OpenSession
120
- private openSessions: Map<string, OpenSession> = new Map();
121
-
122
- private guests: Map<string, GuestUser> = new Map();
123
-
124
- private apiKeyManager: ApiKeyManager;
125
-
126
- private customMcpServerManager: CustomMcpServerManager;
127
-
128
- constructor(
129
- private db: Database,
130
- private connectionManager: IUserConnectionManager<ServerToClient>,
131
- private xmcpUrl: string
132
- ) {
133
- this.apiKeyManager = new ApiKeyManager(db);
134
- this.customMcpServerManager = new CustomMcpServerManager(db);
135
- }
136
-
137
- /**
138
- * Add user to session membership.
139
- * Creates session tracking if it doesn't exist.
140
- */
141
- private addUserToSessionMemory(userId: string, sessionId: string): void {
142
- logger.debug(
143
- `[SessionRegistry] Adding user ${userId} to session ${sessionId}`
144
- );
145
-
146
- // Add to sessionUsers
147
- if (!this.sessionUsers.has(sessionId)) {
148
- this.sessionUsers.set(sessionId, new Set());
149
- }
150
- const sessionUserSet = this.sessionUsers.get(sessionId);
151
- if (sessionUserSet) {
152
- sessionUserSet.add(userId);
153
- }
154
-
155
- // Add to userSessions
156
- if (!this.userSessions.has(userId)) {
157
- this.userSessions.set(userId, new Set());
158
- }
159
- const userSessionSet = this.userSessions.get(userId);
160
- if (userSessionSet) {
161
- userSessionSet.add(sessionId);
162
- }
163
- }
164
-
165
- /**
166
- * Remove user from session membership.
167
- * Cleans up empty sessions and triggers session cleanup if needed.
168
- */
169
- private removeUserFromSessionMemory(userId: string, sessionId: string): void {
170
- logger.debug(
171
- `[SessionRegistry] Removing user ${userId} from session ${sessionId}`
172
- );
173
-
174
- // Remove from sessionUsers
175
- const sessionUserSet = this.sessionUsers.get(sessionId);
176
- if (sessionUserSet) {
177
- sessionUserSet.delete(userId);
178
- if (sessionUserSet.size === 0) {
179
- this.sessionUsers.delete(sessionId);
180
- // Clean up session instance when empty
181
- const session = this.openSessions.get(sessionId);
182
- if (session) {
183
- logger.debug(
184
- `[SessionRegistry] Triggering cleanup for empty session ` +
185
- sessionId
186
- );
187
- // The onEmpty callback will remove from openSessions map
188
-
189
- logger.debug(
190
- `[SessionRegistry] Session ${sessionId} empty. Removing`
191
- );
192
- session.onEmpty();
193
- this.openSessions.delete(sessionId);
194
- }
195
- }
196
- }
197
-
198
- // Remove from userSessions
199
- const userSessionSet = this.userSessions.get(userId);
200
- if (userSessionSet) {
201
- userSessionSet.delete(sessionId);
202
- if (userSessionSet.size === 0) {
203
- this.userSessions.delete(userId);
204
- }
205
- }
206
-
207
- logger.info(
208
- `[SessionRegistry] User ${userId} removed from session ${sessionId}`
209
- );
210
- }
211
-
212
- /**
213
- * Get all users in a session.
214
- */
215
- async getSessionUsers(sessionId: string): Promise<Set<string>> {
216
- const users = await this.db.sessionGetParticipants(sessionId);
217
- return new Set(users.map((u) => u.user_uuid));
218
- }
219
-
220
- /**
221
- * Get all users in a session.
222
- */
223
- getInMemorySessionUsers(sessionId: string): Set<string> {
224
- return this.sessionUsers.get(sessionId) || new Set();
225
- }
226
-
227
- /**
228
- * Get all sessions a user belongs to.
229
- */
230
- getUserSessions(userId: string): Set<string> {
231
- return this.userSessions.get(userId) || new Set();
232
- }
233
-
234
- /**
235
- * Get all sessions a user belongs to in memory.
236
- */
237
- getInMemoryUserSessions(userId: string): Set<string> {
238
- return this.userSessions.get(userId) || new Set();
239
- }
240
-
241
- // IMessageProcessor<ClientToServer>
242
- async authenticate(token: string): Promise<string | undefined> {
243
- // Parse the api key to determine the type of connection
244
- const {
245
- prefix,
246
- apiKey,
247
- payload: sessionUUID,
248
- } = ApiKeyManager.parseToken(token);
249
-
250
- if (prefix === GUEST_TOKEN_PREFIX) {
251
- if (!sessionUUID) {
252
- throw new Error(`invalid token ${token}`);
253
- }
254
-
255
- const descriptor = await this.getSessionDescriptor(sessionUUID);
256
- if (!descriptor) {
257
- throw new Error(`no such session ${sessionUUID}`);
258
- }
259
- if (descriptor.access_token !== token) {
260
- throw new Error(`invalid guest key ${apiKey}`);
261
- }
262
-
263
- // For now, add a new user to the DB (so simplify the process of adding
264
- // users).
265
-
266
- const user_uuid = uuidv4();
267
- const email = `${user_uuid}@`;
268
- const nickname = "Guest";
269
- const timezone = "UTC";
270
-
271
- await this.db.createUser(user_uuid, email, nickname, timezone);
272
-
273
- const user: GuestUser = {
274
- uuid: user_uuid,
275
- nickname: "Guest",
276
- email: "guest",
277
- timezone: "UTC",
278
- guest_for_session: sessionUUID,
279
- };
280
- this.guests.set(user_uuid, user);
281
- return user.uuid;
282
-
283
- // throw new Error("unimpl");
284
- // // return "";
285
- }
286
-
287
- const userData = await this.apiKeyManager.verifyApiKey(apiKey);
288
- if (!userData) {
289
- throw new ChatFatalError("invalid api key");
290
- }
291
- return userData.uuid;
292
- }
293
-
294
- // IMessageProcessor<ClientToServer>
295
- async processMessage(
296
- connectionId: string,
297
- userId: string,
298
- message: ClientToServer
299
- ): Promise<void> {
300
- if (isClientControlMessage(message)) {
301
- // handle connection level message
302
- await this.processClientControlMessage(connectionId, userId, message);
303
- } else {
304
- // handle session level message
305
- this.processSessionMessage(userId, message as ClientSessionMessage);
306
- }
307
- }
308
-
309
- private sendControlError(
310
- connectionId: string,
311
- clientMsgId: string,
312
- errorMessage: string
313
- ): void {
314
- // TODO: Why is this managed at the transport level?
315
-
316
- const errorMsg: ServerControlError = {
317
- type: "control_error",
318
- message: errorMessage,
319
- client_message_id: clientMsgId,
320
- };
321
- this.connectionManager.sendToConnection(connectionId, errorMsg);
322
- }
323
-
324
- private async processClientControlMessage(
325
- connectionId: string,
326
- userId: string,
327
- message: ClientControlMessage
328
- ): Promise<void> {
329
- switch (message.type) {
330
- case "control_agent_profile_create":
331
- await this.handleAgentProfileCreate(userId, message);
332
- break;
333
- case "control_agent_profile_delete":
334
- await this.handleAgentProfileDelete(userId, message);
335
- break;
336
- case "control_get_session_list":
337
- await this.handleGetSessionList(connectionId, userId, message);
338
- break;
339
- case "control_session_create":
340
- await this.handleSessionCreate(connectionId, userId, message);
341
- break;
342
- case "control_session_join":
343
- await this.handleSessionJoin(connectionId, userId, message);
344
- break;
345
- case "control_session_delete":
346
- await this.handleSessionDelete(connectionId, userId, message);
347
- break;
348
- case "control_team_create":
349
- await this.handleTeamCreate(connectionId, userId, message);
350
- break;
351
- case "control_add_team_user":
352
- await this.handleAddTeamUser(connectionId, userId, message);
353
- break;
354
- case "control_remove_team_user":
355
- await this.handleRemoveTeamUser(connectionId, userId, message);
356
- break;
357
- case "control_add_custom_mcp_server":
358
- await this.handleAddCustomMcpServer(userId, message);
359
- break;
360
- case "control_remove_custom_mcp_server":
361
- await this.handleRemoveCustomMcpServer(userId, message);
362
- break;
363
- default: {
364
- const exhaustive: never = message;
365
- // unknown connection type should be a fatal error
366
- throw new ChatFatalError(
367
- `Unknown connection message type: ${String(exhaustive)}`
368
- );
369
- }
370
- }
371
- }
372
-
373
- private async handleAddCustomMcpServer(
374
- userId: string,
375
- message: ClientControlAddCustomMcpServer
376
- ): Promise<void> {
377
- await this.customMcpServerManager.addForUser(
378
- userId,
379
- message.server_name,
380
- message.description,
381
- message.url
382
- );
383
- this.connectionManager.sendToUsers([userId], {
384
- type: "control_custom_mcp_server_added",
385
- server_name: message.server_name,
386
- url: message.url,
387
- });
388
- }
389
-
390
- private async handleRemoveCustomMcpServer(
391
- userId: string,
392
- message: ClientControlRemoveCustomMcpServer
393
- ): Promise<void> {
394
- await this.customMcpServerManager.removeForUser(
395
- userId,
396
- message.server_name
397
- );
398
- this.connectionManager.sendToUsers([userId], {
399
- type: "control_custom_mcp_server_removed",
400
- server_name: message.server_name,
401
- });
402
- }
403
-
404
- /**
405
- * Handle session_list request
406
- */
407
- private async handleGetSessionList(
408
- connectionId: string,
409
- userId: string,
410
- message: ClientControlGetSessionList
411
- ): Promise<void> {
412
- try {
413
- const [userSessions, teamSessions, userAgents] =
414
- await this.getAllAgentsAndSessionsByUser(userId);
415
-
416
- // Mark any guest session as a user session
417
-
418
- const guest = this.guests.get(userId);
419
- if (guest) {
420
- const guestSession = await this.db.sessionGetDescriptorById(
421
- guest.guest_for_session
422
- );
423
- if (guestSession) {
424
- userSessions.push(guestSession);
425
- }
426
- }
427
-
428
- const response: ServerControlSessionList = {
429
- type: "control_session_list",
430
- user_sessions: userSessions,
431
- team_sessions: teamSessions,
432
- user_agents: userAgents,
433
- client_message_id: message.client_message_id,
434
- custom_mcp_servers: this.customMcpServerManager.getForUser(userId),
435
- };
436
-
437
- this.connectionManager.sendToConnection(connectionId, response);
438
- logger.info(
439
- `[ConnectionManager] Sent ` +
440
- `${String(userSessions.length)} user sessions and ` +
441
- `${String(teamSessions.length)} team sessions to user ${userId}`
442
- );
443
- } catch (error) {
444
- logger.error(`[ConnectionManager] Failed to get session list:`, error);
445
- this.sendControlError(
446
- connectionId,
447
- message.client_message_id,
448
- JSON.stringify(error)
449
- );
450
- }
451
- }
452
-
453
- private async handleSessionDelete(
454
- connectionId: string,
455
- userId: string,
456
- message: ClientControlSessionDelete
457
- ): Promise<void> {
458
- const sessionId = message.target_session_id;
459
- try {
460
- // validate the session access
461
- await this.validateSessionAccess(sessionId, userId, "owner");
462
-
463
- // get users/team-uuid before deletion
464
- const [users, sessionData] = await Promise.all([
465
- this.getSessionUsers(sessionId),
466
- this.db.sessionGetDescriptorById(sessionId),
467
- ]);
468
- const teamUuid = sessionData?.team_uuid;
469
-
470
- if (!sessionData) {
471
- throw new ChatFatalError(`No such session: ${sessionId}`);
472
- }
473
-
474
- // delete the session from database
475
- await this.db.sessionDeleteById(sessionId);
476
-
477
- // remove the session from memory (should be no thrown from here)
478
- const session = this.openSessions.get(sessionId);
479
- if (session) {
480
- const users = this.getInMemorySessionUsers(sessionId);
481
- for (const user of users) {
482
- this.removeUserFromSessionMemory(user, sessionId);
483
- }
484
- }
485
- this.openSessions.delete(sessionId);
486
-
487
- // send confirmation to the client
488
- const response: ServerControlSessionDeleted = {
489
- type: "control_session_deleted",
490
- session_id: sessionId,
491
- team_uuid: teamUuid,
492
- agent_profile_uuid: sessionData.agent_profile_uuid,
493
- client_message_id: message.client_message_id,
494
- };
495
- this.connectionManager.sendToUsers(users, response);
496
- } catch (error) {
497
- logger.error(
498
- `[SessionRegistry] Error delete session ${sessionId}:`,
499
- error
500
- );
501
- this.sendControlError(
502
- connectionId,
503
- message.client_message_id,
504
- String(error)
505
- );
506
- }
507
- }
508
-
509
- /**
510
- * Handle team_create_request message - creates a new team.
511
- * @param connectionId connection id
512
- * @param userId user id
513
- * @param message team create request message
514
- */
515
- private async handleTeamCreate(
516
- connectionId: string,
517
- userId: string,
518
- message: ClientControlTeamCreate
519
- ): Promise<void> {
520
- try {
521
- // Resolve all initial members in parallel
522
- // this contains undefined to mark failed lookups
523
- const resolvedMemberIds = await Promise.all(
524
- message.initial_members.map((member) =>
525
- resolveUserIdentifier(this.db, member)
526
- )
527
- );
528
-
529
- const failedLookups: string[] = [];
530
- const validMemberIds: string[] = [];
531
- const participants: TeamParticipant[] = [];
532
-
533
- // No need to filter out the owner here, that is done in
534
- // `createTeamWithParticipants`
535
- resolvedMemberIds.forEach((user, index) => {
536
- if (user) {
537
- validMemberIds.push(user.uuid);
538
- participants.push({
539
- user_uuid: user.uuid,
540
- nickname: user.nickname || "",
541
- email: user.email,
542
- role: "participant",
543
- });
544
- } else {
545
- failedLookups.push(message.initial_members[index]);
546
- }
547
- });
548
-
549
- // Create the team with initial participants
550
- const teamUuid = await this.db.createTeamWithParticipants(
551
- message.team_name,
552
- userId,
553
- validMemberIds
554
- );
555
-
556
- // Create a default agent for the new team
557
- const defaultAgentProfile = await this.db.createAgentProfile(
558
- undefined,
559
- teamUuid,
560
- DEFAULT_AGENT_PROFILE_NAME,
561
- DEFAULT_AGENT_PROFILE
562
- );
563
-
564
- if (!defaultAgentProfile) {
565
- throw new Error(
566
- `Failed to create default agent profile for team ${teamUuid}`
567
- );
568
- }
569
-
570
- // Get owner's information and add to participants for the response
571
- const ownerInfo = await this.db.getUserFromUuid(userId);
572
- if (!ownerInfo) {
573
- throw new Error(`Cannot find owner user data for user ${userId}`);
574
- }
575
-
576
- const ownerParticipant: TeamParticipant = {
577
- user_uuid: userId,
578
- nickname: ownerInfo.nickname || "",
579
- email: ownerInfo.email,
580
- role: "owner",
581
- };
582
-
583
- const response: ServerControlTeamCreated = {
584
- type: "control_team_created",
585
- team_uuid: teamUuid,
586
- team_owner_uuid: userId,
587
- team_name: message.team_name,
588
- members: [ownerParticipant, ...participants],
589
- failed_lookups: failedLookups,
590
- };
591
- const members = new Set(validMemberIds);
592
- members.add(userId);
593
- this.connectionManager.sendToUsers(members, response);
594
-
595
- // Broadcast the default agent profile to all team members
596
- this.connectionManager.sendToUsers(members, {
597
- type: "control_agent_profile_created",
598
- profile: defaultAgentProfile,
599
- });
600
- } catch (error) {
601
- logger.error(`[SessionRegistry] Error creating team: ${String(error)}`);
602
- this.sendControlError(
603
- connectionId,
604
- message.client_message_id,
605
- String(error)
606
- );
607
- }
608
- }
609
-
610
- /**
611
- * Process session-scoped client message.
612
- * Handles membership messages here, others to OpenSession.
613
- */
614
- private processSessionMessage(
615
- userId: string,
616
- message: ClientSessionMessage
617
- ): void {
618
- const sessionId = message.session_id;
619
- logger.info(
620
- `[SessionRegistry] Processing message from user ${userId} in session ` +
621
- sessionId
622
- );
623
-
624
- const session = this.openSessions.get(sessionId);
625
- if (!session) {
626
- throw new ChatFatalError(`Internal error: No such session ${sessionId}`);
627
- }
628
-
629
- // Forward all other messages to OpenSession
630
- session.onClientSessionMessage(userId, message);
631
- }
632
-
633
- /**
634
- * Handle add_user message - adds user to team.
635
- */
636
- private async handleAddTeamUser(
637
- connectionId: string,
638
- fromUserId: string,
639
- message: ClientControlAddTeamUser
640
- ): Promise<void> {
641
- // Validate permissions - only owner can add users
642
- const access = await this.validateTeamAccess(
643
- message.target_team_id,
644
- fromUserId,
645
- "owner"
646
- );
647
-
648
- if (!access) {
649
- this.sendControlError(
650
- connectionId,
651
- message.client_message_id,
652
- "Only team owner can add users"
653
- );
654
- return;
655
- }
656
- // Resolve user identifier
657
- const user = await resolveUserIdentifier(
658
- this.db,
659
- message.user_uuid_or_email
660
- );
661
- if (!user) {
662
- this.sendControlError(
663
- connectionId,
664
- message.client_message_id,
665
- "User not found"
666
- );
667
- return;
668
- }
669
-
670
- // Check if user is already a participant
671
- const participants = await this.db.teamGetMembers(message.target_team_id);
672
- if (participants.some((p) => p.user_uuid === user.uuid)) {
673
- this.sendControlError(
674
- connectionId,
675
- message.client_message_id,
676
- "User is already a participant"
677
- );
678
- return;
679
- }
680
-
681
- // Update database
682
- try {
683
- await this.db.teamAddMember(message.target_team_id, user.uuid);
684
- } catch (error) {
685
- this.sendControlError(
686
- connectionId,
687
- message.client_message_id,
688
- "Server Internal Error: cannot add user."
689
- );
690
- logger.error(
691
- `[SessionRegistry] Error adding user ${user.uuid}` +
692
- ` to team ${message.target_team_id}:`,
693
- error
694
- );
695
- return;
696
- }
697
-
698
- // add user to related active sessions
699
- const sessions = await this.db.teamGetSessions(message.target_team_id);
700
- for (const sessionData of sessions) {
701
- const session = this.openSessions.get(sessionData.session_uuid);
702
- if (session) {
703
- session.addParticipant(user.uuid, {
704
- user_uuid: user.uuid,
705
- nickname: user.nickname || "",
706
- email: user.email,
707
- role: "participant",
708
- });
709
- }
710
- }
711
-
712
- // Notify all team members about the updated member list
713
- const updatedParticipants = await this.db.teamGetMembers(
714
- message.target_team_id
715
- );
716
- this.connectionManager.sendToUsers(
717
- new Set(updatedParticipants.map((p) => p.user_uuid)),
718
- {
719
- type: "control_team_members_updated",
720
- team_uuid: message.target_team_id,
721
- members: updatedParticipants,
722
- }
723
- );
724
-
725
- logger.info(
726
- `[SessionRegistry] User ${user.uuid}` +
727
- `added to team ${message.target_team_id}`
728
- );
729
- }
730
-
731
- /**
732
- * Handle remove_user message - removes user from session membership.
733
- * Only session owner can remove users.
734
- */
735
- private async handleRemoveTeamUser(
736
- connectionId: string,
737
- fromUserId: string,
738
- message: ClientControlRemoveTeamUser
739
- ): Promise<void> {
740
- // Validate permissions - only owner can remove users
741
- const access = await this.validateTeamAccess(
742
- message.target_team_id,
743
- fromUserId,
744
- "owner"
745
- );
746
- if (!access) {
747
- this.sendControlError(
748
- connectionId,
749
- message.client_message_id,
750
- "Only team owner can remove users"
751
- );
752
- return;
753
- }
754
-
755
- // Resolve user identifier
756
- const user = await resolveUserIdentifier(
757
- this.db,
758
- message.user_uuid_or_email
759
- );
760
- if (!user) {
761
- this.sendControlError(
762
- connectionId,
763
- message.client_message_id,
764
- "User not found"
765
- );
766
- return;
767
- }
768
-
769
- // owner cannot remove her/himself
770
- if (user.uuid === fromUserId) {
771
- this.sendControlError(
772
- connectionId,
773
- message.client_message_id,
774
- "Owner cannot remove herself/himself"
775
- );
776
- return;
777
- }
778
-
779
- // Check if user is actually a participant
780
- const participants = await this.db.teamGetMembers(message.target_team_id);
781
- if (!participants.some((p) => p.user_uuid === user.uuid)) {
782
- this.sendControlError(
783
- connectionId,
784
- message.client_message_id,
785
- "User is not a participant"
786
- );
787
- return;
788
- }
789
-
790
- // Remove from database
791
- try {
792
- await this.db.teamRemoveMember(message.target_team_id, user.uuid);
793
- } catch (error) {
794
- this.sendControlError(
795
- connectionId,
796
- message.client_message_id,
797
- "Server Internal Error: cannot remove user."
798
- );
799
- logger.error(
800
- `[SessionRegistry] Error removing user ${user.uuid}` +
801
- ` from team ${message.target_team_id}:`,
802
- error
803
- );
804
- return;
805
- }
806
-
807
- // Update OpenSession's participant map and in memory tracking
808
- const sessions = await this.db.teamGetSessions(message.target_team_id);
809
- for (const sessionData of sessions) {
810
- const session = this.openSessions.get(sessionData.session_uuid);
811
- if (session) {
812
- session.removeParticipant(user.uuid);
813
- this.removeUserFromSessionMemory(user.uuid, sessionData.session_uuid);
814
- }
815
- }
816
-
817
- // Notify all remaining team members about the updated member list
818
- const updatedParticipants = await this.db.teamGetMembers(
819
- message.target_team_id
820
- );
821
- this.connectionManager.sendToUsers(
822
- new Set(updatedParticipants.map((p) => p.user_uuid)),
823
- {
824
- type: "control_team_members_updated",
825
- team_uuid: message.target_team_id,
826
- members: updatedParticipants,
827
- }
828
- );
829
-
830
- logger.info(
831
- `[SessionRegistry] User ${user.uuid} ` +
832
- `removed from team ${message.target_team_id}`
833
- );
834
- }
835
-
836
- /**
837
- * Get session instance, if the session has not been initialized,
838
- * it will be.
839
- */
840
- private async getAndActivateSession(
841
- sessionId: string
842
- ): Promise<{ session: OpenSession; isNew: boolean } | undefined> {
843
- if (this.openSessions.has(sessionId)) {
844
- logger.info(`[SessionRegistry] Session ${sessionId} already exists`);
845
- const session = this.openSessions.get(sessionId);
846
- if (!session) {
847
- throw new ChatFatalError(
848
- `Internal error: No such session: ${sessionId}`
849
- );
850
- }
851
- return { session, isNew: false };
852
- } else {
853
- logger.info(`[SessionRegistry] loading session ${sessionId}`);
854
- const session = await OpenSession.initWithExistingSession(
855
- this.db,
856
- sessionId,
857
- this.xmcpUrl,
858
- this.connectionManager
859
- );
860
- return { session, isNew: true };
861
- }
862
- }
863
-
864
- /**
865
- * Handle user joining a session.
866
- * Activates the session if not already active.
867
- * return the session info to the client joining the session.
868
- */
869
- async handleSessionJoin(
870
- connectionId: string,
871
- userId: string,
872
- message: ClientControlSessionJoin
873
- ): Promise<void> {
874
- const sessionId = message.target_session_id;
875
- logger.info(
876
- `[SessionRegistry] Joining session ${sessionId} for user ${userId}`
877
- );
878
- try {
879
- // Validate session access permissions
880
- const access = await this.validateSessionAccess(sessionId, userId);
881
- if (!access) {
882
- throw new ChatFatalError(
883
- `User ${userId} is not authorized to join session ${sessionId}`
884
- );
885
- }
886
-
887
- // get or create the session
888
- const sessionInfo = await this.getAndActivateSession(sessionId);
889
- if (!sessionInfo) {
890
- // this in theory should not happen
891
- // since we have validated the access
892
- throw new ChatFatalError(
893
- `Server internal error: ` + `failed to load session ${sessionId}`
894
- );
895
- }
896
- const { session, isNew } = sessionInfo;
897
-
898
- const guest = this.guests.get(userId);
899
- if (guest) {
900
- session.addParticipant(userId, {
901
- user_uuid: guest.uuid,
902
- nickname: guest.nickname || "Guest",
903
- email: guest.email,
904
- role: "participant",
905
- });
906
- }
907
- // To this point, there should be no error thrown.
908
- // Update in-memory session-user/user-session mappings
909
- this.addUserToSessionMemory(userId, sessionId);
910
-
911
- // Register session immediately
912
- if (!this.openSessions.has(sessionId)) {
913
- this.openSessions.set(sessionId, session);
914
- }
915
-
916
- // pass the message to the session to handle the rest
917
- await session.sendSessionData(
918
- connectionId,
919
- message.client_message_id,
920
- isNew
921
- );
922
- } catch (error) {
923
- logger.error(
924
- `[SessionRegistry] Error handling user join: ${String(error)}`
925
- );
926
- this.sendControlError(
927
- connectionId,
928
- message.client_message_id,
929
- String(error)
930
- );
931
- }
932
- }
933
-
934
- async handleAgentProfileCreate(
935
- userId: string,
936
- message: ClientControlAgentProfileCreate
937
- ): Promise<string> {
938
- // get agent profile from template
939
- let agentProfileFromTemplate: AgentProfile | undefined = undefined;
940
- if (message.template_name) {
941
- const template = await this.db.agentTemplateGetByName(
942
- message.template_name
943
- );
944
- if (!template) {
945
- throw new Error(`template ${message.template_name} not found`);
946
- }
947
- agentProfileFromTemplate = template.agent_profile;
948
- }
949
-
950
- const newAgentProfile: AgentProfile =
951
- agentProfileFromTemplate ||
952
- new AgentProfile(
953
- message.model || DEFAULT_AGENT_PROFILE.model,
954
- DEFAULT_AGENT_PROFILE.system_prompt,
955
- DEFAULT_AGENT_PROFILE.mcp_settings
956
- );
957
- const team_uuid = message.team_uuid || undefined;
958
-
959
- if (team_uuid) {
960
- // TODO: should be able to reconstruct the full SavedAgentProfile in one
961
- // call.
962
- const savedAgentProfile = await this.db.createAgentProfile(
963
- undefined,
964
- team_uuid,
965
- message.title,
966
- newAgentProfile
967
- );
968
- if (!savedAgentProfile) {
969
- throw new Error(
970
- "failed creating team agent profile (createAgentProfile)"
971
- );
972
- }
973
-
974
- // Broadcast the new AgentProfile to all participants
975
-
976
- const participants = await this.db.teamGetMembers(team_uuid);
977
- this.connectionManager.sendToUsers(
978
- new Set(participants.map((p) => p.user_uuid)),
979
- { type: "control_agent_profile_created", profile: savedAgentProfile }
980
- );
981
- return savedAgentProfile.uuid;
982
- }
983
-
984
- // User AgentProfile
985
-
986
- const savedAgentProfile = await this.db.createAgentProfile(
987
- userId,
988
- undefined,
989
- message.title,
990
- newAgentProfile
991
- );
992
- if (!savedAgentProfile) {
993
- throw new Error("failed creating agent profile (createAgentProfile)");
994
- }
995
-
996
- // Send the new AgentProfile to the user
997
- this.connectionManager.sendToUsers(new Set([userId]), {
998
- type: "control_agent_profile_created",
999
- profile: savedAgentProfile,
1000
- });
1001
-
1002
- return savedAgentProfile.uuid;
1003
- }
1004
-
1005
- async handleAgentProfileDelete(
1006
- userId: string,
1007
- message: ClientControlAgentProfileDelete
1008
- ): Promise<void> {
1009
- const agentProfileUuid = message.agent_profile_uuid;
1010
-
1011
- // Get the agent profile to determine if it's a team or user profile
1012
- const agentProfile =
1013
- await this.db.getSavedAgentProfileById(agentProfileUuid);
1014
- if (!agentProfile) {
1015
- throw new Error(`Agent profile ${agentProfileUuid} not found`);
1016
- }
1017
-
1018
- // Validate team access: user must own the profile or be in the team
1019
- // TODO: Only allow the owner to delete the profile?
1020
- if (agentProfile.team_uuid) {
1021
- const hasAccess = await this.validateTeamAccess(
1022
- agentProfile.team_uuid,
1023
- userId
1024
- );
1025
- if (!hasAccess) {
1026
- throw new Error(
1027
- `User ${userId} is not authorized to delete this agent profile`
1028
- );
1029
- }
1030
- }
1031
-
1032
- // Delete the agent profile from database
1033
- await this.db.deleteAgentProfile(agentProfileUuid);
1034
-
1035
- // Send confirmation to all relevant users
1036
- if (agentProfile.team_uuid) {
1037
- const participants = await this.db.teamGetMembers(agentProfile.team_uuid);
1038
- this.connectionManager.sendToUsers(
1039
- new Set(participants.map((p) => p.user_uuid)),
1040
- {
1041
- type: "control_agent_profile_deleted",
1042
- profile_uuid: agentProfileUuid,
1043
- }
1044
- );
1045
- } else {
1046
- this.connectionManager.sendToUsers(new Set([userId]), {
1047
- type: "control_agent_profile_deleted",
1048
- profile_uuid: agentProfileUuid,
1049
- });
1050
- }
1051
- }
1052
-
1053
- /**
1054
- * Create a new session for a user.
1055
- * - create session in database (via `newSession`)
1056
- * - create an OpenSession instance
1057
- * - return the session info
1058
- */
1059
- async handleSessionCreate(
1060
- connectionId: string,
1061
- fromUserId: string,
1062
- message: ClientControlSessionCreate
1063
- ): Promise<void> {
1064
- try {
1065
- // If agent not given, create one and inform the client
1066
-
1067
- if (!message.agent_profile_id) {
1068
- logger.info("[handleSessionCreate] creating new AgentProfile");
1069
-
1070
- // Create AgentProfile and inform the client
1071
- message.agent_profile_id = await this.handleAgentProfileCreate(
1072
- fromUserId,
1073
- {
1074
- type: "control_agent_profile_create",
1075
- title: "New Agent " + uuidv4(),
1076
- user_uuid: fromUserId,
1077
- team_uuid: message.team_id,
1078
- }
1079
- );
1080
- }
1081
-
1082
- // Create new session and get its instance
1083
- const { openSession, sessionId } = message.team_id
1084
- ? await this.newTeamSession(
1085
- fromUserId,
1086
- message.team_id,
1087
- message.title,
1088
- message.agent_profile_id
1089
- )
1090
- : await this.newUserSession(
1091
- fromUserId,
1092
- message.title,
1093
- message.agent_profile_id
1094
- );
1095
-
1096
- // there should be no further error thrown from now.
1097
- // Register session immediately
1098
- this.openSessions.set(sessionId, openSession);
1099
-
1100
- // add owner to session memory
1101
- this.addUserToSessionMemory(fromUserId, sessionId);
1102
-
1103
- // send session info to the connection. It has just been created so we
1104
- // must also restore the mcp servers.
1105
- await openSession.sendSessionData(
1106
- connectionId,
1107
- message.client_message_id,
1108
- true
1109
- );
1110
-
1111
- logger.info(
1112
- `[SessionRegistry] new session ${sessionId}:` +
1113
- ` ${message.title} for ${fromUserId}`
1114
- );
1115
- } catch (error) {
1116
- const errStr = getErrorString(error);
1117
- logger.error(`[SessionRegistry] Error in session create: ${errStr}`);
1118
- this.sendControlError(connectionId, message.client_message_id, errStr);
1119
- }
1120
- }
1121
-
1122
- /**
1123
- * Create a new user session.
1124
- */
1125
- private async newUserSession(
1126
- ownerId: string,
1127
- title: string,
1128
- agentProfileId: string
1129
- ): Promise<{ openSession: OpenSession; sessionId: string }> {
1130
- // validate the agent profile
1131
- await this.validateSavedAgentProfile(agentProfileId);
1132
-
1133
- const newSessionData: SessionDescriptor = {
1134
- ...userSessionCreateData(ownerId, title, agentProfileId),
1135
- updated_at: new Date().toISOString(),
1136
- };
1137
- const openSession = await OpenSession.initWithEmptySession(
1138
- this.db,
1139
- newSessionData,
1140
- this.xmcpUrl,
1141
- this.connectionManager
1142
- );
1143
- return { sessionId: newSessionData.session_uuid, openSession };
1144
- }
1145
-
1146
- /**
1147
- * Create a new team session.
1148
- */
1149
- private async newTeamSession(
1150
- fromUserId: string,
1151
- teamId: string,
1152
- title: string,
1153
- agentProfileId: string
1154
- ): Promise<{ openSession: OpenSession; sessionId: string }> {
1155
- // validate agent profile and team access
1156
- const [_savedAgentProfile, access, participants] = await Promise.all([
1157
- this.validateSavedAgentProfile(agentProfileId),
1158
- this.validateTeamAccess(teamId, fromUserId),
1159
- this.db.teamGetMembers(teamId),
1160
- ]);
1161
-
1162
- if (!access) {
1163
- throw new ChatFatalError(
1164
- `User ${fromUserId} is not a participant of team ${teamId}`
1165
- );
1166
- }
1167
-
1168
- const newSessionData: SessionDescriptor = {
1169
- ...teamSessionCreateData(
1170
- teamId,
1171
- participants,
1172
- fromUserId,
1173
- title,
1174
- agentProfileId
1175
- ),
1176
- updated_at: new Date().toISOString(),
1177
- };
1178
-
1179
- // initialize the open session
1180
- const openSession = await OpenSession.initWithEmptySession(
1181
- this.db,
1182
- newSessionData,
1183
- this.xmcpUrl,
1184
- this.connectionManager
1185
- );
1186
- return { sessionId: newSessionData.session_uuid, openSession };
1187
- }
1188
-
1189
- /**
1190
- * Gracefully shutdown all sessions and clean up resources.
1191
- */
1192
- shutdown(): void {
1193
- logger.info(
1194
- `[SessionRegistry] Shutting down ` +
1195
- String(this.openSessions.size) +
1196
- ` sessions`
1197
- );
1198
-
1199
- // Create list of sessions to avoid concurrent modification
1200
- const sessionsToClose = Array.from(this.openSessions.keys());
1201
-
1202
- for (const sessionId of sessionsToClose) {
1203
- const session = this.openSessions.get(sessionId);
1204
- if (session) {
1205
- logger.debug(`[SessionRegistry] Closing session ${sessionId}`);
1206
- try {
1207
- // Trigger the session's cleanup
1208
- session.onEmpty();
1209
- } catch (error) {
1210
- logger.error(
1211
- `[SessionRegistry] Error closing session ${sessionId}:`,
1212
- error
1213
- );
1214
- }
1215
- }
1216
- }
1217
-
1218
- // Clear all maps
1219
- this.sessionUsers.clear();
1220
- this.userSessions.clear();
1221
- this.openSessions.clear();
1222
-
1223
- logger.info(`[SessionRegistry] Shutdown complete`);
1224
- }
1225
-
1226
- /**
1227
- * Handle user disconnect - clean up from all sessions.
1228
- * Called when a connection is closed to ensure proper cleanup.
1229
- */
1230
- async handleUserDisconnect(userId: string): Promise<void> {
1231
- logger.info(`[SessionRegistry] Handling disconnect for user ${userId}`);
1232
-
1233
- // If the user is a guest, remove his DB entry.
1234
- const guest = this.guests.get(userId);
1235
- if (guest) {
1236
- const session = this.openSessions.get(guest.guest_for_session);
1237
- if (session) {
1238
- session.removeParticipant(userId);
1239
- }
1240
- this.guests.delete(userId);
1241
- try {
1242
- logger.info(
1243
- `[SessionRegistry.handleUserDisconnect] deleting user ${userId}`
1244
- );
1245
- await this.db.deleteUser(userId);
1246
- } catch (e) {
1247
- logger.warn(`error removing guest ${userId}: ${getErrorString(e)}`);
1248
- }
1249
- }
1250
-
1251
- // Get all sessions the user is in (copy to avoid concurrent modification)
1252
- const userSessionIds = this.getInMemoryUserSessions(userId);
1253
- const sessionsToLeave = Array.from(userSessionIds);
1254
-
1255
- // Remove user from each session
1256
- for (const sessionId of sessionsToLeave) {
1257
- this.removeUserFromSessionMemory(userId, sessionId);
1258
- }
1259
-
1260
- logger.info(
1261
- `[SessionRegistry] User ${userId} removed` +
1262
- ` from ${String(sessionsToLeave.length)} sessions`
1263
- );
1264
- }
1265
-
1266
- /**
1267
- * Get all sessions for a user,
1268
- * including user solo sessions and team sessions.
1269
- * This will also create a default agent profile if none exists.
1270
- */
1271
- private async getAllAgentsAndSessionsByUser(
1272
- userId: string
1273
- ): Promise<[SessionData[], TeamInfo[], SavedAgentProfile[]]> {
1274
- const [userSessions, teamInfos, userAgents] = await Promise.all([
1275
- this.db.getUserSessions(userId),
1276
- this.db.getTeamInfosByUser(userId),
1277
- this.agentProfilesGetByUserOrDefault(userId),
1278
- ]);
1279
-
1280
- return [userSessions, teamInfos, userAgents];
1281
- }
1282
-
1283
- private async agentProfilesGetByUserOrDefault(
1284
- userId: string
1285
- ): Promise<SavedAgentProfile[]> {
1286
- const agentProfiles = await this.db.agentProfilesGetByUser(userId);
1287
- if (agentProfiles.length === 0) {
1288
- const profileName = DEFAULT_AGENT_PROFILE_NAME;
1289
- const profile = await this.db.createAgentProfile(
1290
- userId,
1291
- undefined,
1292
- profileName,
1293
- DEFAULT_AGENT_PROFILE
1294
- );
1295
- if (!profile) {
1296
- throw new Error(`No such agent profile: ${profileName}`);
1297
- }
1298
- return [profile];
1299
- }
1300
- return agentProfiles;
1301
- }
1302
-
1303
- /**
1304
- * Validates that an agent profile exists in the database.
1305
- */
1306
- private async validateSavedAgentProfile(
1307
- agentProfileId: string
1308
- ): Promise<SavedAgentProfile> {
1309
- const savedAgentProfile =
1310
- await this.db.getSavedAgentProfileById(agentProfileId);
1311
- if (!savedAgentProfile) {
1312
- throw new ChatFatalError(`No such agent profile: ${agentProfileId}`);
1313
- }
1314
- return savedAgentProfile;
1315
- }
1316
-
1317
- /**
1318
- * Validates that a user has permission to access a session.
1319
- */
1320
- async validateSessionAccess(
1321
- sessionId: string,
1322
- userId: string,
1323
- accessType?: "participant" | "owner"
1324
- ): Promise<boolean> {
1325
- const session = this.openSessions.get(sessionId);
1326
- if (session) {
1327
- // in memory session
1328
- const participants = session.getParticipants();
1329
- const role = participants.get(userId);
1330
- if (!role) {
1331
- // Check for guest access
1332
- const guest = this.guests.get(userId);
1333
- if (guest && guest.guest_for_session === sessionId) {
1334
- return !accessType || accessType === "participant";
1335
- }
1336
-
1337
- // Not authorized to join the session
1338
- return false;
1339
- } else {
1340
- return !accessType || role.role === accessType;
1341
- }
1342
- } else {
1343
- // fetch the session from database
1344
- const participants = await this.db.sessionGetParticipants(sessionId);
1345
- const role = participants.find((p) => p.user_uuid === userId)?.role;
1346
- if (!role) {
1347
- // Check for guest access
1348
- const guest = this.guests.get(userId);
1349
- logger.info(`[validateSessionAccess] guest: ${JSON.stringify(guest)}`);
1350
- if (guest && guest.guest_for_session === sessionId) {
1351
- return !accessType || accessType === "participant";
1352
- }
1353
-
1354
- return false;
1355
- } else {
1356
- return !accessType || role === accessType;
1357
- }
1358
- }
1359
- }
1360
-
1361
- /**
1362
- * Validates that a user has permission to access a team.
1363
- */
1364
- private async validateTeamAccess(
1365
- teamId: string,
1366
- userId: string,
1367
- accessType?: "participant" | "owner"
1368
- ): Promise<boolean> {
1369
- const participants = await this.db.teamGetMembers(teamId);
1370
- const role = participants.find((p) => p.user_uuid === userId)?.role;
1371
- if (!role) {
1372
- return false;
1373
- } else {
1374
- return !accessType || role === accessType;
1375
- }
1376
- }
1377
-
1378
- /**
1379
- * Read from DB if the session is not active
1380
- */
1381
- private async getSessionDescriptor(
1382
- sessionId: string
1383
- ): Promise<SessionDescriptor | undefined> {
1384
- const session = this.openSessions.get(sessionId);
1385
- if (session) {
1386
- return session.getDescriptor();
1387
- } else {
1388
- // fetch the session from database
1389
- return this.db.sessionGetDescriptorById(sessionId);
1390
- }
1391
- }
1392
- }
1393
-
1394
- export function userSessionCreateData(
1395
- ownerId: string,
1396
- title: string,
1397
- agentProfileId: string
1398
- ): SessionCreateData {
1399
- return {
1400
- session_uuid: Database.sessionNewUUID(),
1401
- title,
1402
- team_uuid: undefined,
1403
- agent_profile_uuid: agentProfileId,
1404
- user_uuid: ownerId,
1405
- agent_paused: false,
1406
- };
1407
- }
1408
-
1409
- export function teamSessionCreateData(
1410
- teamId: string,
1411
- participants: TeamParticipant[],
1412
- ownerId: string,
1413
- title: string,
1414
- agentProfileId: string
1415
- ): SessionCreateData {
1416
- return {
1417
- session_uuid: Database.sessionNewUUID(),
1418
- title,
1419
- team_uuid: teamId,
1420
- participants,
1421
- agent_profile_uuid: agentProfileId,
1422
- user_uuid: ownerId,
1423
- agent_paused: false,
1424
- };
1425
- }