@peopl-health/nexus 5.68.5 → 5.68.7

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.
@@ -40,7 +40,7 @@ const MAX_CONTEXT_TURNS = 6;
40
40
 
41
41
  const definition = {
42
42
  name: 'extractClinicalInfo',
43
- description: '**Does:** Extracts ALL structured clinical facts from the current patient message — symptoms, labs, medications, diagnoses, appointments, treatments — and returns them as a discriminated-union `clinical_facts[]` list keyed by `kind`. Call ONCE per turn with ALL detected tags in a single call — one call covers every category.\n\n**Required inputs:** `raw_message` (the patient\'s current turn text, VERBATIM — the full contents of `<patient_message>`, every line of it when the turn carries more than one), `detected_tags` (list, at least one of: `symptom`, `indication`, `treatment`, `adherence`, `medication`, `appointment`, `emotion`, `lab_value`, `diagnosis`, `allergy`, `comorbidity`, `performance_status`; empty only for a purely conversational message or when declaring `non_clinical_request`), `intake_source` (`self` | `caregiver` | `team_relay`).\n\n**When to call:** at the start of any turn where the patient message mentions, explicitly or implicitly, any of the 12 supported categories. The result drives every downstream router-side read (landscape / history / risk / patterns).\n\n**When NOT to call:**\n- When the patient SHARED raw numeric results / lab values / files — use `reportResultsReceived` instead (it both logs the share AND emits FHIR Observations directly).\n- More than once per turn — a single call covers every category.\n\n**Returns:** `data.clinical_facts[]` — a discriminated-union list where each record has `kind` (observation | medication | condition | procedure | appointment | allergy) plus common fields `{id, code, effective, notes}` (null/default fields are OMITTED — e.g. `interpretation` appears only when non-default) and kind-specific fields:\n- `kind=observation`: `code_ctcae`, `value`, `grade_scale`, `episode_id`, `trend`. Read `grade_scale` to anchor the grade assigned in `recordClinicalImpression`. It is present whenever the term has a curated CTCAE ladder, and ABSENT when it does not — an absent `grade_scale` means there is no rubric for this term, not that one was withheld. An absent `grade_scale` means the term has no curated rubric, not that one was withheld — so there is nothing to grade against from memory. Call `lookupCtcaeEntry` when you suspect the term itself is wrong; otherwise say where the grade came from in `grade_estimate.source`.\n- `kind=medication`: `adherence_status` (`started` | `ongoing` | `non_adherent` | …).\n- `kind=condition`: `clinical_status` (`active` | `resolved` | …) — patient self-report of an existing diagnosis (distinct from `openCondition` which commits a new agent-tracked case).\n- `kind=procedure`: patient-reported procedure done elsewhere (distinct from `recordIntervention` which is agent-authored); `venue`/`outcome` may be null in the current extraction phase.\n- `kind=appointment`: `scheduled_for`, `purpose` (may be null in the current extraction phase).\nThe system writes the matching FHIR resource (Observation / MedicationStatement / Condition / Procedure / Appointment) + `Provenance` per fact on the audit lane; `allergy`, `comorbidity`, and `performance_status` are captured in a dedicated aggregate, not as a per-fact resource.\n\n**When the result asks you to confirm:** `data.pending_confirmation[]` means the patient reported a REAL clinical event but named it only generically («me suspendieron el tratamiento»), so it was deliberately NOT recorded. Each entry carries `{term, category, missing, quote}`; the list holds EVERY fact withheld this turn, but ask about the FIRST one only. Ask the patient for exactly what its `missing` names, in ONE short natural question, and never state or imply that anything was recorded. When they answer, call `extractClinicalInfo` again on their reply and it will be recorded then. `data.context_gaps[]` — only ever sent when there is nothing to confirm — names ONE stored variable that is stale or incomplete; raise it only if the conversation allows it naturally. Both lists are empty whenever the turn carries a safety flag: never ask housekeeping questions during a safety event.\n\n**Side effects:** System emits a FHIR resource + `Provenance` per fact on the audit lane, except `allergy`/`comorbidity`/`performance_status` (aggregated). Facts listed in `pending_confirmation` are NOT written to the patient record — they are held back until the patient confirms them.',
43
+ description: '**Does:** Extracts ALL structured clinical facts from the current patient message — symptoms, labs, medications, diagnoses, appointments, treatments — and returns them as a discriminated-union `clinical_facts[]` list keyed by `kind`. Call ONCE per turn with ALL detected tags in a single call — one call covers every category.\n\n**Required inputs:** `raw_message` (the patient\'s current turn text, VERBATIM — the full contents of `<patient_message>`, every line of it when the turn carries more than one), `detected_tags` (list, at least one of: `symptom`, `indication`, `treatment`, `adherence`, `medication`, `appointment`, `emotion`, `lab_value`, `diagnosis`, `allergy`, `comorbidity`, `performance_status`; empty only for a purely conversational message or when declaring `non_clinical_request`), `intake_source` (`self` | `caregiver` | `team_relay`).\n\n**When to call:** at the start of any turn where the patient message mentions, explicitly or implicitly, any of the 12 supported categories. The result drives every downstream router-side read (landscape / history / risk / patterns).\n\n**When NOT to call:**\n- When the patient SHARED raw numeric results / lab values / files — use `reportResultsReceived` instead (it both logs the share AND emits FHIR Observations directly).\n- More than once per turn — a single call covers every category.\n\n**Returns:** `data.clinical_facts[]` — a discriminated-union list where each record has `kind` (observation | medication | condition | procedure | appointment | allergy) plus common fields `{id, code, effective, notes}` (null/default fields are OMITTED — e.g. `interpretation` appears only when non-default) and kind-specific fields:\n- `kind=observation`: `code_ctcae`, `value`, `grade_scale`, `episode_id`, `trend`. Read `grade_scale` to anchor the grade assigned in `recordClinicalImpression`. It is present whenever the term has a curated CTCAE ladder, and ABSENT when it does not — an absent `grade_scale` means there is no rubric for this term, not that one was withheld. An absent `grade_scale` means the term has no curated rubric, not that one was withheld — so there is nothing to grade against from memory. Call `lookupCtcaeEntry` when you suspect the term itself is wrong; otherwise say where the grade came from in `grade_estimate.source`.\n- `kind=medication`: `adherence_status` (`started` | `ongoing` | `non_adherent` | …).\n- `kind=condition`: `clinical_status` (`active` | `resolved` | …) — patient self-report of an existing diagnosis (distinct from `openCondition` which commits a new agent-tracked case).\n- `kind=procedure`: patient-reported procedure done elsewhere (distinct from `recordIntervention` which is agent-authored); `venue`/`outcome` may be null in the current extraction phase.\n- `kind=appointment`: `scheduled_for`, `purpose` (may be null in the current extraction phase).\nThe system writes the matching FHIR resource (Observation / MedicationStatement / Condition / Procedure / Appointment) + `Provenance` per fact on the audit lane; `allergy`, `comorbidity`, and `performance_status` are captured in a dedicated aggregate, not as a per-fact resource.\n\n**When the result asks you to confirm:** `data.pending_confirmation[]` means the patient reported a REAL clinical event but named it only generically («me suspendieron el tratamiento»), so it was deliberately NOT recorded. Each entry carries `{term, category, missing, quote}`; the list holds EVERY fact withheld this turn, but ask about the FIRST one only. Ask the patient for exactly what its `missing` names, in ONE short natural question, and never state or imply that anything was recorded. When they answer, call `extractClinicalInfo` again on their reply and it will be recorded then. `data.context_gaps[]` — only ever sent when there is nothing to confirm — names ONE stored variable that is stale or incomplete; raise it only if the conversation allows it naturally. Both lists are empty whenever the turn carries a safety flag: never ask housekeeping questions during a safety event.\n\n**Side effects:** System emits a FHIR resource + `Provenance` per fact on the audit lane, except `allergy`/`comorbidity`/`performance_status` (aggregated). Facts listed in `pending_confirmation` are NOT recorded as facts — they never reach the clinical intake or the patient\'s chart; FHIR keeps them only as unconfirmed drafts until the patient confirms them.',
44
44
  strict: true,
45
45
  parameters: {
46
46
  type: 'object',
@@ -5,6 +5,7 @@ const { reference, patientReference, codeableConcept, extension, toInstant } = r
5
5
  const { baseResource, problemListCategory, observationCategory } = require('../helpers/resourceHelper');
6
6
  const { MENTION_SLUG } = require('../constants/projectionSlugs');
7
7
  const { MENTION_EXT } = require('../constants/extensionSlugs');
8
+ const { PROCEDURE_STATUS, statusFrom } = require('./contextFactProjection');
8
9
 
9
10
  const ACTIVITY_TEXT = 'clinical-mention-extraction';
10
11
  const OBSERVED_FIELD_SYSTEM_KEY = 'observedField';
@@ -137,7 +138,8 @@ function adherenceFor(assertion) {
137
138
 
138
139
  function procedureStatus(fact, provenance) {
139
140
  if (fact.assertion.negated) return 'not-done';
140
- return provenance.needsConfirmation ? 'unknown' : 'completed';
141
+ if (provenance.needsConfirmation) return 'unknown';
142
+ return statusFrom(PROCEDURE_STATUS, fact.observedFields?.status, { absent: 'completed', unrecognised: 'completed' });
141
143
  }
142
144
 
143
145
  function procedureResource({ fact, provenance }) {
@@ -33,7 +33,7 @@ const PROCEDURE_STATUS = {
33
33
  ended: 'completed', realizada: 'completed', realizado: 'completed',
34
34
  stopped: 'stopped', suspendido: 'stopped', suspendida: 'stopped',
35
35
  suspended: 'stopped', discontinued: 'stopped', not_active: 'stopped',
36
- paused: 'on-hold',
36
+ paused: 'on-hold', started: 'in-progress',
37
37
  finalizado: 'completed', end: 'completed', past: 'completed', previous: 'completed',
38
38
  historical: 'completed', historico: 'completed',
39
39
  cancelled: 'not-done', cancelado: 'not-done', cancelada: 'not-done',
@@ -363,6 +363,8 @@ function fromFhir(resources) {
363
363
 
364
364
  module.exports = {
365
365
  CONTEXT_RESOURCE_TYPES,
366
+ PROCEDURE_STATUS,
367
+ statusFrom,
366
368
  toFhir,
367
369
  fromFhir,
368
370
  conceptKeyOf,
@@ -507,6 +507,25 @@ async function recordReviewUpdate({ code, update, now = new Date(), settleInBack
507
507
  }
508
508
  }
509
509
 
510
+ const STALE_INTERRUPTION_NOTES = 'Cerrada automáticamente al llegar una alarma nueva; nadie verificó si la operación interrumpida se completó';
511
+
512
+ async function closeStaleInterruption(EscalationReview, patientCode, now) {
513
+ try {
514
+ const stale = await EscalationReview.findOne({
515
+ patientCode,
516
+ closedAt: null,
517
+ status: { $in: INTERRUPTED_STATUSES },
518
+ updatedAt: { $lte: new Date(now.getTime() - INTERRUPTION_GRACE_MS) },
519
+ }, { updatedAt: 1 }).lean();
520
+ if (!stale) return;
521
+ await closeEscalationReview({
522
+ id: String(stale._id), closedBy: BOT_OPERATOR, interruptionNotes: STALE_INTERRUPTION_NOTES, expectedUpdatedAt: stale.updatedAt,
523
+ });
524
+ } catch (error) {
525
+ logger[error?.statusCode === 409 ? 'warn' : 'error']('[EscalationReview] The interrupted review was not closed before the new alarm', { error: redactedError(error) });
526
+ }
527
+ }
528
+
510
529
  async function recordEscalationAlarm({ code, alarm, now = new Date() } = {}) {
511
530
  try {
512
531
  const patientCode = patientCodeOf(code);
@@ -518,6 +537,7 @@ async function recordEscalationAlarm({ code, alarm, now = new Date() } = {}) {
518
537
  .lean();
519
538
 
520
539
  await ensureActiveReviewIndex(EscalationReview);
540
+ await closeStaleInterruption(EscalationReview, patientCode, now);
521
541
  const joined = await join();
522
542
  if (joined) {
523
543
  await scheduleDoctorAlert(joined, entry, now);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.68.5",
3
+ "version": "5.68.7",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",