# Open problem: the phone never sends the app-state sync key to a newly linked companion Date: 2026-10-28. Client: detz 1.45.0 (`src/whatsapp/`, namespace `detz::wa`), a C++ WhatsApp companion written from scratch after whatsmeow (Go). Device in the last test: `31630045827:54`. ## In one sentence A newly linked companion device logs in and sends fine, but the phone never sends it the app-state sync key. Without that key the address book (collection `critical_unblock_low`) cannot be decrypted, so no contact names arrive. ## What works (confirmed by the user on the phone) - QR pairing, automatic restart (`stream:error 515`), login, upload of 50 prekeys. - Sending an image to the user's own number; it shows in "Message yourself". - Our participant hash equals the server's (`2:TKwG4B1n`), so the device list we send to is correct. ## What does not work Every login runs the contact sync. The log shows: ``` app state critical_unblock_low: missing app state key for the snapshot app state critical_unblock_low: version 0, 0 patches, 0 contacts saved, 0 removed, 0 unnamed app state keys: requesting 1 from the phone app state: no key share arrived; contacts will sync on a later login ``` I then stayed connected for 100 to 120 seconds with a trace of every incoming node (`DETZ_WA_TRACE=1`), after clearing our own 24-hour limit on key requests. The only nodes that arrived were ``, `` and ``. No `` node arrived at all. The database holds 0 app-state keys and one Signal session, with the phone's LID address `173735822786802.0`, created by our own sends. ## What the code does 1. Login succeeds. We request the collection `critical_unblock_low`; the server's reply shows a key is needed. 2. We send a peer message (`category="peer"`, `push_priority="high"`) to the user's own account. It contains a protocol message of type `APP_STATE_SYNC_KEY_REQUEST` and is encrypted to the phone's LID identity. 3. We wait 20 seconds for the phone's `APP_STATE_SYNC_KEY_SHARE`. Nothing arrives. Registration now carries `DeviceProps.historySyncConfig` with whatsmeow's values (added after a first failed attempt; the device was re-linked afterwards). ## Checked against the reference Pairing reply, media upload, key derivation and the key-request bytes match whatsmeow. The app-state crypto (HKDF key expansion, LTHash, MACs) passes against an oracle built from whatsmeow's own packages. The data path is not the problem: the key never arrives, so nothing has been decrypted from real data yet. ## Hypotheses, none confirmed 1. The phone cannot decrypt or accept our key request. The server acknowledges it, but an ack only means the server took it. Related reports: whatsmeow PR 1268 and Volt-Chat PR 5 describe key requests that never leave the process without a Signal session to the phone; WhatsApp Web ensures that session before sending. 2. The phone pushes keys only once, right after pairing, and that happened inside the link session, which I never traced. If messages arrived then and our handler dropped or failed them silently, the keys are lost. Checkable by logging everything during a fresh link. 3. The phone is not answering for another reason: in the background or battery-restricted, WhatsApp not open. Reports also exist of the phone answering with an empty key (openclaw/wacli PR 443). 4. A classification issue like the one in Rich627/whatsapp-claude-plugin PR 45: peer messages from the phone have no `recipient` attribute and were dropped as spoofed. That explains missing keys there, but here no `` arrives at all, so it is not our case yet. ## Questions for an expert - On a fresh link, does the primary phone always push the key share and history to the new companion without being asked? What does it key that on (device props, `platformType`, client version)? - Is a peer key request encrypted to the LID identity of device 0 enough, or must a Signal session with the phone already exist from an earlier exchange? - Does the phone answer key requests from a device that has not yet received the initial history sync? - Is anything in the 2026 pairing flow, such as the `jurisdiction` and `encryption-metadata` nodes seen in `pair-success` for a Dutch number, related to when keys are shared? ## Sources - https://github.com/tulir/whatsmeow/pull/1268 - https://github.com/Volt-Chat/whatsmeow/pull/5 - https://github.com/openclaw/wacli/pull/443 - https://github.com/Rich627/whatsapp-claude-plugin/pull/45 - Research archive: `research/whatsapp-automation/` (whatsmeow-appstate-sync.md, whatsmeow-key-share-and-peer-messages.md, whatsmeow-device-props-history-sync.md) ## Fallback Import the contacts from a `.vcf` file exported on the phone, into the existing contact search. Built: `detz --whatsapp-import contacts.vcf` (see `WaVcf`, docs section 14). ## Resolution (2026-10-27, detz 1.46.0) The request path was verified line by line against whatsmeow main (`send.go`, `message.go`, `appstate.go`, `notification.go`, `client.go`, `pair.go`, `store/clientpayload.go`, fetched 2026-10-27). Registration, login, DeviceProps, the peer node, the key-request bytes and the padding all match the reference. Eight local gaps were fixed anyway (docs section 16 has the table): 24 h rollback on send failure (whatsmeow PR 1268), verified peer send (Volt-Chat PR 5), pkmsg-only device-identity, LID senders accepted plus an arrival log before the filter, empty shares distinguished from silence (openclaw/wacli PR 443), notification/receipt handling, `--whatsapp-sync` (300 s online for a sleeping phone), and the `--whatsapp-import` fallback. `build/whatsapp_test` covers the new behavior; the live answer still needs the phone. Answers to the questions above, from the reference source (not from the phone, which is still unseen): - Fresh link, unsolicited push: yes, the phone pushes the initial history sync and the key shares on its own once it notices the new companion (that is the initial sync `historySyncConfig` declares support for). Re-requests are answered too in a healthy setup (whatsmeow `requestMissingAppStateKeys` sends them on every fetch with missing keys); reports of "just once" come from clients that dropped the first push. - Session before the peer request: yes, required. whatsmeow `encryptMessageForDevice` returns `ErrNoSession` when no session exists and the message never leaves the process. We now verify the session exists after the prekey fetch and say so loudly when it does not. - Requests before the initial history sync: answered in principle (wacli sees even empty shares), but a phone that is asleep or battery-restricted answers nothing until it wakes; that is what `--whatsapp-sync` (300 s) is for. - `jurisdiction` / `encryption-metadata` in `pair-success`: server-side account metadata, ignored by whatsmeow `handlePairSuccess` like any unknown child. Not related to key sharing. Next step if keys still do not arrive: run `detz --whatsapp-sync` with WhatsApp open on the phone and no battery restriction. A 300 s trace with no `` proves the gate is on the phone's side; import the .vcf meanwhile.