@xalia/agent 0.5.8 → 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 (185) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +173 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -53
  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/iAgentEventHandler.js +2 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +22 -23
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  10. package/dist/agent/src/agent/nullPlatform.js +14 -0
  11. package/dist/agent/src/agent/openAILLMStreaming.js +12 -7
  12. package/dist/agent/src/agent/promptProvider.js +63 -0
  13. package/dist/agent/src/agent/repeatLLM.js +5 -5
  14. package/dist/agent/src/agent/sudoMcpServerManager.js +11 -9
  15. package/dist/agent/src/agent/tokenAuth.js +7 -7
  16. package/dist/agent/src/agent/tools.js +1 -1
  17. package/dist/agent/src/chat/client/chatClient.js +733 -0
  18. package/dist/agent/src/chat/client/connection.js +209 -0
  19. package/dist/agent/src/chat/client/connection.test.js +188 -0
  20. package/dist/agent/src/chat/client/constants.js +5 -0
  21. package/dist/agent/src/chat/client/index.js +15 -0
  22. package/dist/agent/src/chat/client/interfaces.js +2 -0
  23. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  24. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  25. package/dist/agent/src/chat/client/teamManager.js +2 -0
  26. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  27. package/dist/agent/src/chat/data/dataModels.js +2 -0
  28. package/dist/agent/src/chat/data/database.js +749 -0
  29. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  30. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  31. package/dist/agent/src/chat/protocol/constants.js +50 -0
  32. package/dist/agent/src/chat/protocol/errors.js +22 -0
  33. package/dist/agent/src/chat/protocol/messages.js +110 -0
  34. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  35. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  36. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  37. package/dist/agent/src/chat/server/conversation.js +198 -0
  38. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  39. package/dist/agent/src/chat/server/openSession.js +869 -0
  40. package/dist/agent/src/chat/server/server.js +177 -0
  41. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  43. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  44. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  45. package/dist/agent/src/chat/server/tools.js +243 -0
  46. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  47. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  48. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  49. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  50. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  51. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  52. package/dist/agent/src/chat/utils/search.js +145 -0
  53. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  54. package/dist/agent/src/chat/{websocket.js → utils/websocket.js} +2 -0
  55. package/dist/agent/src/test/agent.test.js +332 -0
  56. package/dist/agent/src/test/approvalManager.test.js +58 -0
  57. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  58. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  59. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  60. package/dist/agent/src/test/context.test.js +83 -0
  61. package/dist/agent/src/test/conversation.test.js +89 -0
  62. package/dist/agent/src/test/db.test.js +262 -90
  63. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  64. package/dist/agent/src/test/dbTestTools.js +99 -0
  65. package/dist/agent/src/test/imageLoad.test.js +8 -7
  66. package/dist/agent/src/test/mcpServerManager.test.js +21 -18
  67. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  68. package/dist/agent/src/test/openaiStreaming.test.js +12 -11
  69. package/dist/agent/src/test/prompt.test.js +5 -4
  70. package/dist/agent/src/test/promptProvider.test.js +28 -0
  71. package/dist/agent/src/test/responseHandler.test.js +61 -0
  72. package/dist/agent/src/test/sudoMcpServerManager.test.js +14 -12
  73. package/dist/agent/src/test/testTools.js +109 -0
  74. package/dist/agent/src/test/tools.test.js +31 -0
  75. package/dist/agent/src/tool/agentChat.js +21 -10
  76. package/dist/agent/src/tool/agentMain.js +1 -1
  77. package/dist/agent/src/tool/chatMain.js +235 -58
  78. package/dist/agent/src/tool/commandPrompt.js +15 -9
  79. package/dist/agent/src/tool/files.js +20 -16
  80. package/dist/agent/src/tool/nodePlatform.js +47 -3
  81. package/dist/agent/src/tool/options.js +4 -4
  82. package/dist/agent/src/tool/prompt.js +19 -13
  83. package/eslint.config.mjs +14 -1
  84. package/package.json +14 -6
  85. package/scripts/chat_server +8 -0
  86. package/scripts/setup_chat +7 -2
  87. package/scripts/shutdown_chat_server +3 -0
  88. package/scripts/test_chat +135 -17
  89. package/src/agent/agent.ts +270 -135
  90. package/src/agent/agentUtils.ts +136 -95
  91. package/src/agent/compressingContextManager.ts +164 -0
  92. package/src/agent/context.ts +268 -0
  93. package/src/agent/dummyLLM.ts +76 -8
  94. package/src/agent/iAgentEventHandler.ts +54 -0
  95. package/src/agent/iplatform.ts +1 -0
  96. package/src/agent/mcpServerManager.ts +32 -30
  97. package/src/agent/nullAgentEventHandler.ts +20 -0
  98. package/src/agent/nullPlatform.ts +13 -0
  99. package/src/agent/openAILLMStreaming.ts +12 -6
  100. package/src/agent/promptProvider.ts +87 -0
  101. package/src/agent/repeatLLM.ts +5 -5
  102. package/src/agent/sudoMcpServerManager.ts +13 -11
  103. package/src/agent/tokenAuth.ts +7 -7
  104. package/src/agent/tools.ts +3 -1
  105. package/src/chat/client/chatClient.ts +900 -0
  106. package/src/chat/client/connection.test.ts +241 -0
  107. package/src/chat/client/connection.ts +276 -0
  108. package/src/chat/client/constants.ts +3 -0
  109. package/src/chat/client/index.ts +18 -0
  110. package/src/chat/client/interfaces.ts +34 -0
  111. package/src/chat/client/responseHandler.ts +131 -0
  112. package/src/chat/client/sessionClient.ts +443 -0
  113. package/src/chat/client/teamManager.ts +29 -0
  114. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  115. package/src/chat/data/dataModels.ts +85 -0
  116. package/src/chat/data/database.ts +982 -0
  117. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  118. package/src/chat/protocol/connectionMessages.ts +49 -0
  119. package/src/chat/protocol/constants.ts +55 -0
  120. package/src/chat/protocol/errors.ts +16 -0
  121. package/src/chat/protocol/messages.ts +682 -0
  122. package/src/chat/server/README.md +127 -0
  123. package/src/chat/server/chatContextManager.ts +612 -0
  124. package/src/chat/server/connectionManager.test.ts +266 -0
  125. package/src/chat/server/connectionManager.ts +541 -0
  126. package/src/chat/server/conversation.ts +269 -0
  127. package/src/chat/server/errorUtils.ts +28 -0
  128. package/src/chat/server/openSession.ts +1332 -0
  129. package/src/chat/server/server.ts +177 -0
  130. package/src/chat/server/sessionFileManager.ts +239 -0
  131. package/src/chat/server/sessionRegistry.test.ts +138 -0
  132. package/src/chat/server/sessionRegistry.ts +1064 -0
  133. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  134. package/src/chat/server/tools.ts +265 -0
  135. package/src/chat/utils/agentSessionMap.ts +76 -0
  136. package/src/chat/utils/approvalManager.ts +111 -0
  137. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  138. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  139. package/src/chat/utils/htmlToText.ts +61 -0
  140. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  141. package/src/chat/utils/search.ts +139 -0
  142. package/src/chat/utils/userResolver.ts +48 -0
  143. package/src/chat/{websocket.ts → utils/websocket.ts} +2 -0
  144. package/src/test/agent.test.ts +487 -0
  145. package/src/test/approvalManager.test.ts +73 -0
  146. package/src/test/chatContextManager.test.ts +521 -0
  147. package/src/test/clientServerConnection.test.ts +207 -0
  148. package/src/test/compressingContextManager.test.ts +82 -0
  149. package/src/test/context.test.ts +105 -0
  150. package/src/test/conversation.test.ts +109 -0
  151. package/src/test/db.test.ts +351 -103
  152. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  153. package/src/test/dbTestTools.ts +153 -0
  154. package/src/test/imageLoad.test.ts +7 -6
  155. package/src/test/mcpServerManager.test.ts +19 -14
  156. package/src/test/multiAsyncQueue.test.ts +125 -0
  157. package/src/test/openaiStreaming.test.ts +11 -10
  158. package/src/test/prompt.test.ts +4 -3
  159. package/src/test/promptProvider.test.ts +33 -0
  160. package/src/test/responseHandler.test.ts +78 -0
  161. package/src/test/sudoMcpServerManager.test.ts +22 -15
  162. package/src/test/testTools.ts +146 -0
  163. package/src/test/tools.test.ts +39 -0
  164. package/src/tool/agentChat.ts +26 -12
  165. package/src/tool/agentMain.ts +1 -1
  166. package/src/tool/chatMain.ts +283 -100
  167. package/src/tool/commandPrompt.ts +25 -9
  168. package/src/tool/files.ts +25 -19
  169. package/src/tool/nodePlatform.ts +52 -3
  170. package/src/tool/options.ts +4 -2
  171. package/src/tool/prompt.ts +22 -15
  172. package/test_data/dummyllm_script_crash.json +32 -0
  173. package/test_data/frog.png.b64 +1 -0
  174. package/vitest.config.ts +39 -0
  175. package/dist/agent/src/chat/client.js +0 -310
  176. package/dist/agent/src/chat/conversationManager.js +0 -502
  177. package/dist/agent/src/chat/db.js +0 -218
  178. package/dist/agent/src/chat/messages.js +0 -29
  179. package/dist/agent/src/chat/server.js +0 -158
  180. package/src/chat/client.ts +0 -445
  181. package/src/chat/conversationManager.ts +0 -730
  182. package/src/chat/db.ts +0 -304
  183. package/src/chat/messages.ts +0 -266
  184. package/src/chat/server.ts +0 -177
  185. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionManager = void 0;
4
+ const uuid_1 = require("uuid");
5
+ const sdk_1 = require("@xalia/xmcp/sdk");
6
+ const errors_1 = require("../protocol/errors");
7
+ const logger = (0, sdk_1.getLogger)();
8
+ const HEARTBEAT_INTERVAL_MS = parseInt(process.env["HEARTBEAT_INTERVAL_MS"] || String(30 * 1000));
9
+ class Connection {
10
+ constructor(ws, userId, apiKey, heartbeat) {
11
+ this.ws = ws;
12
+ this.userId = userId;
13
+ this.apiKey = apiKey;
14
+ this.heartbeat = heartbeat;
15
+ this.lastMessageTime = Date.now();
16
+ }
17
+ }
18
+ /**
19
+ * Manages WebSocket connections and handles protocol-level message routing.
20
+ *
21
+ * - Connection handshake
22
+ * - Connection lifecycle management
23
+ * - Message routing between connections and sessions
24
+ * - Connection-to-user
25
+ */
26
+ class ConnectionManager {
27
+ // this is a singleton object and can only be initialized by .init method
28
+ constructor(apiKeyManager, sessionMessageProcessor) {
29
+ this.apiKeyManager = apiKeyManager;
30
+ this.sessionMessageProcessor = sessionMessageProcessor;
31
+ // Connections by id
32
+ this.connections = new Map();
33
+ // userId -> connectionIds
34
+ this.userToConnections = new Map();
35
+ }
36
+ static init(apiKeyManager, createMsgProcessor) {
37
+ const connManager = new ConnectionManager(apiKeyManager, {});
38
+ connManager.sessionMessageProcessor = createMsgProcessor(connManager);
39
+ return connManager;
40
+ }
41
+ /**
42
+ * Handle new WebSocket connection with handshake protocol
43
+ * TODO: proper error code returning
44
+ */
45
+ async handleConnection(webSocket, apiKey, req) {
46
+ const connectionId = (0, uuid_1.v4)();
47
+ logger.info(`[ConnectionManager] New connection: ${connectionId}`);
48
+ try {
49
+ // Verify API key
50
+ const userData = await this.apiKeyManager.verifyApiKey(apiKey);
51
+ if (!userData) {
52
+ logger.error(`[ConnectionManager] Invalid API key: ${apiKey}`);
53
+ throw new errors_1.ChatFatalError("invalid api key");
54
+ }
55
+ // Heartbeat interval
56
+ const heartbeat = setInterval(this.onHeartbeat.bind(this, connectionId), HEARTBEAT_INTERVAL_MS);
57
+ // Register connection
58
+ this.connections.set(connectionId, new Connection(webSocket, userData.uuid, apiKey, heartbeat));
59
+ // Add to user connections
60
+ if (!this.userToConnections.has(userData.uuid)) {
61
+ this.userToConnections.set(userData.uuid, new Set());
62
+ }
63
+ const userConnections = this.userToConnections.get(userData.uuid);
64
+ if (userConnections) {
65
+ userConnections.add(connectionId);
66
+ }
67
+ // Setup connection handlers
68
+ this.setupConnectionHandlers(connectionId, webSocket);
69
+ // Send connection_ready after WebSocket is fully open
70
+ const clientVersion = req?.headers["x-client-version"] || "unknown";
71
+ const response = {
72
+ t: "ready",
73
+ c_id: connectionId,
74
+ user_uuid: userData.uuid,
75
+ };
76
+ // Check if connection still exists before sending
77
+ if (this.connections.has(connectionId)) {
78
+ this.sendConnectionReadyMessage(connectionId, response);
79
+ }
80
+ else {
81
+ logger.warn(`[ConnectionManager] Connection ${connectionId} was closed` +
82
+ ` before ready message could be sent`);
83
+ }
84
+ logger.info(`[ConnectionManager] Connection ${connectionId} registered ` +
85
+ `for user ${userData.uuid}, version: ${clientVersion}`);
86
+ }
87
+ catch (error) {
88
+ const errorMessage = error instanceof Error ? error.message : String(error);
89
+ logger.error(`[ConnectionManager] Failed to setup connection ${connectionId}:`, errorMessage);
90
+ // Send error to client if possible
91
+ this.sendErrorDirect(webSocket, connectionId, errorMessage);
92
+ // Force cleanup of any partial state
93
+ this.forceCleanupConnection(connectionId);
94
+ // Close WebSocket - this might trigger handlers if they were attached,
95
+ // but we've already cleaned up state above as a safeguard
96
+ try {
97
+ webSocket.close(4000, "Connection setup failed");
98
+ }
99
+ catch (closeError) {
100
+ logger.error(`[ConnectionManager] Error closing WebSocket ${connectionId}:`, closeError);
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Send connection_ready message with unified error handling
106
+ */
107
+ sendConnectionReadyMessage(connectionId, response) {
108
+ try {
109
+ this.sendConnectionMessage(connectionId, response);
110
+ }
111
+ catch (error) {
112
+ logger.error(`[ConnectionManager] Failed connection_ready to ${connectionId}:`, error);
113
+ this.forceCleanupConnection(connectionId);
114
+ }
115
+ }
116
+ /**
117
+ * Send error message to connection directly.
118
+ * This is used when the connection is possibly not yet registered.
119
+ */
120
+ sendErrorDirect(ws, connectionId, errorMessage) {
121
+ const errorMsg = {
122
+ t: "error",
123
+ e: errorMessage,
124
+ };
125
+ try {
126
+ if (ws.readyState === ws.OPEN) {
127
+ ws.send(JSON.stringify(errorMsg));
128
+ }
129
+ else {
130
+ logger.error(`[ConnectionManager] Conn not open sending error ${connectionId}`);
131
+ }
132
+ }
133
+ catch (error) {
134
+ logger.error(`[ConnectionManager] Failed to send error to connection` +
135
+ ` ${connectionId}:`, error);
136
+ }
137
+ }
138
+ /**
139
+ * Setup message and lifecycle handlers for a connection
140
+ */
141
+ setupConnectionHandlers(connectionId, webSocket) {
142
+ // TODO: keep the Connection object in the closure here instead of always
143
+ // doing many (expensive) lookups.
144
+ // set message handler for incoming messages
145
+ webSocket.on("message", (data) => {
146
+ void (async () => {
147
+ try {
148
+ const msgStr = data.toString("utf8");
149
+ logger.info(`[ConnectionManager] Message from ${connectionId}: ${msgStr}`);
150
+ const message = JSON.parse(msgStr);
151
+ await this.routeMessage(connectionId, message);
152
+ }
153
+ catch (error) {
154
+ logger.error(`[ConnectionManager] Error processing message:`, error);
155
+ this.handleError(connectionId, error);
156
+ }
157
+ })();
158
+ });
159
+ webSocket.on("close", () => {
160
+ logger.debug(`[ConnectionManager] Connection ${connectionId} closed`);
161
+ this.handleConnectionClose(connectionId);
162
+ });
163
+ webSocket.on("error", (error) => {
164
+ logger.error(`[ConnectionManager] WebSocket error on ${connectionId}:`, error);
165
+ this.handleConnectionClose(connectionId);
166
+ });
167
+ }
168
+ /**
169
+ * Route incoming message to appropriate handler based on message type
170
+ */
171
+ async routeMessage(connectionId, msg) {
172
+ const conn = this.connections.get(connectionId);
173
+ if (!conn) {
174
+ throw new errors_1.ChatErrorMessage(`Connection ${connectionId} not found for message routing`);
175
+ }
176
+ conn.lastMessageTime = Date.now();
177
+ // Handle Connection-level messages
178
+ {
179
+ switch (msg.t) {
180
+ case "data":
181
+ await this.sessionMessageProcessor.processMessage(connectionId, conn.userId, msg.d);
182
+ break;
183
+ case "pong":
184
+ // The only operation is to update the `lastMessageTime`, done above
185
+ logger.debug(`[ConnectionManager.routeMessage] pong from ${connectionId}`);
186
+ break;
187
+ default: {
188
+ const _ = msg;
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ }
194
+ onHeartbeat(connectionId) {
195
+ // TODO: move this and other handling to the Connection class so we can
196
+ // avoid all the per-event lookups.
197
+ const conn = this.connections.get(connectionId);
198
+ if (!conn) {
199
+ // TODO: We intentionally don't try to clean up here, to force
200
+ // correctness fo the real cleanup code.
201
+ logger.error(`[onHeartbeat] unexpected callback for connId ${connectionId}`);
202
+ return;
203
+ }
204
+ const now = Date.now();
205
+ const timeSinceLast = now - conn.lastMessageTime;
206
+ logger.debug(`[onHeartbeat] conn ${connectionId} since ${String(timeSinceLast)}`);
207
+ if (timeSinceLast > 2 * HEARTBEAT_INTERVAL_MS) {
208
+ this.forceCleanupConnection(connectionId);
209
+ return;
210
+ }
211
+ const msg = { t: "ping" };
212
+ this.sendConnectionMessage(connectionId, msg);
213
+ }
214
+ /**
215
+ * Force cleanup of connection state - used for error recovery.
216
+ * This method is safe to call multiple times and handles partial state.
217
+ */
218
+ forceCleanupConnection(connectionId) {
219
+ const conn = this.connections.get(connectionId);
220
+ if (conn) {
221
+ // Cancel the heartbeat interval
222
+ clearInterval(conn.heartbeat);
223
+ // Remove from user connections
224
+ const userConnections = this.userToConnections.get(conn.userId);
225
+ if (userConnections) {
226
+ userConnections.delete(connectionId);
227
+ if (userConnections.size === 0) {
228
+ this.userToConnections.delete(conn.userId);
229
+ this.sessionMessageProcessor.handleUserDisconnect(conn.userId);
230
+ }
231
+ }
232
+ this.connections.delete(connectionId);
233
+ }
234
+ logger.debug(`[ConnectionManager] Force cleaned up connection ${connectionId}`);
235
+ }
236
+ /**
237
+ * Handle connection close - cleanup all state.
238
+ * This is the normal cleanup path triggered by WebSocket events.
239
+ */
240
+ handleConnectionClose(connectionId) {
241
+ this.forceCleanupConnection(connectionId);
242
+ logger.info(`[ConnectionManager] Connection ${connectionId} closed`);
243
+ }
244
+ sendConnectionMessage(connectionId, message) {
245
+ const conn = this.connections.get(connectionId);
246
+ if (!conn) {
247
+ logger.warn(`[ConnectionManager] Cannot send to connection ${connectionId} - ` +
248
+ `not found`);
249
+ return;
250
+ }
251
+ const webSocket = conn.ws;
252
+ if (webSocket.readyState !== 1) {
253
+ // WebSocket.OPEN = 1
254
+ logger.warn(`[ConnectionManager] Cannot send to connection ${connectionId} - ` +
255
+ `not open`);
256
+ return;
257
+ }
258
+ const msgString = JSON.stringify(message);
259
+ logger.debug(`[ConnectionManager] Sending to ${connectionId}: ${msgString}`);
260
+ webSocket.send(msgString);
261
+ }
262
+ /**
263
+ * Send message to specific connection.
264
+ */
265
+ sendToConnection(connectionId, message) {
266
+ this.sendConnectionMessage(connectionId, { t: "data", d: message });
267
+ }
268
+ /**
269
+ * Send error message to connection
270
+ */
271
+ sendConnectionError(connectionId, errorMessage) {
272
+ const errorMsg = {
273
+ t: "error",
274
+ e: errorMessage,
275
+ };
276
+ this.sendConnectionMessage(connectionId, errorMsg);
277
+ }
278
+ /**
279
+ * Handle errors during message processing
280
+ */
281
+ handleError(connectionId, error) {
282
+ let message;
283
+ let shouldClose = false;
284
+ if (typeof error === "string") {
285
+ message = error;
286
+ }
287
+ else if (error instanceof errors_1.ChatFatalError) {
288
+ message = error.message;
289
+ shouldClose = true;
290
+ }
291
+ else if (error instanceof errors_1.ChatErrorMessage) {
292
+ message = error.message;
293
+ }
294
+ else if (error instanceof Error) {
295
+ message = "Internal server error: " + error.message;
296
+ }
297
+ else {
298
+ message = "Unknown error occurred";
299
+ }
300
+ logger.warn(`[ConnectionManager] Error on connection ${connectionId}: ${message}`);
301
+ this.sendConnectionError(connectionId, message);
302
+ if (shouldClose) {
303
+ const conn = this.connections.get(connectionId);
304
+ logger.info(`[ConnectionManager] Closing connection ${connectionId} ` +
305
+ `due to error: ${message}`);
306
+ if (conn) {
307
+ conn.ws.close(4000, message);
308
+ }
309
+ }
310
+ }
311
+ /**
312
+ * Implementation of IUserConnectionManager interface.
313
+ * Send message to all active connections of specific users.
314
+ */
315
+ sendToUsers(userIds, message) {
316
+ for (const userId of userIds.values()) {
317
+ const connectionIds = this.getUserConnections(userId);
318
+ for (const connectionId of connectionIds) {
319
+ logger.debug(`[ConnectionManager] sending to user ${userId} ` +
320
+ `connection ${connectionId}`);
321
+ this.sendToConnection(connectionId, message);
322
+ }
323
+ }
324
+ }
325
+ /**
326
+ * Get all active connection IDs for a user.
327
+ * Helper method for sendToUsers implementation.
328
+ */
329
+ getUserConnections(userId) {
330
+ const connections = this.userToConnections.get(userId);
331
+ return connections ? Array.from(connections) : [];
332
+ }
333
+ /**
334
+ * Implementation of IUserConnectionManager interface.
335
+ * Get cached api key for a user. This key will not be available
336
+ * after the user disconnects.
337
+ */
338
+ getLiveUserApiKey(userId) {
339
+ const connectionIds = this.getUserConnections(userId);
340
+ // Return API key from the first active connection
341
+ // (all connections for a user should have the same API key)
342
+ for (const connectionId of connectionIds) {
343
+ const conn = this.connections.get(connectionId);
344
+ if (conn) {
345
+ return conn.apiKey;
346
+ }
347
+ }
348
+ // No active connections for this user
349
+ return undefined;
350
+ }
351
+ }
352
+ exports.ConnectionManager = ConnectionManager;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
5
+ const vitest_1 = require("vitest");
6
+ const connectionManager_1 = require("./connectionManager");
7
+ const mockFactories_1 = require("./test-utils/mockFactories");
8
+ // Mock dependencies
9
+ vitest_1.vi.mock("../data/apiKeyManager");
10
+ vitest_1.vi.mock("ws");
11
+ (0, vitest_1.describe)("ConnectionManager", () => {
12
+ let connectionManager;
13
+ let mockApiKeyManager;
14
+ let mockSessionRegistry;
15
+ let mockWebSocket;
16
+ (0, vitest_1.beforeEach)(() => {
17
+ vitest_1.vi.resetAllMocks();
18
+ // Create mocks using factories
19
+ mockApiKeyManager = (0, mockFactories_1.createMockApiKeyManager)();
20
+ mockSessionRegistry = (0, mockFactories_1.createMockSessionRegistry)();
21
+ mockWebSocket = (0, mockFactories_1.createMockWebSocket)();
22
+ // Setup standard behaviors
23
+ (0, mockFactories_1.setupStandardMockBehaviors)({
24
+ apiKeyManager: mockApiKeyManager,
25
+ });
26
+ // Create ConnectionManager instance using reflection
27
+ // to access private constructor. Since the constructor is private,
28
+ // we need to use type assertion
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ connectionManager = new connectionManager_1.ConnectionManager(mockApiKeyManager.mock, mockSessionRegistry.mock);
31
+ });
32
+ (0, vitest_1.afterEach)(() => {
33
+ vitest_1.vi.restoreAllMocks();
34
+ });
35
+ (0, vitest_1.describe)("handleConnection", () => {
36
+ (0, vitest_1.it)("should send connection_ready after successful setup", async () => {
37
+ await connectionManager.handleConnection(mockWebSocket.mock, "valid-api-key", { headers: {} });
38
+ // Wait for deferred send
39
+ await new Promise((resolve) => setImmediate(resolve));
40
+ (0, vitest_1.expect)(mockApiKeyManager.spies.verifyApiKey).toHaveBeenCalledWith("valid-api-key");
41
+ (0, vitest_1.expect)(mockWebSocket.spies.send).toHaveBeenCalledWith(vitest_1.expect.stringMatching(/"t":"ready"/));
42
+ });
43
+ (0, vitest_1.it)("should reject connection with invalid API key", async () => {
44
+ await connectionManager.handleConnection(mockWebSocket.mock, "invalid-api-key");
45
+ (0, vitest_1.expect)(mockWebSocket.spies.close).toHaveBeenCalledWith(4000, "Connection setup failed");
46
+ });
47
+ (0, vitest_1.it)("should setup WebSocket event handlers", async () => {
48
+ await connectionManager.handleConnection(mockWebSocket.mock, "valid-api-key");
49
+ (0, vitest_1.expect)(mockWebSocket.spies.on).toHaveBeenCalledWith("message", vitest_1.expect.any(Function));
50
+ (0, vitest_1.expect)(mockWebSocket.spies.on).toHaveBeenCalledWith("close", vitest_1.expect.any(Function));
51
+ (0, vitest_1.expect)(mockWebSocket.spies.on).toHaveBeenCalledWith("error", vitest_1.expect.any(Function));
52
+ });
53
+ });
54
+ (0, vitest_1.describe)("user communication", () => {
55
+ (0, vitest_1.beforeEach)(async () => {
56
+ await connectionManager.handleConnection(mockWebSocket.mock, "valid-api-key");
57
+ });
58
+ (0, vitest_1.it)("should send messages to connected users", () => {
59
+ const testMessage = {
60
+ t: "data",
61
+ d: {
62
+ type: "user_msg",
63
+ user_uuid: "user_1",
64
+ session_id: "session_1",
65
+ message: "message text",
66
+ message_idx: 12,
67
+ // connection_id: "test-123",
68
+ // user_uuid: MOCK_USERS.owner.uuid,
69
+ // client_message_id: "test-123",
70
+ },
71
+ };
72
+ connectionManager.sendToUsers(new Set([mockFactories_1.MOCK_USERS.owner.uuid]), testMessage.d);
73
+ (0, vitest_1.expect)(mockWebSocket.spies.send).toHaveBeenCalledWith(JSON.stringify(testMessage));
74
+ });
75
+ (0, vitest_1.it)("should return API key for connected user", () => {
76
+ const apiKey = connectionManager.getLiveUserApiKey(mockFactories_1.MOCK_USERS.owner.uuid);
77
+ (0, vitest_1.expect)(apiKey).toBe("valid-api-key");
78
+ });
79
+ (0, vitest_1.it)("should return undefined for non-existent user", () => {
80
+ const apiKey = connectionManager.getLiveUserApiKey(mockFactories_1.MOCK_USERS.nonExistent.uuid);
81
+ (0, vitest_1.expect)(apiKey).toBeUndefined();
82
+ });
83
+ });
84
+ (0, vitest_1.describe)("connection cleanup", () => {
85
+ (0, vitest_1.beforeEach)(async () => {
86
+ await connectionManager.handleConnection(mockWebSocket.mock, "valid-api-key");
87
+ });
88
+ (0, vitest_1.it)("should clean up connection state on close", () => {
89
+ // Get the close handler from mock calls
90
+ const closeHandlerCall = mockWebSocket.spies.on.mock.calls.find((call) => call[0] === "close");
91
+ (0, vitest_1.expect)(closeHandlerCall).toBeDefined();
92
+ if (closeHandlerCall) {
93
+ const closeHandler = closeHandlerCall[1];
94
+ closeHandler();
95
+ }
96
+ // After cleanup, API key should no longer be available
97
+ const apiKey = connectionManager.getLiveUserApiKey(mockFactories_1.MOCK_USERS.owner.uuid);
98
+ (0, vitest_1.expect)(apiKey).toBeUndefined();
99
+ });
100
+ (0, vitest_1.it)("should clean up connection state on error", () => {
101
+ // Get the error handler from mock calls
102
+ const errorHandlerCall = mockWebSocket.spies.on.mock.calls.find((call) => call[0] === "error");
103
+ (0, vitest_1.expect)(errorHandlerCall).toBeDefined();
104
+ if (errorHandlerCall) {
105
+ const errorHandler = errorHandlerCall[1];
106
+ errorHandler(new Error("WebSocket error"));
107
+ }
108
+ // After cleanup, API key should no longer be available
109
+ const apiKey = connectionManager.getLiveUserApiKey(mockFactories_1.MOCK_USERS.owner.uuid);
110
+ (0, vitest_1.expect)(apiKey).toBeUndefined();
111
+ });
112
+ });
113
+ (0, vitest_1.describe)("message routing", () => {
114
+ let messageHandler;
115
+ (0, vitest_1.beforeEach)(async () => {
116
+ await connectionManager.handleConnection(mockWebSocket.mock, "valid-api-key");
117
+ // Extract the message handler
118
+ const messageHandlerCall = mockWebSocket.spies.on.mock.calls.find((call) => call[0] === "message");
119
+ (0, vitest_1.expect)(messageHandlerCall).toBeDefined();
120
+ if (!messageHandlerCall) {
121
+ throw new Error("Message handler not found");
122
+ }
123
+ messageHandler = messageHandlerCall[1];
124
+ });
125
+ (0, vitest_1.it)("should route session_create to SessionRegistry", async () => {
126
+ mockSessionRegistry.spies.processMessage.mockResolvedValue(undefined);
127
+ const message = {
128
+ t: "data",
129
+ d: {
130
+ type: "control_session_create",
131
+ title: "Test Session",
132
+ agent_profile_id: "agent-123",
133
+ client_message_id: "msg-123",
134
+ },
135
+ };
136
+ await messageHandler(Buffer.from(JSON.stringify(message)));
137
+ (0, vitest_1.expect)(mockSessionRegistry.spies.processMessage).toHaveBeenCalledWith(vitest_1.expect.any(String), // connectionId
138
+ mockFactories_1.MOCK_USERS.owner.uuid, message.d);
139
+ });
140
+ (0, vitest_1.it)("should route session messages to SessionRegistry", async () => {
141
+ const message = {
142
+ t: "data",
143
+ d: {
144
+ type: "msg",
145
+ session_id: "session-123",
146
+ message: "Hello world!",
147
+ client_message_id: "msg-123",
148
+ },
149
+ };
150
+ await messageHandler(Buffer.from(JSON.stringify(message)));
151
+ (0, vitest_1.expect)(mockSessionRegistry.spies.processMessage).toHaveBeenCalledWith(vitest_1.expect.any(String), // connectionId
152
+ mockFactories_1.MOCK_USERS.owner.uuid, message.d);
153
+ });
154
+ (0, vitest_1.it)("should handle invalid JSON gracefully", async () => {
155
+ await messageHandler(Buffer.from("invalid json"));
156
+ (0, vitest_1.expect)(mockWebSocket.spies.send).toHaveBeenCalledWith(vitest_1.expect.stringMatching(/"t":"error"/));
157
+ });
158
+ });
159
+ });