@peopl-health/nexus 5.71.3 → 5.72.0-dev.12915

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.
@@ -33,6 +33,8 @@ const ticketClaims = new WriteClaimStore({ namespace: 'inbound-recovery-ticket',
33
33
  const coldTicketClaims = new WriteClaimStore({ namespace: 'inbound-recovery-cold-ticket', ttlMs: COLD_TICKET_TTL_MS, maxSize: 2000 });
34
34
 
35
35
  const FAILED_SEND = { 'statusInfo.status': 'failed', message_id: null };
36
+ // A holding bubble sent mid-turn, not an answer: a turn that dies after it leaves the patient waiting.
37
+ const BRIDGE = { 'raw.bridge': true };
36
38
  const UNANSWERED_QUESTION = 'El paciente escribió y no recibió respuesta.';
37
39
 
38
40
  async function unansweredThreads(now, { minAgeMs = MIN_AGE_MS, maxAgeMs = MAX_AGE_MS, limit = SCAN_LIMIT } = {}) {
@@ -47,24 +49,26 @@ async function unansweredThreads(now, { minAgeMs = MIN_AGE_MS, maxAgeMs = MAX_AG
47
49
  ).sort({ lastMessageAt: 1 }).limit(limit).lean();
48
50
  }
49
51
 
50
- async function codesWithAFailedSend(now, { maxAgeMs = MAX_AGE_MS, limit = SCAN_LIMIT } = {}) {
52
+ async function codesWithOutbound(match, now, { maxAgeMs = MAX_AGE_MS, limit = SCAN_LIMIT } = {}) {
51
53
  const rows = await aggregateMessages([
52
- { $match: { ...FAILED_SEND, from_me: true, createdAt: { $gte: new Date(now.getTime() - maxAgeMs) } } },
53
- { $group: { _id: '$numero', firstFailedAt: { $min: '$createdAt' } } },
54
+ { $match: { ...match, from_me: true, createdAt: { $gte: new Date(now.getTime() - maxAgeMs) } } },
55
+ { $group: { _id: '$numero', firstAt: { $min: '$createdAt' } } },
54
56
  // _id breaks the tie: without it a page of shared timestamps orders differently each pass.
55
- { $sort: { firstFailedAt: 1, _id: 1 } },
57
+ { $sort: { firstAt: 1, _id: 1 } },
56
58
  { $limit: limit },
57
59
  ]);
58
60
  return rows.map((row) => row._id);
59
61
  }
60
62
 
61
- async function candidateCodes(now, { threadBand = null, failedMaxAgeMs = MAX_AGE_MS, limit = SCAN_LIMIT } = {}) {
62
- const [threads, failedSends] = await Promise.all([
63
+ async function candidateCodes(now, { threadBand = null, outboundMaxAgeMs = MAX_AGE_MS, limit = SCAN_LIMIT } = {}) {
64
+ const outbound = { maxAgeMs: outboundMaxAgeMs, limit };
65
+ const [threads, failedSends, bridges] = await Promise.all([
63
66
  unansweredThreads(now, { ...(threadBand || {}), limit }),
64
- codesWithAFailedSend(now, { maxAgeMs: failedMaxAgeMs, limit }),
67
+ codesWithOutbound(FAILED_SEND, now, outbound),
68
+ codesWithOutbound(BRIDGE, now, outbound),
65
69
  ]);
66
- const atCeiling = threads.length >= limit || failedSends.length >= limit;
67
- const codes = [...new Set([...threads.map((thread) => thread.code), ...failedSends].filter(Boolean))];
70
+ const atCeiling = threads.length >= limit || failedSends.length >= limit || bridges.length >= limit;
71
+ const codes = [...new Set([...threads.map((thread) => thread.code), ...failedSends, ...bridges].filter(Boolean))];
68
72
  return { codes, atCeiling };
69
73
  }
70
74
 
@@ -81,7 +85,7 @@ async function stillUnanswered(code, now = new Date(), { minAgeMs = MIN_AGE_MS,
81
85
  const ageMs = now.getTime() - new Date(newest.createdAt).getTime();
82
86
  if (!Number.isFinite(ageMs) || ageMs < minAgeMs || ageMs > maxAgeMs) return null;
83
87
 
84
- const replies = await countMessages({ numero: code, from_me: true, createdAt: { $gt: newest.createdAt }, $nor: [FAILED_SEND] });
88
+ const replies = await countMessages({ numero: code, from_me: true, createdAt: { $gt: newest.createdAt }, $nor: [FAILED_SEND, BRIDGE] });
85
89
  return replies === 0 ? newest : null;
86
90
  }
87
91
 
@@ -235,7 +239,7 @@ async function sweepColdUnansweredInbound({
235
239
  if (mode === 'off') return { waiting: 0, alerted: 0 };
236
240
 
237
241
  const { codes, atCeiling } = await candidateCodes(now, {
238
- threadBand: COLD_BAND, failedMaxAgeMs: COLD_MAX_AGE_MS, limit: COLD_SCAN_LIMIT,
242
+ threadBand: COLD_BAND, outboundMaxAgeMs: COLD_MAX_AGE_MS, limit: COLD_SCAN_LIMIT,
239
243
  });
240
244
  // Answered conversations are filtered below, after the page is cut, so a full page is a page
241
245
  // that may be hiding patients behind it. Say so rather than let the tail go unseen quietly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.71.3",
3
+ "version": "5.72.0-dev.12915",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",