mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
Tier B's second finding (interop plan §7.1). A sender's own stream entries
came back as `undecryptable`: its Commit, sealed under the pre-commit epoch
key it has since left, and its message, from a ratchet generation already
consumed.
The record of what is ours lived in memory while the cursor beside it was
persisted. That asymmetry is not obvious until something crosses a process
boundary, because posting deliberately does **not** advance the cursor — a
lower cursor may still hold somebody else's unprocessed message — so the next
run is guaranteed to re-read what this one posted, and needs to be told it is
its own.
`EchoState` is that record, saved beside `GroupCursor` on the same beat and
deleted when there is nothing pending. Own-message cursors at or below the
fetch cursor are pruned, because the stream has delivered them and they can
never come round again. Pending Commits are kept until their echo matches,
however long that takes: `Ingestion.SelfEchoUnapplied` is how a client that
posted a Commit and died before adopting it applies its own Commit, and that
echo is the only copy it will ever be offered.
`amy` exposed this on every run because each verb is a process, but it is not
an `amy` bug. The Android app hits it whenever the OS kills it between sending
and syncing, which is the ordinary case rather than a corner. Two levels of
damage, and the quieter one is worse: a visible gap in the sender's own
conversation, and a recovery path that depended on exactly the record that
dying destroyed.
**The tests that let it through.** `a group survives a restart` already
claimed to cover this, with `assertTrue(delivered.none { it is
Delivery.Message })`. An `Undecryptable` is not a `Message`, so a gap in your
own conversation satisfied the assertion. The lesson is narrow and reusable:
an assertion about what a delivery is *not* passes for every outcome nobody
thought of. Both restart tests now assert what it **is** — `Echo` — and both
fail if either half of the persistence is removed (verified by removing each
half in turn).
Feeding an unrecognised Commit back through the engine, rather than reporting
it, would have been worse than the gap: the epoch advances twice and the
sender desynchronises from every other member, with the failure surfacing
several epochs later as "my messages stopped decrypting" and nothing pointing
at the cause. So the pre-existing `Undecryptable` outcome was the right
failure mode to have — it just should never have been reached.
`cli/tests/cordn/tier-b.sh` now passes end to end against the reference
coordinator: handshake, `kp_publish`, group create, invite, Welcome opened
without joining, join, messages both ways with the sender's own traffic
reported as echoes, and both sides agreeing on epoch 1 and the same two
members.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n