diff --git a/PARITY.md b/PARITY.md index b629064..329da7b 100644 --- a/PARITY.md +++ b/PARITY.md @@ -384,3 +384,50 @@ successful create would have rendered a row. It did not — the create threw. ⚠️ Both concern sending **to** the second account. Suggestive, not a diagnosis. The next run needs the thread dump (added) plus the relay's own view of what is queued for that device. + +### Narrowed 2026-08-25: it is the RECEIVE side, and only when both sides opened first + +The headless peer tool settles the direction. `qz89925` (user 402), a real +account driven from a shell, was sent to by the iOS app: + +``` +CrossPlatformUITests/testAMessageReachesANonIOSClient passed (39.2s) +peer poll → {"messages":[{"text":"","from_user":403}, + {"text":"hello from iOS to the peer tool","from_user":403}]} +``` + +✅ **iOS → non-iOS delivery works.** The sender encrypts correctly and the relay +delivers. (The empty first message is the §14.4 delivery-key prelude, as +designed.) So neither the send path nor the relay explains the two-simulator +failure. + +**What differs in the two-simulator case is that both sides initiate a session +with each other at the same time** — each signs up, opens the chat, and sends +before either has received anything. The peer tool never initiates, which is +exactly why it works. + +🔴 **Suspect: `E2EInbox.swift:995`.** + +```swift +// Transient: a non-prekey message arrived before the establishing +// prekey created the session. Do NOT advance -- retry on the next poll. +guard let r = ratchet else { return existing == nil ? false : true } +``` + +`ingest` returning **true means "advance the cursor"** — the envelope is +consumed. `false` holds the cursor so the next poll retries it. The condition +returns `false` (retry) only when `existing == nil`. A recipient that has +already created its OWN outgoing session to that device has `existing != nil`, +so an envelope it cannot yet decrypt returns **true and is skipped forever**. + +That is precisely the simultaneous-initiation case, and it matches every +observation: the peer tool (`existing == nil`) receives fine; a second iOS +client that sent first does not. The comment above the line describes holding +the cursor, which is what the `false` branch does — the `existing != nil` branch +does the opposite. + +⚠️ **Hypothesis, not a confirmed diagnosis.** It has not been instrumented, and +the neighbouring code deliberately archives retired sessions, so there may be an +intended reason for the asymmetry. Verify before changing: the same file's +history shows a previous "advance unconditionally" bug that caused silent +message loss, so this area has form.