From e62317b6f36e8855a3f2afa99a72af4142972dab Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Tue, 25 Aug 2026 17:08:34 -0400 Subject: [PATCH] Reproduce the loss without simulators, and show the fix is not sufficient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- PARITY.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/PARITY.md b/PARITY.md index 818c6c6..ba25675 100644 --- a/PARITY.md +++ b/PARITY.md @@ -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

" +./gradlew -q :interop:peer -Pargs="p2.json create

" +# 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.