Skip to content

fix: invalidate sender key cache on device changes and handle identity notifications - #489

Merged
jlucaso1 merged 2 commits into
mainfrom
fix/sender-key-cache-invalidation-and-identity-change
Apr 6, 2026
Merged

fix: invalidate sender key cache on device changes and handle identity notifications#489
jlucaso1 merged 2 commits into
mainfrom
fix/sender-key-cache-invalidation-and-identity-change

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes "Waiting for messages" in group chats caused by stale sender key device caches.

Device notification fixes (src/client/device_registry.rs)

  • patch_device_add: Detects genuinely new devices and calls sender_key_device_cache.invalidate_all() so SKDM is sent on next group message. Verified against WAWeb/Identity/UpdateDeviceTableApi where new devices enter the participant store with has_key=false.
  • patch_device_remove: Deletes Signal sessions for the removed device under both LID and PN addresses (matching WA Web's deleteRemoteInfo), then invalidates sender key cache.
  • DRY: Extracted delete_sessions_for_devices() shared helper, used by both clear_device_record and patch_device_remove. Previously clear_device_record had an inline loop that was duplicated.

Identity change handler (src/handlers/notification.rs)

  • Handle type="encrypt" notifications with <identity/> child — previously silently ignored because we only checked from == SERVER_JID for encrypt notifications.
  • Clears device record (sessions + sender keys) and invalidates device cache, matching WA Web's WAWebHandleIdentityChange which calls clearDeviceRecordForIdentityChange + deleteRemoteInfo.
  • Ignores companion devices (device != 0), matching WA Web.
  • New IdentityChange event for application layer (wacore/src/types/events.rs).

Test plan

  • test_patch_device_add_invalidates_sender_key_cache — new device triggers cache invalidation
  • test_patch_device_add_no_invalidation_when_device_exists — re-adding existing device is no-op
  • test_patch_device_remove_invalidates_sender_key_cache — removed device triggers cache invalidation + session cleanup
  • test_identity_change_dispatches_event_and_invalidates_cache — identity change clears record and dispatches event
  • test_identity_change_ignores_companion_device — companion device identity change is ignored
  • All 341 existing tests pass, clippy clean

Summary by CodeRabbit

  • New Features

    • Apps now receive identity-change events when a device identity changes; a new event payload includes optional linked identity info.
  • Bug Fixes

    • Sender-key/device registry cache is properly invalidated when devices are added or removed.
    • Signal session cleanup improved when devices are removed.
    • Encrypt notifications with identity info are routed and processed correctly.
  • Tests

    • Added tests validating cache invalidation, session cleanup, and identity-change handling.

…y notifications

Root cause of "Waiting for messages" in group chats: when a participant
added/removed a device, the sender key device cache was not invalidated,
so SKDM was never sent to the new device on next group message.

Device notification fixes (verified against WAWeb/Identity/UpdateDeviceTableApi):
- patch_device_add: detect genuinely new devices and invalidate sender key
  device cache so SKDM is sent on next group message
- patch_device_remove: delete Signal sessions for removed device (matching
  WA Web's deleteRemoteInfo), then invalidate sender key cache
- Extract shared delete_sessions_for_devices() helper (DRY with
  clear_device_record)

Identity change handler (verified against WAWeb/Handle/IdentityChange):
- Handle type="encrypt" notifications with <identity/> child (was silently
  ignored before)
- Clear device record (sessions + sender keys) and invalidate device cache
- Ignore companion devices (device != 0), matching WA Web
- Dispatch new IdentityChange event for application layer
@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8767aa89-7879-4d6f-a101-e61bf2651148

📥 Commits

Reviewing files that changed from the base of the PR and between 80a5f5c and 262167e.

📒 Files selected for processing (1)
  • src/handlers/notification.rs

📝 Walkthrough

Walkthrough

Adds identity-change notification handling and an IdentityChange event; refactors device registry to centralize session deletion and consistently invalidate the sender-key device cache on device add/remove/clear; updates tests to cover cache invalidation and identity-change dispatch.

Changes

Cohort / File(s) Summary
Device Registry — cache & sessions
src/client/device_registry.rs
Refactored device add/remove/clear flows to call delete_sessions_for_devices() and to call sender_key_device_cache.invalidate_all() when device lists change; extracted session-deletion helper; added tests for sender-key cache invalidation on add/remove.
Notification handling — identity changes
src/handlers/notification.rs
Detects <identity/> inside <notification type="encrypt">; adds handle_identity_change() to clear device records, invalidate device registry cache, and dispatch Event::IdentityChange; ignores self-primary and companion-device notifications; added unit tests.
Public event types
wacore/src/types/events.rs
Introduced IdentityChange { user: Jid, lid_user: Option<Jid> } and added Event::IdentityChange(IdentityChange) variant.

Sequence Diagram

sequenceDiagram
    participant Client
    participant NotificationHandler
    participant DeviceRegistry
    participant Cache
    participant EventBus

    Client->>NotificationHandler: receive <notification type="encrypt"> with <identity/>
    NotificationHandler->>NotificationHandler: parse <identity/>, extract from JID and optional lid
    NotificationHandler->>DeviceRegistry: load device record for user
    DeviceRegistry->>DeviceRegistry: clear_device_record(user) -> delete_sessions_for_devices(device_ids)
    DeviceRegistry->>Cache: flush signal_cache / invalidate sender_key_device_cache
    NotificationHandler->>Cache: invalidate device_registry_cache(user)
    NotificationHandler->>EventBus: dispatch Event::IdentityChange{ user, lid_user }
    EventBus->>Client: deliver IdentityChange event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A tiny hop, a gentle cheer,
Devices cleared and caches dear,
Signals flushed, new events chime,
I nibble logs and mark the time,
Hoppity code—identity aligned 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes in the PR: invalidating sender key cache on device changes and handling identity notifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sender-key-cache-invalidation-and-identity-change

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 and usage tips.

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfix/sender-key-cache-invalidation-and-identity-change
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()Instructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
47.13 x 1e3
(+8.18%)Baseline: 43.56 x 1e3
45.74 x 1e3
(103.03%)

Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,197.00
(-3.49%)Baseline: 6,421.42
6,742.49
(91.91%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-24.86%)Baseline: 697,755.92
732,643.72
(71.56%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-5.08%)Baseline: 21,984.04
23,083.25
(90.40%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,202.00
(-12.49%)Baseline: 112,216.85
117,827.70
(83.34%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,230.00
(-8.95%)Baseline: 107,881.03
113,275.08
(86.72%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.09%)Baseline: 533,450.78
560,123.32
(95.15%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,870.00
(-4.14%)Baseline: 16,554.89
17,382.64
(91.30%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,715,201.00
(-7.04%)Baseline: 15,830,231.64
16,621,743.22
(88.53%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,358.00
(-18.45%)Baseline: 145,139.72
152,396.70
(77.66%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.09%)Baseline: 534,872.81
561,616.46
(95.15%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,919.00
(-3.69%)Baseline: 18,605.90
19,536.20
(91.72%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,066,347.00
(-19.40%)Baseline: 34,820,650.53
36,561,683.06
(76.76%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.09%)Baseline: 533,889.78
560,584.27
(95.15%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,843.00
(-6.63%)Baseline: 16,967.53
17,815.91
(88.93%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,716,627.00
(-7.04%)Baseline: 15,831,391.64
16,622,961.23
(88.53%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
107,945.00
(-9.83%)Baseline: 119,711.96
125,697.56
(85.88%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,302.00
(-8.94%)Baseline: 107,953.03
113,350.68
(86.72%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-4.86%)Baseline: 95,618.62
100,399.55
(90.61%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-3.12%)Baseline: 7,615.71
7,996.50
(92.27%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-1.57%)Baseline: 92,453.88
97,076.57
(93.75%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.32%)Baseline: 7,377.73
7,746.61
(95.54%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-1.34%)Baseline: 108,238.88
113,650.82
(93.96%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.26%)Baseline: 8,889.73
9,334.21
(95.49%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-6.40%)Baseline: 44,860.16
47,103.17
(89.14%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-2.01%)Baseline: 2,772.60
2,911.23
(93.33%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+0.68%)Baseline: 552,343.44
579,960.62
(95.88%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.20%)Baseline: 772.54
811.16
(95.05%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,640,988.00
(-0.21%)Baseline: 27,698,241.60
29,083,153.68
(95.04%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,544,828.00
(-0.05%)Baseline: 5,547,600.35
5,824,980.36
(95.19%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
175,061.00
(-1.22%)Baseline: 177,230.97
186,092.52
(94.07%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
175,710.00
(-1.28%)Baseline: 177,996.88
186,896.72
(94.01%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,319,738.00
(+0.23%)Baseline: 17,280,599.56
18,144,629.54
(95.45%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
298,417.00
(+0.51%)Baseline: 296,906.03
311,751.33
(95.72%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,663,477.00
(+0.54%)Baseline: 12,595,650.69
13,225,433.22
(95.75%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
719,597.00
(+0.35%)Baseline: 717,090.33
752,944.85
(95.57%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
🚨 view alert (🔔)
47,129.00
(+8.18%)Baseline: 43,563.81
45,742.00
(103.03%)

libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,771.56
16,339,860.14
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,378,778.00
(-1.67%)Baseline: 5,470,095.20
5,743,599.96
(93.65%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
312,188.00
(-59.25%)Baseline: 766,069.27
804,372.73
(38.81%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,547.00
(+0.16%)Baseline: 2,825,921.93
2,967,218.03
(95.39%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,452,844.00
(-0.48%)Baseline: 3,469,556.88
3,643,034.73
(94.78%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
125,728,758.00
(+0.29%)Baseline: 125,361,706.19
131,629,791.50
(95.52%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
12,002.00
(+1.39%)Baseline: 11,837.73
12,429.62
(96.56%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,930.00
(+2.25%)Baseline: 3,843.48
4,035.66
(97.38%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,073.00
(-0.71%)Baseline: 87,692.92
92,077.56
(94.56%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,022.00
(-0.88%)Baseline: 79,720.76
83,706.80
(94.40%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
50,375.00
(-0.96%)Baseline: 50,865.23
53,408.49
(94.32%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,956.00
(+2.83%)Baseline: 5,792.02
6,081.62
(97.93%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,227.00
(+4.05%)Baseline: 2,140.37
2,247.39
(99.09%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(-0.00%)Baseline: 21,920.19
23,016.20
(95.24%)
🐰 View full continuous benchmarking report in Bencher

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80a5f5c9d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

// Invalidate device cache so next send triggers fresh usync
client.invalidate_device_cache(&from_jid.user).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply identity cleanup to LID alias before invalidation

handle_identity_change invalidates caches by from_jid.user only, while the optional lid attribute is merely forwarded in the event and never used to refresh lookup aliases first. When PN↔LID mapping is missing or expired, this means load_device_record/invalidate_device_cache only touch the PN key and can leave LID-keyed device records/sessions behind, so the identity-change cleanup is incomplete and stale state can continue to affect decryption.

Useful? React with 👍 / 👎.

Comment on lines +303 to +306
let servers = [
wacore_binary::jid::HIDDEN_USER_SERVER,
wacore_binary::jid::DEFAULT_USER_SERVER,
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include all user servers when deleting stale sessions

delete_sessions_for_devices now hard-codes only HIDDEN_USER_SERVER and DEFAULT_USER_SERVER, so clear_device_record/patch_device_remove no longer honor other server types when cleaning sessions. For contacts/devices on supported non-PN/LID user domains (for example hosted variants), session deletion is skipped, leaving stale Signal sessions after device removals or identity-change cleanup.

Useful? React with 👍 / 👎.

@jlucaso1

jlucaso1 commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 6, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 53s —— View job


Code Review: Sender Key Cache Invalidation & Identity Change Handling

  • Read src/client/device_registry.rs changes
  • Read src/handlers/notification.rs changes
  • Read wacore/src/types/events.rs changes
  • Analyze existing code for context
  • Post review feedback

Overall this is solid work — the cache invalidation fixes address a real correctness issue, the DRY refactor is clean, and the tests are well-structured. A few observations:

Things that look good

  1. patch_device_add new-device detection — Snapshotting devices_before and comparing after mutation is a correct and readable approach. The any() check avoids false positives from re-adding existing devices, and the test test_patch_device_add_no_invalidation_when_device_exists validates this nicely.

  2. delete_sessions_for_devices extraction — Good DRY refactor. The old clear_device_record had an inline triple-nested loop that was nearly identical to what patch_device_remove now needs. Sharing this is the right call.

  3. handle_identity_change — Clean handler structure: early-return on missing from, skip companion devices, load-then-clear pattern, dispatch event. The companion device filter (device != 0) correctly matches WA Web behavior.

  4. Test coverage — Five new tests covering the key scenarios (add with/without new device, remove, identity change event dispatch, companion device ignore). Good use of the existing TestEventCollector pattern.

Issues & suggestions

1. _server parameter now unused in clear_device_record (device_registry.rs:329)

The server parameter was renamed to _server since delete_sessions_for_devices always uses both HIDDEN_USER_SERVER and DEFAULT_USER_SERVER. This is functionally correct (the old code only used the server fallback for UserLookupKeys::Unknown, and even then both servers would cover it). However, keeping a dead parameter is a bit of a code smell — consider removing it from the signature and updating the one call site at line 240.

2. Behavior change for UserLookupKeys::Unknown in session deletion (device_registry.rs:301-306)

The old clear_device_record had a subtle fallback: when resolve_lookup_keys returned Unknown, it used the server from the notification (std::slice::from_ref(&server)) rather than both servers. The new delete_sessions_for_devices always iterates both lid and s.whatsapp.net servers regardless. This is arguably more correct (sessions could exist under either server), but it's a silent behavior change worth being aware of. The Codex reviewer flagged concern about missing other server types — in practice, WhatsApp only uses s.whatsapp.net and lid for Signal sessions, so this is fine.

3. device_id type mismatch: u32 vs u16 (device_registry.rs:375, 301)

patch_device_remove receives device_id: u32 and casts it to u16 when calling delete_sessions_for_devices(user, &[device_id as u16]). Similarly in clear_device_record, device_id from DeviceInfo is u32 cast to u16. The Jid.device field is u16, so this cast is technically correct for valid WhatsApp device IDs (which are small), but as u16 silently truncates if someone ever passes a value > 65535. A try_into() or at least a debug assertion would be more robust, though this is low risk in practice.

4. Identity change doesn't apply cleanup to LID alias (notification.rs:352)

The Codex reviewer's P1 point has some merit: handle_identity_change calls clear_device_record(&from_jid.user, ...) and invalidate_device_cache(&from_jid.user) using only the user from the from JID (the PN). The optional lid attribute from the notification node is extracted but only forwarded in the event. If there are device records cached under the LID key that resolve_lookup_keys can't map (e.g. the LID→PN mapping was lost), those would remain stale.

In practice, invalidate_device_cache already calls resolve_lookup_keys internally which should handle the bidirectional LID↔PN resolution. But if lid_pn_cache doesn't have the mapping (which can happen if this is a fresh connection), explicitly invalidating by the lid attribute too would be a safety net:

// After the existing invalidate_device_cache call:
if let Some(lid_jid) = node.attrs().optional_jid("lid") {
    client.invalidate_device_cache(&lid_jid.user).await;
}

This is a minor robustness improvement, not a blocker.

5. invalidate_all() granularity (device_registry.rs:275, 357, 383)

All three paths (patch_device_add, clear_device_record, patch_device_remove) call sender_key_device_cache.invalidate_all() — a global cache nuke. For a single-user change, this could be overly aggressive in high-traffic scenarios (all groups' SKDM caches get invalidated, not just the affected user's groups). This matches WA Web behavior per the PR description, so it's correct, but just noting this is a known trade-off.

Benchmark alert

The Bencher alert on bench_decrypt_with_previous_session (+8.18%) is in an unrelated benchmark (session_optimization_group) and doesn't touch any code modified in this PR — likely CI noise.

Verdict

The PR correctly addresses the "Waiting for messages" bug from stale sender key caches. The implementation is well-aligned with WA Web's behavior, the DRY refactor improves maintainability, and test coverage is good. The issues noted above are minor and non-blocking.


WA Web's isMePrimary check (IdentityChange.js:58) prevents processing
identity change notifications for our own primary device. Without this,
we would clear our own sessions and device record.
@jlucaso1
jlucaso1 merged commit 39d10fd into main Apr 6, 2026
8 checks passed
@jlucaso1
jlucaso1 deleted the fix/sender-key-cache-invalidation-and-identity-change branch April 6, 2026 13:59

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 262167e142

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

);

// Load existing record to pass to clear_device_record
if let Some(record) = client.load_device_record(&from_jid.user).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear sender-key state even when device record is missing

Identity-change cleanup is currently gated on load_device_record succeeding, so when the registry was already invalidated (for example the hash-only device-update path calls invalidate_device_cache), this branch skips clear_device_record entirely. In that case stale sender-key tracking is left intact (no clear_all_sender_key_devices() / sender_key_device_cache.invalidate_all()), and later group sends can still treat old devices as has_key=true and skip SKDM redistribution, reproducing undecryptable “waiting for messages” behavior.

Useful? React with 👍 / 👎.

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