Narrow the delivery failure to the receive side

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 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-25 16:35:55 -04:00
co-authored by Claude Opus 5
parent 95659f4685
commit 18499ae746
+47
View File
@@ -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.