Phase 10 moved the child's ceremony into the parent's Marmot room -- the thing `docs/subgroups.md` Phase 4 deferred and said to revisit -- and it landed between this note being written and being read. Four of its claims were about the world before that move. **Two of the costs the note claimed adoption avoids no longer exist, and a third changed shape.** It argued that skipping the ceremony also skips the NIP-17 sibling room, the member-derived room id, the collision guard and `DkgSession.parentChatRoomId` as a discriminator. The first two are gone from founding already, and the collision is now `DkgSessionDao.getLatestSubgroupSessionFor` over `(room, parent, participants)` rather than an address that can only be occupied once. So the case is remade on what is left -- the ChillDKG itself, still the biggest cost `subgroups.md` names, and the room creation -- which is enough on its own and did not need the padding. **`DkgSession` is still untouched by adoption, and that is now a stronger claim.** Phase 10 added two columns to that table, `participantPublicKeys` and `subject`, and adoption opens no ceremony so it needs neither. Worth noting for a second reason: the sorted comma-joined set matched whole by `getLatestSubgroupSessionFor` is precedent for the canonical-string shape this note reached for and then did not need. **Founding with two parents is now harder, and the rejected-alternatives section says so on new grounds.** It used to be rejected partly on the discriminator work Phase 10 has since done, which would have been a free win. The real objection is better: the ceremony runs in *the parent's room*, and a child with two parents has two candidate rooms and no principled way to choose. `DkgSession.chatRoomId` is one column, the ceremony's messages ride that room's MLS transport, and the members of the other parent -- who may well be admins of the child -- are not in it to receive them. Both rooms means two ceremonies and two keys, which is two children. One room means the other parent's admins take part in a ceremony held somewhere they cannot see, which MLS will not do. Adoption sidesteps it by holding no ceremony at all. **`signingPath`'s un-self-checkable case has two arms now** -- `isCeremonyRoom && (mlsGroupState == null || isSubgroupCeremony)` rather than `mlsGroupState == null` -- and adoption enters neither, because the child's own room is neither the ceremony's room nor its claimed parent. The conclusion is unchanged; the sentence describing it was not. **`completedKey`'s last fallback is now `getLatestOwnSessionForChatRoom`**, the same query scoped to `parentChatRoomId IS NULL`, so a parent hosting its child's ceremony cannot resolve the child's key as its own. Noted where the picker's cost is discussed, since that is the branch a candidate room falls down. Schema **19 -> 20**: 19 is Phase 10's. `refuseCeremonyRoom` is `refuseSubgroup` at both sites that named it, and `ceremonyRoomIdFor` -- which the note did not reference -- is gone. Re-checked and unchanged: `GroupKeyStateManager.propose`'s `check(chatRoomId == signingRoomId || ...)`, which is what lets a room propose its own key state and so is the whole of step 2; `signingPath`'s first candidate, which is why adoption never reaches the special case above; and all four items under "found while reading" -- `ChatRoom.parentChatRoomId` is still write-only, the four parent-indexed DAO reads are still test-only, `ProposedEvent`'s 30326 arm still says nothing about parentage, and `describe(purpose, path)` still takes no parent. The `ChatMessage` 30329 arm this note wants to extend is untouched; what Phase 10 added beside it is a `DkgRitualEvents.ALL -> null` arm, for ceremony traffic now arriving in a Marmot room. Docs only -- no code changed, so there are no test counts to report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 |
| subgroups.md | a group making another group — the four ceremonies, what the parent's signature actually covers, and why the child's key is fresh rather than derived |
| subgroups-multiple-parents.md | adding a parent to a group that already exists — two signings and no ceremony, why the child's consent is the whole gate, and the one screen that currently asks without saying what it is asking |
| 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 |
| material-design-conformance.md | what the M3 foundations actually require, measured against all 43 screens — the colour pairing that renders the app's own proposals invisible, and eight phases that put the decisions back in the theme |
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. The subgroups note is a phased plan that has been built; it assumes both shared-key notes and reads as the ceremony's second half — what a group does once it has a key, and what it can say about a group that does not yet. The multiple-parents note is research rather than a plan, and reads only after it: it takes the one assumption that note never had to state — that a lineage is settled at founding — and finds that dropping it costs less than founding a subgroup does, because no ceremony is involved. The Material Design note is a phased plan that has not been built, and is the only one about what the app looks like rather than what it does; read the jvm-target note first if you want to know why its adaptive-layout phase exists.