Files
mantra-kmp/composeApp
Kgothatso Ngako 0c65f61ea2 Fix chat initiation between profiles created in the app
A profile created in the app could never start a chat with another
profile created in the app: the peer always looked like it had no
metadata, no DM relay list and no MLS key package.

Root cause was in neither the chat code nor the relay list — nothing a
new profile signed ever reached a relay. Three queue observers used
`createdAt > :createdAt` with a `Clock.System.now()` default argument.
Kotlin evaluates that default once, at the call site, and Room binds it
for the life of the Flow; Instants persist at second resolution, so
every request enqueued in the observer's own start second (the whole
profile-creation burst) and everything left pending by a previous
session was permanently invisible. Nothing else drains those tables.
The failure was silent because `publishNostrEvent` stamps `signedAt`
and indexes the Profile in one transaction, satisfying the
ProfileLoaded branch before the UnannouncedProfile gate could be
reached — so a device-only profile looked fully announced.

Dropping the cutoff needs no schema change, so existing installs
self-heal on next launch: the stranded rows are still pending.

Also fixed, since they gate the same flow once events start moving:

- Broadcasts now always reach a terminal status (outer timeout plus
  try/catch — `.catch` cannot see the suspend call that builds the
  flow), interrupted ones are requeued once at startup, `OK: false` is
  a failure rather than a recorded success, fan-out is bounded, and an
  uncorrelated NOTICE no longer fails whatever publish shares the
  socket. `take(1)` keeps the publish timeout from firing after a
  success on a SharedFlow that never completes.
- CLOSED is parsed and handled, so a relay refusing a NEG subscription
  falls back to REQ instead of waiting forever; NOTICE is parsed as
  the two-element frame it is; negentropy timestamps use seconds, the
  unit relays use.
- Both chat gates observe the peer's key package instead of reading it
  once and latching a terminal error, and queue the sync they claimed
  to be doing. Same-minute retries are no longer swallowed by IGNORE.
- Group rooms were keyed by the MLS group id instead of the Marmot
  nostrGroupId (unrelated randoms, so neither side saw the other's
  events); inviting a member wrote no Participant row, so the Welcome
  produced no gift wraps, and discarded the post-addMember group state;
  the invite reported success unconditionally.
- An inverted `containsKey` made the "missing peer DM relay list"
  recovery a no-op, and the wrong RelayTag class wrote "r" tags where
  NIP-51 relay lists expect "relay".

Verified with `:composeApp:compileDebugKotlinAndroid`, including that
Room's KSP regenerated the DAO impls without the frozen cutoff. Not yet
exercised against live relays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:51:47 +02:00
..
2026-07-15 01:14:46 +02:00