@peopl-health/nexus 5.68.7 → 5.68.8

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.
@@ -98,6 +98,7 @@ const draftSchema = new mongoose.Schema({
98
98
  missingCharacterization: { type: [String], default: undefined },
99
99
  suggestCall: { type: suggestCallSchema, default: undefined },
100
100
  sectionsUnavailable: { type: [String], default: undefined },
101
+ snapshotAt: { type: Date, default: undefined },
101
102
  }, { timestamps: { createdAt: true, updatedAt: false } });
102
103
 
103
104
  const doctorHandoffSchema = new mongoose.Schema({
@@ -44,7 +44,7 @@ const AUDIT_SOURCE = 'escalation_review';
44
44
  const RECENT_REVIEW_LIMIT = 20;
45
45
  const TIMELINE_LIMIT = 500;
46
46
  const MAX_REVIEW_UPDATES = 50;
47
- const DRAFT_DEBOUNCE_MS = 60 * 1000;
47
+ const DRAFT_DEBOUNCE_MS = 20 * 1000;
48
48
  const DRAFT_OPERATOR = 'bot:escalationDraft';
49
49
  const DIRECT_CHAT_ATTEMPTS = 3;
50
50
  const RECHECK_DELAY_MS = 60 * 1000;
@@ -1098,6 +1098,7 @@ async function settleFollowUp(snapshot, { background = true } = {}) {
1098
1098
  }
1099
1099
 
1100
1100
  async function generateBotDraft({ id, lease = null } = {}) {
1101
+ const startedAt = new Date();
1101
1102
  const review = await loadReview(reviewIdOf(id));
1102
1103
  if (review.closedAt || !DOCUMENTING_STATUSES.includes(review.status)) return { review: null, retry: false };
1103
1104
  const latest = (review.drafts ?? []).at(-1);
@@ -1127,6 +1128,7 @@ async function generateBotDraft({ id, lease = null } = {}) {
1127
1128
  }
1128
1129
 
1129
1130
  const entry = {
1131
+ snapshotAt: startedAt,
1130
1132
  doctorText: withIdentity({ identity, review, doctorText: draft.doctorText }),
1131
1133
  patientMessage: draft.patientMessage,
1132
1134
  writtenBy: DRAFT_OPERATOR,
@@ -1137,13 +1139,16 @@ async function generateBotDraft({ id, lease = null } = {}) {
1137
1139
  ...(draft.suggestCall && { suggestCall: draft.suggestCall }),
1138
1140
  ...(draft.sectionsUnavailable?.length && { sectionsUnavailable: draft.sectionsUnavailable }),
1139
1141
  };
1140
- const saved = await getEscalationReview().findOneAndUpdate(
1141
- { _id: review._id, status: { $in: DOCUMENTING_STATUSES }, draftDueAt: lease, drafts: { $size: (review.drafts ?? []).length } },
1142
- { $push: { drafts: entry }, $set: { status: 'draft_ready', draftDueAt: null, draftFailedAt: null }, $unset: { identityBlock: '' } },
1142
+ const EscalationReview = getEscalationReview();
1143
+ const saved = await EscalationReview.findOneAndUpdate(
1144
+ { _id: review._id, status: { $in: DOCUMENTING_STATUSES }, draftDueAt: { $lte: lease }, drafts: { $size: (review.drafts ?? []).length } },
1145
+ { $push: { drafts: entry }, $set: { status: 'draft_ready', draftFailedAt: null }, $unset: { identityBlock: '' } },
1143
1146
  { new: true },
1144
1147
  ).lean();
1145
- if (saved) await audit(saved, DRAFT_OPERATOR, null, { action: 'draft_saved', draftNumber: saved.drafts.length });
1146
- return { review: saved, retry: false };
1148
+ if (!saved) return { review: null, retry: false };
1149
+ const released = await EscalationReview.updateOne({ _id: review._id, draftDueAt: lease }, { $set: { draftDueAt: null } }, BOOKKEEPING);
1150
+ await audit(saved, DRAFT_OPERATOR, null, { action: 'draft_saved', draftNumber: saved.drafts.length });
1151
+ return { review: released.matchedCount ? { ...saved, draftDueAt: null } : saved, retry: false };
1147
1152
  }
1148
1153
 
1149
1154
  async function noteFactsContext(review) {
@@ -1361,7 +1366,7 @@ async function handOffToDoctor({ id, mode, physicianEmail = null, sentBy, reques
1361
1366
  if (!approver) throw httpError(403, 'The physician is not an escalation review approver');
1362
1367
  }
1363
1368
 
1364
- const draftedAt = draft.createdAt ? new Date(draft.createdAt).getTime() : null;
1369
+ const draftedAt = draftTimeOf(draft) ? new Date(draftTimeOf(draft)).getTime() : null;
1365
1370
  if (draftedAt && (review.alarms ?? []).some((alarm) => new Date(alarm.raisedAt).getTime() > draftedAt)) {
1366
1371
  throw httpError(409, 'A new alarm arrived after this draft was written; review it and save a new draft before sending it to the doctor');
1367
1372
  }
@@ -1457,7 +1462,7 @@ async function recordDoctorDecision({
1457
1462
  // Truthiness, not != null: an empty textarea is a decline with nothing to say.
1458
1463
  if (decision === 'needs_intervention' && finalText) approvedText = requiredText(finalText, 'finalText');
1459
1464
 
1460
- if (APPROVALS.includes(decision) && arrivedAfter(review, draft.createdAt)) {
1465
+ if (APPROVALS.includes(decision) && arrivedAfter(review, draftTimeOf(draft))) {
1461
1466
  if (review.status === 'awaiting_doctor') {
1462
1467
  const redraft = draft.writtenBy === DRAFT_OPERATOR ? { draftDueAt: new Date(), draftAttempts: 0 } : {};
1463
1468
  const returned = await updateReview(reviewId, ['awaiting_doctor'], { $set: { status: 'documenting', ...redraft } }, {
@@ -1572,6 +1577,8 @@ const latestUpdateGuard = (review) => {
1572
1577
  return { updates: { $size: updates.length }, [`updates.${updates.length - 1}.turnId`]: updates.at(-1).turnId };
1573
1578
  };
1574
1579
 
1580
+ const draftTimeOf = (draft) => draft.snapshotAt ?? draft.createdAt;
1581
+
1575
1582
  const arrivedAfter = (review, since) => Boolean(since) && [
1576
1583
  ...(review.alarms ?? []).map((alarm) => alarm.raisedAt),
1577
1584
  ...(review.updates ?? []).map((update) => update.at),
@@ -6,7 +6,7 @@ const { getEscalationReview } = require('../models/escalationReviewModel');
6
6
  const { BOOKKEEPING, DOCUMENTING_STATUSES, generateBotDraft } = require('./escalationReviewService');
7
7
 
8
8
  const SWEEP_LIMIT = 1;
9
- const DEFAULT_SWEEP_INTERVAL_MS = 30 * 1000;
9
+ const DEFAULT_SWEEP_INTERVAL_MS = 10 * 1000;
10
10
  const RETRY_DELAY_MS = 5 * 60 * 1000;
11
11
  const MAX_DRAFT_ATTEMPTS = 3;
12
12
  const LEASE_MS = 10 * 60 * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.68.7",
3
+ "version": "5.68.8",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",