Skip to content

feat(history-sync): learn PN-LID mappings from phoneNumberToLidMappings - #1010

Merged
jlucaso1 merged 2 commits into
oxidezap:mainfrom
blaueeiner:feat/history-sync-lid-mappings
Jul 8, 2026
Merged

jlucaso1 merged 2 commits into
oxidezap:mainfrom
blaueeiner:feat/history-sync-lid-mappings

Conversation

@blaueeiner

Copy link
Copy Markdown
Contributor

Motivation

HistorySync ships a bulk PN↔LID identity table alongside the chats — field 15, phoneNumberToLidMappings — but the streaming extractor currently drops it. A freshly paired client therefore only learns peers' LID↔PN pairs one at a time from live traffic (message sender_alt, usync responses, notifications), and LID-addressed chats imported from history stay unresolvable until each peer happens to show up live.

whatsmeow harvests exactly this field on every history-sync chunk (storeHistoricalPNLIDMappings, message.go); this PR brings the same behavior here.

What it does

  • wacore: the streaming walk in process_history_sync_streaming extracts each PhoneNumberToLIDMapping into HistorySyncResult.lid_mappings (new HistoryLidMapping { phone_number, lid }, bare user parts). Same best-effort leniency as the pushname extractor: malformed entries are skipped, never fatal. Legacy @c.us phones are accepted as PN (whatsmeow normalizes them the same way); wrong namespaces and incomplete entries are rejected. Tag constants are pinned in the existing const-assert block.
  • whatsapp-rust: after a successful parse, process_history_sync_task feeds the pairs to learn_lid_pn_mappings_batch (LearningSource::MigrationSyncLatest), so cache, store, and session/registry migrations follow the same path as the other batch learns (persist + migrations detached).

Notes for review

  • LearningSource: I reused MigrationSyncLatest — its doc comment ("learned from latest history sync migration") finally matches a real history-sync writer. Happy to add a dedicated HistorySync variant instead if you prefer.
  • Migration timing: whatsmeow's history-sync harvest is store-only and lets Signal sessions migrate lazily on the next live message/send. learn_lid_pn_mappings_batch additionally runs the registry/session migrations on its detached task; if you'd rather match whatsmeow strictly for large histories, I can switch to the store-only half.

Tests

  • wacore: extraction test covering valid pairs, legacy @c.us, device-suffixed users, wrong namespaces, and incomplete entries.
  • whatsapp-rust: end-to-end process_history_sync_task test asserting the pair lands in the LID-PN cache.
  • cargo fmt --check and cargo clippy clean; full history_sync + lid_pn test modules pass.

HistorySync carries a bulk PN-LID identity table (field 15,
phoneNumberToLidMappings) alongside the chats, but the streaming
extractor dropped it: a freshly paired client only learned peers'
LID-PN pairs one by one from live traffic (message sender_alt, usync,
notifications), so LID-addressed chats from the history stayed
unresolvable until each peer next showed up live.

Extract the pairs during the existing streaming walk (same best-effort
leniency as the pushname extractor: a malformed entry is skipped, never
fatal; legacy @c.us phones are accepted as PN, wrong namespaces are
rejected) and learn them through learn_lid_pn_mappings_batch after the
parse. This mirrors whatsmeow, which harvests the same field in
storeHistoricalPNLIDMappings on every history-sync chunk.

Persist and session/registry migrations run detached like the other
batch-learn paths.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dfc38e60-1975-4993-94fc-2f962557d30a

📥 Commits

Reviewing files that changed from the base of the PR and between 63f914f and 75590ce.

📒 Files selected for processing (2)
  • src/history_sync.rs
  • wacore/src/history_sync.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • History sync now recognizes more phone-number-to-identifier links during sync, improving contact matching and lookup accuracy.
  • Bug Fixes

    • Better handling of sync data that includes legacy or partially malformed mapping entries, while skipping invalid records safely.
    • Improved consistency between different history-sync processing paths so mapped numbers are learned more reliably.

Walkthrough

History sync now extracts phone-number-to-LID mappings from the protobuf, exposes them on HistorySyncResult, and uses them in process_history_sync_task to learn PN↔LID cache entries. The parser skips malformed or invalid mappings, and tests cover extraction parity plus the downstream learning path.

Changes

PN↔LID Mapping Extraction and Learning

Layer / File(s) Summary
HistorySyncResult contract and mapping extraction
wacore/src/history_sync.rs
Adds HistoryLidMapping and lid_mappings, parses PHONE_NUMBER_TO_LID_MAPPINGS with best-effort validation, keeps streaming and full-walk parity, and tests valid/invalid extraction cases.
Consuming mappings for learning
src/history_sync.rs
process_history_sync_task learns extracted mappings via learn_lid_pn_mappings_batch, and a test verifies the cache update path.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HistorySyncProto
  participant process_history_sync_streaming
  participant extract_lid_mapping
  participant process_history_sync_task
  participant learn_lid_pn_mappings_batch
  participant lid_pn_cache

  HistorySyncProto->>process_history_sync_streaming: phoneNumberToLidMappings field
  process_history_sync_streaming->>extract_lid_mapping: parse mapping entries
  extract_lid_mapping-->>process_history_sync_streaming: filtered lid_mappings
  process_history_sync_streaming-->>process_history_sync_task: HistorySyncResult
  process_history_sync_task->>learn_lid_pn_mappings_batch: (lid, phone_number) pairs
  learn_lid_pn_mappings_batch->>lid_pn_cache: persist PN↔LID learning
Loading

Suggested labels: api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main history-sync change: learning PN↔LID mappings from phoneNumberToLidMappings.
Description check ✅ Passed The description is directly related to the changeset and accurately explains the new extraction and learning behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

Harvests HistorySync.phoneNumberToLidMappings (field 15) during the streaming parse and feeds the extracted pairs to learn_lid_pn_mappings_batch, bringing PN↔LID bootstrap behavior on par with whatsmeow's storeHistoricalPNLIDMappings. Previously, a freshly paired client could only resolve LID-addressed history-sync chats after each peer appeared in live traffic.

  • wacore: new HistoryLidMapping struct, read_str_field helper, and extract_lid_mapping parser; HistorySyncResult.lid_mappings accumulates validated pairs; schema const-asserts pin the three new proto field numbers; unit tests cover valid, legacy @c.us, device-suffixed, wrong-namespace, and incomplete entries.
  • whatsapp-rust: process_history_sync_task checks lid_mappings.is_empty() before calling learn_lid_pn_mappings_batch with LearningSource::MigrationSyncLatest and is_offline=false, matching the ordering convention (lid, phone_number) used by the usync and groups callers; end-to-end test asserts the pair reaches the LID-PN cache.

Confidence Score: 5/5

Safe to merge; extraction is best-effort (malformed entries skipped, never fatal to the sync), pair ordering and LearningSource match existing callers, and both wacore unit tests and the end-to-end test cover the new path.

The extraction logic is well-guarded: Jid::parse + is_pn/is_lid/user_base checks filter invalid inputs, schema const-asserts catch proto renumbering at compile time, and the batch call follows the exact same pattern as usync and groups callers. No existing code paths are modified, and the change is purely additive.

No files require special attention.

Important Files Changed

Filename Overview
wacore/src/history_sync.rs Adds HistoryLidMapping struct, read_str_field helper, and extract_lid_mapping parser; plumbs the new lid_mappings field through HistorySyncResult; schema const-asserts pin the new proto field numbers. Validation is thorough and best-effort approach matches the pushname extractor pattern.
src/history_sync.rs Feeds lid_mappings from HistorySyncResult into learn_lid_pn_mappings_batch with correct pair ordering and LearningSource, consistent with existing usync and groups callers. End-to-end test verifies the mapping lands in the LID-PN cache.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant WA as WhatsApp Server
    participant HS as process_history_sync_task
    participant PS as process_history_sync_streaming
    participant EX as extract_lid_mapping
    participant LB as learn_lid_pn_mappings_batch
    participant CA as LidPnCache / Store

    WA->>HS: HistorySyncNotification (compressed payload)
    HS->>PS: decompress + stream parse
    loop For each PHONE_NUMBER_TO_LID_MAPPINGS field
        PS->>EX: raw field bytes
        EX-->>PS: "Option<HistoryLidMapping> (None = skip)"
    end
    PS-->>HS: "HistorySyncResult { lid_mappings, … }"
    alt lid_mappings non-empty
        HS->>LB: "Vec<(lid, phone_number)>, MigrationSyncLatest, is_offline=false"
        LB->>CA: populate in-memory cache (sync)
        LB-->>LB: spawn detached task: persist + migrate registry
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant WA as WhatsApp Server
    participant HS as process_history_sync_task
    participant PS as process_history_sync_streaming
    participant EX as extract_lid_mapping
    participant LB as learn_lid_pn_mappings_batch
    participant CA as LidPnCache / Store

    WA->>HS: HistorySyncNotification (compressed payload)
    HS->>PS: decompress + stream parse
    loop For each PHONE_NUMBER_TO_LID_MAPPINGS field
        PS->>EX: raw field bytes
        EX-->>PS: "Option<HistoryLidMapping> (None = skip)"
    end
    PS-->>HS: "HistorySyncResult { lid_mappings, … }"
    alt lid_mappings non-empty
        HS->>LB: "Vec<(lid, phone_number)>, MigrationSyncLatest, is_offline=false"
        LB->>CA: populate in-memory cache (sync)
        LB-->>LB: spawn detached task: persist + migrate registry
    end
Loading

Reviews (2): Last reviewed commit: "chore: address PR review comments" | Re-trigger Greptile

Comment thread src/history_sync.rs Outdated
Comment thread wacore/src/history_sync.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/src/history_sync.rs (1)

2964-2975: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parity oracle doesn't actually cover the new field.

Look, every other extracted field here (nct_salt, own_pushname, tc_token_candidates, msg_secret_records) is protected by reference_full_walk + streaming_extraction_matches_full_buffer_reference. lid_mappings only got the struct-literal update to keep the compiler happy — reference_full_walk never parses PHONE_NUMBER_TO_LID_MAPPINGS, parity_fixture() carries no such data, and the parity test never asserts lid_mappings equality. A future streaming-path regression on this field would sail right through this differential test. We built this whole oracle mechanism specifically to catch this class of bug — let's not leave the newest field as the one blind spot.

✅ Sketch of the missing parity coverage
             tags::history_sync::NCT_SALT if wt == wire_type::LENGTH_DELIMITED => {
                 ...
             }
+            tags::history_sync::PHONE_NUMBER_TO_LID_MAPPINGS
+                if wt == wire_type::LENGTH_DELIMITED =>
+            {
+                let (len, vlen) = read_varint(&decompressed[pos..]).unwrap();
+                pos += vlen;
+                let end = checked_end(pos, len, decompressed.len()).unwrap();
+                if let Some(mapping) = extract_lid_mapping(&decompressed[pos..end]) {
+                    result.lid_mappings.push(mapping);
+                }
+                pos = end;
+            }
             _ => {
                 pos = skip_field(wt, decompressed, pos).unwrap();
             }

Then add a phoneNumberToLidMappings entry to parity_fixture() and assert_eq!(result.lid_mappings, reference.lid_mappings) in streaming_extraction_matches_full_buffer_reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/src/history_sync.rs` around lines 2964 - 2975, The parity test
coverage for the new lid_mappings field is missing, so a streaming regression
could bypass the oracle. Update reference_full_walk to parse
PHONE_NUMBER_TO_LID_MAPPINGS into result.lid_mappings, extend parity_fixture()
with a phoneNumberToLidMappings entry, and add an assert_eq! for lid_mappings in
streaming_extraction_matches_full_buffer_reference so the streaming and
full-buffer paths are compared for this field too.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/history_sync.rs`:
- Around line 626-676: The two length-delimited parsing branches in
extract_lid_mapping are duplicated and should be consolidated to prevent drift.
Extract the shared read logic into a small helper used for both
tags::phone_number_to_lid_mapping::PN_JID and
tags::phone_number_to_lid_mapping::LID_JID, keeping the existing read_varint,
smoothutf8::from_utf8, and pos advancement behavior unchanged while only varying
the destination variable.

---

Outside diff comments:
In `@wacore/src/history_sync.rs`:
- Around line 2964-2975: The parity test coverage for the new lid_mappings field
is missing, so a streaming regression could bypass the oracle. Update
reference_full_walk to parse PHONE_NUMBER_TO_LID_MAPPINGS into
result.lid_mappings, extend parity_fixture() with a phoneNumberToLidMappings
entry, and add an assert_eq! for lid_mappings in
streaming_extraction_matches_full_buffer_reference so the streaming and
full-buffer paths are compared for this field too.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c28ada07-f125-47c2-9328-6eb00ccc7d14

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0f241 and 63f914f.

📒 Files selected for processing (2)
  • src/history_sync.rs
  • wacore/src/history_sync.rs

Comment thread wacore/src/history_sync.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 2 files

Confidence score: 4/5

  • In wacore/src/history_sync.rs, the parity test path around reference_full_walk no longer validates HistorySyncResult.lid_mappings, so a regression in that field could ship without being caught by the full-buffer check — teach the reference walk to populate lid_mappings and include it in parity assertions before merging.
  • In wacore/src/history_sync.rs, the duplicated length-delimited parsing for PN_JID and LID_JID increases the chance of the two paths drifting and producing inconsistent decode behavior over time — extract a shared helper (for example read_len_field) to keep fixes and validation consistent.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="wacore/src/history_sync.rs">

<violation number="1" location="wacore/src/history_sync.rs:284">
P3: The full-buffer parity test no longer covers the new `HistorySyncResult.lid_mappings` field, because `reference_full_walk` always leaves it empty and the parity assertions skip it. Consider teaching the reference walk to parse `PHONE_NUMBER_TO_LID_MAPPINGS` and asserting equality so future streaming regressions are caught.</violation>

<violation number="2" location="wacore/src/history_sync.rs:648">
P3: The length-delimited field read logic for `PN_JID` and `LID_JID` is duplicated verbatim (differing only in the target variable). Consider extracting a small helper like `read_len_field(data, &mut pos) -> Option<&str>` to avoid the two copies drifting independently in future edits.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread wacore/src/history_sync.rs
Comment thread wacore/src/history_sync.rs
@blaueeiner
blaueeiner marked this pull request as draft July 8, 2026 15:44
- trim the learn-site comment to motivation only (greptile)
- drop the pure-what comment on the streaming match arm (greptile)
- dedupe the PN_JID/LID_JID reads into read_str_field (coderabbit, cubic)
- teach the full-buffer parity oracle field 15 and assert lid_mappings
  parity, incl. a wrong-namespace entry both walks must skip (cubic,
  coderabbit)
@blaueeiner
blaueeiner marked this pull request as ready for review July 8, 2026 17:02

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Adds PN-LID mapping from history sync into the production LID-PN cache, touching core logic in wacore and whatsapp-rust. Data integrity and migration timing concerns require human review.

Re-trigger cubic

@jlucaso1 jlucaso1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks a lot

@jlucaso1
jlucaso1 merged commit 18c78f4 into oxidezap:main Jul 8, 2026
26 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants