Skip to content

feat(lid-pn): source-aware write policy matching WA Web createLidPnMappings - #1011

Merged
jlucaso1 merged 4 commits into
mainfrom
claude/review-whatsapp-pr-x8hdcz
Jul 8, 2026
Merged

jlucaso1 merged 4 commits into
mainfrom
claude/review-whatsapp-pr-x8hdcz

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Follow-up to #1010. The LID-PN learn path overwrote the cache unconditionally on every learn, ignoring LearningSource — the source string was pure provenance. WhatsApp Web's createLidPnMappings (WAWebDBCreateLidPnMappings) does the opposite: it branches on learningSource to decide whether an incoming pair may overwrite what the cache already holds. This PR brings that source-differentiated write policy over so the cache behaves like WA Web instead of last-write-wins.

The concrete gap this closes: a bulk/observational seed (history sync field 15, group participant lists, device/contact notifications) could clobber a freshly, authoritatively learned LID for a peer. WA Web deliberately refuses to trust those sources over live data and instead re-resolves via a live query.

What changed

Mirrors WA Web's switch (learningSource) (the v/b/S logic in createLidPnMappings):

Source WA Web string Policy
Usync usync write if new or conflicting (c || C)
PeerPnMessage, PeerLidMessage, RecipientLatestLid, MigrationSyncLatest, MigrationSyncOld, BlocklistActive, BlocklistInactive directed overwrite on any change (!y)
Other, Pairing, DeviceNotification other (default) seed new LIDs only; on conflict with a known LID, don't clobber — request a live re-resolve (v = c, b = C)
MigrationSyncOld, BlocklistInactive stamped created_at = 0 so they never outrank fresh data (S)
  • New lid_pn_write_policy() / is_stale_source() encode the switch; every learn (single + batch) routes through it via RecordOutcome { Skipped | Written | NeedsUsync }.
  • Conflicting observational pairs fire one detached LidQuerySpec usync (the analog of WA Web's syncContactListJob({mode:"query"})), learning the authoritative result as Usync — loop-safe, since Usync never re-queries.
  • The durability/eviction re-warm for self-consistent pairs is preserved (exact forward match, or a reverse-only match after a capacity-bounded-cache PN eviction), so a half-evicted pair is not mistaken for a conflict.
  • The history-sync field-15 harvest source moves MigrationSyncLatestOther, matching WA Web's learningSource: "other" for that seed (verified in WAWebHistorySyncChunk).

LidPnCache::add is unchanged; it never read learning_source and still resolves the PN→LID direction by most-recent created_at.

Evidence

Cross-referenced against captured WhatsApp Web JS: createLidPnMappings branches on learningSource, and every bulk/observational call site (HistorySyncChunk, DeviceNotification, ContactNotification, status, voip, contact-sync) passes "other"; "migration-sync-latest" is used only by the live LID 1:1 migration flow, not the history seed.

Scope boundaries

  • Kept the existing migration-on-discovery mechanism (migrate_*_on_lid_discovery, gated on is_new); did not port WA Web's separate handleNewIdentity identity-key-change subsystem.
  • Kept batch pre-dedup (last-lid-wins). WA Web instead records the superseded entry with created_at = 0; dropping it yields the same resolved PN→LID mapping.
  • No sync-type-aware throttling (WA Web's createLidPnMappings vs createLidPnMappingsInBatches split) — out of scope for this change.

Behavioral note

For observational sources, a conflicting update is now applied eventually (via the async LID re-query) rather than immediately overwriting. This is the intended WA-Web-parity behavior: the client declines to trust an unverified observational claim over the live mapping, and self-heals through the authoritative usync. A genuinely new LID still writes immediately.

Tests

  • Pure switch-matrix unit test covering all 11 sources × (lid_unseen, exact).
  • Behavioral tests: observational conflict preserves the live mapping and routes to usync; directed source overwrites on conflict; observational source still seeds a brand-new LID; exact-match durability heal (no migration); stale-source created_at = 0 ordering (does not outrank fresh; still seeds an empty cache); batch splits written entries vs. usync phones; end-to-end history-sync-style seed preserves a live mapping.
  • Full suite green: 965 lib tests pass, cargo clippy --tests clean, cargo fmt clean.

Review

Reviewed for correctness, WA-Web fidelity, and blast-radius, with each finding independently verified against the code. This surfaced one low-severity regression — an observational re-learn of a pair whose PN-cache side was evicted returned NeedsUsync instead of re-warming from the surviving LID side — which is fixed here. Two further items were a doc clarification (the created_at = 0 ordering guards only the PN→LID direction) and the intended async-heal behavior noted above.

…ppings

The LID-PN learn path previously overwrote the cache unconditionally on
every learn, ignoring the LearningSource. WhatsApp Web's
createLidPnMappings instead branches on learningSource: directed sources
(usync, peer messages, migration, blocklist-active) overwrite on any
change, while observational bulk sources ("other": history sync, group
seeds, device/contact notifications, status, voip) only seed genuinely
new LIDs and, on a conflict with an already-known LID, re-resolve the
phone via a live LID query rather than clobbering the live mapping.
Known-stale sources (migration-sync-old, blocklist-inactive) are stamped
created_at=0 so they never outrank fresh data.

- Add lid_pn_write_policy() / is_stale_source() mirroring the WA Web
  switch, and route every learn (single + batch) through it via a
  RecordOutcome { Skipped | Written | NeedsUsync }.
- Fire a single detached LidQuerySpec usync for conflicted phones
  (WA Web syncContactListJob mode:query); loop-safe because the result
  learns under LearningSource::Usync, which never re-queries.
- Preserve the durability/eviction re-warm for self-consistent pairs
  (exact forward match, or reverse-only after a bounded-cache PN
  eviction) so a half-evicted pair is not mistaken for a conflict.
- Switch the history-sync field-15 harvest from MigrationSyncLatest to
  Other, matching WA Web's learningSource:"other" for this bulk seed.

Adds a pure switch-matrix unit test plus behavioral tests for
conservative/directed conflict resolution, stale-source ordering,
exact-match heal, and batch usync collection.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@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: 0a45fe11-c37d-44dc-82f4-5327f3611c0c

📥 Commits

Reviewing files that changed from the base of the PR and between b8076a2 and 0dda6a9.

📒 Files selected for processing (1)
  • src/client/lid_pn.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added background reconciliation for observational mapping conflicts to heal inconsistencies without disrupting active sync.
    • Enhanced batch learning with outcome-based handling for written updates vs conflict-triggered flows.
  • Bug Fixes

    • Prevented stale learning sources from overriding fresher, live-learned mappings.
    • Improved cache safety so the appropriate learning sources update mappings while avoiding unintended clobbering.
  • Documentation

    • Clarified how learning-source priority affects LID↔phone write and conflict resolution, including stale-source behavior.
  • Tests

    • Added decision-matrix and end-to-end coverage for policy outcomes, stale ordering, conflict vs exact-match healing, batch splitting, and learning-source safety.

Walkthrough

The PR changes LID↔phone mapping learning to use explicit write-policy outcomes, routes observational conflicts through async usync reconciliation, switches history-sync seeding to LearningSource::Other, and expands the related docs and tests.

Changes

LID-PN mapping write-policy refactor

Layer / File(s) Summary
Write-policy and learning-source docs
src/client/lid_pn.rs, wacore/src/types/lid_pn.rs
Adds lid_pn_write_policy, is_stale_source, RecordOutcome, BatchRecordOutcome, the LidQuerySpec import, and updated learning-source documentation.
Single mapping learn paths
src/client/lid_pn.rs
add_lid_pn_mapping and learn_lid_pn_mapping_fast now branch on RecordOutcome, persisting and migrating only for Written and triggering reconcile for NeedsUsync.
Batch learn and migration-sync handler
src/client/lid_pn.rs
learn_lid_pn_mappings_batch, record_lid_pn_batch_in_memory, and handle_lid_migration_mapping_sync use BatchRecordOutcome, split written entries from usync_phones, and keep migration sync on the written subset.
History-sync source and tests
src/history_sync.rs, src/client/lid_pn.rs
History-sync now seeds with LearningSource::Other; tests cover the policy matrix, stale-source writes, conflict routing, batch splitting, and live-mapping preservation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#578: Refactors the same record_lid_pn_in_memory / add_lid_pn_mapping / learn_lid_pn_mapping_fast path this PR builds on.
  • oxidezap/whatsapp-rust#579: Touches the same batch mapping learning flow that this PR changes with BatchRecordOutcome and reconcile handling.
  • oxidezap/whatsapp-rust#1010: Modifies the same process_history_sync_task call site that this PR switches to LearningSource::Other.

Suggested labels: api-design

Suggested reviewers: cubic-dev-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific, concise, and accurately summarizes the main source-aware LID-PN write-policy change.
Description check ✅ Passed The description clearly matches the changeset and explains the WA Web parity behavior, tests, and scope.
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
  • Commit unit tests in branch claude/review-whatsapp-pr-x8hdcz

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.

@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

🤖 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 `@src/client/lid_pn.rs`:
- Around line 282-327: Repeated LID usync reconciles are being spawned for every
NeedsUsync without any deduplication or in-flight guard. Update
spawn_lid_usync_reconcile and reconcile_lid_mappings_via_usync in
src/client/lid_pn.rs to reuse the existing batched dirty-set path or add
per-phone in-flight/recent-query tracking so the same phone does not trigger
multiple concurrent network queries. Keep the best-effort behavior, but ensure
repeated conflicts coalesce before calling execute or spawning the background
task.
🪄 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: d79ff0a6-3907-4365-9953-8dbfdabea1d0

📥 Commits

Reviewing files that changed from the base of the PR and between 18c78f4 and b3a65d1.

📒 Files selected for processing (3)
  • src/client/lid_pn.rs
  • src/history_sync.rs
  • wacore/src/types/lid_pn.rs

Comment thread src/client/lid_pn.rs
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR ports WhatsApp Web's createLidPnMappings source-differentiated write policy into the Rust client, replacing the previous last-write-wins approach where every learn unconditionally overwrote the cache. It also corrects the history-sync field-15 harvest from MigrationSyncLatest (directed, always overwrites) to Other (observational, seed-only), matching the WA Web "other" learning-source for that path.

  • lid_pn_write_policy / is_stale_source encode the WA Web switch(learningSource) switch: Usync and directed sources overwrite on any change; observational bulk sources (Other, Pairing, DeviceNotification) only seed brand-new LIDs and fire a background LidQuerySpec re-resolve on conflict; MigrationSyncOld / BlocklistInactive are stamped created_at = 0 so they never outrank fresher PN→LID data.
  • spawn_lid_usync_reconcile + reconcile_lid_mappings_via_usync implement the WA Web syncContactListJob({mode:\"query\"}) analog; the result is re-learned under LearningSource::Usync, which can never trigger another reconcile, so the query→learn→query loop is provably absent.
  • All existing and new tests pass (965 lib tests); an all_sources_is_exhaustive compile-time guard keeps ALL_SOURCES in step with the enum to prevent silent coverage gaps.

Confidence Score: 5/5

Safe to merge — the write-policy logic mirrors the verified WA Web switch(learningSource) switch, the loop-safety argument is sound (Usync never re-queues), and every behavioral branch is covered by an independent test.

The new record_lid_pn_in_memory path is a clean refactor with well-defined outcomes; the usync reconcile is fire-and-forget and fails gracefully; the history-sync source change is a single-line fix with clear provenance. No correctness gaps were found across the write-policy matrix, the re-warm/eviction branch, or the loop-safety analysis.

No files require special attention.

Important Files Changed

Filename Overview
src/client/lid_pn.rs Core of the PR: introduces lid_pn_write_policy, is_stale_source, RecordOutcome, and BatchRecordOutcome; routes all single and batch learns through the source-aware write policy; adds spawn_lid_usync_reconcile + reconcile_lid_mappings_via_usync for best-effort live re-resolution; comprehensive test suite added.
src/history_sync.rs Single-line source change from MigrationSyncLatest to Other for the field-15 LID harvest; now correctly treats history-sync as an observational seed that will not clobber live-learned mappings.
wacore/src/types/lid_pn.rs Doc-comment update to LearningSource: clarifies that the source is load-bearing (drives the write policy), not merely provenance. No logic changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[learn LID-PN pair
source + lid + phone] --> B{can_skip_relearn?}
    B -- yes --> C[Skipped]
    B -- no --> D[get current_lid
get reverse_pn]
    D --> E{exact match
or forward-evicted?}
    E -- yes --> F[re-warm cache
Written is_new=false]
    E -- no --> G{lid_pn_write_policy
source, lid_unseen, exact}
    G -- write=true --> H[LidPnEntry::with_timestamp
created_at=0 if stale source]
    H --> I[cache.add
Written is_new=current_lid.is_none]
    G -- needs_usync=true --> J[NeedsUsync]
    G -- neither --> C
    J --> K[spawn_lid_usync_reconcile]
    K --> L[reconcile_lid_mappings_via_usync
LidQuerySpec IQ]
    L --> M{IQ response}
    M -- Ok --> N[add_lid_pn_mapping
LearningSource::Usync]
    M -- Err --> O[debug log
cache unchanged]
    N --> A
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"}}}%%
flowchart TD
    A[learn LID-PN pair
source + lid + phone] --> B{can_skip_relearn?}
    B -- yes --> C[Skipped]
    B -- no --> D[get current_lid
get reverse_pn]
    D --> E{exact match
or forward-evicted?}
    E -- yes --> F[re-warm cache
Written is_new=false]
    E -- no --> G{lid_pn_write_policy
source, lid_unseen, exact}
    G -- write=true --> H[LidPnEntry::with_timestamp
created_at=0 if stale source]
    H --> I[cache.add
Written is_new=current_lid.is_none]
    G -- needs_usync=true --> J[NeedsUsync]
    G -- neither --> C
    J --> K[spawn_lid_usync_reconcile]
    K --> L[reconcile_lid_mappings_via_usync
LidQuerySpec IQ]
    L --> M{IQ response}
    M -- Ok --> N[add_lid_pn_mapping
LearningSource::Usync]
    M -- Err --> O[debug log
cache unchanged]
    N --> A
Loading

Reviews (4): Last reviewed commit: "refactor(lid-pn): heal self-consistent p..." | Re-trigger Greptile

Comment thread src/client/lid_pn.rs Outdated
Comment thread src/client/lid_pn.rs Outdated
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.82 MiB 10.83 MiB +7.31 KiB (+0.07%) 🔺
bin .text 8.82 MiB 8.83 MiB +6.75 KiB (+0.07%) 🔺
bin allocated (text+data+bss) 10.82 MiB 10.83 MiB +8.07 KiB (+0.07%) 🔺
llvm-lines wacore 505,440 505,440 0
llvm-lines wacore copies 17,345 17,345 0
llvm-lines whatsapp-rust lib 765,731 767,242 +1,511 (+0.20%) 🔺
llvm-lines whatsapp-rust lib copies 24,902 24,947 +45 (+0.18%) 🔺
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.64 MiB 1.65 MiB +6.63 KiB (+0.39%) 🔺
.text wacore 526.97 KiB 526.09 KiB -900 B (-0.17%) 🔽
.text wacore_binary 157.88 KiB 157.88 KiB 0
.text wacore_libsignal 179.42 KiB 179.42 KiB 0
.text wacore_appstate 158.25 KiB 158.25 KiB 0
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.60 MiB 1.60 MiB 0
.text whatsapp_rust_sqlite_storage 513.00 KiB 513.00 KiB 0
.text whatsapp_rust_tokio_transport 43.79 KiB 43.79 KiB 0
.text whatsapp_rust_ureq_http_client 10.47 KiB 10.47 KiB 0
.text std 1.00 MiB 1.00 MiB +992 B (+0.09%) 🔺
.text other deps 2.95 MiB 2.95 MiB 0
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.64 MiB 1.65 MiB +6.63 KiB (+0.39%)
rustix 191 B 1.88 KiB +1.69 KiB (+908.38%)
buffa_descriptor 4.67 KiB 2.98 KiB -1.69 KiB (-36.25%)

Baseline: 18c78f46c (latest main run) · Head: c2bafca84 · Graphs

@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.

1 issue found across 3 files

Confidence score: 3/5

  • In src/client/lid_pn.rs, spawn_lid_usync_reconcile in the learn_lid_pn_mapping_fire_and_forget hot path can launch repeated detached usync calls on each NeedsUsync result without in-flight/per-phone dedup, which risks request storms, extra load, and user-visible latency under bursty traffic — add an in-flight or per-phone dedup/throttle guard before merging.
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="src/client/lid_pn.rs">

<violation number="1" location="src/client/lid_pn.rs:288">
P2: `spawn_lid_usync_reconcile` fires a detached usync network round-trip on every `NeedsUsync` outcome with no in-flight or per-phone dedup guard. In the per-message hot path (`learn_lid_pn_mapping_fire_and_forget`), a noisy observational conflict for the same phone could spawn many concurrent reconcile queries. Consider adding per-phone deduplication (e.g., a set of in-flight phones checked before spawning) or batching these into the existing dirty-set flush cadence to avoid redundant network calls.</violation>
</file>

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

Re-trigger cubic

Comment thread wacore/src/types/lid_pn.rs Outdated
Comment thread src/client/lid_pn.rs
Comment thread src/client/lid_pn.rs
- Trim the re-warm comment to the reasoning (why, not what).
- Cover PeerLidMessage/RecipientLatestLid in the is_stale_source test by
  iterating the full source set minus the stale ones, so a new variant is
  checked automatically.
- Qualify the LearningSource doc: created_at=0 orders only the per-phone
  PN->LID direction; the LID->PN reverse map always takes the latest write.
greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 8, 2026

@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.

All reported issues were addressed across 2 files (changes from recent commits).

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

Re-trigger cubic

Comment thread src/client/lid_pn.rs Outdated
The is_stale_source / switch-matrix tests iterate a hand-written
ALL_SOURCES array. Add all_sources_is_exhaustive: a wildcard-free match
that fails to compile when a LearningSource variant is added, with an
arm-count assertion tied to ALL_SOURCES.len(), so a new variant must be
appended to both. Drops the misleading "covered automatically" comment.
@greptile-apps
greptile-apps Bot dismissed their stale review July 8, 2026 22:35

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@coderabbitai coderabbitai Bot removed the api-design label Jul 8, 2026

@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)
src/client/lid_pn.rs (1)

391-424: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep reverse-only self-heals out of the directed-write path.

Line 393 writes before Line 411’s same_pair_forward_evicted check. For directed sources, a self-consistent reverse-only cache entry is treated as Written { is_new: true }, which can re-run persistence/migration instead of preserving the intended eviction re-warm behavior.

Proposed fix
         let exact = current_lid.as_deref() == Some(lid);
         let lid_unseen = reverse_pn.is_none();
         let (write, needs_usync) = lid_pn_write_policy(source, lid_unseen, exact);
 
+        // Re-warm/re-affirm durability for a pair that is already the cached
+        // mapping (exact, or reverse-only after a bounded-cache PN eviction).
+        // Must precede write/conflict handling so a self-consistent pair is
+        // not classified as a new directed overwrite.
+        let same_pair_forward_evicted =
+            current_lid.is_none() && reverse_pn.as_deref() == Some(phone_number);
+        if exact || same_pair_forward_evicted {
+            let existing = match self.lid_pn_cache.get_entry_by_phone(phone_number).await {
+                Some(entry) => Some(entry),
+                None => self.lid_pn_cache.get_entry_by_lid(lid).await,
+            };
+            return match existing {
+                Some(entry) => {
+                    self.lid_pn_cache.add(&entry).await;
+                    RecordOutcome::Written {
+                        entry,
+                        is_new: false,
+                    }
+                }
+                None => RecordOutcome::Skipped,
+            };
+        }
+
         if write {
             let created_at = if is_stale_source(source) {
                 0
             } else {
                 wacore::time::now_secs()
@@
                 is_new: current_lid.is_none(),
             };
         }
-
-        // Re-warm/re-affirm durability for a pair that is already the cached
-        // mapping (exact, or reverse-only after a bounded-cache PN eviction).
-        // Must precede the conflict branch so a half-evicted but self-consistent
-        // pair is not mistaken for an observational conflict and re-queried.
-        let same_pair_forward_evicted =
-            current_lid.is_none() && reverse_pn.as_deref() == Some(phone_number);
-        if exact || same_pair_forward_evicted {
-            let existing = match self.lid_pn_cache.get_entry_by_phone(phone_number).await {
-                Some(entry) => Some(entry),
-                None => self.lid_pn_cache.get_entry_by_lid(lid).await,
-            };
-            return match existing {
-                Some(entry) => {
-                    self.lid_pn_cache.add(&entry).await;
-                    RecordOutcome::Written {
-                        entry,
-                        is_new: false,
-                    }
-                }
-                None => RecordOutcome::Skipped,
-            };
-        }
🤖 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 `@src/client/lid_pn.rs` around lines 391 - 424, The directed-write logic in
`RecordOutcome` handling allows reverse-only self-heals to take the main write
path before `same_pair_forward_evicted` is checked, which can incorrectly treat
a self-consistent cached pair as a new write. Update the decision flow around
`lid_pn_write_policy`, `same_pair_forward_evicted`, and the `exact` branch so
reverse-only cache re-warms are handled only by the re-affirmation path and
never by the directed-write `Written { is_new: true }` path.
🤖 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 `@src/client/lid_pn.rs`:
- Around line 1102-1128: The `all_sources_is_exhaustive` guard only compares
`ALL_SOURCES.len()` to the `LearningSource` arm count, so a duplicate entry can
still pass even if one variant is missing. Update this test to validate
`ALL_SOURCES` contains each `LearningSource` variant exactly once, using
`ALL_SOURCES` and `all_sources_is_exhaustive` as the main check points. Keep the
existing exhaustive `match` on `LearningSource` for compile-time coverage, and
add a uniqueness/deduplication assertion so duplicates are rejected as well.

---

Outside diff comments:
In `@src/client/lid_pn.rs`:
- Around line 391-424: The directed-write logic in `RecordOutcome` handling
allows reverse-only self-heals to take the main write path before
`same_pair_forward_evicted` is checked, which can incorrectly treat a
self-consistent cached pair as a new write. Update the decision flow around
`lid_pn_write_policy`, `same_pair_forward_evicted`, and the `exact` branch so
reverse-only cache re-warms are handled only by the re-affirmation path and
never by the directed-write `Written { is_new: true }` path.
🪄 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: 2facb0de-9728-43b1-a650-2d45583b4cdd

📥 Commits

Reviewing files that changed from the base of the PR and between bc32fd7 and b8076a2.

📒 Files selected for processing (1)
  • src/client/lid_pn.rs

Comment thread src/client/lid_pn.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.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/client/lid_pn.rs
- Move the exact / reverse-only re-warm ahead of the write-policy branch
  so a forward-evicted but self-consistent pair (bounded caches only) is
  re-affirmed with is_new=false instead of re-written as a fresh directed
  learn that re-runs the idempotent PN->LID migration. Compute the write
  policy lazily, only when the pair is not already cached.
- Reject duplicate ALL_SOURCES entries in all_sources_is_exhaustive so a
  repeated variant cannot mask a missing one behind the count check.

@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.

1 issue found across 1 file (changes from recent commits).

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="src/client/lid_pn.rs">

<violation number="1" location="src/client/lid_pn.rs:416">
P2: Observational overwrite protection assumes a negative `get_phone_number` result means the incoming LID is genuinely brand-new, but the LID→PN reverse cache can be independently evicted under a capacity bound. When that happens, a known LID appears unseen, so observational sources (e.g. `Other`) get `(write=true, needs_usync=false)` from `lid_pn_write_policy` and immediately overwrite the phone's mapping instead of returning `NeedsUsync`. The codebase already handles forward-only eviction (`same_pair_forward_evicted`) and has tests proving reverse-cache misses are possible, but there is no corresponding guard here.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/client/lid_pn.rs
@jlucaso1
jlucaso1 merged commit f95eb2d into main Jul 8, 2026
24 checks passed
@jlucaso1
jlucaso1 deleted the claude/review-whatsapp-pr-x8hdcz branch July 8, 2026 23:02
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