From 18499ae746d43236de79fb7b4d51526e877e5b11 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Tue, 25 Aug 2026 16:35:55 -0400 Subject: [PATCH] Narrow the delivery failure to the receive side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The headless peer tool settles the direction. iOS sent to a real shell-driven account and it arrived: testAMessageReachesANonIOSClient passed (39.2s) peer poll -> "hello from iOS to the peer tool", from_user 403 So the send path and the relay are fine, and neither explains two iOS clients failing one way. What differs there is that both sides initiate a session simultaneously; the peer tool never initiates, which is why it works. Suspect is E2EInbox.swift:995. `ingest` returning true means "advance the cursor" — consume the envelope — and false means hold it for the next poll. The retry branch is taken only when `existing == nil`. A recipient that already created its own outgoing session has `existing != nil`, so an envelope it cannot yet decrypt is skipped forever. That is exactly the simultaneous- initiation case. Recorded as a hypothesis with its evidence, not as a fix. It has not been instrumented, and this file's history includes a previous advance-uncondition- ally bug that lost messages silently, so the area deserves care rather than a quick patch. Co-Authored-By: Claude Opus 5 --- PARITY.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) 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.