The same data loss as the iOS side, in the other direction. When a
contact reinstalls, every message they send before this phone accepts
their new keys was destroyed: the poll advances its watermark whatever
happens, so the relay marked them delivered and forgot them, and the
catch in ingest swallowed the refusal and moved on. Tapping "Accept new
keys" brought nothing back. The sender saw delivered messages and this
side saw an empty thread under a security banner.
The identity gate is right and stays. The discard is what changes.
A refused envelope is now set aside as it arrived, still ciphertext, in
the sealed vault so it survives the app being closed -- which is the
ordinary way this happens. Bounded at a hundred per sender and thirty
days, expired on the way in so it needs no timer that a rarely-opened
app would never run. Blocking or removing a contact drops their hold
rather than keeping a removed contact's messages.
Two extractions were needed to make a replay possible, and both are
worth having on their own. The per-envelope work in the poll loop is
now `consume`, so a held envelope can go through exactly the path a
fresh one does; the replay calls it directly rather than through the
de-duplicating check at the top of the loop, which has already seen
every one of those envelope ids and would deliver nothing while
reporting success. And everything `receive` does with a batch of
decrypted messages is now `absorb`, because a replay that stopped at
the decrypt would write nothing and show nothing, which is this same
bug one layer further in.
The banner says how many are waiting, and accepting re-reads the thread
it just filled rather than only re-badging the trust state.
engine 663, client 464, app 582 across both variants, all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>