Skip to content

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

Merged
rmyndharis merged 2 commits into
rmyndharis:mainfrom
tobiasstrebitzer:feat/typed-waid-349
Jun 20, 2026
Merged

feat(engine): typed WaId + persistent lid->phone resolution table + message from-filter#374
rmyndharis merged 2 commits 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.
…llback

Small maintainer touch-ups on top of the contributor's work:
- pass a LidMappingStoreService fake as EngineFactory's new 4th constructor arg in
  all engine.factory.spec call sites (they constructed with 3 args, so the new
  wiring was exercised as undefined)
- DROP INDEX/TABLE IF EXISTS in the migration down() so rollback is safe when the
  table was created by the synchronize path (auto-named index)
- add a migration spec mirroring the sibling AddBaileysStoredMessages test
@rmyndharis

Copy link
Copy Markdown
Owner

Thank you for this — it's a genuinely thorough piece of work, and I appreciate how carefully you staged the persistence and kept resolvePhone synchronous. I reviewed it closely against the source (migration portability on both SQLite and Postgres, the fire-and-forget write-through with its dedup, the boot-loaded cache, and the from-filter), and it all holds up.

I hope you don't mind — I pushed three small maintainer touch-ups directly to the branch so we could land it without a round-trip:

  • The EngineFactory specs were still constructing with 3 args after you added the LidMappingStore 4th parameter, so that new wiring was being exercised as undefined (it slips past CI because specs aren't type-checked). They now pass a LidMappingStore fake.
  • DROP INDEX/TABLE IF EXISTS in the migration down(), so a rollback is safe even when the table was created by the synchronize path (auto-named index).
  • A small migration spec mirroring the sibling AddBaileysStoredMessages test.

Everything else is yours, and the design decisions are unchanged. One note for release framing: the contact/chat listing-id flip to @c.us is the consumer-visible piece we deferred in v0.4.5 — you've already documented it correctly in the CHANGELOG; we'll just settle patch-vs-minor at tag time.

Merging now. Thanks again for the careful work — this is a strong foundation for the identity arc.

@rmyndharis rmyndharis merged commit a7cb605 into rmyndharis:main Jun 20, 2026
5 checks passed
rmyndharis pushed a commit that referenced this pull request Jun 20, 2026
…debug logging (#375)

Pass shouldSyncHistoryMessage: () => true so the Baileys initial contacts/chats/app-state sync actually runs (full-archive download stays opt-in via BAILEYS_SYNC_FULL_HISTORY); make the chatId history filter dialect-agnostic (@c.us also returns @s.whatsapp.net-stored rows); add an opt-in, silent-by-default BAILEYS_LOG_LEVEL logger. Builds on #374.
@tobiasstrebitzer

Copy link
Copy Markdown
Contributor Author

Thanks @rmyndharis appreciate your additional fixes and kind words

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

2 participants