Skip to content

feat(engine): typed WaId + persistent lid->phone resolution table + message from-filter#374

Open
tobiasstrebitzer wants to merge 1 commit into
rmyndharis:mainfrom
tobiasstrebitzer:feat/typed-waid-349
Open

feat(engine): typed WaId + persistent lid->phone resolution table + message from-filter#374
tobiasstrebitzer wants to merge 1 commit into
rmyndharis:mainfrom
tobiasstrebitzer:feat/typed-waid-349

Conversation

@tobiasstrebitzer

@tobiasstrebitzer tobiasstrebitzer commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Closes #349.

Typed WaId value object + a persistent, cross-session lid -> phone resolution table + a from filter on message history. On top of main @ v0.4.5.

Composes with the v0.4.5 Baileys work (not duplicates)

Motivation

Lid resolution was non-deterministic and ephemeral: a lid sender could be unresolved (@lid) early and resolved (@c.us) later once the lid -> pn map populated, and that map lived only in one Baileys session's memory (lost on restart, not shared across sessions). So a phone-based from filter matched a DM but silently missed the same person's already-resolved group message. A durable, shared table closes that; a typed WaId makes "a lid whose phone we don't know" first-class in the type.

New

  • src/engine/identity/wa-id.value.ts - the WaId value object over the wa-id.ts primitives. In-memory only; toNeutral()/toString()/toJSON() yield today's neutral string; three-valued refersToSamePerson() (matched / didn't match / couldn't tell).
  • src/engine/identity/lid-mapping.entity.ts + src/database/migrations/1781200000000-AddLidMappings.ts - LidMapping on the data connection (portable Postgres + SQLite migration).
  • src/engine/identity/lid-mapping-store.service.ts - boot-loads the table into an in-memory map, synchronous forward (getCached) + reverse (lidsForPhone) lookups, write-through remember().

Wired (no contract changes)

  • The store threads EngineFactory -> BaileysPlugin -> adapter config -> BaileysSessionStore, mirroring the existing messageStore seam.
  • addLidMappings / upsertContacts write through to the table; resolvePhone falls back to the table's in-memory cache (kept synchronous).
  • GET /api/sessions/:sessionId/messages?from= resolves through the table - a phone matches @c.us/@s.whatsapp.net stored ids and any lid resolving to it.
  • Baileys contact/chat listing ids now emit @c.us (the feat(engine): engine-neutral WhatsApp identities (Baileys inbound conformance) #342-domain conformance that was deferred), merged with fix(baileys): show saved/contact names in the Chats list (#369) #370's name resolution; read-back lookups fold a neutral id back to the engine dialect so the round-trip still hits.

Acceptance criteria (from your review)

  • Response shapes stay byte-identical. WaId is internal; a spec asserts WaId.toNeutral() equals toNeutralJid() for every representative id.
  • Resolution table persists on the data connection. Entity on data, portable migration, boot-verified.
  • From-filter test - a lid-resolved match becomes a hit. With the table mapping lid 111 -> 628999, filtering from=628999 returns the lid-authored row; a control test shows the prior silent miss.

Settled open questions

  1. Wire/storage format = neutral string, WaId in-memory. ✅
  2. 6.7.23 has no signalRepository.lidMapping lid->phone lookup, so unknown lids degrade to "couldn't tell" (@lid); resolution is fed by passive sources (inbound senderPn/participantPn via fix(baileys): resolve @lid senders to a phone number (#362) #372, phoneNumberShare, contacts, history sync). ✅
  3. RESOLVE_LID_TO_PHONE resolves internally into the table and gates only what's exposed (privacy flag, not a correctness toggle). ✅

Deferred (issue #349 scope steps 3-6)

Module-by-module migration of consumers onto WaId (behind the existing string fields); active, lazy per-miss lid->phone lookup. The lid key-flip limitation you're tracking under #349 is addressed at the foundation (durable, cross-session identity); re-keying the specific in-memory consumers is part of the deferred migration.

A follow-up PR (feat/baileys-sync-hardening) stacks Baileys sync hardening on top of this.

…essage from-filter

Implements rmyndharis#349 (the typed-WaId + resolution-table follow-up to rmyndharis#342).

- WaId value object (src/engine/identity/wa-id.value.ts): in-memory only, serializes
  byte-identically to today's neutral JID; three-valued refersToSamePerson.
- lid_mappings table on the data connection (global, last-write-wins, nullable phone for
  a negative cache) + portable pg/sqlite migration; LidMappingStore loads on boot and
  writes through, keeping resolvePhone synchronous.
- Back resolvePhone with the table; populate it at runtime from the lid<->phone pairs the
  Baileys engine actually carries: inbound senderPn/participantPn, chats.phoneNumberShare,
  contacts (jid), and history sync.
- from-filter on GET messages that resolves a phone to its lids, so a lid-resolved match
  becomes a hit (closes the silent-miss gap); covered by a test.
- On-demand POST :chatId/history/sync (Baileys fetchMessageHistory) to backfill mappings
  on an already-authed session.
- Canonicalize Baileys contact/chat listing ids to @c.us (read-back folds the neutral id
  back to the engine dialect so send/mark-read round-trip).

RESOLVE_LID_TO_PHONE resolves internally into the table and gates only what is exposed
(privacy flag, not a correctness toggle), per maintainer guidance on rmyndharis#349.
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.

[Feature]: typed WaId identity value object + a lid <-> phone resolution table

1 participant