7.0 KiB
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
<success>, <ib edge_routing> and <ib dirty type=account_sync>. No <message> 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
- Login succeeds. We request the collection
critical_unblock_low; the server's reply shows a key is needed. - We send a peer message (
category="peer",push_priority="high") to the user's own account. It contains a protocol message of typeAPP_STATE_SYNC_KEY_REQUESTand is encrypted to the phone's LID identity. - 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
- 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.
- 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.
- 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).
- A classification issue like the one in Rich627/whatsapp-claude-plugin PR 45: peer messages from the phone
have no
recipientattribute and were dropped as spoofed. That explains missing keys there, but here no<message>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
jurisdictionandencryption-metadatanodes seen inpair-successfor 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
historySyncConfigdeclares support for). Re-requests are answered too in a healthy setup (whatsmeowrequestMissingAppStateKeyssends 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
encryptMessageForDevicereturnsErrNoSessionwhen 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-metadatainpair-success: server-side account metadata, ignored by whatsmeowhandlePairSuccesslike 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 <message> proves the gate is on the phone's side; import the .vcf meanwhile.