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>
This commit is contained in:
Kgothatso NgakoandClaude Opus 5 committed 2026-09-06 16:27:36 +02:00
1 parent a69d80d38f
commit ea11e8b233
33 files changed
+6506 -606

No files matched your search

@@ -177,7 +177,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
UnsignedNostrEvent::class,
Zap::class
],
version = 13,
version = 14,
autoMigrations = [
// v2 only adds the DkgSession/DkgParticipantMessage tables, so Room can
// generate the migration itself — nothing existing changes shape.
@@ -232,12 +232,13 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
// a room that could only propose one thing at a time cannot have written
// two sessions' lines into the same stretch of transcript.
AutoMigration(from = 10, to = 11),
// v12 adds the nullable ChatRoom.archiveRequestedAt, which stops a device
// with no signed work in a room asking the group for its history on every
// launch while an answer is in flight. Rooms written before it read back
// null, meaning "never asked" -- true of all of them, and harmless: the
// request only goes out for a room that holds no signed work, and a room
// that holds some will not ask.
// v12 adds the nullable ChatRoom.archiveRequestedAt -- renamed to
// chronicleRequestedAt in v14 -- which stops a device with no signed work
// in a room asking the group for its history on every launch while an
// answer is in flight. Rooms written before it read back null, meaning
// "never asked" -- true of all of them, and harmless: the request only
// goes out for a room that holds no signed work, and a room that holds
// some will not ask.
AutoMigration(from = 11, to = 12),
// v13 adds the GroupSignedEvent table and the nullable
// `groupSignedEventId` on every row a signed event turns into. A new
@@ -246,7 +247,14 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
// hold the event behind this row" -- true of all of them, since nothing
// kept it. They are not backfilled: the events are gone, and inventing
// an id for one would point a row at a signature nobody can produce.
AutoMigration(from = 12, to = 13)
AutoMigration(from = 12, to = 13),
// v14 changes one column name and three stored strings, all of which say
// "archive" about the group's signed record. The word was wanted back for
// the ordinary thing a user does to a chat, and a column called
// `archiveRequestedAt` sitting on ChatRoom is exactly where the two
// meanings would have met. Room can rename a column and cannot rewrite the
// rows in the same breath, so this is a manual migration passed to the
// builder rather than an entry here. See MIGRATION_13_14.
]
)
@ColumnTypeConverters(MantraConverters::class)