Skip to content

fix(send): never memoize own devices in the sender-key map (WA Web parity) - #999

Merged
jlucaso1 merged 1 commit into
mainfrom
claude/fix-own-companion-skdm-never-memoize
Jul 7, 2026
Merged

fix(send): never memoize own devices in the sender-key map (WA Web parity)#999
jlucaso1 merged 1 commit into
mainfrom
claude/fix-own-companion-skdm-never-memoize

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What

Stop memoizing our own companion devices in the group sender-key has_key map, mirroring WA Web. Follow-up to #996 (the residual that a device marked has_key=true without an SKDM actually being encrypted for it could be orphaned).

Why (the orphan bug)

Two paths were asymmetric:

  • Warm markupdate_sender_key_devicesset_sender_key_status_for_devices(..., has_key=true, exclude_own_devices=false): marked the full distribution target, including our own companions, even devices whose per-device SKDM encryption failed.
  • Forgetmark_forget_sender_keyset_sender_key_status_for_devices(..., false, exclude_own_devices=true): excludes own devices — a deliberate defense (see cold_mark_excludes_own_devices) so an inbound retry receipt naming our own device can't force us to forget our own sender key and tear down our own group session.

So an own companion whose one SKDM encryption failed (or that was marked warm without a node ever reaching it) is marked warm → never re-targeted, yet can never be un-marked (forget skips own). It stays keyless and can't decrypt our group messages until an unrelated wholesale rotation (participant removal / PN↔LID migration). External devices recover via the retry-receipt mark_forget_sender_key; own companions have no such path.

How WA Web does it (source of truth)

docs/captured-js/WAWeb/Api/ParticipantStore.js — the store helper C(record, deviceList, flag) behind both markHasSenderKey and markForgetSenderKey guards every mutation with if (!isMeDevice(e)). Own devices are never tracked in the sender-key memory at all. The read side (getGroupSenderKeyList) therefore always puts own companions in skDistribList, so they get a fresh SKDM (DSM-wrapped) on every send. That is precisely why WA Web can't orphan its own companions, and why excluding own devices from forget is consistent.

The fix

Exclude own devices from the warm mark too (exclude_own_devices=true), matching WA Web's universal !isMeDevice guard. Own companions are then never memoized, and filter_skdm_targets (which already targets any device that isn't device_and_primary_warm — a never-marked device qualifies) re-distributes their SKDM on every send. No orphaning is possible.

  • External devices: unchanged (full-list warm mark + retry-receipt repair — WA-parity preserved).
  • phash: computed from the resolved participant set, not the has_key marks (build_group_phash_set), so it's unaffected.
  • Cost: a few extra <enc> nodes per send when the account has companions — exactly the trade-off WA Web accepts (guaranteed delivery over a micro-optimization). Zero change for a bot with no companions.

Tests

  • warm_mark_excludes_own_devices (unit) — the warm mark leaves an external member has_key=true and our own companion unmemoized.
  • own_companion_is_never_memoized_so_it_redistributes_every_send (end-to-end) — after a send marks its targets, the DB shows the external member warm and the own companion absent; the next send's filter_skdm_targets re-targets only the own companion.
  • cargo fmt / clippy clean; wacore send suite (96) + whatsapp-rust sender-key/skdm suite (24) pass. Existing failed_device_is_still_marked_has_key (prepare-time full list) unaffected — the full set is still returned; only the persist step excludes own.

…rity)

The warm mark (update_sender_key_devices) recorded has_key=true for our own
companion devices, but the retry/forget path (mark_forget_sender_key) excludes
own devices — a deliberate defense so an inbound retry receipt naming our device
can't tear down our own group session. That asymmetry orphans an own companion
whose single SKDM encryption failed: it's marked warm (so never re-targeted) yet
can never be un-marked (forget skips own), so it can't decrypt our group messages
until an unrelated full rotation.

WA Web's ParticipantStore guards EVERY markHasSenderKey/markForgetSenderKey
mutation with !isMeDevice, so own devices are never memoized at all: they fall
into skDistribList on every send and get a fresh SKDM each time — which is exactly
why WA Web can't orphan its own companions. Mirror that: exclude own devices from
the warm mark too. filter_skdm_targets already re-targets any un-memoized device
(a never-marked companion is never device_and_primary_warm), so own companions now
re-distribute every send. External devices are unchanged (full-list mark + retry
repair); phash is computed from the resolved set, not the marks, so it's unaffected.
@chatgpt-codex-connector

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 323e2448-8ed2-4e94-9900-4814a17a6f82

📥 Commits

Reviewing files that changed from the base of the PR and between 5ba4201 and 9cddc52.

📒 Files selected for processing (3)
  • src/client/sender_keys.rs
  • src/send/mod.rs
  • wacore/src/send/group.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved sender-key handling so your own companion devices are no longer treated as already warmed, preventing them from being skipped incorrectly.
    • External members now stay marked for reuse after successful sharing, reducing unnecessary re-sending.
    • Fixed repeated sender-key distribution for own devices, which helps ensure they receive fresh updates when needed.

Walkthrough

This PR flips the exclude_own_devices flag in update_sender_key_devices's call to set_sender_key_status_for_devices from false to true, updates related documentation, and adds unit/end-to-end tests verifying own companion devices are never memoized as warm sender-key targets.

Changes

Own-Companion Warm Memoization Exclusion

Layer / File(s) Summary
Doc update for persist/filter semantics
wacore/src/send/group.rs
PreparedGroupStanza.skdm_devices doc comment rewritten to describe persisting has_key=true for the full target set and filtering out own devices during persist.
Flip exclusion flag after SKDM send
src/send/mod.rs
update_sender_key_devices now passes true (was false) as the exclude-own-devices argument to set_sender_key_status_for_devices, aligning with the !isMeDevice guard.
Tests for warm exclusion and redistribution
src/client/sender_keys.rs, src/send/mod.rs
New tests confirm external devices get marked warm while own companion devices are never persisted as warm, and that own companions are re-targeted for SKDM on the next send.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

Suggested reviewers: greptile-apps, cubic-dev-ai

Listen, this is a small diff but it touches sender-key correctness, which is core infrastructure — we don't move fast and break encryption. One flag flip, false to true, but it changes who gets memoized as warm. That's exactly the kind of "small change, big blast radius" thing I personally review line by line. Tests look solid — they cover the unit-level status write and the end-to-end redistribution path, which is the right way to validate this. Ship it, but only after someone double-checks the !isMeDevice parity claim against WA Web's actual behavior. No shortcuts on messaging reliability.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: excluding own devices from sender-key memoization for WA Web parity.
Description check ✅ Passed The description matches the code changes and tests, explaining the sender-key warm-mark fix and its motivation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-own-companion-skdm-never-memoize

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.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes the sender-key orphan bug for own companion devices by passing exclude_own_devices=true to update_sender_key_devices, mirroring WA Web's universal !isMeDevice guard on markHasSenderKey. Own companions are now never memoized as warm, so filter_skdm_targets always re-distributes their SKDM — preventing the previously impossible-to-repair state where a companion marked warm (but never actually delivered a key) could never be un-marked by the forget path (which also excludes own devices).

  • Core fix (src/send/mod.rs): single-line change — exclude_own_devices: false → true in update_sender_key_devices. External-device behavior is unchanged; warm memo skips for groups with companions, trading a micro-optimization for guaranteed delivery on every send.
  • Tests (src/client/sender_keys.rs, src/send/mod.rs): two new tests — warm_mark_excludes_own_devices (unit, confirms own companion absent from DB after warm mark) and own_companion_is_never_memoized_so_it_redistributes_every_send (end-to-end, confirms filter_skdm_targets re-targets only the own companion on the next send).

Confidence Score: 5/5

Safe to merge — the change is a single flag flip with a clear, well-documented rationale, matched by two new tests that pin both the DB-level exclusion and the redistribution behavior.

The one-line production change (false to true) is fully symmetric with the pre-existing exclude_own_devices=true on the forget path, eliminating the asymmetry that caused the orphan. Both new tests are structurally sound: the unit test reads back the DB rows directly (no cache involvement), and the end-to-end test exercises the full filter_skdm_targets path with a realistic companion/member mix. The skdm_warm_memo consequence (never fires for groups where the bot has companions) is explicitly documented and matches WA Web behavior. No existing test is broken, no data migration is required, and the change has zero effect on bots without companion devices.

No files require special attention.

Important Files Changed

Filename Overview
src/send/mod.rs One-line fix changing exclude_own_devices from false to true in update_sender_key_devices, plus updated doc comment and a new end-to-end test verifying redistribution every send.
src/client/sender_keys.rs Adds warm_mark_excludes_own_devices unit test; no logic changes to production code in this file.
wacore/src/send/group.rs Doc comment update only — clarifies that skdm_devices own-device filtering now happens at persist time; no production logic change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant S as send_group_stanza
    participant U as update_sender_key_devices
    participant SK as set_sender_key_status_for_devices
    participant DB as DB / sender_key_device_cache
    participant F as filter_skdm_targets (next send)

    S->>U: skdm_devices (own companion + external members)
    U->>SK: "has_key=true, exclude_own_devices=true"
    SK->>SK: filter: drop own-user JIDs
    SK->>DB: "persist only external members as has_key=true"
    SK->>DB: invalidate cache for group
    U->>DB: invalidate cache (second call, idempotent)

    Note over F: Next send — own companion never in DB
    F->>DB: device_and_primary_warm(own_companion) false
    F-->>S: own companion included in needs_skdm, fresh SKDM sent
    F->>DB: device_and_primary_warm(external_member) true
    F-->>S: external member excluded from needs_skdm, no resend
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant S as send_group_stanza
    participant U as update_sender_key_devices
    participant SK as set_sender_key_status_for_devices
    participant DB as DB / sender_key_device_cache
    participant F as filter_skdm_targets (next send)

    S->>U: skdm_devices (own companion + external members)
    U->>SK: "has_key=true, exclude_own_devices=true"
    SK->>SK: filter: drop own-user JIDs
    SK->>DB: "persist only external members as has_key=true"
    SK->>DB: invalidate cache for group
    U->>DB: invalidate cache (second call, idempotent)

    Note over F: Next send — own companion never in DB
    F->>DB: device_and_primary_warm(own_companion) false
    F-->>S: own companion included in needs_skdm, fresh SKDM sent
    F->>DB: device_and_primary_warm(external_member) true
    F-->>S: external member excluded from needs_skdm, no resend
Loading

Reviews (1): Last reviewed commit: "fix(send): never memoize own devices in ..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: This PR changes sender-key memoization to exclude own companion devices, a deliberate change in group encryption distribution. Despite tests and docs, encryption path changes carry risk and require human verification.

Re-trigger cubic

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.81 MiB 10.81 MiB +64 B (+0.00%) 🔺
bin .text 8.81 MiB 8.81 MiB +64 B (+0.00%) 🔺
bin allocated (text+data+bss) 10.80 MiB 10.80 MiB 0
llvm-lines wacore 504,310 504,310 0
llvm-lines wacore copies 17,277 17,277 0
llvm-lines whatsapp-rust lib 757,359 757,359 0
llvm-lines whatsapp-rust lib copies 24,575 24,575 0
deps crates (Cargo.lock) 466 466 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.62 MiB 1.62 MiB +112 B (+0.01%) 🔺
.text wacore 531.46 KiB 531.46 KiB 0
.text wacore_binary 157.70 KiB 157.70 KiB 0
.text wacore_libsignal 178.73 KiB 178.73 KiB 0
.text wacore_appstate 156.45 KiB 156.45 KiB 0
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.60 MiB 1.60 MiB 0
.text whatsapp_rust_sqlite_storage 512.98 KiB 512.98 KiB 0
.text whatsapp_rust_tokio_transport 43.61 KiB 43.61 KiB 0
.text whatsapp_rust_ureq_http_client 10.47 KiB 10.47 KiB 0
.text std 1.00 MiB 1.00 MiB 0
.text other deps 2.95 MiB 2.95 MiB 0

Baseline: 5ba420102 (latest main run) · Head: 19d3d229a · Graphs

@jlucaso1
jlucaso1 merged commit 251c238 into main Jul 7, 2026
19 checks passed
@jlucaso1
jlucaso1 deleted the claude/fix-own-companion-skdm-never-memoize branch July 7, 2026 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant