@juspay/neurolink 7.37.1 → 7.38.1

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 (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/baseProvider.d.ts +4 -0
  3. package/dist/core/baseProvider.js +40 -0
  4. package/dist/core/redisConversationMemoryManager.d.ts +98 -15
  5. package/dist/core/redisConversationMemoryManager.js +665 -203
  6. package/dist/lib/core/baseProvider.d.ts +4 -0
  7. package/dist/lib/core/baseProvider.js +40 -0
  8. package/dist/lib/core/redisConversationMemoryManager.d.ts +98 -15
  9. package/dist/lib/core/redisConversationMemoryManager.js +665 -203
  10. package/dist/lib/neurolink.d.ts +33 -1
  11. package/dist/lib/neurolink.js +64 -0
  12. package/dist/lib/providers/anthropic.js +8 -0
  13. package/dist/lib/providers/anthropicBaseProvider.js +8 -0
  14. package/dist/lib/providers/azureOpenai.js +8 -0
  15. package/dist/lib/providers/googleAiStudio.js +8 -0
  16. package/dist/lib/providers/googleVertex.js +10 -0
  17. package/dist/lib/providers/huggingFace.js +8 -0
  18. package/dist/lib/providers/litellm.js +8 -0
  19. package/dist/lib/providers/mistral.js +8 -0
  20. package/dist/lib/providers/openAI.js +12 -2
  21. package/dist/lib/providers/openaiCompatible.js +8 -0
  22. package/dist/lib/types/conversation.d.ts +52 -2
  23. package/dist/lib/utils/conversationMemory.js +3 -1
  24. package/dist/lib/utils/messageBuilder.d.ts +10 -2
  25. package/dist/lib/utils/messageBuilder.js +22 -1
  26. package/dist/lib/utils/redis.d.ts +10 -6
  27. package/dist/lib/utils/redis.js +71 -70
  28. package/dist/neurolink.d.ts +33 -1
  29. package/dist/neurolink.js +64 -0
  30. package/dist/providers/anthropic.js +8 -0
  31. package/dist/providers/anthropicBaseProvider.js +8 -0
  32. package/dist/providers/azureOpenai.js +8 -0
  33. package/dist/providers/googleAiStudio.js +8 -0
  34. package/dist/providers/googleVertex.js +10 -0
  35. package/dist/providers/huggingFace.js +8 -0
  36. package/dist/providers/litellm.js +8 -0
  37. package/dist/providers/mistral.js +8 -0
  38. package/dist/providers/openAI.js +12 -2
  39. package/dist/providers/openaiCompatible.js +8 -0
  40. package/dist/types/conversation.d.ts +52 -2
  41. package/dist/utils/conversationMemory.js +3 -1
  42. package/dist/utils/messageBuilder.d.ts +10 -2
  43. package/dist/utils/messageBuilder.js +22 -1
  44. package/dist/utils/redis.d.ts +10 -6
  45. package/dist/utils/redis.js +71 -70
  46. package/package.json +2 -2
@@ -2,20 +2,27 @@
2
2
  * Redis Conversation Memory Manager for NeuroLink
3
3
  * Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
4
4
  */
5
+ import { randomUUID } from "crypto";
5
6
  import { ConversationMemoryError } from "../types/conversation.js";
6
- import { DEFAULT_MAX_TURNS_PER_SESSION, DEFAULT_MAX_SESSIONS, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
7
+ import { DEFAULT_MAX_SESSIONS, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
7
8
  import { logger } from "../utils/logger.js";
8
9
  import { NeuroLink } from "../neurolink.js";
9
- import { createRedisClient, getSessionKey, getNormalizedConfig, serializeMessages, deserializeMessages, scanKeys, } from "../utils/redis.js";
10
- /**
11
- * Redis-based implementation of the ConversationMemoryManager
12
- * Uses the same interface but stores data in Redis
13
- */
10
+ import { createRedisClient, getSessionKey, getUserSessionsKey, getNormalizedConfig, serializeConversation, deserializeConversation, scanKeys, } from "../utils/redis.js";
14
11
  export class RedisConversationMemoryManager {
15
12
  config;
16
13
  isInitialized = false;
17
14
  redisConfig;
18
15
  redisClient = null;
16
+ /**
17
+ * Temporary storage for tool execution data to prevent race conditions
18
+ * Key format: "${sessionId}:${userId}"
19
+ */
20
+ pendingToolExecutions = new Map();
21
+ /**
22
+ * Track sessions currently generating titles to prevent race conditions
23
+ * Key format: "${sessionId}:${userId}"
24
+ */
25
+ titleGenerationInProgress = new Set();
19
26
  constructor(config, redisConfig = {}) {
20
27
  this.config = config;
21
28
  this.redisConfig = getNormalizedConfig(redisConfig);
@@ -61,6 +68,149 @@ export class RedisConversationMemoryManager {
61
68
  throw new ConversationMemoryError("Failed to initialize Redis conversation memory", "CONFIG_ERROR", { error: error instanceof Error ? error.message : String(error) });
62
69
  }
63
70
  }
71
+ /**
72
+ * Get all sessions for a specific user
73
+ */
74
+ async getUserSessions(userId) {
75
+ // Ensure initialization
76
+ await this.ensureInitialized();
77
+ if (!this.redisClient) {
78
+ logger.warn("[RedisConversationMemoryManager] Redis client not available", { userId });
79
+ return [];
80
+ }
81
+ try {
82
+ const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
83
+ const sessions = await this.redisClient.sMembers(userSessionsKey);
84
+ return sessions;
85
+ }
86
+ catch (error) {
87
+ logger.error("[RedisConversationMemoryManager] Failed to get user sessions", {
88
+ userId,
89
+ error: error instanceof Error ? error.message : String(error),
90
+ });
91
+ return [];
92
+ }
93
+ }
94
+ /**
95
+ * Add a session to user's session set (private method)
96
+ */
97
+ async addUserSession(userId, sessionId) {
98
+ if (!this.redisClient || !userId) {
99
+ return;
100
+ }
101
+ try {
102
+ const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
103
+ await this.redisClient.sAdd(userSessionsKey, sessionId);
104
+ if (this.redisConfig.ttl > 0) {
105
+ await this.redisClient.expire(userSessionsKey, this.redisConfig.ttl);
106
+ }
107
+ }
108
+ catch (error) {
109
+ logger.error("[RedisConversationMemoryManager] Failed to add session to user set", {
110
+ userId,
111
+ sessionId,
112
+ error: error instanceof Error ? error.message : String(error),
113
+ });
114
+ }
115
+ }
116
+ /**
117
+ * Remove a session from user's session set (private method)
118
+ */
119
+ async removeUserSession(userId, sessionId) {
120
+ if (!this.redisClient || !userId) {
121
+ return false;
122
+ }
123
+ try {
124
+ const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
125
+ const result = await this.redisClient.sRem(userSessionsKey, sessionId);
126
+ return result > 0;
127
+ }
128
+ catch (error) {
129
+ logger.error("[RedisConversationMemoryManager] Failed to remove session from user set", {
130
+ userId,
131
+ sessionId,
132
+ error: error instanceof Error ? error.message : String(error),
133
+ });
134
+ return false;
135
+ }
136
+ }
137
+ /**
138
+ * Generate next message ID for a conversation
139
+ */
140
+ generateMessageId(conversation) {
141
+ const currentCount = conversation?.messages?.length || 0;
142
+ return `msg_${currentCount + 1}`;
143
+ }
144
+ /**
145
+ * Generate current timestamp in ISO format
146
+ */
147
+ generateTimestamp() {
148
+ return new Date().toISOString();
149
+ }
150
+ /**
151
+ * Generate a unique conversation ID using UUID v4
152
+ */
153
+ generateUniqueId() {
154
+ return randomUUID();
155
+ }
156
+ /**
157
+ * Store tool execution data for a session (temporarily to avoid race conditions)
158
+ */
159
+ async storeToolExecution(sessionId, userId, toolCalls, toolResults) {
160
+ logger.debug("[RedisConversationMemoryManager] Storing tool execution temporarily", {
161
+ sessionId,
162
+ userId,
163
+ toolCallsCount: toolCalls?.length || 0,
164
+ toolResultsCount: toolResults?.length || 0,
165
+ });
166
+ try {
167
+ const normalizedUserId = userId || "randomUser";
168
+ const pendingKey = `${sessionId}:${normalizedUserId}`;
169
+ // Store tool execution data temporarily to prevent race conditions
170
+ const pendingData = {
171
+ toolCalls: toolCalls || [],
172
+ toolResults: toolResults || [],
173
+ timestamp: Date.now(),
174
+ };
175
+ // Check if there's existing pending data and merge
176
+ const existingData = this.pendingToolExecutions.get(pendingKey);
177
+ if (existingData) {
178
+ logger.debug("[RedisConversationMemoryManager] Merging with existing pending tool data", {
179
+ sessionId,
180
+ existingToolCalls: existingData.toolCalls.length,
181
+ existingToolResults: existingData.toolResults.length,
182
+ newToolCalls: toolCalls?.length || 0,
183
+ newToolResults: toolResults?.length || 0,
184
+ });
185
+ // Merge tool calls and results
186
+ pendingData.toolCalls = [
187
+ ...existingData.toolCalls,
188
+ ...pendingData.toolCalls,
189
+ ];
190
+ pendingData.toolResults = [
191
+ ...existingData.toolResults,
192
+ ...pendingData.toolResults,
193
+ ];
194
+ }
195
+ this.pendingToolExecutions.set(pendingKey, pendingData);
196
+ logger.debug("[RedisConversationMemoryManager] Tool execution stored temporarily", {
197
+ sessionId,
198
+ userId: normalizedUserId,
199
+ pendingKey,
200
+ totalToolCalls: pendingData.toolCalls.length,
201
+ totalToolResults: pendingData.toolResults.length,
202
+ });
203
+ // Clean up stale pending data (older than 5 minutes)
204
+ this.cleanupStalePendingData();
205
+ }
206
+ catch (error) {
207
+ logger.error("[RedisConversationMemoryManager] Failed to store tool execution temporarily", {
208
+ sessionId,
209
+ error: error instanceof Error ? error.message : String(error),
210
+ });
211
+ // Don't throw - tool storage failures shouldn't break generation
212
+ }
213
+ }
64
214
  /**
65
215
  * Store a conversation turn for a session
66
216
  */
@@ -77,69 +227,131 @@ export class RedisConversationMemoryManager {
77
227
  throw new Error("Redis client not initialized");
78
228
  }
79
229
  // Generate Redis key
80
- const redisKey = getSessionKey(this.redisConfig, sessionId);
81
- // Get existing messages
82
- const messagesData = await this.redisClient.get(redisKey);
83
- const messages = deserializeMessages(messagesData);
84
- logger.info("[RedisConversationMemoryManager] Deserialized messages", {
85
- messageCount: messages.length,
86
- roles: messages.map((m) => m.role),
230
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
231
+ // Get existing conversation object
232
+ const conversationData = await this.redisClient.get(redisKey);
233
+ let conversation = deserializeConversation(conversationData);
234
+ const currentTime = new Date().toISOString();
235
+ const normalizedUserId = userId || "randomUser";
236
+ // If no existing conversation, create a new one
237
+ if (!conversation) {
238
+ // Generate title asynchronously in the background (non-blocking)
239
+ const titleGenerationKey = `${sessionId}:${normalizedUserId}`;
240
+ setImmediate(async () => {
241
+ // Check if title generation is already in progress for this session
242
+ if (this.titleGenerationInProgress.has(titleGenerationKey)) {
243
+ logger.debug("[RedisConversationMemoryManager] Title generation already in progress, skipping", {
244
+ sessionId,
245
+ userId: normalizedUserId,
246
+ titleGenerationKey,
247
+ });
248
+ return;
249
+ }
250
+ // Mark title generation as in progress
251
+ this.titleGenerationInProgress.add(titleGenerationKey);
252
+ try {
253
+ const title = await this.generateConversationTitle(userMessage);
254
+ logger.info("[RedisConversationMemoryManager] Successfully generated conversation title", {
255
+ sessionId,
256
+ userId: normalizedUserId,
257
+ title,
258
+ });
259
+ const updatedRedisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
260
+ const updatedConversationData = await this.redisClient?.get(updatedRedisKey);
261
+ const updatedConversation = deserializeConversation(updatedConversationData || null);
262
+ if (updatedConversation) {
263
+ updatedConversation.title = title;
264
+ updatedConversation.updatedAt = new Date().toISOString();
265
+ const serializedData = serializeConversation(updatedConversation);
266
+ await this.redisClient?.set(updatedRedisKey, serializedData);
267
+ if (this.redisConfig.ttl > 0) {
268
+ await this.redisClient?.expire(updatedRedisKey, this.redisConfig.ttl);
269
+ }
270
+ }
271
+ }
272
+ catch (titleError) {
273
+ logger.warn("[RedisConversationMemoryManager] Failed to generate conversation title in background", {
274
+ sessionId,
275
+ userId: normalizedUserId,
276
+ error: titleError instanceof Error
277
+ ? titleError.message
278
+ : String(titleError),
279
+ });
280
+ }
281
+ finally {
282
+ // Always remove from tracking set when done (success or failure)
283
+ this.titleGenerationInProgress.delete(titleGenerationKey);
284
+ logger.debug("[RedisConversationMemoryManager] Title generation completed, removed from tracking", {
285
+ sessionId,
286
+ userId: normalizedUserId,
287
+ titleGenerationKey,
288
+ remainingInProgress: this.titleGenerationInProgress.size,
289
+ });
290
+ }
291
+ });
292
+ conversation = {
293
+ id: this.generateUniqueId(), // Generate unique UUID v4 for conversation
294
+ title: "New Conversation", // Temporary title until generated
295
+ sessionId,
296
+ userId: normalizedUserId,
297
+ createdAt: currentTime,
298
+ updatedAt: currentTime,
299
+ messages: [],
300
+ };
301
+ }
302
+ else {
303
+ // Update existing conversation timestamp
304
+ conversation.updatedAt = currentTime;
305
+ }
306
+ logger.info("[RedisConversationMemoryManager] Processing conversation", {
307
+ isNewConversation: !conversationData,
308
+ messageCount: conversation.messages.length,
309
+ sessionId: conversation.sessionId,
310
+ userId: conversation.userId,
87
311
  });
88
- // Add new messages
89
- messages.push({ role: "user", content: userMessage }, { role: "assistant", content: aiResponse });
312
+ // Add new messages to conversation history with new format
313
+ const userMsg = {
314
+ id: this.generateMessageId(conversation),
315
+ timestamp: this.generateTimestamp(),
316
+ role: "user",
317
+ content: userMessage,
318
+ };
319
+ conversation.messages.push(userMsg);
320
+ await this.flushPendingToolData(conversation, sessionId, normalizedUserId);
321
+ const assistantMsg = {
322
+ id: this.generateMessageId(conversation),
323
+ timestamp: this.generateTimestamp(),
324
+ role: "assistant",
325
+ content: aiResponse,
326
+ };
327
+ conversation.messages.push(assistantMsg);
90
328
  logger.info("[RedisConversationMemoryManager] Added new messages", {
91
- newMessageCount: messages.length,
329
+ newMessageCount: conversation.messages.length,
92
330
  latestMessages: [
93
331
  {
94
- role: messages[messages.length - 2]?.role,
95
- contentLength: messages[messages.length - 2]?.content.length,
332
+ role: conversation.messages[conversation.messages.length - 2]?.role,
333
+ contentLength: conversation.messages[conversation.messages.length - 2]?.content
334
+ .length,
96
335
  },
97
336
  {
98
- role: messages[messages.length - 1]?.role,
99
- contentLength: messages[messages.length - 1]?.content.length,
337
+ role: conversation.messages[conversation.messages.length - 1]?.role,
338
+ contentLength: conversation.messages[conversation.messages.length - 1]?.content
339
+ .length,
100
340
  },
101
341
  ],
102
342
  });
103
- // Handle summarization or message limit
104
- if (this.config.enableSummarization) {
105
- const userAssistantCount = messages.filter((msg) => msg.role === "user" || msg.role === "assistant").length;
106
- const currentTurnCount = Math.floor(userAssistantCount / MESSAGES_PER_TURN);
107
- logger.debug("[RedisConversationMemoryManager] Checking summarization threshold", {
108
- userAssistantCount,
109
- currentTurnCount,
110
- summarizationThreshold: this.config.summarizationThresholdTurns || 20,
111
- shouldSummarize: currentTurnCount >=
112
- (this.config.summarizationThresholdTurns || 20),
113
- });
114
- if (currentTurnCount >= (this.config.summarizationThresholdTurns || 20)) {
115
- await this._summarizeMessages(sessionId, userId, messages);
116
- return;
117
- }
118
- }
119
- else {
120
- const maxMessages = (this.config.maxTurnsPerSession || DEFAULT_MAX_TURNS_PER_SESSION) *
121
- MESSAGES_PER_TURN;
122
- logger.debug("[RedisConversationMemoryManager] Checking message limit", {
123
- currentMessageCount: messages.length,
124
- maxMessages,
125
- shouldTrimMessages: messages.length > maxMessages,
126
- });
127
- if (messages.length > maxMessages) {
128
- const trimCount = messages.length - maxMessages;
129
- logger.debug("[RedisConversationMemoryManager] Trimming messages", {
130
- beforeCount: messages.length,
131
- trimCount,
132
- afterCount: maxMessages,
133
- });
134
- messages.splice(0, messages.length - maxMessages);
135
- }
136
- }
137
- // Save updated messages
138
- const serializedData = serializeMessages(messages);
139
- logger.debug("[RedisConversationMemoryManager] Saving messages to Redis", {
343
+ // Save updated conversation object
344
+ const serializedData = serializeConversation(conversation);
345
+ logger.debug("[RedisConversationMemoryManager] Saving conversation to Redis", {
140
346
  redisKey,
141
- messageCount: messages.length,
347
+ messageCount: conversation.messages.length,
142
348
  serializedDataLength: serializedData.length,
349
+ title: conversation.title,
350
+ });
351
+ logger.info("Storing conversation data to Redis", {
352
+ sessionId,
353
+ dataLength: serializedData.length,
354
+ messageCount: conversation.messages.length,
143
355
  });
144
356
  await this.redisClient.set(redisKey, serializedData);
145
357
  // Set TTL if configured
@@ -150,11 +362,16 @@ export class RedisConversationMemoryManager {
150
362
  });
151
363
  await this.redisClient.expire(redisKey, this.redisConfig.ttl);
152
364
  }
365
+ // Add session to user's session set
366
+ if (userId) {
367
+ await this.addUserSession(userId, sessionId);
368
+ }
153
369
  // Enforce session limit
154
370
  await this.enforceSessionLimit();
155
371
  logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
156
372
  sessionId,
157
- totalMessages: messages.length,
373
+ totalMessages: conversation.messages.length,
374
+ title: conversation.title,
158
375
  });
159
376
  }
160
377
  catch (error) {
@@ -167,33 +384,27 @@ export class RedisConversationMemoryManager {
167
384
  /**
168
385
  * Build context messages for AI prompt injection
169
386
  */
170
- async buildContextMessages(sessionId) {
387
+ async buildContextMessages(sessionId, userId) {
171
388
  logger.info("[RedisConversationMemoryManager] Building context messages", {
172
389
  sessionId,
390
+ userId,
173
391
  method: "buildContextMessages",
174
392
  });
175
- await this.ensureInitialized();
176
- if (!this.redisClient) {
177
- logger.warn("[RedisConversationMemoryManager] Redis client not available, returning empty context", {
393
+ const messages = await this.getUserSessionHistory(userId || "randomUser", sessionId);
394
+ if (!messages) {
395
+ logger.info("[RedisConversationMemoryManager] No context messages found", {
178
396
  sessionId,
397
+ userId,
179
398
  });
180
399
  return [];
181
400
  }
182
- const redisKey = getSessionKey(this.redisConfig, sessionId);
183
- logger.info("[RedisConversationMemoryManager] Getting messages from Redis", {
184
- sessionId,
185
- redisKey,
186
- });
187
- const messagesData = await this.redisClient.get(redisKey);
188
- logger.info("[RedisConversationMemoryManager] Retrieved message data from Redis", {
189
- sessionId,
190
- redisKey,
191
- hasData: !!messagesData,
192
- dataLength: messagesData?.length || 0,
401
+ logger.info("[RedisConversationMemoryManager] Retrieved messages", {
402
+ messageCount: messages.length,
403
+ hasMessages: messages.length > 0,
193
404
  });
194
- const messages = deserializeMessages(messagesData);
195
- logger.info("[RedisConversationMemoryManager] Deserialized messages for context", {
405
+ logger.info("[RedisConversationMemoryManager] Retrieved context messages", {
196
406
  sessionId,
407
+ userId,
197
408
  messageCount: messages.length,
198
409
  messageRoles: messages.map((m) => m.role),
199
410
  firstMessagePreview: messages[0]?.content?.substring(0, 50),
@@ -202,57 +413,229 @@ export class RedisConversationMemoryManager {
202
413
  return messages;
203
414
  }
204
415
  /**
205
- * Get session data
416
+ * Get session metadata for a specific user session (optimized for listing)
417
+ * Fetches only essential metadata without heavy message arrays
418
+ *
419
+ * @param userId The user identifier
420
+ * @param sessionId The session identifier
421
+ * @returns Session metadata or null if session doesn't exist
206
422
  */
207
- async getSession(sessionId) {
208
- logger.debug("[RedisConversationMemoryManager] Getting session", {
423
+ async getUserSessionMetadata(userId, sessionId) {
424
+ logger.debug("[RedisConversationMemoryManager] Getting user session metadata", {
425
+ userId,
209
426
  sessionId,
210
- method: "getSession",
211
427
  });
212
428
  await this.ensureInitialized();
213
429
  if (!this.redisClient) {
214
- logger.warn("[RedisConversationMemoryManager] Redis client not available", {
430
+ logger.warn("[RedisConversationMemoryManager] Redis client not available", { userId, sessionId });
431
+ return null;
432
+ }
433
+ try {
434
+ const sessionKey = getSessionKey(this.redisConfig, sessionId, userId);
435
+ const conversationData = await this.redisClient.get(sessionKey);
436
+ if (!conversationData) {
437
+ logger.debug("[RedisConversationMemoryManager] No session data found", {
438
+ userId,
439
+ sessionId,
440
+ sessionKey,
441
+ });
442
+ return null;
443
+ }
444
+ // Deserialize conversation object but extract only metadata
445
+ const conversation = deserializeConversation(conversationData);
446
+ if (conversation) {
447
+ return {
448
+ id: conversation.sessionId,
449
+ title: conversation.title,
450
+ createdAt: conversation.createdAt,
451
+ updatedAt: conversation.updatedAt,
452
+ };
453
+ }
454
+ logger.debug("[RedisConversationMemoryManager] No valid conversation data found", {
455
+ userId,
215
456
  sessionId,
457
+ sessionKey,
216
458
  });
217
- return undefined;
459
+ return null;
218
460
  }
219
- const redisKey = getSessionKey(this.redisConfig, sessionId);
220
- logger.debug("[RedisConversationMemoryManager] Getting session data from Redis", {
221
- sessionId,
222
- redisKey,
223
- });
224
- const messagesData = await this.redisClient.get(redisKey);
225
- logger.debug("[RedisConversationMemoryManager] Retrieved session data", {
461
+ catch (error) {
462
+ logger.error("[RedisConversationMemoryManager] Failed to get user session metadata", {
463
+ userId,
464
+ sessionId,
465
+ error: error instanceof Error ? error.message : String(error),
466
+ stack: error instanceof Error ? error.stack : undefined,
467
+ });
468
+ return null;
469
+ }
470
+ }
471
+ /**
472
+ * Get conversation history for a specific user session
473
+ *
474
+ * @param userId The user identifier
475
+ * @param sessionId The session identifier
476
+ * @returns Array of chat messages or null if session doesn't exist
477
+ */
478
+ async getUserSessionHistory(userId, sessionId) {
479
+ logger.debug("[RedisConversationMemoryManager] Getting user session history via getUserSessionObject", {
480
+ userId,
226
481
  sessionId,
227
- hasData: !!messagesData,
228
- dataLength: messagesData?.length || 0,
229
482
  });
230
- if (!messagesData) {
231
- logger.debug("[RedisConversationMemoryManager] No session data found", {
483
+ try {
484
+ const sessionObject = await this.getUserSessionObject(userId, sessionId);
485
+ if (!sessionObject) {
486
+ logger.debug("[RedisConversationMemoryManager] No session object found, returning null", {
487
+ userId,
488
+ sessionId,
489
+ });
490
+ return null;
491
+ }
492
+ return sessionObject.messages;
493
+ }
494
+ catch (error) {
495
+ logger.error("[RedisConversationMemoryManager] Failed to get user session history via getUserSessionObject", {
496
+ userId,
232
497
  sessionId,
233
- redisKey,
498
+ error: error instanceof Error ? error.message : String(error),
499
+ errorName: error instanceof Error ? error.name : "UnknownError",
500
+ stack: error instanceof Error ? error.stack : undefined,
234
501
  });
235
- return undefined;
502
+ return null;
236
503
  }
237
- const messages = deserializeMessages(messagesData);
238
- logger.debug("[RedisConversationMemoryManager] Deserialized session messages", {
504
+ }
505
+ /**
506
+ * Get the complete conversation object for a specific user session
507
+ *
508
+ * This method returns the full conversation object including title, metadata,
509
+ * timestamps, and all chat messages. Unlike getUserSessionHistory() which returns
510
+ * only the messages array, this method provides the complete conversation context.
511
+ *
512
+ * @param userId The user identifier who owns the session
513
+ * @param sessionId The unique session identifier
514
+ * @returns Complete conversation object with all data, or null if session doesn't exist
515
+ */
516
+ async getUserSessionObject(userId, sessionId) {
517
+ logger.debug("[RedisConversationMemoryManager] Getting complete user session object", {
518
+ userId,
239
519
  sessionId,
240
- messageCount: messages.length,
241
- messageRoles: messages.map((m) => m.role),
520
+ method: "getUserSessionObject",
242
521
  });
243
- // We don't store the full SessionMemory object in Redis,
244
- // just the messages, so we recreate the SessionMemory object here
245
- const session = {
246
- sessionId,
247
- messages,
248
- createdAt: Date.now(), // We don't have this information
249
- lastActivity: Date.now(), // We don't have this information
250
- };
251
- logger.debug("[RedisConversationMemoryManager] Created session memory object", {
252
- sessionId,
253
- messageCount: session.messages.length,
522
+ // Validate input parameters
523
+ if (!userId || typeof userId !== "string") {
524
+ logger.warn("[RedisConversationMemoryManager] Invalid userId provided", {
525
+ userId,
526
+ sessionId,
527
+ });
528
+ return null;
529
+ }
530
+ if (!sessionId || typeof sessionId !== "string") {
531
+ logger.warn("[RedisConversationMemoryManager] Invalid sessionId provided", { userId, sessionId });
532
+ return null;
533
+ }
534
+ await this.ensureInitialized();
535
+ if (!this.redisClient) {
536
+ logger.warn("[RedisConversationMemoryManager] Redis client not available for getUserSessionObject", { userId, sessionId });
537
+ return null;
538
+ }
539
+ try {
540
+ const sessionKey = getSessionKey(this.redisConfig, sessionId, userId);
541
+ const conversationData = await this.redisClient.get(sessionKey);
542
+ if (!conversationData) {
543
+ logger.debug("[RedisConversationMemoryManager] No conversation data found in Redis", {
544
+ userId,
545
+ sessionId,
546
+ sessionKey,
547
+ });
548
+ return null;
549
+ }
550
+ // Deserialize the complete conversation object
551
+ const conversation = deserializeConversation(conversationData);
552
+ if (!conversation) {
553
+ logger.debug("[RedisConversationMemoryManager] Failed to deserialize conversation data", {
554
+ userId,
555
+ sessionId,
556
+ sessionKey,
557
+ dataLength: conversationData.length,
558
+ });
559
+ return null;
560
+ }
561
+ // Validate conversation object structure
562
+ if (!conversation.messages || !Array.isArray(conversation.messages)) {
563
+ logger.warn("[RedisConversationMemoryManager] Invalid conversation structure - missing messages array", {
564
+ userId,
565
+ sessionId,
566
+ hasMessages: !!conversation.messages,
567
+ messagesType: typeof conversation.messages,
568
+ });
569
+ return null;
570
+ }
571
+ return conversation;
572
+ }
573
+ catch (error) {
574
+ logger.error("[RedisConversationMemoryManager] Failed to get complete user session object", {
575
+ userId,
576
+ sessionId,
577
+ error: error instanceof Error ? error.message : String(error),
578
+ errorName: error instanceof Error ? error.name : "UnknownError",
579
+ stack: error instanceof Error ? error.stack : undefined,
580
+ });
581
+ return null;
582
+ }
583
+ }
584
+ /**
585
+ * Generate a conversation title from the first user message
586
+ * Uses AI to create a concise, descriptive title (5-8 words)
587
+ */
588
+ async generateConversationTitle(userMessage) {
589
+ logger.debug("[RedisConversationMemoryManager] Generating conversation title", {
590
+ userMessageLength: userMessage.length,
591
+ userMessagePreview: userMessage.substring(0, 100),
254
592
  });
255
- return session;
593
+ try {
594
+ // Create a NeuroLink instance for title generation
595
+ const titleGenerator = new NeuroLink({
596
+ conversationMemory: { enabled: false },
597
+ });
598
+ const titlePrompt = `Generate a short, descriptive title (5-8 words maximum) for a conversation that starts with this user message. The title should capture the main topic or intent. Only return the title, nothing else.
599
+
600
+ User message: "${userMessage}"
601
+
602
+ Title:`;
603
+ const result = await titleGenerator.generate({
604
+ input: { text: titlePrompt },
605
+ provider: this.config.summarizationProvider || "vertex",
606
+ model: this.config.summarizationModel || "gemini-2.5-flashb",
607
+ disableTools: false,
608
+ });
609
+ // Clean up the generated title
610
+ let title = result.content?.trim() || "New Conversation";
611
+ // Remove common prefixes/suffixes that might be added by the AI
612
+ title = title.replace(/^(Title:|Here's a title:|The title is:)\s*/i, "");
613
+ title = title.replace(/['"]/g, ""); // Remove quotes
614
+ title = title.replace(/\.$/, ""); // Remove trailing period
615
+ if (title.length > 60) {
616
+ title = title.substring(0, 57) + "...";
617
+ }
618
+ if (title.length < 3) {
619
+ title = "New Conversation";
620
+ }
621
+ logger.debug("[RedisConversationMemoryManager] Generated conversation title", {
622
+ originalLength: result.content?.length || 0,
623
+ cleanedTitle: title,
624
+ titleLength: title.length,
625
+ });
626
+ return title;
627
+ }
628
+ catch (error) {
629
+ logger.error("[RedisConversationMemoryManager] Failed to generate conversation title", {
630
+ error: error instanceof Error ? error.message : String(error),
631
+ userMessagePreview: userMessage.substring(0, 100),
632
+ });
633
+ // Fallback to a simple title based on the user message
634
+ const fallbackTitle = userMessage.length > 30
635
+ ? userMessage.substring(0, 30) + "..."
636
+ : userMessage || "New Conversation";
637
+ return fallbackTitle;
638
+ }
256
639
  }
257
640
  /**
258
641
  * Create summary system message
@@ -292,9 +675,11 @@ export class RedisConversationMemoryManager {
292
675
  // Count messages in each session
293
676
  let totalTurns = 0;
294
677
  for (const key of keys) {
295
- const messagesData = await this.redisClient.get(key);
296
- const messages = deserializeMessages(messagesData);
297
- totalTurns += messages.length / MESSAGES_PER_TURN;
678
+ const conversationData = await this.redisClient.get(key);
679
+ const conversation = deserializeConversation(conversationData);
680
+ if (conversation?.messages) {
681
+ totalTurns += conversation.messages.length / MESSAGES_PER_TURN;
682
+ }
298
683
  }
299
684
  return {
300
685
  totalSessions: keys.length,
@@ -304,14 +689,18 @@ export class RedisConversationMemoryManager {
304
689
  /**
305
690
  * Clear a specific session
306
691
  */
307
- async clearSession(sessionId) {
692
+ async clearSession(sessionId, userId) {
308
693
  await this.ensureInitialized();
309
694
  if (!this.redisClient) {
310
695
  return false;
311
696
  }
312
- const redisKey = getSessionKey(this.redisConfig, sessionId);
697
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
313
698
  const result = await this.redisClient.del(redisKey);
314
699
  if (result > 0) {
700
+ // Remove session from user's session set
701
+ if (userId) {
702
+ await this.removeUserSession(userId, sessionId);
703
+ }
315
704
  logger.info("Redis session cleared", { sessionId });
316
705
  return true;
317
706
  }
@@ -325,123 +714,196 @@ export class RedisConversationMemoryManager {
325
714
  if (!this.redisClient) {
326
715
  return;
327
716
  }
328
- const pattern = `${this.redisConfig.keyPrefix}*`;
717
+ const conversationPattern = `${this.redisConfig.keyPrefix}*`;
718
+ const userSessionsPattern = `${this.redisConfig.userSessionsKeyPrefix}*`;
329
719
  // Use SCAN instead of KEYS to avoid blocking the server
330
- const keys = await scanKeys(this.redisClient, pattern);
331
- logger.debug("[RedisConversationMemoryManager] Got session keys with SCAN for clearing", {
332
- pattern,
333
- keyCount: keys.length,
720
+ const conversationKeys = await scanKeys(this.redisClient, conversationPattern);
721
+ const userSessionsKeys = await scanKeys(this.redisClient, userSessionsPattern);
722
+ const allKeys = [...conversationKeys, ...userSessionsKeys];
723
+ logger.debug("[RedisConversationMemoryManager] Got all keys with SCAN for clearing", {
724
+ conversationPattern,
725
+ userSessionsPattern,
726
+ conversationKeyCount: conversationKeys.length,
727
+ userSessionsKeyCount: userSessionsKeys.length,
728
+ totalKeyCount: allKeys.length,
334
729
  });
335
- if (keys.length > 0) {
730
+ if (allKeys.length > 0) {
336
731
  // Process keys in batches to avoid blocking Redis for too long
337
732
  const batchSize = 100;
338
- for (let i = 0; i < keys.length; i += batchSize) {
339
- const batch = keys.slice(i, i + batchSize);
733
+ for (let i = 0; i < allKeys.length; i += batchSize) {
734
+ const batch = allKeys.slice(i, i + batchSize);
340
735
  await this.redisClient.del(batch);
341
- logger.debug("[RedisConversationMemoryManager] Cleared batch of sessions", {
736
+ logger.debug("[RedisConversationMemoryManager] Cleared batch of sessions and user mappings", {
342
737
  batchIndex: Math.floor(i / batchSize) + 1,
343
738
  batchSize: batch.length,
344
739
  totalProcessed: i + batch.length,
345
- totalKeys: keys.length,
740
+ totalKeys: allKeys.length,
346
741
  });
347
742
  }
348
- logger.info("All Redis sessions cleared", { clearedCount: keys.length });
743
+ logger.info("All Redis sessions and user session mappings cleared", {
744
+ clearedCount: allKeys.length,
745
+ conversationSessions: conversationKeys.length,
746
+ userSessionMappings: userSessionsKeys.length,
747
+ });
349
748
  }
350
749
  }
351
750
  /**
352
- * Summarize messages for a session
751
+ * Ensure Redis client is initialized
353
752
  */
354
- async _summarizeMessages(sessionId, userId, messages) {
355
- logger.info(`[RedisConversationMemory] Summarizing session ${sessionId}...`);
356
- logger.debug("[RedisConversationMemoryManager] Starting message summarization", {
357
- sessionId,
358
- userId,
359
- messageCount: messages.length,
360
- messageTypes: messages.map((m) => m.role),
361
- });
362
- const targetTurns = this.config.summarizationTargetTurns || 10;
363
- const splitIndex = Math.max(0, messages.length - targetTurns * MESSAGES_PER_TURN);
364
- const messagesToSummarize = messages.slice(0, splitIndex);
365
- const recentMessages = messages.slice(splitIndex);
366
- if (messagesToSummarize.length === 0) {
367
- return;
753
+ async ensureInitialized() {
754
+ logger.debug("[RedisConversationMemoryManager] Ensuring initialization");
755
+ if (!this.isInitialized) {
756
+ logger.debug("[RedisConversationMemoryManager] Not initialized, initializing now");
757
+ await this.initialize();
368
758
  }
369
- const summarizationPrompt = this._createSummarizationPrompt(messagesToSummarize);
370
- const summarizer = new NeuroLink({
371
- conversationMemory: { enabled: false },
372
- });
759
+ else {
760
+ logger.debug("[RedisConversationMemoryManager] Already initialized");
761
+ }
762
+ }
763
+ /**
764
+ * Get session metadata for all sessions of a user (optimized for listing)
765
+ * Returns only essential metadata without heavy message arrays
766
+ *
767
+ * @param userId The user identifier
768
+ * @returns Array of session metadata objects
769
+ */
770
+ async getUserAllSessionsHistory(userId) {
771
+ await this.ensureInitialized();
772
+ if (!this.redisClient) {
773
+ logger.warn("[RedisConversationMemoryManager] Redis client not available", { userId });
774
+ return [];
775
+ }
776
+ const results = [];
373
777
  try {
374
- const providerName = this.config.summarizationProvider;
375
- // Map provider names to correct format
376
- let mappedProvider = providerName;
377
- if (providerName === "vertex") {
378
- mappedProvider = "googlevertex";
778
+ // Get all session IDs for the user using existing method
779
+ const sessionIds = await this.getUserSessions(userId);
780
+ if (sessionIds.length === 0) {
781
+ return results;
379
782
  }
380
- if (!mappedProvider) {
381
- logger.error(`[RedisConversationMemory] Missing summarization provider`);
382
- return;
383
- }
384
- logger.debug(`[RedisConversationMemory] Using provider: ${mappedProvider} for summarization`);
385
- const summaryResult = await summarizer.generate({
386
- input: { text: summarizationPrompt },
387
- provider: mappedProvider,
388
- model: this.config.summarizationModel,
389
- disableTools: true,
390
- });
391
- if (!this.redisClient) {
392
- throw new Error("Redis client not initialized");
393
- }
394
- if (summaryResult.content) {
395
- const updatedMessages = [
396
- this.createSummarySystemMessage(summaryResult.content),
397
- ...recentMessages,
398
- ];
399
- const redisKey = getSessionKey(this.redisConfig, sessionId);
400
- await this.redisClient.set(redisKey, serializeMessages(updatedMessages));
401
- // Set TTL if configured
402
- if (this.redisConfig.ttl > 0) {
403
- await this.redisClient.expire(redisKey, this.redisConfig.ttl);
783
+ // Fetch metadata for each session using our optimized helper method
784
+ for (const sessionId of sessionIds) {
785
+ try {
786
+ const metadata = await this.getUserSessionMetadata(userId, sessionId);
787
+ if (metadata) {
788
+ results.push(metadata);
789
+ }
790
+ else {
791
+ logger.debug("[RedisConversationMemoryManager] Empty or missing session metadata", {
792
+ userId,
793
+ sessionId,
794
+ });
795
+ }
796
+ }
797
+ catch (sessionError) {
798
+ logger.error("[RedisConversationMemoryManager] Failed to get session metadata", {
799
+ userId,
800
+ sessionId,
801
+ error: sessionError instanceof Error
802
+ ? sessionError.message
803
+ : String(sessionError),
804
+ });
805
+ // Continue with other sessions even if one fails
806
+ continue;
404
807
  }
405
- logger.info(`[RedisConversationMemory] Summarization complete for session ${sessionId}.`);
406
- }
407
- else {
408
- logger.warn(`[RedisConversationMemory] Summarization failed for session ${sessionId}. History not modified.`);
409
808
  }
809
+ return results;
410
810
  }
411
811
  catch (error) {
412
- logger.error(`[RedisConversationMemory] Error during summarization for session ${sessionId}`, { error });
812
+ logger.error("[RedisConversationMemoryManager] Failed to get user all sessions metadata", {
813
+ userId,
814
+ error: error instanceof Error ? error.message : String(error),
815
+ stack: error instanceof Error ? error.stack : undefined,
816
+ });
817
+ return results;
413
818
  }
414
819
  }
415
820
  /**
416
- * Create summarization prompt
821
+ * Clean up stale pending tool execution data
822
+ * Removes data older than 5 minutes to prevent memory leaks
417
823
  */
418
- _createSummarizationPrompt(history) {
419
- const formattedHistory = history
420
- .map((msg) => `${msg.role}: ${msg.content}`)
421
- .join("\n\n");
422
- return `
423
- You are a context summarization AI. Your task is to condense the following conversation history for another AI assistant.
424
- The summary must be a concise, third-person narrative that retains all critical information, including key entities, technical details, decisions made, and any specific dates or times mentioned.
425
- Ensure the summary flows logically and is ready to be used as context for the next turn in the conversation.
426
-
427
- Conversation History to Summarize:
428
- ---
429
- ${formattedHistory}
430
- ---
431
- `.trim();
824
+ cleanupStalePendingData() {
825
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
826
+ const keysToDelete = [];
827
+ for (const [key, data] of this.pendingToolExecutions) {
828
+ if (data.timestamp < fiveMinutesAgo) {
829
+ keysToDelete.push(key);
830
+ }
831
+ }
832
+ if (keysToDelete.length > 0) {
833
+ logger.debug("[RedisConversationMemoryManager] Cleaning up stale pending tool data", {
834
+ stalePendingKeys: keysToDelete.length,
835
+ totalPendingKeys: this.pendingToolExecutions.size,
836
+ });
837
+ keysToDelete.forEach((key) => this.pendingToolExecutions.delete(key));
838
+ }
432
839
  }
433
840
  /**
434
- * Ensure Redis client is initialized
841
+ * Flush pending tool execution data for a session and merge into conversation
435
842
  */
436
- async ensureInitialized() {
437
- logger.debug("[RedisConversationMemoryManager] Ensuring initialization");
438
- if (!this.isInitialized) {
439
- logger.debug("[RedisConversationMemoryManager] Not initialized, initializing now");
440
- await this.initialize();
843
+ async flushPendingToolData(conversation, sessionId, userId) {
844
+ const pendingKey = `${sessionId}:${userId}`;
845
+ const pendingData = this.pendingToolExecutions.get(pendingKey);
846
+ if (!pendingData) {
847
+ logger.debug("[RedisConversationMemoryManager] No pending tool data to flush", {
848
+ sessionId,
849
+ userId,
850
+ pendingKey,
851
+ });
852
+ return;
441
853
  }
442
- else {
443
- logger.debug("[RedisConversationMemoryManager] Already initialized");
854
+ logger.debug("[RedisConversationMemoryManager] Flushing pending tool data", {
855
+ sessionId,
856
+ userId,
857
+ toolCallsCount: pendingData.toolCalls.length,
858
+ toolResultsCount: pendingData.toolResults.length,
859
+ });
860
+ // Create a mapping from toolCallId to toolName for matching tool results
861
+ const toolCallMap = new Map();
862
+ // Create separate messages for tool calls and build the mapping
863
+ for (const toolCall of pendingData.toolCalls) {
864
+ const toolCallId = String(toolCall.toolCallId);
865
+ const toolName = String(toolCall.toolName);
866
+ // Store in mapping for tool results
867
+ toolCallMap.set(toolCallId, toolName);
868
+ const toolCallMessage = {
869
+ id: this.generateMessageId(conversation),
870
+ timestamp: this.generateTimestamp(),
871
+ role: "tool_call",
872
+ content: "", // Can be empty for tool calls
873
+ tool: toolName,
874
+ args: (toolCall.args ||
875
+ toolCall.arguments ||
876
+ toolCall.parameters ||
877
+ {}),
878
+ };
879
+ conversation.messages.push(toolCallMessage);
880
+ }
881
+ // Create separate messages for tool results using the mapping
882
+ for (const toolResult of pendingData.toolResults) {
883
+ const toolCallId = String(toolResult.toolCallId || toolResult.id || "unknown");
884
+ const toolName = toolCallMap.get(toolCallId) || "unknown";
885
+ const toolResultMessage = {
886
+ id: this.generateMessageId(conversation),
887
+ timestamp: this.generateTimestamp(),
888
+ role: "tool_result",
889
+ content: "", // Can be empty for tool results
890
+ tool: toolName, // Now correctly extracted from tool call mapping
891
+ result: {
892
+ success: !toolResult.error,
893
+ result: toolResult.result,
894
+ error: toolResult.error ? String(toolResult.error) : undefined,
895
+ },
896
+ };
897
+ conversation.messages.push(toolResultMessage);
444
898
  }
899
+ // Remove the pending data now that it's been flushed
900
+ this.pendingToolExecutions.delete(pendingKey);
901
+ logger.debug("[RedisConversationMemoryManager] Successfully flushed pending tool data", {
902
+ sessionId,
903
+ userId,
904
+ toolMessagesAdded: pendingData.toolCalls.length + pendingData.toolResults.length,
905
+ totalMessages: conversation.messages.length,
906
+ });
445
907
  }
446
908
  /**
447
909
  * Enforce session limit