Skip to content

fix(baileys): resolve @lid to phone JID across history sync and live messages - #2663

Open
fidelis05 wants to merge 2 commits into
evolution-foundation:developfrom
fidelis05:fix/lid-resolution-history-sync
Open

fix(baileys): resolve @lid to phone JID across history sync and live messages#2663
fidelis05 wants to merge 2 commits into
evolution-foundation:developfrom
fidelis05:fix/lid-resolution-history-sync

Conversation

@fidelis05

@fidelis05 fidelis05 commented Jul 27, 2026

Copy link
Copy Markdown

Problem

WhatsApp increasingly addresses events with LID identifiers (<lid>@lid) instead of the phone-based JID. Business-linked accounts hit this almost exclusively, which leaves those instances with no usable chat history and live messages that never link to their chat.

This is the concrete failure behind the umbrella issue #1872.

Root cause

Two distinct problems, both in whatsapp.baileys.service.ts:

1. messages.upsert resolved @lid too late.
The swap ran after prismaRepository.message.create(), after the Chat lookup and after the Contact upsert — so all three were keyed by @lid while the swap only patched the outgoing webhook payload. The stored row was never corrected.

2. messaging-history.set never resolved @lid on messages at all.
Its chat resolution consulted only contact.phoneNumber, which history payloads frequently omit. Worse, history-sync message keys carry no remoteJidAlt and no addressingMode — verified directly against stored rows — so there is nothing on the key to resolve from.

Because chats resolved (partially) while messages did not, messages and chats ended up keyed differently and simply never joined. That is why chats appeared to contain only their most recent message.

Fix

  • Resolution in messages.upsert now happens before any lookup or write. received.key itself is deliberately left untouched so protocol calls (readMessages, requestPlaceholderResend, fetchMessageHistory, media download) keep addressing the message exactly as WhatsApp delivered it; only a resolved copy is used for our own storage.
  • History sync resolves through Baileys' signal-level LID store (signalRepository.lidMapping), batched once per payload and shared by chats, contacts and messages. The lookup is guarded (if (!lidStore?.getPNsForLIDs) return;) and degrades to previous behaviour if unavailable.
  • Fixed a latent bug where contactsMapLidJid falls back to the contact's own id, so contact.jid could be truthy but still @lid — the old truthy check swallowed it and blocked resolution.

Results

Measured on a Business-linked instance (1101 messages, 91 chats):

metric before after
@lid messages 1035 0
@lid chats 29 2
chats with >1 message 3 47

The 2 remaining @lid chats have no name, no messages and no mapping in the LID store — nothing exists to resolve them to.

Live messages.upsert verified separately: LID-addressed messages in both directions, including media, resolve to the phone JID and land in the existing chat rather than creating a parallel @lid one. Unread counters and contact linkage confirmed correct.

Also included: patches/baileys+7.0.0-rc13.patch

Media on these messages was unrecoverable due to two upstream Baileys bugs (submitted separately to WhiskeySockets/Baileys):

  1. downloadMediaMessage tests error?.status, but getHttpStream throws a Boom carrying the status on output.statusCode. .status is always undefined, so the media re-upload request was never sent — unreachable dead code.
  2. getMediaRetryKey does not normalise a base64-string mediaKey the way its sibling getMediaKeys does. Media keys rehydrated from JSON storage are base64, so HKDF ran over the base64 characters instead of the key bytes, and WhatsApp answered DECRYPTION_ERROR.

With both applied, media inside WhatsApp's retention window downloads again — verified 10/10, output validated as genuine Ogg/Opus with byte length matching the declared size.

Testing notes / limitations

  • Validated on a single Brazilian Business number on PostgreSQL. MySQL and non-BR numbering are untested.
  • Resolution reads client.signalRepository.lidMapping, which is Baileys-internal. It is guarded and degrades gracefully, but it could shift across Baileys versions.
  • tsc --noEmit and eslint both clean.

Summary by Sourcery

Resolve WhatsApp LID-based identifiers to phone JIDs consistently for history sync and live messages, and patch upstream Baileys media download issues.

Bug Fixes:

  • Ensure history-synced messages and chats resolve @lid identifiers to phone JIDs using Baileys’ LID mapping store so they correctly join existing chats.
  • Resolve @lid remoteJid and participant values to phone JIDs before chat and contact lookups in live messages.upsert so messages no longer create parallel @lid chats or orphaned contacts.
  • Fix pushName resolution during history sync to fall back across both phone JIDs and LID aliases, reducing nameless messages.
  • Correct handling of Baileys media download failures by inspecting Boom error status and normalizing base64 media keys so retained media can be decrypted and fetched successfully.

Enhancements:

  • Introduce a shared in-memory LID-to-JID map for history sync to reuse resolved mappings across contacts, chats and messages.
  • Improve guarding and logging around LID store lookups to provide safer degradation and better observability of resolution outcomes.

…messages

WhatsApp increasingly addresses events with LID identifiers
(<lid>@lid) instead of the phone-based JID. Business accounts hit
this almost exclusively, which left those instances with no usable
chat history and messages that never linked to their chats.

Two distinct problems are fixed:

1. messages.upsert resolved @lid only *after* the message row was
   written, so the Message row, the Chat lookup and the Contact
   upsert were all keyed by @lid while the swap only patched the
   outgoing webhook payload. Resolution now happens before any
   lookup or write. received.key itself is left untouched so
   protocol calls (readMessages, requestPlaceholderResend,
   fetchMessageHistory, media download) keep addressing the message
   exactly as WhatsApp delivered it.

2. messaging-history.set never resolved @lid on messages at all, and
   its chat resolution only consulted contact.phoneNumber - which
   history payloads frequently omit. History-sync message keys carry
   no remoteJidAlt or addressingMode, so resolution now goes through
   Baileys' signal-level LID store (signalRepository.lidMapping),
   batched once per payload and shared by chats, contacts and
   messages. The lookup is guarded and degrades to previous
   behaviour when the store is unavailable.

Measured on a Business-linked instance (1101 messages, 91 chats):

  @lid messages         1035 -> 0
  @lid chats              29 -> 2   (2 have no mapping, name or messages)
  chats with >1 message    3 -> 47

Verified for live messages.upsert as well: LID-addressed messages in
both directions, including media, resolve to the phone JID and land
in the existing chat rather than creating a parallel @lid one.

Also includes patches/baileys+7.0.0-rc13.patch, fixing two upstream
bugs that made media on those messages unrecoverable:

- downloadMediaMessage tested error?.status, but getHttpStream throws
  a Boom carrying the status on output.statusCode, so the re-upload
  request was never sent.
- getMediaRetryKey did not normalise a base64-string mediaKey the way
  getMediaKeys does, so the retry request was encrypted with a key
  derived from the base64 characters and WhatsApp replied
  DECRYPTION_ERROR.

With both applied, media within WhatsApp's ~30 day retention window
downloads again (verified 10/10, output validated as real Ogg/Opus).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR ensures WhatsApp LID-based identifiers are resolved to phone JIDs consistently for both history sync and live messages, and patches upstream Baileys media-download bugs, so that chats, messages, and media are correctly linked and retrievable.

Sequence diagram for resolving LID to phone JID on live messages.upsert

sequenceDiagram
  actor WhatsApp
  participant BaileysClient
  participant BaileysStartupService
  participant PrismaChat as PrismaRepository.chat
  participant PrismaMessage as PrismaRepository.message
  participant Webhook as DataWebhook

  WhatsApp->>BaileysClient: incoming message (key.remoteJid = lid@lid)
  BaileysClient->>BaileysStartupService: messageHandle.messages.upsert(received)

  BaileysStartupService->>BaileysStartupService: prepare resolvedRemoteJid / resolvedParticipant
  alt [resolvedRemoteJid differs from received.key.remoteJid]
    BaileysStartupService->>BaileysStartupService: build messageForPersist with resolved key
  else [no LID]
    BaileysStartupService->>BaileysStartupService: messageForPersist = received
  end

  BaileysStartupService->>PrismaChat: chat.findFirst({ instanceId, remoteJid: resolvedRemoteJid })
  PrismaChat-->>BaileysStartupService: existingChat or null

  BaileysStartupService->>BaileysStartupService: prepareMessage(messageForPersist)
  BaileysStartupService->>PrismaMessage: message.create({ data: messageData })
  PrismaMessage-->>BaileysStartupService: msg

  BaileysStartupService->>Webhook: sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw)
  Webhook-->>BaileysStartupService: ack

  BaileysStartupService->>PrismaChat: contact.findFirst({ remoteJid: resolvedRemoteJid })
  BaileysStartupService->>BaileysStartupService: profilePicture(resolvedRemoteJid)
Loading

File-Level Changes

Change Details Files
Pre-resolve LID identifiers to phone JIDs during history sync using a shared LID→JID cache, and apply the resolved JIDs consistently to contacts, chats, and messages.
  • Added a per-run historySyncLidToJidMap cache and a resolveLidsIntoHistoryMap helper that queries Baileys’ signalRepository.lidMapping and stores LID→JID mappings.
  • Cleared the LID→JID cache at the start of each history sync batch and seeded it with LID/JID pairs discovered from contacts and chats.
  • Updated contact resolution to fall back to the LID→JID map when phoneNumber is absent and to populate the LID→JID cache when a non-LID jid is found.
  • Updated chat resolution so LID-addressed chats use either a non-LID contact.jid or a cached LID→JID mapping and backfill the cache from resolved chats.
  • Normalized history-sync messages by rewriting message keys’ remoteJid/participant from @lid to phone JID when possible, storing the original LID in remoteJidAlt/participantAlt and using alt IDs to resolve pushName from contacts.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Resolve LID identifiers before live message persistence so DB entities and webhooks use phone JIDs while preserving the raw Baileys key for protocol operations.
  • Introduced local resolvedRemoteJid/participant variables (with *_Alt and addressingMode) derived from the raw Baileys key, swapping to alt values when they represent the phone JID and the primary contains @lid.
  • Used resolvedRemoteJid to look up existing chats, decide group vs non-group behaviour, and log chat insert ignores.
  • Conditionally cloned the received event into a messageForPersist structure with resolved JIDs so prepareMessage and Prisma writes store phone-based remoteJid/participant, leaving the original Baileys key unchanged for protocol use.
  • Switched message caching, message-key computation, media storage paths, and contact queries to use resolvedRemoteJid instead of the raw LID-based JID.
  • Removed the previous post-persist remoteJid/remoteJidAlt swap on messageRaw in favour of the new pre-persist resolution path.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Improve robustness of pushName resolution and contact linkage when only LID information is available.
  • When deriving pushName for history-sync messages, look up contacts by both resolved participantJid and participantLid (via participantAlt/remoteJidAlt).
  • Ensured contactsMapLidJid always records a mapping for the contact.id and feeds the shared LID→JID cache when a non-LID JID is known.
  • Guarded LID resolution paths to no-op when the Baileys lidMapping store or its getPNsForLIDs method is unavailable, preserving previous behaviour.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Add an upstream Baileys patch file to fix media download issues related to error handling and media key derivation.
  • Added patches/baileys+7.0.0-rc13.patch to patch Baileys so downloadMediaMessage correctly inspects Boom errors (output.statusCode) and actually triggers media re-upload requests.
  • Patched Baileys’ getMediaRetryKey to normalise base64 string mediaKey inputs similarly to getMediaKeys, ensuring HKDF operates on key bytes instead of base64 characters and preventing DECRYPTION_ERROR responses.
patches/baileys+7.0.0-rc13.patch

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The JID/LID detection logic mixes includes('@lid') and endsWith('@lid') checks in different places; consider standardising on one approach (likely endsWith) or centralising this in a helper to avoid subtle mismatches on non-user JIDs.
  • The historySyncLidToJidMap cache is only cleared on syncType === 'INITIAL_STATUS_V4' but is reused across multiple messaging-history.set batches; double-check that this lifetime is intentional and that mappings from previous history windows or older sessions cannot leak into unrelated payloads.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The JID/LID detection logic mixes `includes('@lid')` and `endsWith('@lid')` checks in different places; consider standardising on one approach (likely `endsWith`) or centralising this in a helper to avoid subtle mismatches on non-user JIDs.
- The `historySyncLidToJidMap` cache is only cleared on `syncType === 'INITIAL_STATUS_V4'` but is reused across multiple `messaging-history.set` batches; double-check that this lifetime is intentional and that mappings from previous history windows or older sessions cannot leak into unrelated payloads.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Addresses review feedback on evolution-foundation#2663: the added LID handling mixed
includes('@lid') and endsWith('@lid'). endsWith is the stricter and
correct check for a JID suffix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant