fix(appstate): bound pairing key-share wait by the 180s critical deadline - #974
Conversation
Reproduces, without any server/mock, the failure PR #972 works around: the critical `critical_unblock_low` snapshot fails to decode with KeyNotFound while the app-state key-share is still in flight, and decodes cleanly the instant the key lands. Proves the failure is purely a key-ordering race, not a bad snapshot.
…line
At fresh pairing the client fetches the critical app-state snapshots
(contacts + push name) as soon as the stream is up, but the app-state
sync-key-share E2E message the primary auto-sends can land later when a
heavy history sync saturates the stream. The old path waited a fixed 5s
for the key, then ran the critical batched IQ; if the key-share arrived
after that window the snapshot failed to decrypt with KeyNotFound
("didn't find app state key") and a "patch snapshot MAC mismatch", the
critical sync failed, and only the 180s watchdog eventually forced a
reconnect — during which saved contacts never synced.
Arm the critical-sync watchdog first so its single 180s deadline bounds
the whole path, then make the key-share wait event-driven up to that same
deadline instead of a fixed 5s. A healthy pairing still wakes instantly
when the key-share is processed; a delayed key-share is tolerated up to
the critical deadline rather than failing at 5s. This mirrors WhatsApp
Web's WAWebSyncBootstrap, whose syncCriticalData is event-driven with a
single 180s critical-data deadline and no fixed pre-wait.
Add a deterministic repro at the processor layer: the same critical
snapshot fails with KeyNotFound while the key is missing and decodes once
the key is present, proving the failure is purely key-share ordering.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughCritical app-state sync now carries an explicit deadline through batching, caller wiring, and fresh-pairing waits. The missing-key path is bounded, and a regression test covers the key-not-found ordering race. ChangesCritical sync deadline plumbing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client as Client::handle_success
participant Notifier as initial_keys_synced_notifier
participant AppState as sync_collections_batched
Client->>Client: arm critical_deadline
Client->>Notifier: listen for initial_app_state_keys_received
Notifier-->>Client: key-share arrives
Client->>AppState: sync_collections_batched(..., Some(critical_deadline))
Client->>AppState: sync_collections_batched(..., None)
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
Per AGENTS.md style — the comments narrated the mechanism; keep only the rationale (single deadline bounding the whole path; why the wait is deadline-bounded rather than a fixed window).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2641c9aa44
ℹ️ 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".
There was a problem hiding this comment.
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/node_io.rs`:
- Around line 956-981: The app-state wait in node_io::NodeIo can miss a
notification because it checks initial_app_state_keys_received before creating
the initial_keys_synced_notifier listener. Update this branch to follow the
request_keys_and_wait pattern by registering the listener first, then checking
the flag and only awaiting if keys are still not received; keep the wait logic
in NodeIo and use initial_keys_synced_notifier.listen() so a notify from
src/message/special.rs cannot be lost in the gap.
🪄 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: 2653ff2d-549b-4d32-bc14-2e6eeb3ad9a9
📒 Files selected for processing (2)
src/client/node_io.rswacore/appstate/src/processor.rs
initial_keys_synced_notifier is a non-sticky event_listener: a key-share processed in the gap between the load() and listen() would lose the wake and burn the full critical deadline even though the keys already landed. Register the listener first (matching request_keys_and_wait) so the wake can't slip through.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/client/node_io.rs (1)
953-969: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winThis load-then-listen ordering can drop the key-share wakeup — and now it costs you 180 seconds, not 5.
Here's the thing: you check
initial_app_state_keys_receivedat Line 953, but you don't create the listener until Line 967 insidert_timeout. The producer insrc/message/special.rsdoes it storesinitial_app_state_keys_receivedtrue and then notifiesinitial_keys_synced_notifier.event_listeneris fire-and-forget — if the key-share lands in the gap between the load andlisten(), no listener is registered yet, so the wakeup is lost.Move fast, but the connection has to actually work. Previously a missed wakeup wasted 5s; with the extended deadline it now stalls the full 180s, and the watchdog at Line 924 will observe an empty
push_nameand force a spuriousreconnect_immediately(). Register the listener first, then check the flag — mirror therequest_keys_and_waitpattern.🔒️ Proposed fix: register the listener before the flag check
+ let key_share_listener = + client_clone.initial_keys_synced_notifier.listen(); if !client_clone .initial_app_state_keys_received .load(Ordering::Relaxed) { // Bounded by the critical deadline, not a fixed 5s window: a late // key-share (under heavy history sync) would otherwise lose the race // and fail the critical snapshot with KeyNotFound. debug!( target: "Client/AppState", "Waiting up to {CRITICAL_SYNC_TIMEOUT_SECS}s for app state keys..." ); let _ = rt_timeout( &*client_clone.runtime, Duration::from_secs(CRITICAL_SYNC_TIMEOUT_SECS), - client_clone.initial_keys_synced_notifier.listen(), + key_share_listener, ) .await; // Check if connection was replaced while waiting check_generation!(); }🤖 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/node_io.rs` around lines 953 - 969, The app-state key wait in the client can miss a wakeup because `initial_app_state_keys_received` is checked before `initial_keys_synced_notifier.listen()` is registered, so a key-share arriving in that gap is lost and the timeout can stretch unnecessarily. Update the `Client/AppState` wait block in `node_io.rs` to follow the same ordering as `request_keys_and_wait`: create/register the listener first, then re-check the `initial_app_state_keys_received` flag and only wait if it is still false. Keep the change localized around the `rt_timeout`/`initial_keys_synced_notifier` path so the wakeup cannot be dropped.
🤖 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.
Duplicate comments:
In `@src/client/node_io.rs`:
- Around line 953-969: The app-state key wait in the client can miss a wakeup
because `initial_app_state_keys_received` is checked before
`initial_keys_synced_notifier.listen()` is registered, so a key-share arriving
in that gap is lost and the timeout can stretch unnecessarily. Update the
`Client/AppState` wait block in `node_io.rs` to follow the same ordering as
`request_keys_and_wait`: create/register the listener first, then re-check the
`initial_app_state_keys_received` flag and only wait if it is still false. Keep
the change localized around the `rt_timeout`/`initial_keys_synced_notifier` path
so the wakeup cannot be dropped.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 33db4fbf-0c51-4b7e-9989-1ed400f0d026
📒 Files selected for processing (1)
src/client/node_io.rs
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Confidence score: 3/5
- In
src/client/node_io.rs, the wait logic aroundinitial_app_state_keys_receivedcan miss an already-delivered key-share signal, causing a full 180s stall and then an unnecessary reconnect even though keys are available; this is a concrete user-facing latency/regression risk—adjust the signaling/check order (or re-check under the same synchronization point) so the wake-up cannot be missed before merging.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Changes modify critical sync timing logic in the app state bootstrap path, including wait duration and listener registration.
Re-trigger cubic
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3d00faee7
ℹ️ 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".
📦 Binary size report
.text per crate
Top movers (cargo-bloat attribution)
Baseline: |
The previous change waited the full 180s critical deadline for the auto-shared key *before* sending the critical IQ. But the explicit AppStateSyncKeyRequest recovery (request_keys_and_wait) can only run after the IQ discovers the snapshot's key id — so blocking the whole deadline on the pre-IQ wait starved that fallback: if the key was never auto-shared (vs. merely late), the IQ never ran, the explicit request never fired, and the watchdog forced a 180s timeout + reconnect instead of recovering on-connection in seconds. Move the deadline where the recovery actually happens: - The pre-IQ wait is now a brief grace (10s) for the auto-shared key — purely an optimization to skip a redundant explicit request in the common fast case. Correctness no longer depends on its length. - request_keys_and_wait takes an explicit timeout; the initial critical bootstrap threads the shared 180s deadline (as an Instant) through sync_collections_batched so a late *or* never-auto-shared key recovers via the explicit request on the same connection, within the deadline. - Non-critical callers (background, groups, ib server_sync) pass None and keep the fixed 10s wait — unchanged. This also fixes the original bug's deeper root cause: the fallback's fixed 10s single-shot window, which the prior fix left unreachable.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
left a comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ca8d53186
ℹ️ 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".
left a comment
•
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Now that the critical bootstrap passes the 180s deadline as the key-wait timeout, the pre-existing lost-wakeup gap became expensive: if the key-share is persisted (and its non-sticky notify fires) between the caller collecting the missing ids and listen() being registered here, the wait would burn the full deadline before the post-wait store check confirms the key is already present. Register the listener first, then re-check the store and return immediately if every requested key is already present — mirroring the subscribe-then-recheck pattern used at the node_io grace wait.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
left a comment
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/client/app_state.rs (2)
615-628: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winClean up the stale doc comment — it contradicts the current contract.
Look, when I'm reading a function's doc comment, I need it to tell me the truth about what the function returns — no exceptions. Lines 615-619 say this helper returns
true"iff the caller should refetch (a request was sent and we waited)", but the second block (620-628) — which matches the actual code below it — saystruemeans every key landed and processing may proceed. These are opposite contracts stacked on the same function. Whoever refactors this next without reading the code closely could easily invert the check.🧹 Proposed fix
- /// Shared missing-key repair step for both sync paths: request the given keys and, - /// only if a fresh request actually went out (the per-key dedup didn't suppress it), - /// wait briefly for the primary to re-share. Returns true iff the caller should - /// refetch (a request was sent and we waited); false means nothing was requested - /// (empty or deduped), so the caller proceeds without stalling. /// Request the missing decode keys, wait briefly for the re-share, then VERIFY they /// actually landed. Returns true only when every requested key is now stored (the🤖 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/app_state.rs` around lines 615 - 628, Update the doc comment on the shared missing-key repair helper in app_state.rs so it matches the actual return contract used by the implementation. Remove the stale “caller should refetch” wording and keep only the description that true means all requested keys were successfully stored and the caller may proceed, while false means the share did not arrive in time and the caller must skip processing. Use the nearby helper’s comment block and its return behavior as the source of truth.
651-660: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueSequential per-key store lookups — fine at current scale.
Each id in
all_sync_keys_presentis awaited one at a time. With the typical small number of missing keys this is a non-issue, but if this list ever grows meaningfully,futures::future::join_allwould cut the wall-clock cost.🤖 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/app_state.rs` around lines 651 - 660, The `all_sync_keys_present` method in `AppState` does sequential `backend.get_sync_key` awaits, which can become slow if the `ids` list grows. Update this check to run the per-key lookups concurrently, using `futures::future::join_all` (or equivalent) over the `get_sync_key` calls, then preserve the existing “missing if any lookup returns none/error” behavior.
🤖 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.
Outside diff comments:
In `@src/client/app_state.rs`:
- Around line 615-628: Update the doc comment on the shared missing-key repair
helper in app_state.rs so it matches the actual return contract used by the
implementation. Remove the stale “caller should refetch” wording and keep only
the description that true means all requested keys were successfully stored and
the caller may proceed, while false means the share did not arrive in time and
the caller must skip processing. Use the nearby helper’s comment block and its
return behavior as the source of truth.
- Around line 651-660: The `all_sync_keys_present` method in `AppState` does
sequential `backend.get_sync_key` awaits, which can become slow if the `ids`
list grows. Update this check to run the per-key lookups concurrently, using
`futures::future::join_all` (or equivalent) over the `get_sync_key` calls, then
preserve the existing “missing if any lookup returns none/error” behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 822f63e8-f788-4031-9f27-991044512e72
📒 Files selected for processing (1)
src/client/app_state.rs
The doc block had two contradictory contracts stacked: an old paragraph saying true means 'the caller should refetch' and the accurate one saying true means every requested key is now stored. Drop the stale paragraph so the doc matches the verify-based return value.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
left a comment
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Modifies core app state sync timing logic, adds deadline-based waiting and recovery for key-share race condition. Touches critical pairing path with non-trivial logic changes.
Re-trigger cubic
left a comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a812ea4ec
ℹ️ 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".
The Err branch intended the 180s watchdog to stay alive and force a reconnect, but returning there dropped critical_sync_timeout_handle — and AbortHandle aborts its task on drop. So a failed critical sync (e.g. the deadline-bound key wait expiring) cancelled the very watchdog meant to recover it, and no reconnect happened. detach() the handle before the early return so the watchdog survives and fires as documented.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
left a comment
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: This is a high-impact refactor of the critical app-state sync path that needs human validation due to its broad impact on recovery, deadlines, and abort handling.
Re-trigger cubic
left a comment
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/client/app_state.rs (1)
624-644: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winLoop until all requested keys are present or the deadline expires. This waits for only one notification, but a share can cover just part of
missing, so a partial or unrelated wake returnsfalsewhile time is still left. That defers the whole sync; re-listen and re-check until every key lands.🤖 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/app_state.rs` around lines 624 - 644, The request_keys_and_wait flow in app_state::request_keys_and_wait only waits on a single notifier wake and then returns based on one re-check, which can miss cases where only part of missing arrives. Change the logic to keep looping on initial_keys_synced_notifier.listen() and re-check all_sync_keys_present until every requested key is present or rt_timeout reaches the deadline, and keep using request_missing_keys_with_dedup for the initial request. Ensure the loop preserves the existing fast-path check and the final timeout behavior while handling partial or unrelated notifications correctly.
🤖 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.
Outside diff comments:
In `@src/client/app_state.rs`:
- Around line 624-644: The request_keys_and_wait flow in
app_state::request_keys_and_wait only waits on a single notifier wake and then
returns based on one re-check, which can miss cases where only part of missing
arrives. Change the logic to keep looping on
initial_keys_synced_notifier.listen() and re-check all_sync_keys_present until
every requested key is present or rt_timeout reaches the deadline, and keep
using request_missing_keys_with_dedup for the initial request. Ensure the loop
preserves the existing fast-path check and the final timeout behavior while
handling partial or unrelated notifications correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 408fd64b-3cfb-4a1d-b6f7-0d4ef3ce9840
📒 Files selected for processing (2)
src/client/app_state.rssrc/client/node_io.rs
…line The wait resolved on a single notifier wake and one re-check. But the notifier is global and a key-share can cover only part of the missing set, so a partial or unrelated wake returned false while deadline budget still remained — deferring the whole critical sync (and forcing a reconnect) instead of waiting for the rest. Loop: re-arm the listener, re-check, and send the explicit request once, until every requested key is present or the deadline truly expires. This actually uses the full deadline the earlier commit intended.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
left a comment
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Core business-logic change to app-state sync and key-share wait/fallout handling, affecting connection recovery; tests pass, but potential impact on reconnect timing warrants human review.
Re-trigger cubic
left a comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 11b22b5216
ℹ️ 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".
The watchdog reconnected only when get_push_name() was empty, using it as a proxy for "critical sync completed". But a business account has its push_name set from business_name during pairing (src/pair.rs) while still needing the initial full sync — so if critical sync then failed, the watchdog saw a non-empty push_name and stood down, leaving the client with neither a dispatched Connected nor a scheduled retry. Replace the proxy with an explicit critical_sync_done flag set on the Ok path. The watchdog now reconnects whenever critical sync didn't actually complete, independent of how push_name got populated.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
left a comment
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Modifies critical app state sync logic including new deadline, reordered key wait, and watchdog changes. Touches core pairing and sync paths; blast radius is high and a bug could break contact syncing for all users.
Re-trigger cubic
Problem
At fresh pairing the client fetches the critical app-state snapshots (contacts + push name) as soon as the stream is up. But the app-state sync-key-share — the E2E message the primary phone auto-sends carrying the key that decrypts those snapshots — can land later when a heavy history sync saturates the stream.
The old path waited a fixed 5s for the key, then ran the critical batched IQ regardless. When the key-share arrived after that window:
KeyNotFound("didn't find app state key") and a "patch snapshot MAC mismatch",The deeper root cause: the on-connection recovery —
request_keys_and_wait, which sends an explicitAppStateSyncKeyRequestand waits for the re-share — used a fixed 10s single-shot window, too short when the stream is saturated. After 10s it gave up and deferred the whole critical sync.Fix
A single 180s critical-sync deadline (matching WhatsApp Web's
WAWebSyncBootstrap) now bounds the entire critical path, and the recovery is placed where it actually runs:critical_deadline(Instant), so every step below is bounded by the same clock.request_keys_and_waitnow takes an explicit timeout; the initial critical bootstrap threads the shared deadline throughsync_collections_batched, so a late or never-auto-shared key recovers via the explicitAppStateSyncKeyRequeston the same connection, any time within 180s — instead of stalling to the watchdog + reconnect.request_keys_and_waitregisters its listener, then re-checks the store and returns immediately if the keys are already present — so a key persisted in the gap beforelisten()(whose non-sticky notify would be lost) can't burn the now-180s timeout.Errthe early return must not cancel the watchdog (it's the reconnect safety net), butAbortHandleaborts its task on drop — so the return was silently killing the watchdog. The handle is nowdetach()ed before returning, so a failed critical sync actually reconnects as intended.server_sync,ibdirty-resync) passNoneand keep the existing fixed 10s wait — unchanged.WhatsApp Web parity
Verified against the captured
WAWebSyncBootstrapbundle:ue = 180,setTimeout(…, ue*1e3)→ the 180s critical-data deadline (ourCRITICAL_SYNC_TIMEOUT_SECS).syncCriticalDataarms the timeout (this.$15()) beforemarkCollectionsForSync([CriticalBlock, CriticalUnblockLow])→ our watchdog-first ordering, same two collections.$16()=SettingPushNameactionSuccess→ ourpush_namewatchdog proxy.HandleMissingKeys.requestAllMissingKeysexplicitly requests the specific missing key ids from snapshot/patch records → ourAppStateSyncKeyRequestfallback.(WA Web's syncd engine is notification-driven and has no pre-IQ grace; our 10s grace is a benign adaptation of that async model to our synchronous IQ path, with the deadline-bounded fallback as the real guarantee. On timeout WA Web
socketLogouts; we reconnect to preserve credentials — an existing, intentional divergence.)Test
Deterministic repro at the
process_snapshotlayer (no server, no mock): the same critical snapshot fails withKeyNotFoundwhile the key is missing and decodes cleanly once the key is present — proving the failure is purely key-share ordering, not snapshot contents or MACs.Validation
cargo fmt --allcleancargo clippy --all --testscleancargo test -p wacore-appstate— 38 passed (incl. the repro)cargo test -p whatsapp-rust --lib— 917 passedReview notes
Thanks to the review bots for four genuine catches, all fixed — the design converged on one deadline with a lost-wakeup-safe wait at every step and a watchdog that actually survives failure:
AbortHandleaborts on drop, so theErrreturn was killing the watchdog; nowdetach()ed.🤖 Generated with Claude Code