`DkgSession.id` is minted by its coordinator and carried on every message, so it is one string for every participant -- and two profiles on one device are two participants. Keyed by that id alone there was one row for both of them: `acceptProposal` asked "do I know this ceremony?", found the row the first profile wrote, and returned it. The second then ran the ritual out of the first's row: the first's host key, the first's randomness, and in the end the first's share, while the protocol had them as two members with two of each. `679d5249` refuses the second profile's *Welcome*, so this is unreachable for a Marmot group. A NIP-17 room has no Welcome and no equivalent check, and a group's own key ceremony is held in exactly such a room, so it was reachable where it matters most -- at the founding. **The key is the rule.** `(id, userPublicKey)` is "one row per participant per ceremony", and `DkgParticipantMessage` gains the receiving profile for two reasons: a message is an input to *that* profile's recomputation, alongside its own randomness, and a foreign key needs a unique parent to point at -- a child keyed by the ceremony alone could not reference the new parent at all, and deleting a room would leave its rituals' messages behind. Reads and key are one change, not two. Every query in `DkgSessionDao` now names a profile, because with this key a ceremony id or a room identifies no row -- it would return whichever the database ordered first, and hand back that profile's host key, randomness and share. `completedKey` and `canSign` take the asking profile and scope all three of their answers by it; `DatabaseDkgRepository` joins the chat and signing repositories in taking `activeIdentity` at construction, so a screen asking about "the ceremony" means its own and no call site can forget. One read names no profile and says so: `getCeremonyThresholdPublicKey` projects a single public column, the same value on every participant's row, for the two flows that want the room a ceremony's key derives. Scoping it would make a screen resolve nothing while no profile is open, for a fact that does not depend on one. The migration copies both tables into backups holding no foreign key before dropping anything, which is what makes it safe rather than the drop order: `DROP TABLE` fires cascades, and whether Room left them enabled is not a thing to depend on. `DkgSessionPerProfileMigrationJvmTest` runs with foreign keys **on** and passes with the two drops in either order, which is the evidence. Schema 22 rather than 21: the subgroups-multiple-parents line allocated 21 while this was being written, and the rule in docs/curated-to-mantra-profiles.md is that a version belongs to whichever tree migrates first. 21 leaves both ritual tables untouched, so the rebuild's DDL is unchanged apart from the versions it runs between. `TwoProfilesInOneCeremonyJvmTest` drives both profiles through one ceremony in one database; all five of its tests fail against the old lookup. `ProfileScopedSigningKeyJvmTest` covers `completedKey`'s three routes and the repository seam the UI reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…