"A subgroup cannot be the whole group. Leave at least one member out." That rule shipped in Phase 8 and it was wrong twice. **It refused something legitimate.** A subgroup is a logical division -- a group deciding that some of its work belongs to a differently-keyed room -- not a group carving out a smaller membership. Every member being in it is an ordinary case, and no guard here had any business deciding otherwise. **And it was a proxy, not a check.** The thing it stood in for is real: a ceremony room is derived from its admins, so a subgroup over everybody lands in the room the group's *own* ceremony was held in, and `proposeRitual` handing back that ceremony would quietly make the child the parent. But set size does not detect that. A parent whose membership has changed since its own ceremony derives a different room -- so the sizes can match with no collision, and differ with one. **Sharing the room was never the problem; sharing a ceremony was.** `DkgSession.parentChatRoomId` already told two ceremonies apart, so the fix is to scope the lookup by it rather than to forbid the selection. `DkgSessionDao.getLatestSessionFor(room, parent)` replaces `...ForChatRoom` at the three places that decide whether a ceremony already exists: `proposeRitual`'s one-at-a-time guard, `refuseCeremonyRoom`, and the two repository observers the ritual screen follows. A room may now hold the group's own ceremony and a subgroup's at once. Nothing below that lookup had to learn about the second one. A ceremony's messages, approvals and transcript are already keyed by session id; only the question "what is this room's current ceremony" was ever room-scoped, and that question was always really "for what purpose". One collision survives and it is degenerate: the same parent, over the same admins, twice. Those two have nothing left to distinguish them -- which is another way of saying they are one subgroup asked for twice, and that is what the message now says. `a subgroup that is the whole group is refused` becomes `a subgroup may be the whole group`, and two new cases pin the scoping: the group's own ceremony sitting in the derived room does not block a subgroup there, and the same parent asking twice over the same admins still does while a different admin set is untouched. `docs/subgroups.md` keeps the withdrawn rule struck through in the refusals table with a section saying why, rather than quietly deleting it -- the reasoning that led to it is the reasoning somebody would repeat. 397 common tests, 713 jvm tests, `m3Audit` meets every budget. 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 |
| 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 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.