@xalia/agent 0.5.7 → 0.6.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 (186) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +176 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -59
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/mcpServerManager.js +23 -24
  8. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  9. package/dist/agent/src/agent/nullPlatform.js +14 -0
  10. package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
  11. package/dist/agent/src/agent/promptProvider.js +63 -0
  12. package/dist/agent/src/agent/repeatLLM.js +5 -5
  13. package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
  14. package/dist/agent/src/agent/tokenAuth.js +7 -7
  15. package/dist/agent/src/agent/tools.js +1 -1
  16. package/dist/agent/src/chat/client/chatClient.js +733 -0
  17. package/dist/agent/src/chat/client/connection.js +209 -0
  18. package/dist/agent/src/chat/client/connection.test.js +188 -0
  19. package/dist/agent/src/chat/client/constants.js +5 -0
  20. package/dist/agent/src/chat/client/index.js +15 -0
  21. package/dist/agent/src/chat/client/interfaces.js +2 -0
  22. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  23. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  24. package/dist/agent/src/chat/client/teamManager.js +2 -0
  25. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  26. package/dist/agent/src/chat/data/dataModels.js +2 -0
  27. package/dist/agent/src/chat/data/database.js +749 -0
  28. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  29. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  30. package/dist/agent/src/chat/protocol/constants.js +50 -0
  31. package/dist/agent/src/chat/protocol/errors.js +22 -0
  32. package/dist/agent/src/chat/protocol/messages.js +110 -0
  33. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  34. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  35. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  36. package/dist/agent/src/chat/server/conversation.js +198 -0
  37. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  38. package/dist/agent/src/chat/server/openSession.js +869 -0
  39. package/dist/agent/src/chat/server/server.js +177 -0
  40. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  41. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  43. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  44. package/dist/agent/src/chat/server/tools.js +243 -0
  45. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  46. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  47. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  48. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  49. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  50. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  51. package/dist/agent/src/chat/utils/search.js +145 -0
  52. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  53. package/dist/agent/src/chat/utils/websocket.js +16 -0
  54. package/dist/agent/src/test/agent.test.js +332 -0
  55. package/dist/agent/src/test/approvalManager.test.js +58 -0
  56. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  57. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  58. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  59. package/dist/agent/src/test/context.test.js +83 -0
  60. package/dist/agent/src/test/conversation.test.js +89 -0
  61. package/dist/agent/src/test/db.test.js +271 -83
  62. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  63. package/dist/agent/src/test/dbTestTools.js +99 -0
  64. package/dist/agent/src/test/imageLoad.test.js +8 -7
  65. package/dist/agent/src/test/mcpServerManager.test.js +23 -20
  66. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  67. package/dist/agent/src/test/openaiStreaming.test.js +64 -35
  68. package/dist/agent/src/test/prompt.test.js +5 -4
  69. package/dist/agent/src/test/promptProvider.test.js +28 -0
  70. package/dist/agent/src/test/responseHandler.test.js +61 -0
  71. package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
  72. package/dist/agent/src/test/testTools.js +109 -0
  73. package/dist/agent/src/test/tools.test.js +31 -0
  74. package/dist/agent/src/tool/agentChat.js +21 -10
  75. package/dist/agent/src/tool/agentMain.js +1 -1
  76. package/dist/agent/src/tool/chatMain.js +241 -58
  77. package/dist/agent/src/tool/commandPrompt.js +22 -17
  78. package/dist/agent/src/tool/files.js +20 -16
  79. package/dist/agent/src/tool/nodePlatform.js +47 -3
  80. package/dist/agent/src/tool/options.js +4 -4
  81. package/dist/agent/src/tool/prompt.js +19 -13
  82. package/eslint.config.mjs +14 -1
  83. package/package.json +14 -6
  84. package/scripts/chat_server +8 -0
  85. package/scripts/setup_chat +7 -2
  86. package/scripts/shutdown_chat_server +3 -0
  87. package/scripts/test_chat +135 -17
  88. package/src/agent/agent.ts +283 -138
  89. package/src/agent/agentUtils.ts +143 -108
  90. package/src/agent/compressingContextManager.ts +164 -0
  91. package/src/agent/context.ts +268 -0
  92. package/src/agent/dummyLLM.ts +76 -8
  93. package/src/agent/iAgentEventHandler.ts +54 -0
  94. package/src/agent/iplatform.ts +1 -0
  95. package/src/agent/mcpServerManager.ts +35 -31
  96. package/src/agent/nullAgentEventHandler.ts +20 -0
  97. package/src/agent/nullPlatform.ts +13 -0
  98. package/src/agent/openAILLMStreaming.ts +26 -13
  99. package/src/agent/promptProvider.ts +87 -0
  100. package/src/agent/repeatLLM.ts +5 -5
  101. package/src/agent/sudoMcpServerManager.ts +30 -29
  102. package/src/agent/tokenAuth.ts +7 -7
  103. package/src/agent/tools.ts +3 -1
  104. package/src/chat/client/chatClient.ts +900 -0
  105. package/src/chat/client/connection.test.ts +241 -0
  106. package/src/chat/client/connection.ts +276 -0
  107. package/src/chat/client/constants.ts +3 -0
  108. package/src/chat/client/index.ts +18 -0
  109. package/src/chat/client/interfaces.ts +34 -0
  110. package/src/chat/client/responseHandler.ts +131 -0
  111. package/src/chat/client/sessionClient.ts +443 -0
  112. package/src/chat/client/teamManager.ts +29 -0
  113. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  114. package/src/chat/data/dataModels.ts +85 -0
  115. package/src/chat/data/database.ts +982 -0
  116. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  117. package/src/chat/protocol/connectionMessages.ts +49 -0
  118. package/src/chat/protocol/constants.ts +55 -0
  119. package/src/chat/protocol/errors.ts +16 -0
  120. package/src/chat/protocol/messages.ts +682 -0
  121. package/src/chat/server/README.md +127 -0
  122. package/src/chat/server/chatContextManager.ts +612 -0
  123. package/src/chat/server/connectionManager.test.ts +266 -0
  124. package/src/chat/server/connectionManager.ts +541 -0
  125. package/src/chat/server/conversation.ts +269 -0
  126. package/src/chat/server/errorUtils.ts +28 -0
  127. package/src/chat/server/openSession.ts +1332 -0
  128. package/src/chat/server/server.ts +177 -0
  129. package/src/chat/server/sessionFileManager.ts +239 -0
  130. package/src/chat/server/sessionRegistry.test.ts +138 -0
  131. package/src/chat/server/sessionRegistry.ts +1064 -0
  132. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  133. package/src/chat/server/tools.ts +265 -0
  134. package/src/chat/utils/agentSessionMap.ts +76 -0
  135. package/src/chat/utils/approvalManager.ts +111 -0
  136. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  137. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  138. package/src/chat/utils/htmlToText.ts +61 -0
  139. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  140. package/src/chat/utils/search.ts +139 -0
  141. package/src/chat/utils/userResolver.ts +48 -0
  142. package/src/chat/utils/websocket.ts +16 -0
  143. package/src/test/agent.test.ts +487 -0
  144. package/src/test/approvalManager.test.ts +73 -0
  145. package/src/test/chatContextManager.test.ts +521 -0
  146. package/src/test/clientServerConnection.test.ts +207 -0
  147. package/src/test/compressingContextManager.test.ts +82 -0
  148. package/src/test/context.test.ts +105 -0
  149. package/src/test/conversation.test.ts +109 -0
  150. package/src/test/db.test.ts +358 -89
  151. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  152. package/src/test/dbTestTools.ts +153 -0
  153. package/src/test/imageLoad.test.ts +7 -6
  154. package/src/test/mcpServerManager.test.ts +21 -16
  155. package/src/test/multiAsyncQueue.test.ts +125 -0
  156. package/src/test/openaiStreaming.test.ts +71 -36
  157. package/src/test/prompt.test.ts +4 -3
  158. package/src/test/promptProvider.test.ts +33 -0
  159. package/src/test/responseHandler.test.ts +78 -0
  160. package/src/test/sudoMcpServerManager.test.ts +32 -30
  161. package/src/test/testTools.ts +146 -0
  162. package/src/test/tools.test.ts +39 -0
  163. package/src/tool/agentChat.ts +26 -12
  164. package/src/tool/agentMain.ts +1 -1
  165. package/src/tool/chatMain.ts +292 -100
  166. package/src/tool/commandPrompt.ts +28 -19
  167. package/src/tool/files.ts +25 -19
  168. package/src/tool/nodePlatform.ts +52 -3
  169. package/src/tool/options.ts +4 -2
  170. package/src/tool/prompt.ts +22 -15
  171. package/test_data/dummyllm_script_crash.json +32 -0
  172. package/test_data/frog.png.b64 +1 -0
  173. package/vitest.config.ts +39 -0
  174. package/dist/agent/src/chat/client.js +0 -349
  175. package/dist/agent/src/chat/conversationManager.js +0 -392
  176. package/dist/agent/src/chat/db.js +0 -209
  177. package/dist/agent/src/chat/frontendClient.js +0 -74
  178. package/dist/agent/src/chat/server.js +0 -158
  179. package/src/chat/client.ts +0 -455
  180. package/src/chat/conversationManager.ts +0 -595
  181. package/src/chat/db.ts +0 -290
  182. package/src/chat/frontendClient.ts +0 -123
  183. package/src/chat/messages.ts +0 -235
  184. package/src/chat/server.ts +0 -177
  185. /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
  186. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,700 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionRegistry = void 0;
4
+ exports.userSessionDataCreate = userSessionDataCreate;
5
+ exports.teamSessionDataCreate = teamSessionDataCreate;
6
+ const uuid_1 = require("uuid");
7
+ const sdk_1 = require("@xalia/xmcp/sdk");
8
+ const messages_1 = require("../protocol/messages");
9
+ const openSession_1 = require("./openSession");
10
+ const database_1 = require("../data/database");
11
+ const userResolver_1 = require("../utils/userResolver");
12
+ const errors_1 = require("../protocol/errors");
13
+ const agentUtils_1 = require("../../agent/agentUtils");
14
+ const errorUtils_1 = require("./errorUtils");
15
+ const logger = (0, sdk_1.getLogger)();
16
+ class SessionRegistry {
17
+ constructor(db, connectionManager, llmUrl, xmcpUrl) {
18
+ this.db = db;
19
+ this.connectionManager = connectionManager;
20
+ this.llmUrl = llmUrl;
21
+ this.xmcpUrl = xmcpUrl;
22
+ // In memory session-user/user-session mappings
23
+ // Note: this mappings ONLY trackes online users and will
24
+ // be cleaned up when the user disconnects.
25
+ // sessionId -> userIds
26
+ this.sessionUsers = new Map();
27
+ // userId -> sessionIds
28
+ this.userSessions = new Map();
29
+ // Session instances
30
+ // sessionId -> OpenSession
31
+ this.openSessions = new Map();
32
+ }
33
+ /**
34
+ * Add user to session membership.
35
+ * Creates session tracking if it doesn't exist.
36
+ */
37
+ addUserToSessionMemory(userId, sessionId) {
38
+ logger.debug(`[SessionRegistry] Adding user ${userId} to session ${sessionId}`);
39
+ // Add to sessionUsers
40
+ if (!this.sessionUsers.has(sessionId)) {
41
+ this.sessionUsers.set(sessionId, new Set());
42
+ }
43
+ const sessionUserSet = this.sessionUsers.get(sessionId);
44
+ if (sessionUserSet) {
45
+ sessionUserSet.add(userId);
46
+ }
47
+ // Add to userSessions
48
+ if (!this.userSessions.has(userId)) {
49
+ this.userSessions.set(userId, new Set());
50
+ }
51
+ const userSessionSet = this.userSessions.get(userId);
52
+ if (userSessionSet) {
53
+ userSessionSet.add(sessionId);
54
+ }
55
+ }
56
+ /**
57
+ * Remove user from session membership.
58
+ * Cleans up empty sessions and triggers session cleanup if needed.
59
+ */
60
+ removeUserFromSessionMemory(userId, sessionId) {
61
+ logger.debug(`[SessionRegistry] Removing user ${userId} from session ${sessionId}`);
62
+ // Remove from sessionUsers
63
+ const sessionUserSet = this.sessionUsers.get(sessionId);
64
+ if (sessionUserSet) {
65
+ sessionUserSet.delete(userId);
66
+ if (sessionUserSet.size === 0) {
67
+ this.sessionUsers.delete(sessionId);
68
+ // Clean up session instance when empty
69
+ const session = this.openSessions.get(sessionId);
70
+ if (session) {
71
+ logger.debug(`[SessionRegistry] Triggering cleanup for empty session ` +
72
+ sessionId);
73
+ // The onEmpty callback will remove from openSessions map
74
+ logger.debug(`[SessionRegistry] Session ${sessionId} empty. Removing`);
75
+ session.onEmpty();
76
+ this.openSessions.delete(sessionId);
77
+ }
78
+ }
79
+ }
80
+ // Remove from userSessions
81
+ const userSessionSet = this.userSessions.get(userId);
82
+ if (userSessionSet) {
83
+ userSessionSet.delete(sessionId);
84
+ if (userSessionSet.size === 0) {
85
+ this.userSessions.delete(userId);
86
+ }
87
+ }
88
+ logger.info(`[SessionRegistry] User ${userId} removed from session ${sessionId}`);
89
+ }
90
+ /**
91
+ * Get all users in a session.
92
+ */
93
+ async getSessionUsers(sessionId) {
94
+ const users = await this.db.sessionGetParticipants(sessionId);
95
+ return new Set(users.map((u) => u.user_uuid));
96
+ }
97
+ /**
98
+ * Get all users in a session.
99
+ */
100
+ getInMemorySessionUsers(sessionId) {
101
+ return this.sessionUsers.get(sessionId) || new Set();
102
+ }
103
+ /**
104
+ * Get all sessions a user belongs to.
105
+ */
106
+ getUserSessions(userId) {
107
+ return this.userSessions.get(userId) || new Set();
108
+ }
109
+ /**
110
+ * Get all sessions a user belongs to in memory.
111
+ */
112
+ getInMemoryUserSessions(userId) {
113
+ return this.userSessions.get(userId) || new Set();
114
+ }
115
+ async processMessage(connectionId, userId, message) {
116
+ if ((0, messages_1.isClientControlMessage)(message)) {
117
+ // handle connection level message
118
+ await this.processClientControlMessage(connectionId, userId, message);
119
+ }
120
+ else {
121
+ // handle session level message
122
+ this.processSessionMessage(userId, message);
123
+ }
124
+ }
125
+ sendControlError(connectionId, clientMsgId, errorMessage) {
126
+ // TODO: Why is this managed at the transport level?
127
+ const errorMsg = {
128
+ type: "control_error",
129
+ message: errorMessage,
130
+ client_message_id: clientMsgId,
131
+ };
132
+ this.connectionManager.sendToConnection(connectionId, errorMsg);
133
+ }
134
+ async processClientControlMessage(connectionId, userId, message) {
135
+ switch (message.type) {
136
+ case "control_agent_profile_create":
137
+ await this.handleAgentProfileCreate(message, userId);
138
+ break;
139
+ case "control_agent_profile_delete":
140
+ // TODO:
141
+ throw new Error("not implemented yet");
142
+ case "control_get_session_list":
143
+ await this.handleGetSessionList(connectionId, userId, message);
144
+ break;
145
+ case "control_session_create":
146
+ await this.handleSessionCreate(connectionId, userId, message);
147
+ break;
148
+ case "control_session_join":
149
+ await this.handleSessionJoin(connectionId, userId, message);
150
+ break;
151
+ case "control_session_delete":
152
+ await this.handleSessionDelete(connectionId, userId, message);
153
+ break;
154
+ case "control_team_create":
155
+ await this.handleTeamCreate(connectionId, userId, message);
156
+ break;
157
+ case "control_add_team_user":
158
+ await this.handleAddTeamUser(connectionId, userId, message);
159
+ break;
160
+ case "control_remove_team_user":
161
+ await this.handleRemoveTeamUser(connectionId, userId, message);
162
+ break;
163
+ default: {
164
+ const exhaustive = message;
165
+ // unknown connection type should be a fatal error
166
+ throw new errors_1.ChatFatalError(`Unknown connection message type: ${String(exhaustive)}`);
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * Handle session_list request
172
+ */
173
+ async handleGetSessionList(connectionId, userId, message) {
174
+ try {
175
+ const [userSessions, teamSessions, userAgents] = await this.getAllAgentsAndSessionsByUser(userId);
176
+ const response = {
177
+ type: "control_session_list",
178
+ user_sessions: userSessions,
179
+ team_sessions: teamSessions,
180
+ user_agents: userAgents,
181
+ client_message_id: message.client_message_id,
182
+ };
183
+ this.connectionManager.sendToConnection(connectionId, response);
184
+ logger.info(`[ConnectionManager] Sent ` +
185
+ `${String(userSessions.length)} user sessions and ` +
186
+ `${String(teamSessions.length)} team sessions to user ${userId}`);
187
+ }
188
+ catch (error) {
189
+ logger.error(`[ConnectionManager] Failed to get session list:`, error);
190
+ this.sendControlError(connectionId, message.client_message_id, JSON.stringify(error));
191
+ }
192
+ }
193
+ async handleSessionDelete(connectionId, userId, message) {
194
+ const sessionId = message.target_session_id;
195
+ try {
196
+ // validate the session access
197
+ await this.validateSessionAccess(sessionId, userId, "owner");
198
+ // get users/team-uuid before deletion
199
+ const [users, teamUuid, sessionData] = await Promise.all([
200
+ this.getSessionUsers(sessionId),
201
+ this.db.sessionGetTeamUuid(sessionId),
202
+ this.db.sessionGetById(sessionId),
203
+ ]);
204
+ if (!sessionData) {
205
+ throw new errors_1.ChatFatalError(`No such session: ${sessionId}`);
206
+ }
207
+ // delete the session from database
208
+ await this.db.sessionDeleteById(sessionId);
209
+ // remove the session from memory (should be no thrown from here)
210
+ const session = this.openSessions.get(sessionId);
211
+ if (session) {
212
+ const users = this.getInMemorySessionUsers(sessionId);
213
+ for (const user of users) {
214
+ this.removeUserFromSessionMemory(user, sessionId);
215
+ }
216
+ }
217
+ this.openSessions.delete(sessionId);
218
+ // send confirmation to the client
219
+ const response = {
220
+ type: "control_session_deleted",
221
+ session_id: sessionId,
222
+ team_uuid: teamUuid,
223
+ agent_profile_uuid: sessionData.agent_profile_uuid,
224
+ client_message_id: message.client_message_id,
225
+ };
226
+ this.connectionManager.sendToUsers(users, response);
227
+ }
228
+ catch (error) {
229
+ logger.error(`[SessionRegistry] Error delete session ${sessionId}:`, error);
230
+ this.sendControlError(connectionId, message.client_message_id, String(error));
231
+ }
232
+ }
233
+ /**
234
+ * Handle team_create_request message - creates a new team.
235
+ * @param connectionId connection id
236
+ * @param userId user id
237
+ * @param message team create request message
238
+ */
239
+ async handleTeamCreate(connectionId, userId, message) {
240
+ try {
241
+ // Resolve all initial members in parallel
242
+ // this contains undefined to mark failed lookups
243
+ const resolvedMemberIds = await Promise.all(message.initial_members.map((member) => (0, userResolver_1.resolveUserIdentifier)(this.db, member)));
244
+ const failedLookups = [];
245
+ const validMemberIds = [];
246
+ const participants = [];
247
+ // filter out the owner and extract failed lookups
248
+ resolvedMemberIds.forEach((user, index) => {
249
+ if (user && user.uuid !== userId) {
250
+ validMemberIds.push(user.uuid);
251
+ participants.push({
252
+ user_uuid: user.uuid,
253
+ nickname: user.nickname || "",
254
+ email: user.email,
255
+ role: "participant",
256
+ });
257
+ }
258
+ else if (user === undefined) {
259
+ failedLookups.push(message.initial_members[index]);
260
+ }
261
+ });
262
+ // Create the team with initial participants
263
+ const teamUuid = await this.db.createTeamWithParticipants(message.team_name, userId, validMemberIds);
264
+ const response = {
265
+ type: "control_team_created",
266
+ team_uuid: teamUuid,
267
+ team_owner_uuid: userId,
268
+ team_name: message.team_name,
269
+ members: participants,
270
+ failed_lookups: failedLookups,
271
+ };
272
+ const members = new Set(validMemberIds);
273
+ members.add(userId);
274
+ this.connectionManager.sendToUsers(members, response);
275
+ }
276
+ catch (error) {
277
+ logger.error(`[SessionRegistry] Error creating team: ${String(error)}`);
278
+ this.sendControlError(connectionId, message.client_message_id, String(error));
279
+ }
280
+ }
281
+ /**
282
+ * Process session-scoped client message.
283
+ * Handles membership messages here, others to OpenSession.
284
+ */
285
+ processSessionMessage(userId, message) {
286
+ const sessionId = message.session_id;
287
+ logger.info(`[SessionRegistry] Processing message from user ${userId} in session ` +
288
+ sessionId);
289
+ const session = this.openSessions.get(sessionId);
290
+ if (!session) {
291
+ throw new errors_1.ChatFatalError(`Internal error: No such session ${sessionId}`);
292
+ }
293
+ // Forward all other messages to OpenSession
294
+ session.onClientSessionMessage(userId, message);
295
+ }
296
+ /**
297
+ * Handle add_user message - adds user to team.
298
+ */
299
+ async handleAddTeamUser(connectionId, fromUserId, message) {
300
+ // Validate permissions - only owner can add users
301
+ const access = await this.validateTeamAccess(message.target_team_id, fromUserId, "owner");
302
+ if (!access) {
303
+ this.sendControlError(connectionId, message.client_message_id, "Only team owner can add users");
304
+ return;
305
+ }
306
+ // Resolve user identifier
307
+ const user = await (0, userResolver_1.resolveUserIdentifier)(this.db, message.user_uuid_or_email);
308
+ if (!user) {
309
+ this.sendControlError(connectionId, message.client_message_id, "User not found");
310
+ return;
311
+ }
312
+ // Check if user is already a participant
313
+ const participants = await this.db.teamGetMembers(message.target_team_id);
314
+ if (participants.some((p) => p.user_uuid === user.uuid)) {
315
+ this.sendControlError(connectionId, message.client_message_id, "User is already a participant");
316
+ return;
317
+ }
318
+ // Update database
319
+ try {
320
+ await this.db.teamAddMember(message.target_team_id, user.uuid);
321
+ }
322
+ catch (error) {
323
+ this.sendControlError(connectionId, message.client_message_id, "Server Internal Error: cannot add user.");
324
+ logger.error(`[SessionRegistry] Error adding user ${user.uuid}` +
325
+ ` to team ${message.target_team_id}:`, error);
326
+ return;
327
+ }
328
+ // add user to related active sessions
329
+ const sessions = await this.db.teamGetSessions(message.target_team_id);
330
+ for (const sessionData of sessions) {
331
+ const session = this.openSessions.get(sessionData.session_uuid);
332
+ if (session) {
333
+ session.addParticipant(user.uuid, {
334
+ user_uuid: user.uuid,
335
+ nickname: user.nickname || "",
336
+ email: user.email,
337
+ role: "participant",
338
+ });
339
+ }
340
+ }
341
+ logger.info(`[SessionRegistry] User ${user.uuid}` +
342
+ `added to team ${message.target_team_id}`);
343
+ }
344
+ /**
345
+ * Handle remove_user message - removes user from session membership.
346
+ * Only session owner can remove users.
347
+ */
348
+ async handleRemoveTeamUser(connectionId, fromUserId, message) {
349
+ // Validate permissions - only owner can remove users
350
+ const access = await this.validateTeamAccess(message.target_team_id, fromUserId, "owner");
351
+ if (!access) {
352
+ this.sendControlError(connectionId, message.client_message_id, "Only team owner can remove users");
353
+ return;
354
+ }
355
+ // Resolve user identifier
356
+ const user = await (0, userResolver_1.resolveUserIdentifier)(this.db, message.user_uuid_or_email);
357
+ if (!user) {
358
+ this.sendControlError(connectionId, message.client_message_id, "User not found");
359
+ return;
360
+ }
361
+ // owner cannot remove her/himself
362
+ if (user.uuid === fromUserId) {
363
+ this.sendControlError(connectionId, message.client_message_id, "Owner cannot remove herself/himself");
364
+ return;
365
+ }
366
+ // Check if user is actually a participant
367
+ const participants = await this.db.teamGetMembers(message.target_team_id);
368
+ if (!participants.some((p) => p.user_uuid === user.uuid)) {
369
+ this.sendControlError(connectionId, message.client_message_id, "User is not a participant");
370
+ return;
371
+ }
372
+ // Remove from database
373
+ try {
374
+ await this.db.teamRemoveMember(message.target_team_id, user.uuid);
375
+ }
376
+ catch (error) {
377
+ this.sendControlError(connectionId, message.client_message_id, "Server Internal Error: cannot remove user.");
378
+ logger.error(`[SessionRegistry] Error removing user ${user.uuid}` +
379
+ ` from team ${message.target_team_id}:`, error);
380
+ return;
381
+ }
382
+ // Update OpenSession's participant map and in memory tracking
383
+ const sessions = await this.db.teamGetSessions(message.target_team_id);
384
+ for (const sessionData of sessions) {
385
+ const session = this.openSessions.get(sessionData.session_uuid);
386
+ if (session) {
387
+ session.removeParticipant(user.uuid);
388
+ this.removeUserFromSessionMemory(user.uuid, sessionData.session_uuid);
389
+ }
390
+ }
391
+ logger.info(`[SessionRegistry] User ${user.uuid} ` +
392
+ `removed from team ${message.target_team_id}`);
393
+ }
394
+ /**
395
+ * Get session instance, if the session has not been initialized,
396
+ * it will be.
397
+ */
398
+ async getAndActivateSession(sessionId) {
399
+ if (this.openSessions.has(sessionId)) {
400
+ logger.info(`[SessionRegistry] Session ${sessionId} already exists`);
401
+ const openSession = this.openSessions.get(sessionId);
402
+ if (!openSession) {
403
+ throw new errors_1.ChatFatalError(`Internal error: No such session: ${sessionId}`);
404
+ }
405
+ return openSession;
406
+ }
407
+ else {
408
+ logger.info(`[SessionRegistry] loading session ${sessionId}`);
409
+ return openSession_1.OpenSession.initWithExistingSession(this.db, sessionId, this.llmUrl, this.xmcpUrl, this.connectionManager);
410
+ }
411
+ }
412
+ /**
413
+ * Handle user joining a session.
414
+ * Activates the session if not already active.
415
+ * return the session info to the client joining the session.
416
+ */
417
+ async handleSessionJoin(connectionId, userId, message) {
418
+ const sessionId = message.target_session_id;
419
+ logger.info(`[SessionRegistry] Joining session ${sessionId} for user ${userId}`);
420
+ try {
421
+ // Validate session access permissions
422
+ const access = await this.validateSessionAccess(sessionId, userId);
423
+ if (!access) {
424
+ throw new errors_1.ChatFatalError(`User ${userId} is not authorized to ` + `join session ${sessionId}`);
425
+ }
426
+ // get or create the session
427
+ const session = await this.getAndActivateSession(sessionId);
428
+ if (!session) {
429
+ // this in theory should not happen
430
+ // since we have validated the access
431
+ throw new errors_1.ChatFatalError(`Server internal error: ` + `failed to load session ${sessionId}`);
432
+ }
433
+ // To this point, there should be no error thrown.
434
+ // Update in-memory session-user/user-session mappings
435
+ this.addUserToSessionMemory(userId, sessionId);
436
+ // Register session immediately
437
+ if (!this.openSessions.has(sessionId)) {
438
+ this.openSessions.set(sessionId, session);
439
+ }
440
+ // pass the message to the session to handle the rest
441
+ session.sendSessionData(connectionId, message.client_message_id);
442
+ }
443
+ catch (error) {
444
+ logger.error(`[SessionRegistry] Error handling user join: ${String(error)}`);
445
+ this.sendControlError(connectionId, message.client_message_id, String(error));
446
+ }
447
+ }
448
+ async handleAgentProfileCreate(message, from) {
449
+ // get agent profile from template
450
+ let agentProfileFromTemplate = undefined;
451
+ if (message.template_name) {
452
+ const template = await this.db.agentTemplateGetByName(message.template_name);
453
+ if (!template) {
454
+ throw new Error(`template ${message.template_name} not found`);
455
+ }
456
+ agentProfileFromTemplate = template.agent_profile;
457
+ }
458
+ const newAgentProfile = agentProfileFromTemplate || {
459
+ model: agentUtils_1.DEFAULT_LLM_MODEL,
460
+ system_prompt: sdk_1.DEFAULT_AGENT_PROFILE_SYSTEM_PROMPT,
461
+ mcp_settings: {},
462
+ };
463
+ const team_uuid = message.team_uuid || undefined;
464
+ if (team_uuid) {
465
+ // TODO: should be able to reconstruct the full SavedAgentProfile in one
466
+ // call.
467
+ const savedAgentProfile = await this.db.createAgentProfile(undefined, team_uuid, message.title, newAgentProfile);
468
+ if (!savedAgentProfile) {
469
+ throw new Error("failed creating team agent profile (createAgentProfile)");
470
+ }
471
+ // Broadcast the new AgentProfile to all participants
472
+ const participants = await this.db.teamGetMembers(team_uuid);
473
+ this.connectionManager.sendToUsers(new Set(participants.map((p) => p.user_uuid)), { type: "control_agent_profile_created", profile: savedAgentProfile });
474
+ return savedAgentProfile.uuid;
475
+ }
476
+ // User AgentProfile
477
+ const savedAgentProfile = await this.db.createAgentProfile(from, undefined, message.title, newAgentProfile);
478
+ if (!savedAgentProfile) {
479
+ throw new Error("failed creating agent profile (createAgentProfile)");
480
+ }
481
+ // Send the new AgentProfile to the user
482
+ this.connectionManager.sendToUsers(new Set([from]), {
483
+ type: "control_agent_profile_created",
484
+ profile: savedAgentProfile,
485
+ });
486
+ return savedAgentProfile.uuid;
487
+ }
488
+ /**
489
+ * Create a new session for a user.
490
+ * - create session in database (via `newSession`)
491
+ * - create an OpenSession instance
492
+ * - return the session info
493
+ */
494
+ async handleSessionCreate(connectionId, fromUserId, message) {
495
+ try {
496
+ // If agent not given, create one and inform the client
497
+ if (!message.agent_profile_id) {
498
+ logger.info("[handleSessionCreate] creating new AgentProfile");
499
+ // Create AgentProfile and inform the client
500
+ message.agent_profile_id = await this.handleAgentProfileCreate({
501
+ type: "control_agent_profile_create",
502
+ title: "New Agent " + (0, uuid_1.v4)(),
503
+ user_uuid: fromUserId,
504
+ team_uuid: message.team_id,
505
+ }, fromUserId);
506
+ }
507
+ // Create new session and get its instance
508
+ const { openSession, sessionId } = message.team_id
509
+ ? await this.newTeamSession(fromUserId, message.team_id, message.title, message.agent_profile_id)
510
+ : await this.newUserSession(fromUserId, message.title, message.agent_profile_id);
511
+ // there should be no further error thrown from now.
512
+ // Register session immediately
513
+ this.openSessions.set(sessionId, openSession);
514
+ // add owner to session memory
515
+ this.addUserToSessionMemory(fromUserId, sessionId);
516
+ // send session info to the connection
517
+ openSession.sendSessionData(connectionId, message.client_message_id);
518
+ logger.info(`[SessionRegistry] new session ${sessionId}:` +
519
+ ` ${message.title} for ${fromUserId}`);
520
+ }
521
+ catch (error) {
522
+ const errStr = (0, errorUtils_1.getErrorString)(error);
523
+ logger.error(`[SessionRegistry] Error in session create: ${errStr}`);
524
+ this.sendControlError(connectionId, message.client_message_id, errStr);
525
+ }
526
+ }
527
+ /**
528
+ * Create a new user session.
529
+ */
530
+ async newUserSession(ownerId, title, agentProfileId) {
531
+ // validate the agent profile
532
+ await this.validateSavedAgentProfile(agentProfileId);
533
+ const newSessionData = {
534
+ ...userSessionDataCreate(ownerId, title, agentProfileId),
535
+ updated_at: new Date().toISOString(),
536
+ };
537
+ const openSession = await openSession_1.OpenSession.initWithEmptySession(this.db, newSessionData, this.llmUrl, this.xmcpUrl, this.connectionManager);
538
+ return { sessionId: newSessionData.session_uuid, openSession };
539
+ }
540
+ /**
541
+ * Create a new team session.
542
+ */
543
+ async newTeamSession(fromUserId, teamId, title, agentProfileId) {
544
+ // validate agent profile and team access
545
+ const [_savedAgentProfile, access] = await Promise.all([
546
+ this.validateSavedAgentProfile(agentProfileId),
547
+ this.validateTeamAccess(teamId, fromUserId),
548
+ ]);
549
+ if (!access) {
550
+ throw new errors_1.ChatFatalError(`User ${fromUserId} is not a participant of team ${teamId}`);
551
+ }
552
+ const newSessionData = {
553
+ ...teamSessionDataCreate(teamId, fromUserId, title, agentProfileId),
554
+ updated_at: new Date().toISOString(),
555
+ };
556
+ // initialize the open session
557
+ const openSession = await openSession_1.OpenSession.initWithEmptySession(this.db, newSessionData, this.llmUrl, this.xmcpUrl, this.connectionManager);
558
+ return { sessionId: newSessionData.session_uuid, openSession };
559
+ }
560
+ /**
561
+ * Gracefully shutdown all sessions and clean up resources.
562
+ */
563
+ shutdown() {
564
+ logger.info(`[SessionRegistry] Shutting down ` +
565
+ String(this.openSessions.size) +
566
+ ` sessions`);
567
+ // Create list of sessions to avoid concurrent modification
568
+ const sessionsToClose = Array.from(this.openSessions.keys());
569
+ for (const sessionId of sessionsToClose) {
570
+ const session = this.openSessions.get(sessionId);
571
+ if (session) {
572
+ logger.debug(`[SessionRegistry] Closing session ${sessionId}`);
573
+ try {
574
+ // Trigger the session's cleanup
575
+ session.onEmpty();
576
+ }
577
+ catch (error) {
578
+ logger.error(`[SessionRegistry] Error closing session ${sessionId}:`, error);
579
+ }
580
+ }
581
+ }
582
+ // Clear all maps
583
+ this.sessionUsers.clear();
584
+ this.userSessions.clear();
585
+ this.openSessions.clear();
586
+ logger.info(`[SessionRegistry] Shutdown complete`);
587
+ }
588
+ /**
589
+ * Handle user disconnect - clean up from all sessions.
590
+ * Called when a connection is closed to ensure proper cleanup.
591
+ */
592
+ handleUserDisconnect(userId) {
593
+ logger.info(`[SessionRegistry] Handling disconnect for user ${userId}`);
594
+ // Get all sessions the user is in (copy to avoid concurrent modification)
595
+ const userSessionIds = this.getInMemoryUserSessions(userId);
596
+ const sessionsToLeave = Array.from(userSessionIds);
597
+ // Remove user from each session
598
+ for (const sessionId of sessionsToLeave) {
599
+ this.removeUserFromSessionMemory(userId, sessionId);
600
+ }
601
+ logger.info(`[SessionRegistry] User ${userId} removed` +
602
+ ` from ${String(sessionsToLeave.length)} sessions`);
603
+ }
604
+ /**
605
+ * Get all sessions for a user,
606
+ * including user solo sessions and team sessions.
607
+ * This will also create a default agent profile if none exists.
608
+ */
609
+ async getAllAgentsAndSessionsByUser(userId) {
610
+ return Promise.all([
611
+ this.db.getUserSessions(userId),
612
+ this.db.getTeamInfosByUser(userId),
613
+ this.agentProfilesGetByUserOrDefault(userId),
614
+ ]);
615
+ }
616
+ async agentProfilesGetByUserOrDefault(userId) {
617
+ const agentProfiles = await this.db.agentProfilesGetByUser(userId);
618
+ if (agentProfiles.length === 0) {
619
+ const profileName = sdk_1.DEFAULT_AGENT_PROFILE_NAME;
620
+ const profile = await this.db.createAgentProfile(userId, undefined, profileName, sdk_1.DEFAULT_AGENT_PROFILE);
621
+ if (!profile) {
622
+ throw new Error(`No such agent profile: ${profileName}`);
623
+ }
624
+ return [profile];
625
+ }
626
+ return agentProfiles;
627
+ }
628
+ /**
629
+ * Validates that an agent profile exists in the database.
630
+ */
631
+ async validateSavedAgentProfile(agentProfileId) {
632
+ const savedAgentProfile = await this.db.getSavedAgentProfileById(agentProfileId);
633
+ if (!savedAgentProfile) {
634
+ throw new errors_1.ChatFatalError(`No such agent profile: ${agentProfileId}`);
635
+ }
636
+ return savedAgentProfile;
637
+ }
638
+ /**
639
+ * Validates that a user has permission to access a session.
640
+ */
641
+ async validateSessionAccess(sessionId, userId, accessType) {
642
+ const session = this.openSessions.get(sessionId);
643
+ if (session) {
644
+ // in memory session
645
+ const participants = session.getParticipants();
646
+ const role = participants.get(userId);
647
+ if (!role) {
648
+ return false;
649
+ }
650
+ else {
651
+ return !accessType || role.role === accessType;
652
+ }
653
+ }
654
+ else {
655
+ // fetch the session from database
656
+ const participants = await this.db.sessionGetParticipants(sessionId);
657
+ const role = participants.find((p) => p.user_uuid === userId)?.role;
658
+ if (!role) {
659
+ return false;
660
+ }
661
+ else {
662
+ return !accessType || role === accessType;
663
+ }
664
+ }
665
+ }
666
+ /**
667
+ * Validates that a user has permission to access a team.
668
+ */
669
+ async validateTeamAccess(teamId, userId, accessType) {
670
+ const participants = await this.db.teamGetMembers(teamId);
671
+ const role = participants.find((p) => p.user_uuid === userId)?.role;
672
+ if (!role) {
673
+ return false;
674
+ }
675
+ else {
676
+ return !accessType || role === accessType;
677
+ }
678
+ }
679
+ }
680
+ exports.SessionRegistry = SessionRegistry;
681
+ function userSessionDataCreate(ownerId, title, agentProfileId) {
682
+ return {
683
+ session_uuid: database_1.Database.sessionNewUUID(),
684
+ title,
685
+ team_uuid: undefined,
686
+ agent_profile_uuid: agentProfileId,
687
+ workspace: undefined,
688
+ user_uuid: ownerId,
689
+ };
690
+ }
691
+ function teamSessionDataCreate(teamId, ownerId, title, agentProfileId) {
692
+ return {
693
+ session_uuid: database_1.Database.sessionNewUUID(),
694
+ title,
695
+ team_uuid: teamId,
696
+ agent_profile_uuid: agentProfileId,
697
+ workspace: undefined,
698
+ user_uuid: ownerId,
699
+ };
700
+ }