Files
mantra-kmp/composeApp
Kgothatso Ngako 873203e4b4 feat: let a member with none of the group's work ask for it
Phase 5 of docs/member-archive.md, and the half that makes the whole thing
reliable. A device opening a room it holds no signed work for asks the group;
any member holding the work answers, addressed to whoever asked.

**A request cannot lose the race a push loses.** Pushing an archive at an invitee
is an application message in the epoch the add created, and one that overtakes
the Welcome is dropped rather than deferred -- silently, while the inviter sees a
success. That is marmot-membership.md's failure mode arriving in a new costume.
Sending a request cannot lose it, because being able to send one is the proof it
was won: a device that can put an application message into the room has processed
its Welcome and is at the group's epoch.

It also covers three things no invite-time push reaches, and they are answered by
one rule because they are indistinguishable from inside the database: a member
added after the work, a reinstall whose invite is long past, and a second device
that was never invited at all. Hence the crude condition -- no dialects and no
artifacts -- rather than anything that tries to tell them apart.

**Anyone may answer and nobody is elected to.** A duplicate answer costs
bandwidth and nothing else: pages are idempotent and every member who is not the
named recipient ignores them. So the stand-down that would avoid the waste is an
optimisation to add later rather than a correctness gap to close now. A member
with nothing signed answers nothing at all, which is the honest reply from one
still catching up themselves, and beats an empty archive that looks like an
answer.

**Queued with no chat line**, the way a signing message travels. Broadcast does
not depend on one -- `encryptAndSendMarmotInnerEvent` inserts its
`BroadcastNostrEventRequest` unconditionally, which is what let the FROST rounds
travel with no transcript -- and an archive that filed a line per page would put
a row of envelopes in the room's history. One line per archive is the right
number and it is not writable from here, since the pages are indistinguishable
from each other at this point.

**Schema 11 -> 12**: `ChatRoom.archiveRequestedAt`, nullable, so Room generates
the migration. It stops a device asking again on every launch while an answer is
in flight. Rooms written before it read back null, meaning "never asked", which
is true of all of them and harmless.

Cleared as soon as an archive applies anything -- not when a sender's page count
claims the archive was complete. A page count is the sender's word about the
transfer rather than about the group's record, so a member who left work out must
not get the last word on whether to ask again. A partial answer is followed by
another request rather than by silence.

The trigger is opening the room, through `ChatRepository` rather than from the
view model into the database. Cheap to call every time: it stops at a room that
already holds work and at one still waiting. A failure is not reported, because
nothing acknowledges a request and the next open asks again.

Eight tests: a device with nothing asks and does not ask twice, a device with
work does not ask, answering queues pages addressed to the asker with every
archivable kind in them and no chat line, a member with nothing signed answers
nothing, a member does not answer themselves, an applied archive clears the stamp
so a partial answer can be followed up, and the whole round trip -- ask, answer,
apply -- leaves the joiner holding the sender's rows.

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