Files
amethyst/commons
Claude e4dc1c52d4 feat(cordn): persist messages, the only copy there will ever be
A cordn room's history lived in a LinkedHashMap and nowhere else, while
the cursor that fetches it was persisted pointing past everything. So a
relaunch opened every room empty and no amount of syncing refilled it:
the one handle to history had already been spent. Rewinding it would not
have helped either — both seal keys are epoch-derived, so the copy the
coordinator still holds cannot be opened once the group has ratcheted.
Ingestion is the only moment the message is in the clear.

**EncryptedAppendLog moves to a neutral package.** It takes encrypt and
decrypt lambdas and stores opaque strings; nothing in it knows what
Marmot is. It only lived under `marmot` because that is where it was
written first, and the independence guard reads that as a dependency, so
cordn could not touch it. Now `commons.storage`, used by both, owned by
neither — which makes the two features more separable, not less. Marmot
changes by one import line.

**The write.** `CordnDeliveredMessageCodec` stores the envelope plus its
cursor: the cursor is the coordinator's sequence, which the room orders
on and the unread divider compares against, and `created_at` is the
sender's clock, which is a claim. `FileCordnGroupStore` appends a segment
per message rather than rewriting the conversation, so receiving costs
the same on message ten thousand as on message one.

**The ordering is the correctness property.** `persist` writes messages
*before* the cursor. Crash between them and the message is on disk while
the cursor still points before it: the next catch-up re-delivers it and
dedup drops it. The other order loses it permanently. That is why the
write lives next to the cursor write rather than at the call sites.

Dedup keys on the envelope id, not the stored entry: a re-delivery
carries the same envelope with a different cursor, so the bytes differ
while naming one message.

**The read, in two parts.** The conversation loads when a room opens,
through `addAll`, so the annotation fold and ordering are rebuilt by the
same code that handles live delivery. A one-entry summary per group
loads at login for the inbox line — which is why every cordn room read
"No messages yet" after a relaunch however much had been said.

Deleting a group takes the log and the summary with it. History that
outlived its MLS state would be the plaintext of an end-to-end encrypted
conversation left behind after the key that read it was destroyed.

Mutation-checked: inverting the write order, dedup'ing on the whole entry
instead of the id, and forgetting to drop history on delete each fail
exactly one test and nothing else.

Not yet carried into backup or device migration — the plan
(commons/plans/2026-09-24-cordn-message-store.md) recommends migration
yes, backup no, and that is still an open call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
2026-09-24 15:08:10 +00:00
..
2026-09-24 14:42:32 +00:00