Files
mantra-kmp/docs/README.md
Kgothatso Ngako ea11e8b233 refactor: call it a chronicle, and keep "archive" for what a user does to a chat
Archiving a chat is an ordinary thing a user will want to do to a conversation,
and it is not this. This is the group's signed record, handed to a member who
joined after the work was done so their room stops being empty. Two unrelated
meanings of one word in one app is a bug waiting to be written, and
`ChatRoom.archiveRequestedAt` is exactly where they would have met: a column on
the chat row, named for the thing that is not the chat.

So the whole feature is Chronicle now -- `press.mantra.compose.nostr.chronicle`,
`ChronicleEvent` (30327), `ChronicleRequestEvent` (30328), the three tags,
`ChronicleManager`, `docs/member-chronicle.md`. The kind numbers do not move;
only the words do.

**The wire tags move too**, `archiveId` -> `chronicleId` and `archivePage` ->
`chroniclePage`, which is free exactly once. Both kinds are new and there is no
old build to stay compatible with -- the design note says so in as many words --
so the alternative was carrying the old spelling on the wire forever to save a
rename that costs nothing today. The recipient tag stays `p`; it was never ours.

**Schema v14, because two things had the old word written into stored data.**

`ChatRoom.archiveRequestedAt` becomes `chronicleRequestedAt`, renamed rather than
dropped and re-added: while it is set it is the only record that a device with an
empty room has already asked the group for its history, and a device that lost it
mid-flight would ask again on its next launch, and the one after that.

The three `ChatMessage.messageType` strings become their `chronicle*` spellings,
rewritten rather than left to a legacy constant the way `dkgApprovalNeeded` was.
These lines cannot be regenerated -- a chronicle is announced once, when it is
requested, sent and applied -- and an unrecognised type is not skipped by the
transcript. It renders as an ordinary chat bubble, so "Caught up on 12 items"
would come back attributed to a member as something they said.

`MIGRATION_13_14` does both, because Room can rename a column and cannot rewrite
rows in the same breath. `ALTER TABLE ... RENAME COLUMN` needs SQLite 3.25, which
`getRoomDatabase` guarantees by pinning `BundledSQLiteDriver`, and the column is
in no index, no foreign key, and there is not a view or trigger in the database
-- so nothing has to move with it. Five tests hold the two halves apart: the
value survives, the column keeps its position, a room that never asked still
reads as never having asked, the three types are rewritten, and every other type
is left alone.

**`isArchivable` is `isChroniclable`**, on the "recyclable" pattern, and it keeps
its job unchanged: the allowlist that stands between a replayed
`GroupKeyStateEvent` and the apply path.

No behaviour change beyond the rename. 797 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:27:36 +02:00

2.9 KiB

mantra docs

Notes on the parts of this app whose behaviour is not recoverable by reading the code alone — where the reasoning lives in a protocol, a failure mode that is silent, or a decision that looked arbitrary and was not.

document covers
shared-key-ceremony.md ChillDKG over NIP-17: the rounds, the approval gates, the chat transcript, participant ordering
shared-key-derivation.md deriving further keys from the group's threshold key with FROST tweaks — why not BIP32, why no chain code, and the one rule that must not be broken
frost-batch-signing.md signing several events in one ceremony — why one nonce can never cover two messages, and the phased schema, wire and UI work that follows from it
marmot-membership.md how members join an MLS group, and the epoch race that makes a missing member look like a successful invite
member-chronicle.md handing a member added after the work was done the group's signed record — why the events are not on the wire at all, and why the room's id is enough to verify them
marmot-direct-messages.md a one-to-one message inside a group as a stock NIP-59 gift wrap — what its MIP-03 carve-out costs, why the sender cannot read their own, and the one query that would broadcast it
mls-skipped-keys.md why a group event that arrives a moment late is dropped for good, which flows trigger it, the quartz fix, and the partial mitigation in this app
long-running-sync.md the chat subscriptions that stay open instead of pulling once per screen — why the request queue could not simply hold one, and how the group filter follows the room list
dead-code.md code in the sync and relay stack that nothing calls, why each piece is still there, and which of it is a bug rather than a leftover
jvm-target.md what desktop support cost, phased — why the native chain was already done, why an empty source set in our phoenix fork was the real blocker, and why DAO tests need none of it

Start with the ceremony if you are new to this area; the Marmot notes all assume it. Read the skipped-keys note before debugging any "the other device never got it" report — it is silent, and it looks like every other kind of delivery failure. The sync note stands alone, and the dead-code inventory reads as a follow-up to it. The batch-signing note is a phased plan that has been built: read it after the derivation note, whose one rule is the same one it is built around. The member chronicle note is a phased plan that has not been built, and reads as the membership note's unanswered half: what a member who joins late can be given, and the one thing they cannot. The jvm-target note is unrelated to all of them: it is a build and packaging story.