Skip to content

fix: complete identity change handler to match WA Web - #490

Merged
jlucaso1 merged 3 commits into
mainfrom
fix/complete-identity-change-handler
Apr 6, 2026
Merged

fix: complete identity change handler to match WA Web#490
jlucaso1 merged 3 commits into
mainfrom
fix/complete-identity-change-handler

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Completes the identity change handler from PR #489 with three remaining gaps verified against WAWebHandleIdentityChange:

  • Delete primary device session + identity key so a fresh session can be established (WA Web's deleteRemoteInfo)
  • Delete status@broadcast sender key for forward secrecy on next status send (WA Web's markStatusSenderKeyRotate)
  • Spawn background ensure_e2e_sessions to proactively re-establish session (self-defers when offline via wait_for_offline_delivery_end)
  • Clean up verbose comments (explain why, not what)

Test plan

  • test_identity_change_deletes_primary_session — session + identity removed from signal cache
  • test_identity_change_rotates_status_sender_key — status sender key deleted for forward secrecy
  • test_identity_change_with_offline_attribute — offline notification processed without error
  • All 345 tests pass, clippy clean

Summary by CodeRabbit

  • Bug Fixes

    • Identity-change handling now targets primary devices and avoids clearing local self-identity records.
    • Ensures thorough local cryptographic cleanup (primary session and sender keys) and flushes cached state when identities change.
    • Triggers background session re-establishment so secure communications recover automatically, including when offline.
  • Tests

    • Added async tests for primary-session/identity removal, sender-key rotation, and offline identity-change processing.

- Delete primary device session + identity key so fresh session can be
  established (WA Web's deleteRemoteInfo)
- Delete status@broadcast sender key for forward secrecy on next status
  send (WA Web's markStatusSenderKeyRotate)
- Spawn background ensure_e2e_sessions to proactively re-establish
  session (self-defers when offline)
- Clean up verbose comments
@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 15s —— View job


Code Review: PR #490

  • Read PR diff and changed file
  • Review implementation against WA Web behavior
  • Check test coverage
  • Post review feedback

Overall this is a solid PR that closes the remaining gaps from #489. The three new operations (primary session deletion, status sender key rotation, background session re-establishment) align well with the WA Web reference. A few observations:

Potential Issue: Missing flush after sender key deletion

In the primary session deletion block (lines 367–377), you correctly call flush_signal_cache() to persist the deletions to the backend. However, the sender key deletion block (lines 379–395) does not flush. The delete_sender_key method marks the key as deleted in the cache's dirty state, but without a flush, this deletion won't be persisted to SQLite until the next periodic flush.

If the process crashes between these two points, you'd have the primary session deleted (flushed) but the status sender key still alive on disk — a partial state. Consider adding a second flush after the sender key loop, or combining both blocks before a single flush.

Fix this →

Minor: Duplicate use import in adjacent scopes

use wacore::types::jid::JidExt appears in both the session deletion block (line 369) and the sender key block (line 383). Since these are in separate {} scopes within the same function, each import is local. This works fine but could be cleaner as a single import at the top of the function body, reducing the visual noise of the scoped blocks.

Style nit: to_string() on a string literal

Line 384: status_group.to_string() where status_group is &str. This is fine but "status@broadcast".to_string() inline (without the intermediate binding) would be marginally cleaner since the variable is only used once.

Tests

The three new tests are well-structured and cover the key behaviors:

  • test_identity_change_deletes_primary_session — verifies session + identity removal
  • test_identity_change_rotates_status_sender_key — verifies sender key cleanup
  • test_identity_change_with_offline_attribute — verifies offline notifications still dispatch events

One gap: there's no test asserting that the identity key is also deleted (only session presence is checked in test_identity_change_deletes_primary_session). Consider adding a get_identity check to confirm both are gone.

Design: Fire-and-forget ensure_e2e_sessions

The .detach() pattern (lines 410–417) is the right call for fire-and-forget background work. The error is logged, and ensure_e2e_sessions self-defers when offline, so this won't spam reconnection attempts.


Verdict: Looks good. The flush gap is the only functional concern — the rest is minor style. Nice work closing the gaps from the WA Web reference.

@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

handle_identity_change in src/handlers/notification.rs is changed to run primary-device-only cleanup: it clears device records, deletes primary session and identity entries and status@broadcast sender keys from the signal cache, flushes the cache, dispatches Event::IdentityChange, and spawns a detached background task to call ensure_e2e_sessions.

Changes

Cohort / File(s) Summary
Primary handler logic
src/handlers/notification.rs
Refactored handle_identity_change to treat identity changes as primary-device-only, call clear_device_record, delete primary session & identity from signal cache, delete cached status@broadcast sender keys, flush the signal cache (warn on failure), invalidate device cache, dispatch Event::IdentityChange, and spawn a detached task to run ensure_e2e_sessions.
Tests
tests/..., src/handlers/...
Added/updated async tests to assert deletion of primary session/identity, rotation/deletion of status sender keys, and that an identity-change notification with offline attribute still triggers Event::IdentityChange processing.

Sequence Diagram

sequenceDiagram
    participant Notif as Notification Handler
    participant Cache as Signal Cache
    participant Device as Device Cache
    participant Event as Event Dispatcher
    participant BG as Background Task
    participant E2E as E2E Session Manager

    Notif->>Cache: clear_device_record(jid)
    Notif->>Cache: delete_primary_session(jid)
    Notif->>Cache: delete_identity(jid)
    Notif->>Cache: delete_sender_keys("status@broadcast", jid)
    Notif->>Cache: flush() 
    Note right of Cache: warn on flush failure
    Notif->>Device: invalidate_device_cache(jid)
    Notif->>Event: dispatch(Event::IdentityChange)
    Notif->>BG: spawn detached ensure_e2e_sessions([session_jid])
    BG->>E2E: ensure_e2e_sessions -> establish sessions (self-defer if offline)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nudge the cache with tiny paws, then sweep—
Old sessions fall asleep, new paths to keep.
Sender keys scattered, then sprout anew,
A background hop ensures trust grew.
Hooray — e2e naps wake up and peek!

🚥 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 'fix: complete identity change handler to match WA Web' accurately describes the main change in the PR, which is completing the identity change handler implementation to align with WA Web behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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/complete-identity-change-handler

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.

@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: fbff40e8db

ℹ️ 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".

Comment on lines +390 to +393
client
.signal_cache
.delete_sender_key(sk_name.cache_key())
.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.

P2 Badge Flush status sender-key deletion before returning

delete_sender_key() only marks an in-memory cache entry dirty, but this handler never calls flush_signal_cache() after deleting status@broadcast keys. If ensure_e2e_sessions cannot establish a session (e.g., missing prekeys/offline) or the process restarts before another flush path runs, the old sender key remains in the DB and can be reused after restart, which breaks the intended forward-secrecy rotation on identity change.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfix/complete-identity-change-handler
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.07%)Baseline: 43.61 x 1e3
45.79 x 1e3
(102.93%)

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.35%)Baseline: 6,411.57
6,732.15
(92.05%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-24.61%)Baseline: 695,463.61
730,236.79
(71.80%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-5.01%)Baseline: 21,969.23
23,067.69
(90.46%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,202.00
(-12.28%)Baseline: 111,955.14
117,552.90
(83.54%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,230.00
(-8.82%)Baseline: 107,734.06
113,120.76
(86.84%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.09%)Baseline: 533,443.13
560,115.28
(95.15%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,870.00
(-4.08%)Baseline: 16,544.46
17,371.69
(91.36%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,715,201.00
(-6.94%)Baseline: 15,813,251.48
16,603,914.05
(88.62%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,358.00
(-18.22%)Baseline: 144,731.87
151,968.47
(77.88%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.09%)Baseline: 534,865.28
561,608.54
(95.15%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,919.00
(-3.64%)Baseline: 18,595.44
19,525.21
(91.77%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,066,347.00
(-19.16%)Baseline: 34,717,793.12
36,453,682.77
(76.99%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.09%)Baseline: 533,882.13
560,576.23
(95.15%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,843.00
(-6.55%)Baseline: 16,952.87
17,800.51
(89.00%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,716,627.00
(-6.94%)Baseline: 15,814,415.53
16,605,136.31
(88.63%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
107,945.00
(-9.70%)Baseline: 119,534.36
125,511.08
(86.00%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,302.00
(-8.82%)Baseline: 107,806.06
113,196.36
(86.84%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-4.80%)Baseline: 95,563.98
100,342.18
(90.66%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-3.09%)Baseline: 7,612.91
7,993.56
(92.30%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-1.54%)Baseline: 92,431.81
97,053.40
(93.77%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.31%)Baseline: 7,378.08
7,746.99
(95.53%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-1.32%)Baseline: 108,216.81
113,627.65
(93.98%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.26%)Baseline: 8,890.08
9,334.59
(95.48%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-6.26%)Baseline: 44,791.53
47,031.11
(89.28%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-1.91%)Baseline: 2,769.97
2,908.47
(93.42%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+0.55%)Baseline: 553,045.86
580,698.15
(95.76%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.19%)Baseline: 772.47
811.09
(95.06%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,830,329.00
(+0.48%)Baseline: 27,697,535.77
29,082,412.56
(95.69%)
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,422.41
5,824,793.53
(95.19%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
175,061.00
(-1.18%)Baseline: 177,157.46
186,015.34
(94.11%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
175,710.00
(-1.24%)Baseline: 177,922.30
186,818.42
(94.05%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,271,176.00
(-0.06%)Baseline: 17,281,132.84
18,145,189.48
(95.18%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
298,353.00
(+0.48%)Baseline: 296,924.97
311,771.22
(95.70%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,562,186.00
(-0.26%)Baseline: 12,594,684.64
13,224,418.87
(94.99%)
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,076.09
752,929.89
(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.07%)Baseline: 43,608.75
45,789.19
(102.93%)

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,772.48
16,339,861.10
(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.65%)Baseline: 5,468,944.15
5,742,391.35
(93.67%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
312,188.00
(-58.94%)Baseline: 760,348.08
798,365.48
(39.10%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,547.00
(+0.16%)Baseline: 2,825,979.99
2,967,278.99
(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,347.10
3,642,814.45
(94.79%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
125,728,428.00
(+0.29%)Baseline: 125,359,310.56
131,627,276.09
(95.52%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
12,002.00
(+1.37%)Baseline: 11,839.80
12,431.79
(96.54%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,930.00
(+2.22%)Baseline: 3,844.55
4,036.77
(97.35%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,073.00
(-0.70%)Baseline: 87,683.00
92,067.15
(94.58%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,022.00
(-0.86%)Baseline: 79,709.81
83,695.30
(94.42%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
50,375.00
(-0.95%)Baseline: 50,858.16
53,401.07
(94.33%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,956.00
(+2.79%)Baseline: 5,794.36
6,084.08
(97.89%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,227.00
(+3.99%)Baseline: 2,141.54
2,248.61
(99.04%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(-0.00%)Baseline: 21,920.18
23,016.19
(95.24%)
🐰 View full continuous benchmarking report in Bencher

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/handlers/notification.rs`:
- Around line 407-417: Multiple concurrent identity-change notifications can
spawn duplicate background tasks calling
client_clone.ensure_e2e_sessions(&[session_jid]); add a per-user dedupe lock
(similar to prekey_upload_lock) keyed by session_jid or user JID to avoid
spawning redundant tasks: before runtime.spawn, acquire or check a short-lived
in-memory Mutex/HashSet entry for that jid to ensure only one detached task is
active for that jid, release the entry when the spawned task completes (or use a
oneshot/future map) so subsequent notifications can spawn a new task; update the
logic around client.clone(), runtime.spawn, ensure_e2e_sessions and detach to
consult this dedupe structure.
- Around line 2079-2114: The test test_identity_change_deletes_primary_session
currently only asserts the session was deleted; add an assertion that the
identity was also removed after handle_notification_impl runs. After computing
backend (let backend = client.persistence_manager.backend()), call the
appropriate signal_cache identity check (e.g.
client.signal_cache.has_identity(&addr, &*backend).await.unwrap() or
client.signal_cache.get_identity(&addr, &*backend).await.unwrap() == None) and
assert the identity is gone, referencing the existing use of
client.signal_cache, handle_notification_impl, and addr/target_jid to locate
where to add this assertion.
- Around line 379-395: The sender key deletions performed via
client.signal_cache.delete_sender_key(...) inside the block that constructs
SenderKeyName for "status@broadcast" are not followed by a persistence flush;
add a call to the existing flush method (e.g. client.flush_signal_cache() or the
project's canonical flush_signal_cache function) immediately after the for-loop
to ensure the deletions are persisted like the preceding
delete_session/delete_identity calls (refer to delete_session, delete_identity
usages and flush_signal_cache patterns in src/message.rs, src/retry.rs,
src/features/signal.rs).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3ca8ab9c-c9f5-4443-b44c-31bfa4743673

📥 Commits

Reviewing files that changed from the base of the PR and between 39d10fd and fbff40e.

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

Comment thread src/handlers/notification.rs Outdated
Comment thread src/handlers/notification.rs
Comment thread src/handlers/notification.rs
jlucaso1 added 2 commits April 6, 2026 11:22
Merge session/identity deletion and status sender key deletion into a
single block with one flush, ensuring the sender key tombstone is
persisted before the handler returns.
@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 34s —— View job


  • Read current PR diff and changed file
  • Review fixes from previous feedback
  • Post updated review

Code Review: PR #490 (re-review after fixes)

Both issues from my previous review have been addressed:

  1. Sender key flush (commit 1b8966c) — the sender key deletion loop is now followed by flush_signal_cache() at line 391, inside the same scoped block as the session/identity deletion. The comment "Single flush covers both" is accurate and efficient.

  2. Identity key assertion in test (commit 4aed404) — test_identity_change_deletes_primary_session now asserts both has_session and get_identity are gone.

No remaining functional concerns. The code is clean and well-tested. LGTM.


View job

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/handlers/notification.rs`:
- Around line 2128-2133: The test is directly mutating Device.pn via
client.persistence_manager.modify_device, which bypasses invariants; instead
construct the appropriate DeviceCommand to set the pn (e.g., a command that
updates the pn field) and send it through PersistenceManager::process_command
(using the same process_command() path), then verify the change by calling
client.persistence_manager.get_device_snapshot() rather than reading the device
directly; replace the modify_device(...) closure call with creating the
DeviceCommand, processing it, and reading the snapshot to exercise the supported
surface.
- Around line 367-389: The code uses a stale device_snapshot when rotating
status@broadcast sender keys which can miss current PN/LID after awaiting;
before building SenderKeyName and calling client.signal_cache.delete_sender_key,
re-fetch or refresh the local device snapshot (the source used to populate
device_snapshot.pn and device_snapshot.lid) so that you iterate over the
up-to-date PN/LID values; specifically, obtain an updated snapshot after the
await boundaries (before the for own_jid in
device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) loop) and use that
fresh list to construct SenderKeyName::new(status_group.to_string(),
own_jid.to_protocol_address().to_string()) and call
client.signal_cache.delete_sender_key(...) to ensure you delete the active
sender keys.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9c8b349e-088d-4fd7-aaaf-4b2859e3049f

📥 Commits

Reviewing files that changed from the base of the PR and between fbff40e and 4aed404.

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

Comment on lines +367 to +389
// Delete primary session + identity so a fresh session can be established,
// and rotate status sender key for forward secrecy (clear_device_record only
// cleared device tracking, not the key itself). Single flush covers both.
{
use wacore::libsignal::store::sender_key_name::SenderKeyName;
use wacore::types::jid::JidExt;

let resolved = client.resolve_encryption_jid(&from_jid).await;
let addr = resolved.to_protocol_address();
client.signal_cache.delete_session(&addr).await;
client.signal_cache.delete_identity(&addr).await;

let status_group = "status@broadcast";
for own_jid in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) {
let sk_name = SenderKeyName::new(
status_group.to_string(),
own_jid.to_protocol_address().to_string(),
);
client
.signal_cache
.delete_sender_key(sk_name.cache_key())
.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.

⚠️ Potential issue | 🟠 Major

Refresh the local device snapshot before rotating status@broadcast sender keys.

device_snapshot is frozen at Line 341 and then reused here after several awaits to build the SenderKeyNames. If our PN/LID changes in that window, this loop deletes the old cache keys and leaves the active sender key intact, so the forward-secrecy rotation silently misses the current identity.

♻️ Proposed fix
-        let status_group = "status@broadcast";
-        for own_jid in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) {
+        let own_device_snapshot = client.persistence_manager.get_device_snapshot().await;
+        let status_group = "status@broadcast";
+        for own_jid in own_device_snapshot
+            .pn
+            .iter()
+            .chain(own_device_snapshot.lid.iter())
+        {
             let sk_name = SenderKeyName::new(
                 status_group.to_string(),
                 own_jid.to_protocol_address().to_string(),
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 367 - 389, The code uses a stale
device_snapshot when rotating status@broadcast sender keys which can miss
current PN/LID after awaiting; before building SenderKeyName and calling
client.signal_cache.delete_sender_key, re-fetch or refresh the local device
snapshot (the source used to populate device_snapshot.pn and
device_snapshot.lid) so that you iterate over the up-to-date PN/LID values;
specifically, obtain an updated snapshot after the await boundaries (before the
for own_jid in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) loop)
and use that fresh list to construct
SenderKeyName::new(status_group.to_string(),
own_jid.to_protocol_address().to_string()) and call
client.signal_cache.delete_sender_key(...) to ensure you delete the active
sender keys.

Comment on lines +2128 to +2133
client
.persistence_manager
.modify_device(|d| {
d.pn = Some(own_jid.clone());
})
.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.

🛠️ Refactor suggestion | 🟠 Major

Avoid direct Device mutation in the new test setup.

This seeds pn through modify_device, which bypasses the same mutation path production code uses to preserve device-state invariants. Please set this up via DeviceCommand/process_command() so the test exercises the supported surface.

As per coding guidelines "Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() and read via get_device_snapshot()".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 2128 - 2133, The test is directly
mutating Device.pn via client.persistence_manager.modify_device, which bypasses
invariants; instead construct the appropriate DeviceCommand to set the pn (e.g.,
a command that updates the pn field) and send it through
PersistenceManager::process_command (using the same process_command() path),
then verify the change by calling
client.persistence_manager.get_device_snapshot() rather than reading the device
directly; replace the modify_device(...) closure call with creating the
DeviceCommand, processing it, and reading the snapshot to exercise the supported
surface.

@jlucaso1
jlucaso1 merged commit c208783 into main Apr 6, 2026
14 checks passed
@jlucaso1
jlucaso1 deleted the fix/complete-identity-change-handler branch April 6, 2026 14:34

@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: 4aed40490c

ℹ️ 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".

Comment on lines +376 to +377
client.signal_cache.delete_session(&addr).await;
client.signal_cache.delete_identity(&addr).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 Acquire session lock before deleting primary session

This new primary-session reset path deletes session and identity directly without taking the per-address session mutex, so it is not serialized with in-flight encrypt/decrypt work that uses session_lock_for(...) (see the existing locked deletion pattern in SignalFeature::delete_sessions). If a send is already using the old session when this handler runs, it can repopulate that stale session after these deletes are flushed, and then ensure_e2e_sessions will treat the session as present and skip prekey re-establishment, effectively undoing the identity-change reset for that user.

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.

1 participant