@peopl-health/nexus 3.0.3 → 3.0.4

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.
@@ -43,9 +43,14 @@ class NexusMessaging {
43
43
  };
44
44
  // Message processing with check-after strategy
45
45
  this.processingLocks = new Map(); // Per-chat locks to prevent parallel processing
46
+ this.activeRequests = new Map(); // Track active AI requests per chat
47
+ this.abandonedRuns = new Set(); // Track runs that should be ignored
46
48
  this.batchingConfig = {
47
49
  enabled: config.messageBatching?.enabled ?? true, // Enabled by default with check-after
48
- checkDelayMs: config.messageBatching?.checkDelayMs ?? 100 // Delay before checking for new messages
50
+ abortOnNewMessage: config.messageBatching?.abortOnNewMessage ?? true, // Abort ongoing AI calls when new messages arrive
51
+ immediateRestart: config.messageBatching?.immediateRestart ?? true, // Start new processing immediately without waiting
52
+ batchWindowMs: config.messageBatching?.batchWindowMs ?? 2000, // Wait up to 2s for message bursts
53
+ maxBatchWait: config.messageBatching?.maxBatchWait ?? 5000 // Maximum time to wait for batching
49
54
  };
50
55
  }
51
56
 
@@ -656,6 +661,20 @@ class NexusMessaging {
656
661
 
657
662
  if (this.processingLocks.has(chatId)) {
658
663
  logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
664
+
665
+ if (this.batchingConfig.abortOnNewMessage && this.activeRequests.has(chatId)) {
666
+ const runId = this.activeRequests.get(chatId);
667
+ this.abandonedRuns.add(runId);
668
+ logger.info(`[CheckAfter] Marked run ${runId} as abandoned for ${chatId}`);
669
+
670
+ if (this.batchingConfig.immediateRestart) {
671
+ this.processingLocks.delete(chatId);
672
+ this.activeRequests.delete(chatId);
673
+
674
+ logger.info(`[CheckAfter] Starting immediate reprocessing for ${chatId}`);
675
+ await this._processWithLock(chatId, null);
676
+ }
677
+ }
659
678
  return;
660
679
  }
661
680
 
@@ -668,33 +687,48 @@ class NexusMessaging {
668
687
  async _processWithLock(chatId, existingTypingInterval = null) {
669
688
  this.processingLocks.set(chatId, true);
670
689
  let typingInterval = existingTypingInterval;
690
+ let runId = null;
671
691
 
672
692
  try {
673
693
  if (!typingInterval) {
674
694
  typingInterval = await this._startTypingRefresh(chatId);
675
695
  }
676
- logger.info(`[CheckAfter] Processing messages for ${chatId}`);
677
-
678
- const result = await replyAssistant(chatId);
679
- const botResponse = typeof result === 'string' ? result : result?.output;
680
- const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
681
-
682
- // Small delay to catch very recent DB writes
683
- await new Promise(resolve => setTimeout(resolve, this.batchingConfig.checkDelayMs));
696
+
697
+ const startTime = Date.now();
698
+ let messageCount = await this._getUnprocessedMessageCount(chatId);
699
+ let lastCount = messageCount;
700
+
701
+ while (Date.now() - startTime < this.batchingConfig.batchWindowMs) {
702
+ await new Promise(resolve => setTimeout(resolve, 500));
703
+ const newCount = await this._getUnprocessedMessageCount(chatId);
704
+
705
+ if (newCount > lastCount) {
706
+ lastCount = newCount;
707
+ logger.info(`[Batching] New message detected for ${chatId}, extending wait`);
708
+ }
709
+
710
+ if (Date.now() - startTime >= this.batchingConfig.maxBatchWait) {
711
+ logger.info(`[Batching] Max wait reached for ${chatId}`);
712
+ break;
713
+ }
714
+ }
715
+
716
+ logger.info(`[CheckAfter] Processing ${lastCount} messages for ${chatId} after batching`);
684
717
 
685
- // Check for new unprocessed messages
686
- const hasNewMessages = await Message.exists({
687
- numero: chatId,
688
- processed: false,
689
- from_me: false
690
- });
718
+ runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
719
+ this.activeRequests.set(chatId, runId);
691
720
 
692
- if (hasNewMessages) {
693
- logger.info(`[CheckAfter] New messages detected for ${chatId}, discarding response and reprocessing`);
694
- return await this._processWithLock(chatId, typingInterval);
721
+ const result = await replyAssistant(chatId, null, null, { runId });
722
+
723
+ if (this.abandonedRuns.has(runId)) {
724
+ logger.info(`[CheckAfter] Discarding abandoned run ${runId} for ${chatId}`);
725
+ this.abandonedRuns.delete(runId);
726
+ return;
695
727
  }
728
+
729
+ const botResponse = typeof result === 'string' ? result : result?.output;
730
+ const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
696
731
 
697
- // No new messages - send response and mark processed
698
732
  if (botResponse) {
699
733
  await this.sendMessage({
700
734
  code: chatId,
@@ -712,9 +746,25 @@ class NexusMessaging {
712
746
  } finally {
713
747
  if (typingInterval) clearInterval(typingInterval);
714
748
  this.processingLocks.delete(chatId);
749
+ this.activeRequests.delete(chatId);
750
+ if (this.abandonedRuns.size > 100) {
751
+ this.abandonedRuns.clear();
752
+ }
715
753
  }
716
754
  }
717
755
 
756
+ /**
757
+ * Get count of unprocessed messages for a chat
758
+ */
759
+ async _getUnprocessedMessageCount(chatId) {
760
+ const { Message } = require('../models/messageModel');
761
+ return await Message.countDocuments({
762
+ numero: chatId,
763
+ processed: false,
764
+ from_me: false
765
+ });
766
+ }
767
+
718
768
  /**
719
769
  * Start typing indicator refresh interval
720
770
  */
@@ -726,10 +776,16 @@ class NexusMessaging {
726
776
  const lastMessage = await Message.findOne({
727
777
  numero: chatId,
728
778
  from_me: false,
729
- message_id: { $exists: true, $ne: null }
779
+ processed: false,
780
+ message_id: { $exists: true, $ne: null, $not: /^pending-/ }
730
781
  }).sort({ createdAt: -1 });
731
782
 
732
- if (!lastMessage?.message_id) return null;
783
+ if (!lastMessage?.message_id) {
784
+ logger.debug(`[_startTypingRefresh] No valid message for typing indicator: ${chatId}`);
785
+ return null;
786
+ }
787
+
788
+ logger.debug(`[_startTypingRefresh] Starting typing indicator for message: ${lastMessage.message_id}`);
733
789
 
734
790
  return setInterval(() =>
735
791
  this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
@@ -6,6 +6,7 @@ const {
6
6
  } = require('./OpenAIResponsesProviderTools');
7
7
  const { DefaultConversationManager } = require('../services/DefaultConversationManager');
8
8
  const { logger } = require('../utils/logger');
9
+ const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
9
10
 
10
11
  const CONVERSATION_PREFIX = 'conv_';
11
12
  const RESPONSE_PREFIX = 'resp_';
@@ -202,7 +203,8 @@ class OpenAIResponsesProvider {
202
203
  const clinicalData = await this.conversationManager.getClinicalData(thread.code);
203
204
  const promptVariables = clinicalData ? {
204
205
  clinical_context: clinicalData.clinicalContext || '',
205
- last_symptoms: clinicalData.lastSymptoms || ''
206
+ last_symptoms: clinicalData.lastSymptoms || '',
207
+ current_date: getCurrentMexicoDateTime(),
206
208
  } : null;
207
209
 
208
210
  // Execute with built context
@@ -18,9 +18,17 @@ const dateAndTimeFromStart = (startTime) => {
18
18
  };
19
19
  };
20
20
 
21
+ const getCurrentMexicoDateTime = () => {
22
+ return moment()
23
+ .tz('America/Mexico_City')
24
+ .locale('es')
25
+ .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
26
+ };
27
+
21
28
  module.exports = {
22
29
  ISO_DATE,
23
30
  parseStartTime,
24
31
  addDays,
25
- dateAndTimeFromStart
32
+ dateAndTimeFromStart,
33
+ getCurrentMexicoDateTime
26
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",