@peopl-health/nexus 2.5.11 → 2.7.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.
@@ -66,13 +66,12 @@ class GeneralAssistant extends BaseAssistant {
66
66
  }
67
67
 
68
68
  async function startServer() {
69
- // Initialize Nexus with all services and batching enabled
69
+ // Initialize Nexus with check-after processing (immediate response, checks for new messages after)
70
70
  const nexus = new Nexus({
71
71
  messaging: {
72
72
  messageBatching: {
73
- enabled: true,
74
- baseWaitTime: 10000, // 10 seconds base wait (shorter for testing)
75
- randomVariation: 5000 // 0-5 seconds random variation
73
+ enabled: true, // Enable check-after processing
74
+ checkDelayMs: 100 // Delay before checking for new messages (ms)
76
75
  }
77
76
  }
78
77
  });
@@ -170,22 +170,7 @@ class BaseAssistant {
170
170
  async create(code, context = {}) {
171
171
  this._ensureClient();
172
172
  this.status = 'active';
173
-
174
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
175
- if (!provider || typeof provider.createConversation !== 'function') {
176
- throw new Error('Provider not configured. Cannot create conversation.');
177
- }
178
-
179
- const initialMessages = await this.buildInitialMessages({ code, context });
180
- const conversation = await provider.createConversation({
181
- messages: initialMessages,
182
- });
183
-
184
- this.thread = {
185
- ...conversation,
186
- thread_id: conversation?.id,
187
- code,
188
- };
173
+ this.thread = { code };
189
174
 
190
175
  return this.thread;
191
176
  }
@@ -13,6 +13,7 @@ const Logging_ID = require('./runtimeConfig').get('AIRTABLE_LOGGING_ID') || 'app
13
13
  const Monitoreo_ID = require('./runtimeConfig').get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
14
14
  const Programa_Juntas_ID = require('./runtimeConfig').get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
15
15
  const Symptoms_ID = require('./runtimeConfig').get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
16
+ const Follow_Up_ID = require('./runtimeConfig').get('AIRTABLE_FOLLOW_UP_ID') || 'appBjKw1Ub0KkbZf0';
16
17
  const Webinars_Leads_ID = require('./runtimeConfig').get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
17
18
  const Product_ID = require('./runtimeConfig').get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
18
19
 
@@ -30,6 +31,7 @@ const BASE_MAP = {
30
31
  monitoreo: Monitoreo_ID,
31
32
  programa: Programa_Juntas_ID,
32
33
  symptoms: Symptoms_ID,
34
+ followup: Follow_Up_ID,
33
35
  webinars: Webinars_Leads_ID,
34
36
  product: Product_ID
35
37
  };
@@ -44,6 +46,7 @@ module.exports = {
44
46
  Monitoreo_ID,
45
47
  Programa_Juntas_ID,
46
48
  Symptoms_ID,
49
+ Follow_Up_ID,
47
50
  Webinars_Leads_ID,
48
51
  Product_ID,
49
52
  // Helper function to get base by ID
@@ -13,7 +13,7 @@ const Message = mongoose.models.Message;
13
13
 
14
14
  const getConversationController = async (req, res) => {
15
15
  const startTime = Date.now();
16
- logger.info('Starting getConversationController at', new Date().toISOString());
16
+ logger.info(`Starting getConversationController at ${new Date().toISOString()}`);
17
17
 
18
18
  try {
19
19
  // Parse pagination parameters
@@ -34,7 +34,7 @@ const getConversationController = async (req, res) => {
34
34
  }
35
35
 
36
36
  const messageCount = await Message.countDocuments({});
37
- logger.info('Total message count:', messageCount);
37
+ logger.info(`Total message count: ${messageCount}`);
38
38
 
39
39
  if (messageCount === 0) {
40
40
  logger.info('No messages found in database, returning empty conversations list');
@@ -63,7 +63,7 @@ const getConversationController = async (req, res) => {
63
63
  const totalPages = Math.ceil(total / limit);
64
64
  const totalTime = Date.now() - startTime;
65
65
 
66
- logger.info('Number of conversations found:', conversations?.length || 0);
66
+ logger.info(`Number of conversations found: ${conversations?.length || 0}`);
67
67
  logger.info(`Total controller execution time: ${totalTime}ms`);
68
68
  logger.info(`Filter: ${filter}, Pagination: ${conversations.length} of ${total} conversations (page ${page}/${totalPages})`);
69
69
 
@@ -83,8 +83,8 @@ const getConversationController = async (req, res) => {
83
83
  logger.info('Response sent successfully!');
84
84
 
85
85
  } catch (error) {
86
- logger.error('Error fetching conversations:', error);
87
- logger.error('Error stack:', error.stack);
86
+ logger.error('Error fetching conversations:', { error });
87
+ logger.error('Error stack:', { stack: error.stack });
88
88
  res.status(500).json({
89
89
  success: false,
90
90
  error: error.message || 'Failed to fetch conversations'
@@ -93,10 +93,10 @@ const getConversationController = async (req, res) => {
93
93
  };
94
94
 
95
95
  const getConversationMessagesController = async (req, res) => {
96
- logger.info('Starting getConversationMessagesController at', new Date().toISOString());
96
+ logger.info(`Starting getConversationMessagesController at ${new Date().toISOString()}`);
97
97
  try {
98
98
  const { phoneNumber } = req.params;
99
- logger.info('Requested conversation for phone number:', phoneNumber);
99
+ logger.info(`Requested conversation for phone number: ${phoneNumber}`);
100
100
 
101
101
  if (!phoneNumber) {
102
102
  return res.status(400).json({
@@ -113,13 +113,12 @@ const getConversationMessagesController = async (req, res) => {
113
113
  try {
114
114
  query.createdAt = { $lt: new Date(before) };
115
115
  } catch (parseError) {
116
- logger.warn('Invalid date format for before parameter:', before, parseError);
116
+ logger.warn(`Invalid date format for before parameter: ${before}`, { parseError });
117
117
  query.createdAt = { $lt: before };
118
118
  }
119
119
  }
120
120
 
121
- logger.info('Fetching conversation messages', { query, limit });
122
- logger.info('Executing Message.find', { query });
121
+ logger.info(`Fetching conversation messages ${query}, limit ${limit}`);
123
122
  let messages = [];
124
123
 
125
124
  try {
@@ -136,18 +135,18 @@ const getConversationMessagesController = async (req, res) => {
136
135
 
137
136
  const problematicMessages = messages.filter(msg => {
138
137
  if (!msg || !msg.numero || !msg.createdAt) {
139
- logger.warn('Found message missing required fields:', msg?._id || 'unknown');
138
+ logger.warn(`Found message missing required fields: ${msg?._id || 'unknown'}`);
140
139
  return true;
141
140
  }
142
141
 
143
142
  if (msg.media) {
144
143
  if (!msg.media || typeof msg.media !== 'object') {
145
- logger.warn('Found media message with invalid media data:', msg._id);
144
+ logger.warn(`Found media message with invalid media data: ${msg._id}`);
146
145
  return true;
147
146
  }
148
147
 
149
148
  if (msg.media && (typeof msg.media.data === 'function' || msg.media.data instanceof Buffer)) {
150
- logger.warn('Found media message with Buffer data that might cause serialization issues:', msg._id);
149
+ logger.warn(`Found media message with Buffer data that might cause serialization issues: ${msg._id}`);
151
150
  return true;
152
151
  }
153
152
  }
@@ -157,13 +156,14 @@ const getConversationMessagesController = async (req, res) => {
157
156
 
158
157
  if (problematicMessages.length > 0) {
159
158
  logger.warn(`Found ${problematicMessages.length} potentially problematic messages`);
160
- logger.info('First problematic message IDs:', problematicMessages.slice(0, 3).map(m => m?._id || 'unknown'));
159
+ logger.info(`First problematic message IDs: ${problematicMessages.slice(0, 3).map(m => m?._id || 'unknown')}`);
161
160
  }
162
161
  } else {
163
162
  logger.info('No messages found for this query');
164
163
  }
165
164
  } catch (err) {
166
- logger.error('Database query error in message retrieval:', err);
165
+ logger.error('Database query error in message retrieval:', { err });
166
+ logger.error('Database query error in message retrieval:', { err });
167
167
  messages = [];
168
168
  }
169
169
 
@@ -174,7 +174,7 @@ const getConversationMessagesController = async (req, res) => {
174
174
  JSON.stringify(msg);
175
175
  return msg;
176
176
  } catch (serializationError) {
177
- logger.error(`Found non-serializable message with ID ${msg._id}:`, serializationError);
177
+ logger.error(`Found non-serializable message with ID ${msg._id}:`, { serializationError}) ;
178
178
  return {
179
179
  _id: msg._id?.toString() || 'unknown',
180
180
  numero: msg.numero || phoneNumber,
@@ -197,7 +197,8 @@ const getConversationMessagesController = async (req, res) => {
197
197
  });
198
198
  logger.info('Successfully sent conversation messages response');
199
199
  } catch (error) {
200
- logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, error);
200
+ logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, { error });
201
+ logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, { error });
201
202
  res.status(500).json({
202
203
  success: false,
203
204
  error: error.message || 'Failed to fetch conversation'
@@ -206,7 +207,7 @@ const getConversationMessagesController = async (req, res) => {
206
207
  };
207
208
 
208
209
  const getConversationReplyController = async (req, res) => {
209
- logger.info('Starting getConversationReplyController at', new Date().toISOString());
210
+ logger.info(`Starting getConversationReplyController at ${new Date().toISOString()}`);
210
211
  try {
211
212
  const { phoneNumber, message, mediaData, contentSid, variables } = req.body;
212
213
  logger.info('Reply request params:', {
@@ -225,7 +226,7 @@ const getConversationReplyController = async (req, res) => {
225
226
  }
226
227
 
227
228
  const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
228
- logger.info('Formatted phone number:', formattedPhoneNumber);
229
+ logger.info(`Formatted phone number: ${formattedPhoneNumber}`);
229
230
 
230
231
  const messageData = {
231
232
  code: formattedPhoneNumber,
@@ -235,7 +236,7 @@ const getConversationReplyController = async (req, res) => {
235
236
 
236
237
  // Handle template message (contentSid provided)
237
238
  if (contentSid) {
238
- logger.info('Processing template message with contentSid:', contentSid);
239
+ logger.info(`Processing template message with contentSid: ${contentSid}`);
239
240
  messageData.contentSid = contentSid;
240
241
 
241
242
  if (variables && Object.keys(variables).length > 0) {
@@ -281,10 +282,12 @@ const getConversationReplyController = async (req, res) => {
281
282
  message: 'Reply sent successfully'
282
283
  });
283
284
  } catch (error) {
284
- logger.error('Error sending reply:', error);
285
+ logger.error('Error sending reply:', { error });
286
+ logger.error('Error sending reply:', { error });
285
287
  logger.info('Request body', { body: req.body || {} });
286
288
  const errorMsg = error.message || 'Failed to send reply';
287
- logger.error('Responding with error:', errorMsg);
289
+ logger.error('Responding with error:', { errorMsg });
290
+ logger.error('Responding with error:', { errorMsg });
288
291
  res.status(500).json({
289
292
  success: false,
290
293
  error: errorMsg
@@ -361,7 +364,8 @@ const searchConversationsController = async (req, res) => {
361
364
  batch.map(p => `{whatsapp_id} = "${p}"`).join(', ') +
362
365
  ')';
363
366
  return getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula).catch(error => {
364
- logger.error('Error fetching Airtable batch for search:', error);
367
+ logger.error('Error fetching Airtable batch for search:', { error });
368
+ logger.error('Error fetching Airtable batch for search:', { error });
365
369
  return [];
366
370
  });
367
371
  });
@@ -380,7 +384,8 @@ const searchConversationsController = async (req, res) => {
380
384
  });
381
385
  logger.info(`Found ${Object.keys(airtableNameMap).length} names in Airtable for search results (${batches.length} batches)`);
382
386
  } catch (error) {
383
- logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', error);
387
+ logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', { error });
388
+ logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', { error });
384
389
  }
385
390
  }
386
391
 
@@ -445,7 +450,8 @@ const searchConversationsController = async (req, res) => {
445
450
  });
446
451
 
447
452
  } catch (error) {
448
- logger.error('Error searching conversations:', error);
453
+ logger.error('Error searching conversations:', { error });
454
+ logger.error('Error searching conversations:', { error });
449
455
  res.status(500).json({
450
456
  success: false,
451
457
  error: error.message || 'Failed to search conversations'
@@ -478,7 +484,8 @@ const getConversationsByNameController = async (req, res) => {
478
484
  }))
479
485
  });
480
486
  } catch (error) {
481
- logger.error('Error fetching conversations by name:', error);
487
+ logger.error('Error fetching conversations by name:', { error });
488
+ logger.error('Error fetching conversations by name:', { error });
482
489
  res.status(500).json({
483
490
  success: false,
484
491
  error: error.message || 'Failed to fetch conversations by name'
@@ -524,7 +531,8 @@ const getNewMessagesController = async (req, res) => {
524
531
  messages: messages
525
532
  });
526
533
  } catch (error) {
527
- logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, error);
534
+ logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, { error });
535
+ logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, { error });
528
536
  res.status(500).json({
529
537
  success: false,
530
538
  error: error.message || 'Failed to fetch new messages'
@@ -533,7 +541,7 @@ const getNewMessagesController = async (req, res) => {
533
541
  };
534
542
 
535
543
  const markMessagesAsReadController = async (req, res) => {
536
- logger.info('Starting markMessagesAsReadController at', new Date().toISOString());
544
+ logger.info(`Starting markMessagesAsReadController at ${new Date().toISOString()}`);
537
545
  try {
538
546
  const { phoneNumber } = req.params;
539
547
 
@@ -544,7 +552,7 @@ const markMessagesAsReadController = async (req, res) => {
544
552
  });
545
553
  }
546
554
 
547
- logger.info('Marking messages as read for phone number:', phoneNumber);
555
+ logger.info(`Marking messages as read for phone number: ${phoneNumber}`);
548
556
  const result = await Message.updateMany(
549
557
  {
550
558
  numero: phoneNumber,
@@ -563,8 +571,8 @@ const markMessagesAsReadController = async (req, res) => {
563
571
  modifiedCount: result.nModified || result.modifiedCount
564
572
  });
565
573
  } catch (error) {
566
- logger.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, error);
567
- logger.error('Error stack:', error.stack);
574
+ logger.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, { error });
575
+ logger.error('Error stack:', { errorStack: error.stack });
568
576
  res.status(500).json({
569
577
  success: false,
570
578
  error: error.message || 'Failed to mark messages as read'
@@ -573,7 +581,7 @@ const markMessagesAsReadController = async (req, res) => {
573
581
  };
574
582
 
575
583
  const sendTemplateToNewNumberController = async (req, res) => {
576
- logger.info('Starting sendTemplateToNewNumberController at', new Date().toISOString());
584
+ logger.info(`Starting sendTemplateToNewNumberController at ${new Date().toISOString()}`);
577
585
  try {
578
586
  const { phoneNumber, templateId, variables } = req.body;
579
587
 
@@ -618,7 +626,8 @@ const sendTemplateToNewNumberController = async (req, res) => {
618
626
  messageId: message.sid
619
627
  });
620
628
  } catch (error) {
621
- logger.error('Error sending template to new number:', error);
629
+ logger.error('Error sending template to new number:', { error });
630
+ logger.error('Error sending template to new number:', { error });
622
631
  res.status(500).json({
623
632
  success: false,
624
633
  error: error.message || 'Failed to send template to new number'
@@ -627,14 +636,14 @@ const sendTemplateToNewNumberController = async (req, res) => {
627
636
  };
628
637
 
629
638
  const getOpenAIThreadMessagesController = async (req, res) => {
630
- logger.info('Starting getOpenAIThreadMessagesController at', new Date().toISOString());
639
+ logger.info(`Starting getOpenAIThreadMessagesController at ${new Date().toISOString()}`);
631
640
  try {
632
641
  const { phoneNumber } = req.params;
633
642
  const { limit = 50, order = 'desc', runId } = req.query;
634
643
  const variant = process.env.VARIANT || 'assistants';
635
644
 
636
- logger.info('Fetching OpenAI thread messages for:', phoneNumber);
637
- logger.info('Variant:', variant, 'Limit:', limit, 'Order:', order);
645
+ logger.info(`Fetching OpenAI thread messages for: ${phoneNumber}`);
646
+ logger.info(`Variant: ${variant}, Limit: ${limit}, Order: ${order}`);
638
647
 
639
648
  if (!phoneNumber) {
640
649
  return res.status(400).json({
@@ -649,7 +658,7 @@ const getOpenAIThreadMessagesController = async (req, res) => {
649
658
  }).sort({ createdAt: -1 });
650
659
 
651
660
  if (!thread) {
652
- logger.info('No active OpenAI thread found for:', phoneNumber);
661
+ logger.info(`No active OpenAI thread found for: ${phoneNumber}`);
653
662
  return res.status(404).json({
654
663
  success: false,
655
664
  error: 'No active OpenAI thread found for this phone number'
@@ -657,14 +666,14 @@ const getOpenAIThreadMessagesController = async (req, res) => {
657
666
  }
658
667
 
659
668
  let conversationId = thread.conversation_id;
660
- logger.info('Thread found - Conversation ID:', conversationId);
669
+ logger.info(`Thread found - Conversation ID: ${conversationId}`);
661
670
 
662
671
  const provider = llmConfig.getOpenAIProvider({ instantiate: true, variant });
663
672
  if (!provider) {
664
673
  throw new Error('OpenAI provider not initialized');
665
674
  }
666
675
 
667
- logger.info('Using provider variant:', provider.getVariant());
676
+ logger.info(`Using provider variant: ${provider.getVariant()}`);
668
677
 
669
678
  const queryParams = {
670
679
  threadId: conversationId,
@@ -679,7 +688,8 @@ const getOpenAIThreadMessagesController = async (req, res) => {
679
688
  let messages;
680
689
  let threadRecreated = false;
681
690
 
682
- logger.info('Calling listMessages with params:', queryParams);
691
+ logger.info('Calling listMessages with params:', { queryParams });
692
+ logger.info('Calling listMessages with params:', { queryParams });
683
693
  messages = await withThreadRecovery(
684
694
  async (currentThread = thread) => {
685
695
  if (currentThread !== thread) {
@@ -712,8 +722,8 @@ const getOpenAIThreadMessagesController = async (req, res) => {
712
722
  });
713
723
 
714
724
  } catch (error) {
715
- logger.error('Error fetching OpenAI thread messages:', error);
716
- logger.error('Error stack:', error.stack);
725
+ logger.error('Error fetching OpenAI thread messages:', { error });
726
+ logger.error('Error stack:', { errorStack: error.stack });
717
727
  res.status(500).json({
718
728
  success: false,
719
729
  error: error.message || 'Failed to fetch OpenAI thread messages'
@@ -41,11 +41,11 @@ class NexusMessaging {
41
41
  keyword: [],
42
42
  flow: []
43
43
  };
44
- // Message batching
45
- this.pendingResponses = new Map();
44
+ // Message processing with check-after strategy
45
+ this.processingLocks = new Map(); // Per-chat locks to prevent parallel processing
46
46
  this.batchingConfig = {
47
- enabled: config.messageBatching?.enabled ?? false,
48
- baseWaitTime: config.messageBatching?.baseWaitTime ?? 10000
47
+ enabled: config.messageBatching?.enabled ?? true, // Enabled by default with check-after
48
+ checkDelayMs: config.messageBatching?.checkDelayMs ?? 100 // Delay before checking for new messages
49
49
  };
50
50
  }
51
51
 
@@ -393,10 +393,9 @@ class NexusMessaging {
393
393
  } else if (messageData.flow) {
394
394
  return await this.handleFlow(messageData);
395
395
  } else {
396
- // For regular messages and media, use batching if enabled
397
- logger.info('Batching config:', this.batchingConfig);
396
+ // For regular messages and media, use check-after processing
398
397
  if (this.batchingConfig.enabled && chatId) {
399
- return await this._handleWithBatching(messageData, chatId);
398
+ return await this._handleWithCheckAfter(chatId);
400
399
  } else {
401
400
  if (messageData.media) {
402
401
  return await this.handleMedia(messageData);
@@ -649,77 +648,52 @@ class NexusMessaging {
649
648
  }
650
649
 
651
650
  /**
652
- * Handle message with batching - waits for additional messages before processing
651
+ * Handle message with check-after strategy - process immediately, check for new messages after
653
652
  */
654
- async _handleWithBatching(messageData, chatId) {
655
- const existing = this.pendingResponses.get(chatId);
656
- if (existing) {
657
- clearTimeout(existing.timeoutId);
658
- if (existing.typingInterval) {
659
- clearInterval(existing.typingInterval);
660
- }
661
- logger.info(`Received additional message from ${chatId}, resetting wait timer`);
662
- }
663
-
664
- // Start typing indicator refresh for batching period
665
- const typingInterval = await this._startTypingRefresh(chatId);
666
-
667
- const waitTime = this.batchingConfig.baseWaitTime;
668
- const timeoutId = setTimeout(async () => {
669
- try {
670
- if (typingInterval) {
671
- clearInterval(typingInterval);
672
- }
673
- this.pendingResponses.delete(chatId);
674
- await this._handleBatchedMessages(chatId);
675
- } catch (error) {
676
- logger.error(`Error handling batched messages for ${chatId}:`, error);
677
- }
678
- }, waitTime);
679
-
680
- this.pendingResponses.set(chatId, { timeoutId, typingInterval });
681
- logger.info(`Waiting ${Math.round(waitTime/1000)} seconds for more messages from ${chatId}`);
682
- }
683
-
684
- /**
685
- * Start typing indicator refresh interval
686
- */
687
- async _startTypingRefresh(chatId) {
688
- if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
689
- return null;
653
+ async _handleWithCheckAfter(chatId) {
654
+ // If already processing this chat, just return (message is saved, will be picked up)
655
+ if (this.processingLocks.has(chatId)) {
656
+ logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
657
+ return;
690
658
  }
691
659
 
692
- const lastMessage = await Message.findOne({
693
- numero: chatId,
694
- from_me: false,
695
- message_id: { $exists: true, $ne: null }
696
- }).sort({ createdAt: -1 });
697
-
698
- if (!lastMessage?.message_id) return null;
699
-
700
- return setInterval(() =>
701
- this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
702
- logger.debug('[_startTypingRefresh] Failed', { error: err.message })
703
- ), 5000
704
- );
660
+ await this._processWithLock(chatId);
705
661
  }
706
662
 
707
663
  /**
708
- * Process all batched messages for a chat
664
+ * Process messages with per-chat lock and check-after logic
709
665
  */
710
- async _handleBatchedMessages(chatId) {
666
+ async _processWithLock(chatId) {
667
+ this.processingLocks.set(chatId, true);
711
668
  let typingInterval = null;
712
-
669
+
713
670
  try {
714
- logger.info(`Processing batched messages from ${chatId} (including media if any)`);
715
-
716
671
  typingInterval = await this._startTypingRefresh(chatId);
717
-
718
- // Get assistant response
672
+ logger.info(`[CheckAfter] Processing messages for ${chatId}`);
673
+
674
+ // Process with assistant
719
675
  const result = await replyAssistant(chatId);
720
676
  const botResponse = typeof result === 'string' ? result : result?.output;
721
677
  const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
722
-
678
+
679
+ // Small delay to catch very recent DB writes
680
+ await new Promise(resolve => setTimeout(resolve, this.batchingConfig.checkDelayMs));
681
+
682
+ // Check for new unprocessed messages
683
+ const hasNewMessages = await Message.exists({
684
+ numero: chatId,
685
+ processed: false,
686
+ from_me: false
687
+ });
688
+
689
+ if (hasNewMessages) {
690
+ logger.info(`[CheckAfter] New messages detected for ${chatId}, discarding response and reprocessing`);
691
+ if (typingInterval) clearInterval(typingInterval);
692
+ // Recursively process with new messages
693
+ return await this._processWithLock(chatId);
694
+ }
695
+
696
+ // No new messages - send response and mark processed
723
697
  if (botResponse) {
724
698
  await this.sendMessage({
725
699
  code: chatId,
@@ -729,40 +703,45 @@ class NexusMessaging {
729
703
  tools_executed
730
704
  });
731
705
  }
732
-
733
- // Emit event for batched processing
734
- this.events.emit('messages:batched', { chatId, response: botResponse });
735
-
706
+
707
+ this.events.emit('messages:processed', { chatId, response: botResponse });
708
+
736
709
  } catch (error) {
737
- logger.error('Error in batched message handling:', { error: error.message });
710
+ logger.error('[CheckAfter] Error processing messages:', { chatId, error: error.message });
738
711
  } finally {
739
- if (typingInterval) {
740
- clearInterval(typingInterval);
741
- }
712
+ if (typingInterval) clearInterval(typingInterval);
713
+ this.processingLocks.delete(chatId);
742
714
  }
743
715
  }
744
716
 
745
717
  /**
746
- * Clear pending response for a chat (useful for cleanup)
718
+ * Start typing indicator refresh interval
747
719
  */
748
- clearPendingResponse(chatId) {
749
- const pending = this.pendingResponses.get(chatId);
750
- if (pending) {
751
- if (pending.timeoutId) {
752
- clearTimeout(pending.timeoutId);
753
- }
754
- if (pending.typingInterval) {
755
- clearInterval(pending.typingInterval);
756
- }
757
- this.pendingResponses.delete(chatId);
720
+ async _startTypingRefresh(chatId) {
721
+ if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
722
+ return null;
758
723
  }
724
+
725
+ const lastMessage = await Message.findOne({
726
+ numero: chatId,
727
+ from_me: false,
728
+ message_id: { $exists: true, $ne: null }
729
+ }).sort({ createdAt: -1 });
730
+
731
+ if (!lastMessage?.message_id) return null;
732
+
733
+ return setInterval(() =>
734
+ this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
735
+ logger.debug('[_startTypingRefresh] Failed', { error: err.message })
736
+ ), 5000
737
+ );
759
738
  }
760
739
 
761
740
  /**
762
- * Get batching status for a chat
741
+ * Check if chat is currently being processed
763
742
  */
764
- hasPendingResponse(chatId) {
765
- return this.pendingResponses.has(chatId);
743
+ isProcessing(chatId) {
744
+ return this.processingLocks.has(chatId);
766
745
  }
767
746
  }
768
747
 
@@ -2,31 +2,6 @@ const moment = require('moment-timezone');
2
2
  const { Message } = require('../models/messageModel.js');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
- const addMessageToThread = async (reply, messagesChat, provider, thread) => {
6
- const threadId = thread.getConversationId();
7
-
8
- if (reply.interactive_type === 'flow') {
9
- logger.info(`[addMessageToThread] Skipping flow message (UI only) - ID: ${reply.message_id}`);
10
- return;
11
- }
12
-
13
- if (reply.origin === 'whatsapp_platform') {
14
- await provider.addMessage({
15
- threadId,
16
- role: 'assistant',
17
- content: messagesChat
18
- });
19
- } else if (reply.origin === 'patient') {
20
- await provider.addMessage({
21
- threadId,
22
- role: 'user',
23
- content: messagesChat
24
- });
25
- }
26
-
27
- logger.info(`[addMessageToThread] Message added - ID: ${reply.message_id}, Thread: ${threadId}, Origin: ${reply.origin}`);
28
- };
29
-
30
5
  const updateMessageRecord = async (reply, thread) => {
31
6
  const threadId = thread.getConversationId();
32
7
 
@@ -125,7 +100,6 @@ async function isRecentMessage(chatId) {
125
100
  }
126
101
 
127
102
  module.exports = {
128
- addMessageToThread,
129
103
  updateMessageRecord,
130
104
  getLastMessages,
131
105
  getLastNMessages,
@@ -80,6 +80,12 @@ messageSchema.index({ numero: 1, createdAt: -1 });
80
80
  messageSchema.index({ numero: 1, processed: 1, origin: 1 }, { name: 'numero_processed_origin_idx' });
81
81
  messageSchema.index({ numero: 1, createdAt: -1, processed: 1 }, { name: 'numero_created_processed_idx' });
82
82
 
83
+ // Indexes for conversation aggregation queries
84
+ messageSchema.index({ group_id: 1, createdAt: 1 }, { name: 'conversation_sort_idx' });
85
+ messageSchema.index({ group_id: 1, from_me: 1, read: 1 }, { name: 'unread_filter_idx' });
86
+ messageSchema.index({ group_id: 1, numero: 1, createdAt: -1 }, { name: 'conversation_lookup_idx' });
87
+ messageSchema.index({ createdAt: -1 }, { name: 'global_sort_idx' });
88
+
83
89
  messageSchema.pre('save', function (next) {
84
90
  if (this.timestamp) {
85
91
  this.timestamp = moment.tz(this.timestamp, 'America/Mexico_City').toDate();