decryptPartnerAnswer returned a nullable payload, and null meant two opposite
things: the partner hasn't released their key yet (wait — all is well), or the
keybox is sitting right there and this device cannot open it (your key is gone).
The reveal UI rendered WAITING_FOR_PARTNER for both. Someone whose key was lost
was told to keep waiting for something that had already happened — and could
never report the bug accurately, because the app described it wrongly. A
LOST_LOCAL_KEY phase with honest copy has existed the whole time; this path just
never reached it.
Now a typed PartnerAnswerResult: WaitingForPartner (no keybox), Decrypted, or
KeyUnavailable (keybox present, no local key and no usable escrow — or a payload
we hold the key for and still can't open, which is broken, not pending).
Mutation-checked: collapsing KeyUnavailable back into WaitingForPartner fails
exactly the three tests that assert the distinction.
Repaired collateral from my own edit: the replacement over-cut and deleted
releaseOwnKeyForThread / decryptOwnThreadAnswer / decryptPartnerThreadAnswer.
Compile caught it; restored verbatim from git, then diffed the function list
before/after to prove nothing else went missing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>