@peopl-health/nexus 5.68.1 → 5.68.3

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.
@@ -6,12 +6,12 @@ const URGENCY_LABELS = {
6
6
  stat: 'inmediata', asap: 'lo antes posible', urgent: 'urgente', routine: 'rutina',
7
7
  };
8
8
 
9
- const PROPOSAL_HEADING = 'Propuesta de tratamiento (sugerencia para su aprobación, no es una indicación):';
10
- const REQUESTED_ACTION = 'Acción solicitada: ¿aprueba la propuesta, la modifica o prefiere una valoración presencial?';
11
- const ASSESSMENT_ACTION = 'Acción solicitada: valoración médica prioritaria. ¿La valora en persona, la envía a urgencias o indica otra conducta?';
9
+ const REQUESTED_ACTION = '¿Aprueba la propuesta, la modifica o prefiere una valoración presencial?';
10
+ const ASSESSMENT_ACTION = 'Requiere valoración médica: ¿la valora en persona, la envía a urgencias o indica otra conducta?';
12
11
  const DOCTOR_TITLE = /^(dr|dra)\.?\s/i;
13
12
 
14
13
  const withTitle = (name) => (DOCTOR_TITLE.test(name) ? name : `Dr./Dra. ${name}`);
14
+ const sentence = (text) => (/[.!?]$/.test(text) ? text : `${text}.`);
15
15
 
16
16
  function latestAlarm(review) {
17
17
  const alarms = Array.isArray(review?.alarms) ? review.alarms : [];
@@ -28,19 +28,6 @@ function flagsByStatus(sections) {
28
28
  };
29
29
  }
30
30
 
31
- function symptomLines(sections, stamp, fallbackSymptom = null) {
32
- const grade = [1, 2, 3].includes(sections?.grade) ? `G${sections.grade}` : 'grado no establecido';
33
- const said = [inlineOf(sections?.symptom) || fallbackSymptom || 'síntoma no especificado', grade, ...stamp].filter(Boolean);
34
- const { present, absent, unassessed } = flagsByStatus(sections);
35
- if (!present.length && !absent.length && !unassessed.length) return [`Síntoma: ${said.join(' · ')}`, 'Banderas rojas: no documentadas.'];
36
- return [
37
- `Síntoma: ${said.join(' · ')}`,
38
- `Banderas rojas presentes: ${present.length ? present.join(', ') : 'ninguna'}.`,
39
- `Banderas rojas ausentes: ${absent.length ? absent.join(', ') : 'ninguna revisada'}.`,
40
- ...(unassessed.length ? [`Banderas rojas sin evaluar: ${unassessed.join(', ')}.`] : []),
41
- ];
42
- }
43
-
44
31
  function vitalsLine(vitals) {
45
32
  const parts = [];
46
33
  if (vitals?.temperatureC != null) parts.push(`T ${vitals.temperatureC} °C`);
@@ -48,62 +35,66 @@ function vitalsLine(vitals) {
48
35
  if (vitals?.systolic != null || vitals?.diastolic != null) parts.push(`PA ${vitals.systolic ?? '?'}/${vitals.diastolic ?? '?'} mmHg`);
49
36
  if (vitals?.spo2 != null) parts.push(`SpO2 ${vitals.spo2}%`);
50
37
  if (vitals?.respiratoryRate != null) parts.push(`FR ${vitals.respiratoryRate} rpm`);
51
- return parts.length ? `Signos vitales: ${parts.join(' · ')}.` : 'Signos vitales: no reportados.';
38
+ return parts.length ? `Signos vitales: ${parts.join(' · ')}.` : null;
52
39
  }
53
40
 
54
- function proposalItemLine(symptom, item) {
41
+ function drugOf(symptom, item) {
55
42
  const entry = formularyFor(symptom);
56
43
  const drug = entry ? findDrug(entry, item) : null;
57
44
  if (!drug) return null;
58
- const name = [drug.drug, drug.brand ? `(${drug.brand})` : null, drug.strength].filter(Boolean).join(' ');
59
- const parts = [`- ${name}`];
60
- if (drug.instructions) parts.push(drug.instructions);
61
- parts.push(`posición en la tabla: ${drug.tableLabel}`);
62
- const why = inlineOf(item?.justification?.text);
63
- if (why) parts.push(`motivo: ${why}`);
64
- return parts.join(' — ');
45
+ const name = [drug.drug, drug.strength].filter(Boolean).join(' ');
46
+ const why = inlineOf(item?.justification?.text)?.replace(/[.;]+$/, '');
47
+ return [drug.instructions ? `${name} (${drug.instructions})` : name, why && `motivo: ${why}`].filter(Boolean).join(', ');
65
48
  }
66
49
 
67
- function proposalBody({ proposal, withheldReason }) {
68
- if (!proposal?.items?.length) {
69
- const reason = inlineOf(withheldReason) || 'sin propuesta farmacológica';
70
- return [`No se propone tratamiento sintomático: ${reason}. Se prioriza la valoración médica.`];
71
- }
72
- const lines = proposal.items.map((item) => proposalItemLine(proposal.symptom, item)).filter(Boolean);
73
- const declared = [...(proposal.conditions ?? []), ...(proposal.ruledOut ?? []).map((key) => `descartado: ${key}`)].map(inlineOf).filter(Boolean);
74
- if (declared.length) lines.push(`Condiciones que se declararon para esta propuesta (verificar): ${declared.join(', ')}.`);
75
- return lines;
50
+ const proposedDrugs = ({ proposal }) => (proposal?.items ?? []).map((item) => drugOf(proposal.symptom, item)).filter(Boolean);
51
+
52
+ function proposalLine({ proposal, withheldReason }) {
53
+ const drugs = proposedDrugs({ proposal });
54
+ if (!drugs.length) return `→ Sin propuesta de tratamiento: ${sentence(inlineOf(withheldReason) || 'requiere valoración médica')}`;
55
+ const declared = [...(proposal.conditions ?? []), ...(proposal.ruledOut ?? []).map((key) => `descartado ${key}`)]
56
+ .map((key) => inlineOf(key)?.replace(/_/g, ' ')).filter(Boolean);
57
+ const table = formularyFor(proposal.symptom)?.label;
58
+ return [`→ Propuesta para su aprobación (tabla ${table}): ${sentence(drugs.join('; '))}`, declared.length && `Verificar: ${sentence(declared.join(', '))}`].filter(Boolean).join(' ');
76
59
  }
77
60
 
78
- function proposalHeading(entry, symptomName) {
79
- const tableLabel = entry.proposal?.items?.length ? formularyFor(entry.proposal.symptom)?.label : null;
80
- return tableLabel ? `${symptomName} (tabla: ${tableLabel})` : symptomName;
61
+ function symptomParagraph(entry, fallbackSymptom) {
62
+ const sections = entry.sections ?? {};
63
+ const grade = [1, 2, 3].includes(sections.grade) ? `G${sections.grade}` : 'grado no establecido';
64
+ const { present, absent, unassessed } = flagsByStatus(sections);
65
+ const isUndocumented = !present.length && !absent.length && !unassessed.length;
66
+ const summary = inlineOf(sections.characterizationSummary);
67
+ return [
68
+ `${inlineOf(sections.symptom) || fallbackSymptom || 'Síntoma no especificado'} (${grade}):`,
69
+ summary && sentence(summary),
70
+ present.length && `${present.length > 1 ? 'Banderas rojas presentes' : 'Bandera roja presente'}: ${sentence(present.join(', '))}`,
71
+ unassessed.length && `Falta confirmar: ${sentence(unassessed.join(', '))}`,
72
+ isUndocumented && 'Banderas rojas no documentadas.',
73
+ !present.length && !unassessed.length && absent.length && 'Sin banderas rojas.',
74
+ proposalLine(entry),
75
+ ].filter(Boolean).join(' ');
81
76
  }
82
77
 
83
78
  function composeDoctorBody({ alarm, shared, symptoms }) {
84
- const several = symptoms.length > 1;
85
- const name = (entry) => inlineOf(entry.sections?.symptom) || 'Síntoma';
86
- const stamp = [
87
- URGENCY_LABELS[alarm?.urgencyLevel] ?? inlineOf(alarm?.urgencyLevel),
88
- alarm?.raisedAt ? mexicoStamp(alarm.raisedAt) : null,
89
- ];
90
79
  const context = shared?.oncologyContext;
91
- const value = (text) => inlineOf(text) || 'no disponible';
80
+ const treatment = inlineOf(context?.treatment);
81
+ const lastDose = inlineOf(context?.lastDose);
82
+ const priorManagement = inlineOf(context?.priorManagement);
83
+ const response = inlineOf(context?.response);
84
+ const urgency = [URGENCY_LABELS[alarm?.urgencyLevel] ?? inlineOf(alarm?.urgencyLevel), alarm?.raisedAt ? mexicoStamp(alarm.raisedAt) : null].filter(Boolean);
85
+ const priorities = inlineOf(shared?.doctorPriorities);
92
86
  return [
93
- ...symptoms.flatMap((entry, index) => symptomLines(entry.sections, index === 0 ? stamp : [], several ? null : inlineOf(alarm?.symptom))),
94
- '',
95
- ...symptoms.map((entry) => `${several ? `Caracterización (${name(entry)})` : 'Caracterización'}: ${inlineOf(entry.sections?.characterizationSummary) || 'no disponible'}`),
87
+ urgency.length ? `Urgencia: ${urgency.join(' · ')}` : null,
88
+ `Tratamiento: ${treatment || 'no disponible'}; última dosis ${lastDose || 'no disponible'}.`,
89
+ priorManagement || response ? `Manejo previo: ${sentence([priorManagement, response && `respuesta ${response}`].filter(Boolean).join('; '))}` : null,
96
90
  vitalsLine(shared?.vitals),
97
- ...(inlineOf(shared?.objectiveData) ? [`Datos objetivos: ${inlineOf(shared.objectiveData)}`] : []),
98
- '',
99
- `Contexto oncológico: tratamiento ${value(context?.treatment)}; última dosis ${value(context?.lastDose)}.`,
100
- `Manejo previo y respuesta: ${value(context?.priorManagement)}; respuesta ${value(context?.response)}.`,
91
+ inlineOf(shared?.objectiveData) ? `Datos objetivos: ${sentence(inlineOf(shared.objectiveData))}` : null,
101
92
  '',
102
- PROPOSAL_HEADING,
103
- ...symptoms.flatMap((entry) => (several ? [`${proposalHeading(entry, name(entry))}:`, ...proposalBody(entry)] : proposalBody(entry))),
93
+ ...symptoms.map((entry) => symptomParagraph(entry, symptoms.length > 1 ? null : inlineOf(alarm?.symptom))),
104
94
  '',
105
- symptoms.some((entry) => entry.proposal?.items?.length) ? REQUESTED_ACTION : ASSESSMENT_ACTION,
106
- ].join('\n');
95
+ priorities ? `Prioridades: ${sentence(priorities)}` : null,
96
+ symptoms.some((entry) => proposedDrugs(entry).length) ? REQUESTED_ACTION : ASSESSMENT_ACTION,
97
+ ].filter((line) => line !== null).join('\n');
107
98
  }
108
99
 
109
100
  function identityLines({ identity, review }) {
@@ -1,3 +1,5 @@
1
+ const moment = require('moment-timezone');
2
+
1
3
  const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
2
4
  const { REFUSAL, isPurelyConversational } = require('../helpers/clinicalWorkflowGuard');
3
5
  const { isToolFailure, toolRecoveryContract } = require('../helpers/toolResultHelper');
@@ -14,6 +16,7 @@ const runtimeConfig = require('../../config/runtimeConfig');
14
16
  const { TAIL_PLACEMENT, CURRENT_TURN } = require('../promptPlacement');
15
17
  const { logger } = require('../../utils/logger');
16
18
  const { sanitizeInlineText } = require('../../utils/sanitizerUtils');
19
+ const { DEFAULT_TIMEZONE } = require('../../utils/scheduleUtils');
17
20
  const { isJsonBlob } = require('../../utils/formatUtils');
18
21
  const { isPlainObject, safeParse } = require('../../utils/jsonUtils');
19
22
  const { MAX_ADDITIONAL_TOOL_IDS } = require('../../core/runtimeToolPolicy');
@@ -33,6 +36,11 @@ const outreachMinute = (value) => {
33
36
  return match ? `${match[1]}T${match[2]}` : null;
34
37
  };
35
38
 
39
+ const localFireTime = (value) => {
40
+ const at = moment.tz(value, DEFAULT_TIMEZONE);
41
+ return value && at.isValid() ? `${at.format('YYYY-MM-DD HH:mm')} (hora de la paciente)` : sanitizeInlineText(value);
42
+ };
43
+
36
44
  const NEGATIVE_GUIDANCE_MARKER = '**When NOT to call';
37
45
  const CONTINGENCY_RULE = [
38
46
  '[REGLA DE CONTINGENCIA (prioritaria) para este turno]',
@@ -439,7 +447,7 @@ class BaseLLMProvider {
439
447
  _buildFollowUpMessage(promptVariables) {
440
448
  const pending = Array.isArray(promptVariables?.active_follow_ups) ? promptVariables.active_follow_ups : [];
441
449
  if (!pending.length) return [];
442
- const rows = pending.map((item) => `- ${sanitizeInlineText(item.reminderId)} a las ${sanitizeInlineText(item.fireAt)}: ${sanitizeInlineText(item.message)}`);
450
+ const rows = pending.map((item) => `- ${sanitizeInlineText(item.reminderId)} a las ${localFireTime(item.fireAt)}: ${sanitizeInlineText(item.message, 600)}`);
443
451
  return [{
444
452
  role: 'developer',
445
453
  content: [
@@ -219,9 +219,9 @@ async function applyChoice(code, choice) {
219
219
  const queued = await fileUrgentRequest(code, choice);
220
220
  if (!queued) return { queued, pledge: null };
221
221
 
222
- await cancelPendingFollowUps({ code, reason: 'safety_net_request' }).catch((error) =>
223
- logger.error('[PausedSafetyNet] Pending follow-ups were not stood down', { code, error: error.message })
224
- );
222
+ if (!(await cancelPendingFollowUps({ code, reason: 'safety_net_request' }))) {
223
+ logger.error('[PausedSafetyNet] Pending follow-ups were not stood down', { code });
224
+ }
225
225
 
226
226
  try {
227
227
  return { queued, pledge: await openPledge({ code }) };
@@ -6,11 +6,12 @@ const { recordReminder, readPendingFollowUpsForPatient, cancelFollowUp } = requi
6
6
  const { isWithinTriageProtectionWindow } = require('../services/outreachSweepService');
7
7
  const { UTILITY_BASES, sanitizeVariable } = require('../services/outreachCarrierService');
8
8
  const { logger } = require('../../utils/logger');
9
- const { DEFAULT_TIMEZONE } = require('../../utils/scheduleUtils');
9
+ const { DEFAULT_TIMEZONE, isWithinHourWindow } = require('../../utils/scheduleUtils');
10
+ const { botIsPaused } = require('../../helpers/pausedThreadHelper');
10
11
 
11
12
  const definition = {
12
13
  name: 'schedulePatientReminder',
13
- description: '**Does:** Creates or updates one agent-authored WhatsApp reminder. An ordinary reminder (medication, appointment, exam, or other task) is allowed only when the patient explicitly asks for or explicitly accepts that reminder. Do not infer a reminder merely because the patient mentioned or scheduled something. Schedule it at the exact date and time the patient requested. Never move it earlier to precede an appointment or task unless the patient explicitly requested that offset. On any patient conversation—not only a triage—or on a proactive review, use reminder_type seguimiento when this turn decides both when to check in again and the exact patient-facing words. Schedule one whenever a later contact materially helps continuity and has a concrete utility_basis — typically: you recommended an action (OTC medication, self-care, education) whose effect shows within 24–72h; the patient already gave the deciding datum and you want to know whether the symptom comes back or crosses a threshold you named; you closed a symptom episode worth checking on; or the patient has an upcoming event (starting a treatment, an appointment, an infusion) whose outcome matters. Inactivity alone is not a basis. An open contingency net does not prevent one: the net awaits its datum, the seguimiento asks about what was indicated (e.g. whether a visit or test was booked).\\n\\nA seguimiento is a lightweight engagement plan: it is capped at 600 characters, never truncated, fires at most 48 hours ahead, expires 24 hours after its chosen fire time, and cannot be created while another seguimiento is active. It stays pending when the patient writes again: later turns list it under «Seguimientos ya programados» so you keep it, update it, or cancel it (reminder_action_type cancel) when the conversation already answered it.\\n\\n**When NOT to call:** when the reminder guards unresolved clinical uncertainty, use setContingencyPlan instead; that plan owns its silence window and lifecycle. Never update a contingency-owned `plan_…-patient` reminder here.\\n\\nRequired: future patient-local reminder_date, reminder_description, reminder_type, action type, reminder_id for updates, and utility_basis for seguimiento.',
14
+ description: '**Does:** Creates or updates one agent-authored WhatsApp reminder. An ordinary reminder (medication, appointment, exam, or other task) is allowed only when the patient explicitly asks for or explicitly accepts that reminder. Do not infer a reminder merely because the patient mentioned or scheduled something. Schedule it at the exact date and time the patient requested. Never move it earlier to precede an appointment or task unless the patient explicitly requested that offset. On any patient conversation—not only a triage—or on a proactive review, use reminder_type seguimiento when this turn decides both when to check in again and the exact patient-facing words. Schedule one whenever a later contact materially helps continuity and has a concrete utility_basis — typically: you recommended an action (OTC medication, self-care, education) whose effect shows within 24–72h; the patient already gave the deciding datum and you want to know whether the symptom comes back or crosses a threshold you named; you closed a symptom episode worth checking on; or the patient has an upcoming event (starting a treatment, an appointment, an infusion) whose outcome matters. Inactivity alone is not a basis. An open contingency net does not prevent one: the net awaits its datum, the seguimiento asks about what was indicated (e.g. whether a visit or test was booked).\\n\\nA seguimiento is a lightweight engagement plan: it is capped at 600 characters, never truncated, fires at most 48 hours ahead and only between 11:00 and 21:00 patient-local time, expires 24 hours after its chosen fire time, and cannot be created while another seguimiento is active. It stays pending when the patient writes again: later turns list it under «Seguimientos ya programados» so you keep it, update it, or cancel it (reminder_action_type cancel) when the conversation already answered it.\\n\\n**When NOT to call:** when the reminder guards unresolved clinical uncertainty, use setContingencyPlan instead; that plan owns its silence window and lifecycle. Never update a contingency-owned `plan_…-patient` reminder here.\\n\\nRequired: future patient-local reminder_date, reminder_description, reminder_type, action type, reminder_id for updates, and utility_basis for seguimiento.',
14
15
  strict: true,
15
16
  parameters: {
16
17
  type: 'object',
@@ -52,6 +53,9 @@ const REMINDER_TYPES = definition.parameters.properties.reminder_type.enum;
52
53
  const ACTION_TYPES = definition.parameters.properties.reminder_action_type.enum;
53
54
 
54
55
  const MAX_FOLLOW_UP_LEAD_HOURS = 48;
56
+ const ROUTINE_OUTREACH_START_HOUR = 11;
57
+ const ROUTINE_OUTREACH_END_HOUR = 21;
58
+ const PAUSED_ERROR = 'thread_paused — do not schedule a seguimiento in this turn.';
55
59
 
56
60
  const ISO_DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?$/;
57
61
  const FIRE_AT_FORMATS = ['YYYY-MM-DDTHH:mm:ss', 'YYYY-MM-DDTHH:mm', 'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm'];
@@ -128,7 +132,13 @@ async function handler(args = {}, context = {}) {
128
132
  if (isRoutineOutreach && isWithinTriageProtectionWindow(fireMoment.toDate())) {
129
133
  return JSON.stringify({ success: false, error: 'routine outreach cannot be scheduled during the 07:30-11:00 triage protection window; choose 11:00 or later.', data: {} });
130
134
  }
135
+ if (isRoutineOutreach && !isWithinHourWindow(fireMoment.toDate(), { timeZone, startHour: ROUTINE_OUTREACH_START_HOUR, endHour: ROUTINE_OUTREACH_END_HOUR })) {
136
+ return JSON.stringify({ success: false, error: `routine outreach must go out between ${ROUTINE_OUTREACH_START_HOUR}:00 and ${ROUTINE_OUTREACH_END_HOUR}:00 patient time; choose a time in that range.`, data: {} });
137
+ }
131
138
  if (isRoutineOutreach && actionType === 'create' && runtime.patientCode) {
139
+ if (await botIsPaused(runtime.patientCode, { label: '[schedulePatientReminder]' })) {
140
+ return JSON.stringify({ success: false, error: PAUSED_ERROR, data: {} });
141
+ }
132
142
  const pending = await readPendingFollowUpsForPatient({ code: runtime.patientCode });
133
143
  if (pending.length) {
134
144
  return JSON.stringify({ success: false, error: 'active_follow_up_exists — update the existing plan instead of crossing it.', data: { reminder_id: pending[0].reminderId } });
@@ -182,6 +192,12 @@ async function handler(args = {}, context = {}) {
182
192
  if (!persisted) {
183
193
  return JSON.stringify({ success: false, error: 'reminder persistence failed; retry scheduling', data: {} });
184
194
  }
195
+ if (isRoutineOutreach && actionType === 'create' && await botIsPaused(runtime.patientCode, { label: '[schedulePatientReminder]' })) {
196
+ if (!await cancelFollowUp({ code: runtime.patientCode, reminderId, trace, reason: 'thread_paused' })) {
197
+ logger.error('[schedulePatientReminder] seguimiento created on a paused thread was not stood down', { reminderId });
198
+ }
199
+ return JSON.stringify({ success: false, error: PAUSED_ERROR, data: {} });
200
+ }
185
201
  }
186
202
 
187
203
  trace.setSignals({ reminderIntents: [...existing, intent] });
@@ -9,6 +9,7 @@ const { getThreadInfo, setThreadDoNotAlert, switchThreadStoppedStatus } = requir
9
9
  const { getRecordByFilter } = require('../services/airtableService');
10
10
  const { sendMessage, processInstruction, processSystemMessage } = require('../core/NexusMessaging');
11
11
  const { createAssistant, switchAssistant } = require('../clinical');
12
+ const { cancelPendingFollowUps } = require('../clinical/services/clinicalAirtableService');
12
13
 
13
14
  const asBoolean = (value) => (typeof value === 'string' ? value === 'true' : !!value);
14
15
 
@@ -19,6 +20,9 @@ const stopAssistantController = async (req, res) => {
19
20
  const stopped = asBoolean(stop);
20
21
  try {
21
22
  await switchThreadStoppedStatus(code, stopped, { byOperator: true });
23
+ if (stopped && !(await cancelPendingFollowUps({ code, reason: 'thread_paused' }))) {
24
+ logger.error('[AssistantController] Pending follow-ups were not stood down on pause', { code });
25
+ }
22
26
  return res.status(200).json({ success: true, message: stopped ? 'Assistant stopped' : 'Assistant resumed' });
23
27
  } catch (error) {
24
28
  logger.error('[AssistantController] Failed to stop assistant', { error: error.message, code });
@@ -1050,18 +1050,18 @@ class NexusMessaging {
1050
1050
  };
1051
1051
  }
1052
1052
 
1053
- /*
1054
- * Delivers a message the agent previously authored and scheduled with schedulePatientReminder.
1055
- * The clock only wakes this method; it does not choose copy or timing. Running inside the same
1056
- * patient queue as inbound WhatsApp traffic prevents an already-received message from crossing
1057
- * the follow-up.
1058
- */
1059
1053
  async _retireSkippedFollowUp(code, reminderId, holdReason) {
1060
1054
  const retired = await cancelFollowUp({ code, reminderId, reason: `skipped_${holdReason}` });
1061
1055
  if (!retired) logger.warn('[processScheduledFollowUp] skipped follow-up was not stood down', { reminderId, holdReason });
1062
1056
  return { decision: 'skipped', holdReason, accepted: false, delivered: false, retired };
1063
1057
  }
1064
1058
 
1059
+ /*
1060
+ * Delivers a message the agent previously authored and scheduled with schedulePatientReminder.
1061
+ * The clock only wakes this method; it does not choose copy or timing. Running inside the same
1062
+ * patient queue as inbound WhatsApp traffic prevents an already-received message from crossing
1063
+ * the follow-up.
1064
+ */
1065
1065
  async processScheduledFollowUp(code, { reminderId, firstName, now = new Date() } = {}) {
1066
1066
  if (!code || !reminderId) throw new Error('code and reminderId are required');
1067
1067
  if (typeof firstName !== 'string' || !firstName.trim()) throw new Error('firstName is required');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.68.1",
3
+ "version": "5.68.3",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",