feat: unknown device detection and deferred device sync - #480
Conversation
WA Web checks `isFromKnownDevice(author)` after decryption and rejects messages from devices not in the local device list, triggering a usync query to learn about new devices. We were missing this entirely, causing unrecoverable messages from users who added new devices while we were offline. - Add `UnknownCompanionNoPrekey` retry reason (code 11) matching WA Web - Add `offline` field to `MessageInfo` for online/offline dispatch - Add `is_from_known_device()` check in `process_group_enc_batch` - Add `PendingDeviceSync` to batch offline unknown-device users - Flush pending device sync after offline delivery completes - Use correct retry reason in `NoSenderKeyState` error arm
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds a pending-device sync system: unknown-device reports during offline sync are queued and batch-resolved after offline completion; message decryption paths detect unknown companion devices and trigger device-list fetches; retry reasons adjusted to include UnknownCompanionNoPrekey for early key inclusion; MessageInfo gains an is_offline flag. Changes
Sequence DiagramsequenceDiagram
participant Handler as <ib> Offline Handler
participant Client
participant MsgProc as Message Processor
participant Pending as PendingDeviceSync
participant UserSync as User Sync / get_user_devices
participant Registry as Device Registry
Handler->>Client: complete_offline_sync(count)
Handler->>Client: (spawn) flush_pending_device_sync() after 2s
rect rgba(100, 150, 200, 0.5)
MsgProc->>Registry: attempt decrypt / determine sender state
Registry-->>MsgProc: Success / NoSenderKeyState
MsgProc->>Client: is_from_known_device(sender)?
Client-->>MsgProc: false (unknown) / true (known)
alt unknown
MsgProc->>Client: handle_unknown_device_sync(info)
alt info.is_offline
Client->>Pending: add(sender_jid)
else
Client->>UserSync: invalidate + get_user_devices([user])
end
else known
MsgProc->>MsgProc: proceed to handle plaintext
end
end
Note over Pending,UserSync: Later (flush)
Client->>Pending: take_all()
Pending-->>Client: Vec<Jid>
Client->>UserSync: get_user_devices(&pending)
UserSync-->>Client: device lists
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@claude review |
|
Claude finished @jlucaso1's task in 1m 25s —— View job Code Review: PR #480 — Unknown Device Detection and Deferred Device Sync
Overall this is a well-structured PR that correctly mirrors WA Web's Bug: Successfully decrypted messages are silently dropped for unknown devices
WA Web's This is the most important issue in the PR. Consider processing the decrypted plaintext (call Ok(padded_plaintext) => {
// WA Web: isFromKnownDevice() in preProcessMsg
if !self.is_from_known_device(&info.source.sender).await {
warn!("[msg:{}] Unknown device {}, triggering device sync",
info.id, info.source.sender);
self.handle_unknown_device_sync(info).await;
// Don't send retry or skip — process the message normally
}
if let Err(e) = self.clone()
.handle_decrypted_plaintext("skmsg", &padded_plaintext, padding_version, info)
.await
{
log::warn!("Failed processing group plaintext (batch): {e:?}");
}
}Nit:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c757bb75e
ℹ️ 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".
| let client = Arc::clone(self); | ||
| self.runtime | ||
| .spawn(Box::pin(async move { | ||
| if let Err(e) = client.get_user_devices(&[user_jid]).await { |
There was a problem hiding this comment.
Force-refresh device list for unknown-device recovery
Calling get_user_devices here does not guarantee a server sync when the sender already has a cached/DB device record, because get_user_devices returns registry hits without fetching from network (src/usync.rs, early get_devices_from_registry fast-path). In the exact unknown-device scenario this change targets (known user adds a new companion), the stale record usually exists and this call becomes a no-op, so the new device is never learned and subsequent messages keep failing with retries.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/ib.rs`:
- Around line 164-170: The detached task running
client_clone.flush_pending_device_sync() can run before queued senders finish;
change the flow to wait for the offline delivery barrier first (use
wait_for_offline_delivery_end() where the offline marker is handled) and then
run flush_pending_device_sync() so it executes after the barrier—either call
client.flush_pending_device_sync().await directly or spawn it and await the
JoinHandle instead of .detach(); keep using Arc::clone(&client) and
client.runtime.spawn if you must offload, but ensure you await completion so
pending senders aren’t dropped by reconnect cleanup.
In `@src/usync.rs`:
- Around line 203-213: The code drains the pending queue with take_all() before
calling self.get_user_devices(&pending).await, so on Err(e) those pending users
are lost; change the error branch to reinsert the drained items back into the
pending queue (use the same queue that provided take_all(), e.g., call its
push_all/push or equivalent) and preserve their order so a later flush can
retry; specifically, in the match around self.get_user_devices(&pending).await,
on Err(e) call the queue re-enqueue method to requeue the local pending variable
(only on error) before logging the warning.
In `@wacore/src/protocol/retry.rs`:
- Around line 100-102: The early-key-inclusion condition in src/retry.rs must
match the helper logic: update the computation that currently uses reason ==
RetryReason::NoSession to also consider RetryReason::UnknownCompanionNoPrekey so
unknown-companion retries include keys on retry `#1`; specifically modify the
include_keys_early (or equivalent) boolean in the retry path to use the same
combined condition (reason == RetryReason::NoSession || reason ==
RetryReason::UnknownCompanionNoPrekey) and keep the existing retry_count >=
MIN_RETRY_COUNT_FOR_KEYS logic so the function/method that computes whether to
include keys (referenced as include_keys_early, RetryReason::NoSession,
RetryReason::UnknownCompanionNoPrekey, and MIN_RETRY_COUNT_FOR_KEYS) behaves
identically to the helper change.
In `@wacore/src/types/message.rs`:
- Around line 149-150: The field `offline` in the message struct is currently an
Option<bool> but is only ever set to Some(true) or None and callers use
.is_some(); change the field to a two-state representation (prefer `is_offline:
bool` or a small enum like `DeliveryState::{Online, Offline}`) to remove
unreachable `Some(false)` and clarify intent, then update all
constructors/fallbacks that set `offline` to use the new default (false or
Online) and replace all `.offline.is_some()` checks with the new boolean or enum
pattern matches; ensure the struct definition in message.rs and every usage site
(constructors, deserializers, and conditionals) are updated accordingly.
🪄 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: c78f7796-d9b6-42a8-be23-e72be1f2d93e
📒 Files selected for processing (11)
src/client.rssrc/client/device_registry.rssrc/handlers/ib.rssrc/lib.rssrc/message.rssrc/pdo.rssrc/pending_device_sync.rssrc/usync.rswacore/src/messages.rswacore/src/protocol/retry.rswacore/src/types/message.rs
| let client_clone = Arc::clone(&client); | ||
| client | ||
| .runtime | ||
| .spawn(Box::pin(async move { | ||
| client_clone.flush_pending_device_sync().await; | ||
| })) | ||
| .detach(); |
There was a problem hiding this comment.
Wait for the offline pipeline to drain before flushing.
This is fired off the terminal <offline> marker, but a few lines above the file already treats “offline delivery ended” as a separate barrier via wait_for_offline_delivery_end(). Running flush_pending_device_sync() in a detached task here can miss senders that are still queued for message processing, and reconnect cleanup can clear the pending set before the task runs.
Suggested fix
debug!(target: "Client/OfflineSync", "Offline sync completed, received {} items", count);
client.complete_offline_sync(count);
-
- let client_clone = Arc::clone(&client);
- client
- .runtime
- .spawn(Box::pin(async move {
- client_clone.flush_pending_device_sync().await;
- }))
- .detach();
+ client.wait_for_offline_delivery_end().await;
+ if !client.is_shutting_down() {
+ client.flush_pending_device_sync().await;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/handlers/ib.rs` around lines 164 - 170, The detached task running
client_clone.flush_pending_device_sync() can run before queued senders finish;
change the flow to wait for the offline delivery barrier first (use
wait_for_offline_delivery_end() where the offline marker is handled) and then
run flush_pending_device_sync() so it executes after the barrier—either call
client.flush_pending_device_sync().await directly or spawn it and await the
JoinHandle instead of .detach(); keep using Arc::clone(&client) and
client.runtime.spawn if you must offload, but ensure you await completion so
pending senders aren’t dropped by reconnect cleanup.
| /// Stanza `offline` attribute. `Some` = offline delivery, `None` = online. | ||
| pub offline: Option<bool>, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether `MessageInfo.offline` is actually used as a tri-state anywhere.
rg -n --type rust '\boffline\s*:\s*(Some\(true\)|Some\(false\)|None)' .
rg -n --type rust '\.offline\b' .Repository: jlucaso1/whatsapp-rust
Length of output: 216
Model offline as a two-state field instead of Option<bool>.
Current usage only creates Some(true) or None and checks presence via .is_some(); the Some(false) state is unreachable dead code. Use bool is_offline or a small enum to clarify the contract and remove offline: None boilerplate from fallback constructors.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/types/message.rs` around lines 149 - 150, The field `offline` in
the message struct is currently an Option<bool> but is only ever set to
Some(true) or None and callers use .is_some(); change the field to a two-state
representation (prefer `is_offline: bool` or a small enum like
`DeliveryState::{Online, Offline}`) to remove unreachable `Some(false)` and
clarify intent, then update all constructors/fallbacks that set `offline` to use
the new default (false or Online) and replace all `.offline.is_some()` checks
with the new boolean or enum pattern matches; ensure the struct definition in
message.rs and every usage site (constructors, deserializers, and conditionals)
are updated accordingly.
- P1: invalidate stale device cache before usync query so new devices are actually fetched from network (not served from stale registry) - Fix include_keys_early in src/retry.rs send path to also cover UnknownCompanionNoPrekey (was only checking NoSession); deduplicate by calling wacore helper instead of inlining logic - Re-enqueue pending users on flush failure so they survive transient network errors - Add 2s delay before flush matching WA Web OFFLINE_DEVICE_SYNC_DELAY - Change offline: Option<bool> → is_offline: bool (simpler, no unreachable Some(false) state)
|
@claude review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 560be4e3dc
ℹ️ 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".
| .runtime | ||
| .sleep(std::time::Duration::from_secs(2)) | ||
| .await; | ||
| client_clone.flush_pending_device_sync().await; | ||
| })) |
There was a problem hiding this comment.
Flush pending device sync after offline queue fully settles
This schedules exactly one flush_pending_device_sync() call 2 seconds after the offline end marker, but unknown-device entries are enqueued from handle_unknown_device_sync while offline messages are still being processed. Under large offline backlogs, message handlers can continue adding users after this one-shot flush runs, leaving those users stuck in PendingDeviceSync with no later trigger to flush them, so device lists never refresh and retries can keep failing for those senders.
Useful? React with 👍 / 👎.
|
Claude finished @jlucaso1's task in 1m 45s —— View job Code Review: PR #480 — Unknown Device Detection and Deferred Device Sync
Well-structured PR that correctly mirrors WA Web's Bug: Successfully decrypted messages are dropped for unknown devices
WA Web's This is the most critical issue. The fix is to process the decrypted plaintext normally and only trigger the device sync as a side effect: Ok(padded_plaintext) => {
// WA Web: isFromKnownDevice() in preProcessMsg
if !self.is_from_known_device(&info.source.sender).await {
warn!(
"[msg:{}] Unknown device {}, triggering device sync",
info.id, info.source.sender
);
self.handle_unknown_device_sync(info).await;
// Don't send retry or skip — process the message normally
}
if let Err(e) = self
.clone()
.handle_decrypted_plaintext("skmsg", &padded_plaintext, padding_version, info)
.await
{
log::warn!("Failed processing group plaintext (batch): {e:?}");
}
}Race: Detached flush can miss late-queued senders
Options:
No dedup for immediate (online) device syncs
Nit:
|
- Dedup online device syncs via pending_device_sync set (prevents redundant usync queries when burst of messages from same unknown device) - Use wacore::protocol::retry::should_include_keys() in send path instead of inlining the logic (removes duplication) - Remove unused MIN_RETRY_COUNT_FOR_KEYS constant from src/retry.rs
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/handlers/ib.rs (1)
164-175:⚠️ Potential issue | 🟠 MajorDetached delayed flush can lose pending sync work during reconnect/shutdown.
This fire-and-forget task sleeps 2s and flushes later, but
pending_device_syncis in-memory and can be cleared by connection cleanup before the detached task runs, causing permanent loss of queued users.💡 Proposed fix
- let client_clone = Arc::clone(&client); - client - .runtime - .spawn(Box::pin(async move { - // WA Web: OFFLINE_DEVICE_SYNC_DELAY = 2000ms - client_clone - .runtime - .sleep(std::time::Duration::from_secs(2)) - .await; - client_clone.flush_pending_device_sync().await; - })) - .detach(); + client.wait_for_offline_delivery_end().await; + if !client.is_shutting_down() { + // WA Web: OFFLINE_DEVICE_SYNC_DELAY = 2000ms + client + .runtime + .sleep(std::time::Duration::from_secs(2)) + .await; + if !client.is_shutting_down() { + client.flush_pending_device_sync().await; + } + }#!/bin/bash # Verify ordering/race-sensitive call sites around offline completion and pending queue lifecycle. rg -n -C3 'complete_offline_sync|wait_for_offline_delivery_end|flush_pending_device_sync|pending_device_sync\.clear|\.detach\(' src/handlers/ib.rs src/usync.rs src/client.rs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 164 - 175, The detached sleep+flush task (created via client.runtime.spawn(...).detach()) can run after connection cleanup and lose in-memory pending_device_sync; replace the fire-and-forget detach with a cancellation-aware approach: spawn the delayed task with a JoinHandle tied to the client's lifecycle (do not call .detach()), or capture a Weak reference to the client (instead of Arc::clone) and early-return if it has been dropped, and ensure the client's shutdown/reconnect path awaits or aborts the JoinHandle so flush_pending_device_sync is executed or cancelled deterministically; locate the spawn/detach call, runtime.sleep, flush_pending_device_sync, and pending_device_sync to implement this lifecycle-aware fix.
🤖 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/message.rs`:
- Around line 1117-1133: The code currently derives user_jid via
info.source.sender.to_non_ad(), which preserves `@lid` and causes LID senders to
never refresh companion device lists; change the query JID derivation to prefer
info.source.sender_alt when present and fall back to
info.source.sender.to_non_ad() otherwise, then use that normalized JID for
pending_device_sync.add(), invalidate_device_cache(&query_jid.user), and
get_user_devices(&[query_jid]) so LID→PN normalization matches other call sites
(see functions/methods: pending_device_sync.add, invalidate_device_cache,
get_user_devices, info.source.sender_alt, info.source.sender.to_non_ad()).
In `@src/retry.rs`:
- Around line 726-728: The code duplicates the include-keys logic; replace the
local condition around include_keys_early and the keys_node guard (which uses
reason == RetryReason::NoSession || reason ==
RetryReason::UnknownCompanionNoPrekey and retry_count >=
MIN_RETRY_COUNT_FOR_KEYS) with a call to the shared helper
wacore::protocol::retry::should_include_keys, passing the current reason and
retry_count (remove include_keys_early and the duplicated constants), so the
decision to include keys is delegated to should_include_keys(reason,
retry_count) and both tests and production use the same logic.
---
Duplicate comments:
In `@src/handlers/ib.rs`:
- Around line 164-175: The detached sleep+flush task (created via
client.runtime.spawn(...).detach()) can run after connection cleanup and lose
in-memory pending_device_sync; replace the fire-and-forget detach with a
cancellation-aware approach: spawn the delayed task with a JoinHandle tied to
the client's lifecycle (do not call .detach()), or capture a Weak reference to
the client (instead of Arc::clone) and early-return if it has been dropped, and
ensure the client's shutdown/reconnect path awaits or aborts the JoinHandle so
flush_pending_device_sync is executed or cancelled deterministically; locate the
spawn/detach call, runtime.sleep, flush_pending_device_sync, and
pending_device_sync to implement this lifecycle-aware fix.
🪄 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: 27277a60-fd37-4d65-8455-d4ad1214965f
📒 Files selected for processing (7)
src/handlers/ib.rssrc/message.rssrc/pdo.rssrc/retry.rssrc/usync.rswacore/src/messages.rswacore/src/types/message.rs
| let user_jid = info.source.sender.to_non_ad(); | ||
|
|
||
| if info.is_offline { | ||
| log::debug!("Queueing {} for pending device sync (offline)", user_jid); | ||
| self.pending_device_sync.add(user_jid).await; | ||
| } else { | ||
| log::debug!("Triggering immediate device sync for {}", user_jid); | ||
| let client = Arc::clone(self); | ||
| self.runtime | ||
| .spawn(Box::pin(async move { | ||
| // Invalidate stale record so get_user_devices hits the network | ||
| client.invalidate_device_cache(&user_jid.user).await; | ||
| if let Err(e) = client.get_user_devices(&[user_jid]).await { | ||
| log::warn!("Immediate device sync failed: {e:?}"); | ||
| } | ||
| })) | ||
| .detach(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== handle_unknown_device_sync =="
sed -n '1115,1135p' src/message.rs
echo
echo "== get_user_devices / flush_pending_device_sync definitions =="
rg -nP --type rust -C8 '^\s*(pub(?:\([^)]*\))?\s+)?async\s+fn\s+(get_user_devices|flush_pending_device_sync)\b'
echo
echo "== LID->PN normalization around device queries =="
rg -n --type rust -C4 'get_phone_number|phone_jid_for_lid_user|sender_alt|is_lid\(|to_non_ad\(|pending_device_sync'Repository: jlucaso1/whatsapp-rust
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== handle_unknown_device_sync function signature and context ==="
sed -n '1100,1140p' src/message.rs | head -50
echo
echo "=== Client fields and lid_pn_cache access ==="
rg -n 'struct Client|pub.*lid_pn_cache' src/client.rs | head -20
echo
echo "=== Check if sender_alt is available in handle_unknown_device_sync (MessageInfo struct) ==="
rg -n 'pub struct MessageInfo|pub sender_alt' --type rust wacore/ | grep -A2 MessageInfo
echo
echo "=== Examples of sender_alt usage for LID→PN conversion elsewhere ==="
rg -n 'sender_alt' src/message.rs | head -10Repository: jlucaso1/whatsapp-rust
Length of output: 2628
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== MessageInfo struct with sender_alt field ==="
sed -n '132,150p' wacore/src/types/message.rs
echo
echo "=== Verify sender_alt is populated in group messages with LID senders ==="
sed -n '170,195p' wacore/src/messages.rs
echo
echo "=== cache_lid_pn_from_message implementation ==="
rg -n 'cache_lid_pn_from_message' src/message.rs -A 10 | head -20Repository: jlucaso1/whatsapp-rust
Length of output: 2736
Use sender_alt to derive the device-query JID for LID senders.
info.source.sender.to_non_ad() preserves @lid identifiers, and this value is passed directly to both pending_device_sync.add() and get_user_devices(). For group messages with LID-mode addressing, info.source.sender_alt contains the corresponding phone-number JID and is reliably populated. Without using sender_alt here, LID senders will continuously refresh against their cached @lid identity and never update companion device lists.
Derive the query JID as:
- If
sender_alt.is_some(), prefer it (the alternate form for device queries) - Otherwise, fall back to
sender.to_non_ad()(for non-group or PN-mode groups)
This matches the pattern used elsewhere in the codebase (e.g., wacore/src/send.rs:1010–1018) where LID→PN normalization is the caller's responsibility before issuing device queries.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/message.rs` around lines 1117 - 1133, The code currently derives user_jid
via info.source.sender.to_non_ad(), which preserves `@lid` and causes LID senders
to never refresh companion device lists; change the query JID derivation to
prefer info.source.sender_alt when present and fall back to
info.source.sender.to_non_ad() otherwise, then use that normalized JID for
pending_device_sync.add(), invalidate_device_cache(&query_jid.user), and
get_user_devices(&[query_jid]) so LID→PN normalization matches other call sites
(see functions/methods: pending_device_sync.add, invalidate_device_cache,
get_user_devices, info.source.sender_alt, info.source.sender.to_non_ad()).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75b32fda0e
ℹ️ 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".
| if !self.pending_device_sync.add(user_jid.clone()).await { | ||
| return; |
There was a problem hiding this comment.
Clear pending flag after online sync attempts
handle_unknown_device_sync inserts every sender into pending_device_sync before branching on info.is_offline, but the online path never removes that user after the spawned immediate usync finishes (success or failure). In an online session, if that first usync fails transiently, subsequent unknown-device messages for the same user hit this early return and skip all further sync attempts; and because flush_pending_device_sync() is only triggered by the IB offline-end flow, recovery can remain stuck until a future reconnect/offline cycle.
Useful? React with 👍 / 👎.
Summary
isFromKnownDevice()check after group message decryptionUnknownCompanionNoPrekey(code 11) instead ofNoSession(code 1) for unknown devicesPendingDeviceSync, flush with a single usync query after offline delivery completes (with 2s delay matching WA Web'sOFFLINE_DEVICE_SYNC_DELAY)offlineattribute from message stanzas to distinguish online vs offline dispatchContext
When receiving group messages (
skmsg) from a device not in our device list (e.g., sender added device:10while we were offline and we only know:8/:9), we were:NoSenderKeyStateerrorNoSession = 1)WA Web handles this differently (
WAWebHandleMsgProcessUtils.preProcessMsg):isFromKnownDevice(author)after successful decryptionsyncDeviceListJob(usync query)OfflinePendingDeviceCache, flushes viadoPendingDeviceSync()after offline delivery ends (2s delay)UnknownCompanionNoPrekey = 11This was observed causing 13+ unrecoverable messages per offline sync from a single user who added a new device.
Test plan
cargo fmt --allpassescargo clippy --all --testscleanUnknownCompanionNoPrekeyinstead ofNoSessionfor unknown devicesSummary by CodeRabbit
New Features
Improvements