Local resource

Waha Integration

_docs/WAHA_INTEGRATION.md

WAHA Integration

adapters/waha.py transforms raw WAHA webhook events into FlowPayload. It is pure: no downloads, no external APIs, no writes and no workflow side effects.

Entry

WAHA producers can call POST /analyze with a raw event batch:

{
  "flow": "initial-ingestion-report",
  "payload": {
    "source": "waha",
    "monitored": {"id": "minor-1", "name": "Felipe"},
    "events": []
  }
}

Single event payloads with {event, payload} are also accepted. If the payload already contains conversations, the adapter is skipped and the payload is validated directly as FlowPayload; request_normalizer still runs chat_name_unification for WAHA sources.

For the full producer-facing contract, including canonical FlowPayload fields and the flow-specific differences, see _docs/PAYLOAD_CONTRACTS.md.

Adapter Flow

flowchart LR
  A["WAHA events"] --> B["drop operational events"]
  B --> C["normalize message and synthetic events"]
  C --> D["group by chat_id"]
  D --> E["map sender/contact/media/status metadata"]
  E --> F["chat_name_unification"]
  F --> G["FlowPayload.conversations[]"]

Supported Events

Message events:

  • message
  • message.any
  • message.waiting

Synthetic analyzable events:

  • message.edited
  • message.revoked
  • message.reaction
  • poll.vote
  • event.response
  • call.received
  • call.accepted
  • call.rejected
  • group.v2.join
  • group.v2.leave
  • group.v2.update

Typed message bodies inside message* events are normalized when WAHA provides the relevant fields:

  • poll -> type=poll
  • vCards[] / vcard / BEGIN:VCARD body -> type=contact_card
  • location / liveLocation -> type=location
  • buttonResponse / listResponse -> type=interactive
  • order / product -> type=order|product
  • unknown typed message without analyzable body/media -> visible
  • [MENSAJE_NO_SOPORTADO: <type>] text

Status messages are detected from status@broadcast in from, to or message id and are emitted as chat.type=status. Newsletter JIDs are emitted as chat.type=channel.

Local Dump Analyzer Normalization

/dev/whatsapp-dump-analyzer and /dev/dumps/flow-payload do not receive live webhook events. They read local dump artifacts (messages.ndjson, chats.ndjson, media_files.ndjson) through services/dev_whatsapp_dumps.py and emit source="waha-dump" FlowPayload objects for the same downstream Conversation Builder.

The dump normalizer must preserve structured WhatsApp elements that are often embedded inside raw message payloads rather than represented as standalone events:

  • message._data.SourceWebMsg.reactions[] -> synthetic type=reaction
  • messages with [REACCION] ... text. Date filtering uses each reaction senderTimestampMS when present, so a reaction to an older message can appear in a newer selected window.

  • callLogMesssage under quoted/replied payloads -> synthetic type=call
  • messages with [LLAMADA] or [VIDEOLLAMADA], outcome, and duration when available.

  • contactMessage / contactsArrayMessage -> type=contact_card with
  • [CONTACT_CARD] | nombre=... | telefono=....

  • pollCreationMessage / pollCreationMessageV3 -> type=poll with
  • [ENCUESTA_CREADA] | pregunta=... | opciones=....

  • locationMessage / liveLocationMessage -> type=location with
  • [UBICACION] ....

  • stickerMessage -> type=sticker; it must not be collapsed into image.
  • group.v2.join, group.v2.leave, group.v2.update rows -> type=group_event.

Alias unification still runs after dump parsing, so duplicated @lid/@c.us rows collapse before the payload reaches the Conversation Builder.

Ignored Events

Operational events are counted as ignored and never rendered:

  • session.status
  • message.ack
  • message.ack.group
  • presence.update
  • chat.archive
  • label.upsert
  • label.deleted
  • label.chat.added
  • label.chat.deleted
  • group.v2.participants
  • engine.event
  • poll.vote.failed
  • event.response.failed
  • deprecated group.join, group.leave, state.change

Unknown event names are counted as unsupported_events. If no analyzable message remains, the adapter raises waha_no_analyzable_messages.

Output Contract

The adapter returns FlowPayload:

{
  "source": "waha",
  "monitored": {"id": "minor-1", "name": "Felipe", "phone": null},
  "conversations": [
    {
      "chat": {"id": "120@g.us", "name": "120@g.us", "type": "group"},
      "messages": []
    }
  ],
  "metadata": {},
  "adapter_summary": {
    "input_events": 10,
    "ignored_events": 2,
    "unsupported_events": 0,
    "compatibility": {
      "contract_version": "wardian.waha-cis-compatibility-summary.v1",
      "taxonomy_version": "wardian.waha-cis-compatibility-taxonomy.v1",
      "adapter_policy_version": "wardian.cis-waha-adapter-policy.v1",
      "stage": "cis_raw_adapter",
      "outcomes": {
        "supported": 8,
        "policy_ignored": 2,
        "unexpected_unsupported": 0,
        "decode_failed": 0
      },
      "families": {
        "message_content": {
          "supported": 8,
          "policy_ignored": 0,
          "unexpected_unsupported": 0,
          "decode_failed": 0
        },
        "ack_delivery": {
          "supported": 0,
          "policy_ignored": 1,
          "unexpected_unsupported": 0,
          "decode_failed": 0
        },
        "session_lifecycle": {
          "supported": 0,
          "policy_ignored": 1,
          "unexpected_unsupported": 0,
          "decode_failed": 0
        }
      }
    },
    "conversation_count": 1,
    "chat_name_unification": {
      "input_chats": 2,
      "output_chats": 1,
      "aliases_collapsed": 1
    }
  }
}

The compatibility summary is aggregate-only. decode_failed means a supported event shape could not produce a canonical message, failed canonical message validation, duplicated a message ID in the same chat, or the batch item was not an event object. Validation and duplicate failures are isolated to the affected event, so valid peers remain supported and continue into the canonical payload. unexpected_unsupported means the event decoded but has neither explicit support nor an ignore policy. The production Control Plane canonical payload normally bypasses this raw adapter and must be observed separately as cis_input_validation.

GET /v1/internal/capabilities advertises the supported taxonomy and raw adapter policy versions under waha_compatibility. Those fields are static, declared compatibility only; live ratios, freshness and hysteresis remain a Control Plane responsibility.

Each message keeps:

  • canonical English type
  • prompt-visible Spanish text for structured events
  • metadata.event_name
  • metadata.reply_to for quotes when WAHA provides it
  • metadata.is_status / metadata.is_channel
  • media metadata such as is_view_once, is_ephemeral, media_error

Event Render Mapping

The adapter prepares structured text that the Conversation Builder renders. Text redaction is controlled by the active privacy policy: debug_plaintext preserves raw prompt text for local debugging, while protected redacts WhatsApp JIDs and phone-like strings:

WAHA sourceCanonical typePrompt-visible tag
poll definitionpoll[ENCUESTA_CREADA]
poll.votepoll[ENCUESTA_VOTO] Voto en encuesta: ...
message.reactionreaction[REACCION] ...
message.editededit[EDITADO] ...
message.revokedrevoke[MENSAJE_ELIMINADO] ...
event.responseevent[EVENTO] ...
call.*call[LLAMADA] or [VIDEOLLAMADA]
dump callLogMesssagecall[LLAMADA] or [VIDEOLLAMADA]
group.v2.*group_event[SE UNIO AL GRUPO], [SE FUE DEL GRUPO], [ACTUALIZACION DEL GRUPO]
dump SourceWebMsg.reactions[]reaction[REACCION] ...
status messageoriginal content typebuilder prefixes [STATUS]
location/live locationlocation[UBICACION] ...
vCardcontact_card[CONTACT_CARD] ...
order/productorder / product[PEDIDO] / [PRODUCTO]
button/list replyinteractive[RESPUESTA_BOTON] / [RESPUESTA_LISTA]

Media

The adapter only maps media metadata:

  • media_url
  • mime_type
  • duration_seconds when available
  • metadata.media_unavailable=true when WAHA says media exists but no usable URL
  • metadata.media_error from WAHA when present, for example media_expired

Temporal media activities decide whether to download/process media based on the selected recipe. Text-only recipes still keep the canonical media message so the Conversation Builder can render [MEDIA_NO_PROCESADA] rather than dropping the event.

After request normalization and before Temporal workflow start, CIS compacts runtime identity metadata. Values from repeated message.metadata.raw_info are projected into existing canonical fields such as sender_aliases, group_participants, direct_identity, and contact_identity; only then is raw_info removed from the internal Temporal request. The public WAHA/flow request contract is unchanged, and saved contact names retain priority over push names.

Privacy

The adapter does not render raw payloads directly to prompts and does not fetch media bytes. The Conversation Builder redacts phone-like strings and WhatsApp JIDs before prompt text is sent to AI.

Chat And Sender Name Resolution

WAHA can expose the same direct conversation under more than one identifier, especially a phone-backed @c.us JID and a linked @lid JID. services/chat_name_unification.py collapses those aliases before the conversation reaches services/conversation_builder.py. The builder is intentionally dumb here: it renders the normalized chat.name and message.sender.name it receives.

Canonical identity rules:

  • Preserve the original upstream ids in metadata, for example canonical_chat_id, aliases, original_chat_id, and name_source.
  • Prefer a human name over a raw JID when the source is trustworthy.
  • Never invent a name from message text.
  • Keep raw @lid values only as the final fallback.

Group chats:

1. Saved/group metadata name from WAHA, such as group_name, title, or the group subject in chat.name. 2. Explicit upstream display name for the same group id, if present in a chat metadata row. 3. Stable group id, usually @g.us, only if no group name exists.

Groups generally have one canonical group subject, but participant names inside the group still need per-sender resolution. Resolve each inbound participant independently with the direct-contact priority chain below.

Direct chats and group participants:

1. Saved contact name from the monitored account address book or WAHA chat/contact metadata, such as chat_name, title, display_name, or chat.name. 2. Name from a paired alias row. Example: if 153...@lid and 549...@c.us share message ids, use the saved name from the @c.us row for both aliases. 3. Contact/LID mapping exported by WAHA, when available, including a known phone JID for a LID. 4. Participant-level name fields, such as participantName, authorName, or notifyName. 5. Push name from the message payload, such as pushName or _data.Info.PushName. 6. Phone-derived JID from @c.us, _data.Info.SenderAlt, or _data.Info.RecipientAlt. 7. Raw @lid/JID as the final fallback.

The saved contact name is first because it is often the relationship label that matters most to the monitored account, for example Papa, Mama, or a school nickname. Push names are useful, but they are self-declared by the sender and should not override the monitored account's address book.

Failure Behavior

  • Missing events or single event: waha_events_required.
  • No analyzable message events: waha_no_analyzable_messages.
  • Invalid normalized conversations are rejected by FlowPayload validation with
  • verbose API errors.

  • Missing, malformed, out-of-range, ambiguous or conflicting message clocks fail with
  • waha_temporal_reconciliation_required; CIS never substitutes wall-clock now(). Failed technical poll/event-response notifications are ignored as operational events, not converted into risk evidence.

Temporal evidence

The pure WAHA adapter resolves event-family-specific timestamps before creating ConversationMessage: reaction sender time, poll vote time and event response time precede generic message-local fields. Unix seconds/ms/us/ns and offset-aware RFC3339 are supported. Values outside TEMPORAL_MIN_EVENT_AT and validation time plus TEMPORAL_FUTURE_SKEW_SECONDS require reconciliation. Revoke after.timestamp, webhook envelope time and transport receipt time are not semantic fallbacks; the validation clock is only an upper bound and is never substituted as event time.

Eligible messages carry sanitized metadata.timestamp_provenance. Aggregate counts and enum reasons live under adapter_summary.temporal_evidence (wardian-temporal-evidence.v1). Raw WAHA requests are fail-closed because CIS has no durable reconciliation store.