Fix join flow: skip stale groups in discovery + add sync fallback#517
Conversation
When consumeInboxOnly() is used for the scanner flow, the pre-created 'New Convo' group exists in XMTP but may be cleaned up from the local DB. discoverNewConversations() would then re-discover it and write it back with isUnused: false, making it visible as 'New Convo' in the list. Skip groups where we are the creator and the only member — these are unused pre-created conversations that should not be re-introduced.
The XMTP conversation stream does not call sync_welcomes() at startup (unlike StreamAllMessages), so it cannot discover groups the client is added to after stream subscription. This causes the join flow to get stuck at 'Verifying' indefinitely when the stream fails to deliver. Add requestDiscovery() to SyncingManager that runs syncAllConversations + discoverNewConversations on demand. Thread it through InboxStateMachine and InboxStateManager protocols. In ConversationStateMachine.handleJoin, start a discovery loop alongside the DB observation that periodically syncs welcomes with backoff (3s, 6s, 12s, 15s cap). The DB observation fires as soon as the group is written by discoverNewConversations. The loop is cancelled on success, cancellation, or error.
|
Claude finished @yewreeka's task —— View job Code Review Complete ✅Reviewing PR #517: Fix join flow with sync fallback and stale group filtering Todo List:
SummaryThis PR addresses two legitimate bugs in the join flow with well-reasoned solutions. The changes are focused, maintain architectural integrity, and include appropriate cleanup in all exit paths. Overall quality is high with only minor suggestions for improvement. Code Quality & Best Practices ✅Strengths:
Minor Suggestions:
Potential Bugs & Edge Cases
|
messagingServiceSync could create duplicate services for the same clientId when called concurrently. The nonisolated getAwakeService check and the async registerExternalService had a race window where two callers would both see nil and both create services. Add getOrSetAwakeService to InboxLifecycleManager that atomically checks the lock-based cache and stores the service if no entry exists. If a duplicate is detected, the redundant service is stopped immediately.
Dismissing my prior approval and re-evaluating approvability for 630481d
This reverts commit 630481d.
When messagingServiceSync is called concurrently for the same clientId, duplicate MessagingService instances can be created before the first one is registered. The second call to registerExternalService finds the inbox already tracked and previously just silently returned, leaving the duplicate's SyncingManager running orphaned streams. Now stop the rejected service so its streams and auth flow are cleaned up.
Move duplicate service stop() from registerExternalService to the caller in SessionManager. registerExternalService now returns Bool indicating success — the caller stops the orphan if registration was rejected. This avoids stopping a service that another caller may already be using. Also add discoveryTask?.cancel() to ConversationStateMachine deinit for consistency with other task cleanup.
d77ddb6 to
42fc68a
Compare
Dismissing my prior approval and re-evaluating approvability for 42fc68a
Problem
Two issues causing join flow failures:
1. Scanner shows "New Convo" instead of joined conversation
After scanning a QR code and joining, the app would sometimes show an empty "New Convo" instead of the actual conversation. This happened because
discoverNewConversations()inSyncingManagerwas re-introducing self-created single-member groups (from the unused conversation cache) back into the database after they had been cleaned up.Fix: Filter out groups where
creatorInboxId == client.inboxId && memberCount <= 1indiscoverNewConversations(). These are unused pre-created conversations that should not be re-processed.2. Join gets stuck at "Verifying" indefinitely
The XMTP conversation stream (
StreamConversations) does not callsync_welcomes()at startup — unlikeStreamAllMessageswhich does. This means the stream cannot discover groups the client is added to after subscription. The joiner sends a join request, the host adds them, but the joiner's stream never delivers the new group.The existing fallback (
discoverNewConversationson app resume) is unreliable due to lifecycle edge cases where theSyncingManagermisses the background/foreground transition.Fix: Add
requestDiscovery()toSyncingManager(syncs welcomes + discovers new groups on demand). InConversationStateMachine.handleJoin, start a discovery loop alongside the DB observation that periodically callsrequestDiscovery()with backoff (3s → 6s → 12s → 15s cap). The DB observation fires as soon asdiscoverNewConversationsprocesses the group. The loop is cancelled on success.Changes
SyncingManager.swift— Filter self-created single-member groups indiscoverNewConversations(), addrequestDiscovery()to protocol and implementationInboxStateMachine.swift— ExposerequestDiscovery()InboxStateManager.swift— AddrequestDiscovery()to protocol and implementationConversationStateMachine.swift— Add discovery loop inhandleJoin, cancel in all exit pathsTesting
Note
Run periodic discovery during invite join and skip self-created single-member groups, with
ConversationStateMachinestarting a 3s→15s exponential-backoff loop andSyncingManagerfiltering stale groupsAdd a background discovery loop in
ConversationStateMachinewith exponential backoff (3s doubling to 15s) that cancels on completion or cleanup, introducerequestDiscoverythroughInboxStateManagertoSyncingManager, and haveSyncingManagerskip self-created single-member groups whileSessionManagerstops services whenInboxLifecycleManager.registerExternalServicereturns false.📍Where to Start
Start at
joinhandling and discovery loop setup in ConversationStateMachine.swift, then followInboxStateManager.requestDiscoverytoSyncingManager.requestDiscoveryin InboxStateManager.swift and SyncingManager.swift.Macroscope summarized 42fc68a.