Reproduce the loss without simulators, and show the fix is not sufficient
Two :interop:peer accounts against relay1, both running the retired-session fix.
Neither polls before sending, which is the simultaneous-initiation case:
p1 send / p2 send -> delivered:1, failed:0 (each on its own chain)
p1 poll / p2 poll -> both first messages arrive
p1 send / p2 send -> delivered:1, failed:0
p1 poll / p2 poll -> {"messages":[]} / {"messages":[]}
The second exchange is lost in both directions, silently, with the relay
reporting success. Same failure as the two-simulator run with every iOS variable
removed, and it reproduces in about two minutes instead of forty.
So retaining the retired session is necessary and not sufficient. The receive
side can now open the old chain; the loop still does not close. The remaining
gap looks to be on the send side — after adopting the peer's chain each client
encrypts on the adopted one while the peer still reads the one it kept, so
neither is listening where the other is talking. Resolving this properly means
both sides converging on the SAME chain by a deterministic tie-break, not each
adopting the other's.
Recorded so the earlier commit is not read as having closed it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
999ab2bd26
commit
e62317b6f3
@@ -461,3 +461,44 @@ section and a tab bar.
|
||||
A DM run takes 20-40 minutes per side. The group phase has never completed
|
||||
inside a sitting. **Budget hours, not minutes** — or give the pair a trimmed
|
||||
screen to work against before running it.
|
||||
|
||||
### 🔴 REPRODUCED CHEAPLY — and the retired-session fix does NOT close it
|
||||
|
||||
2026-08-25, two `:interop:peer` accounts against `relay1`, no simulators, no UI.
|
||||
Both had the retired-session fix compiled in.
|
||||
|
||||
```
|
||||
p1 send p2 first-from-P1 {"delivered":1,"failed":0,"to":408}
|
||||
p2 send p1 first-from-P2 {"delivered":1,"failed":0,"to":407} # neither had polled
|
||||
p1 poll {"messages":[{"text":"first-from-P2",...}]} ✅
|
||||
p2 poll {"messages":[{"text":"first-from-P1",...}]} ✅
|
||||
p1 send p2 second-from-P1 {"delivered":1,"failed":0,"to":408}
|
||||
p2 send p1 second-from-P2 {"delivered":1,"failed":0,"to":407}
|
||||
p1 poll {"messages":[]} ❌
|
||||
p2 poll {"messages":[]} ❌
|
||||
```
|
||||
|
||||
**First messages cross. The second exchange is lost in both directions, silently,
|
||||
with the relay reporting `delivered:1, failed:0`.**
|
||||
|
||||
This is the same failure seen between two simulators, with every iOS variable
|
||||
removed — two Kotlin clients and a live relay. **Reproduce it in about two
|
||||
minutes** rather than the 20-40 the simulator pair costs:
|
||||
|
||||
```
|
||||
./gradlew -q :interop:peer -Pargs="p1.json create <relay> <h1>"
|
||||
./gradlew -q :interop:peer -Pargs="p2.json create <relay> <h2>"
|
||||
# add both ways, then send both ways WITHOUT polling, then poll, then send again
|
||||
```
|
||||
|
||||
⚠️ **Retaining the retired session is necessary but NOT sufficient**, which the
|
||||
commit that added it does not say clearly enough. The receive side can now open
|
||||
the old chain; what this shows is that the loop still does not close. The
|
||||
likely remaining gap is on the SEND side — after adopting the peer's chain,
|
||||
each client encrypts on the adopted one while the peer is still reading the one
|
||||
it kept, so neither is listening where the other is talking. Resolving
|
||||
simultaneous initiation properly means both sides converging on the SAME chain
|
||||
by a deterministic rule (Signal breaks the tie on the identity keys), not each
|
||||
adopting the other's.
|
||||
|
||||
⚠️ Do not treat the ratchet-overwrite commit as having fixed this.
|
||||
|
||||
Reference in New Issue
Block a user