fix: path persistence + proactive path resolution#664
fix: path persistence + proactive path resolution#664torlando-tech merged 9 commits intorelease/v0.9.xfrom
Conversation
Fix three compounding bugs that destroyed Reticulum's path table on every restart, causing messages to fail until paths were rediscovered (~15 min): 1. shutdown() cleared destination_table before persisting — now calls RNS.Transport.persist_data() before clearing global state 2. _clear_stale_ble_paths() was too aggressive, removing all BLE paths >60s old at startup — now only clears timestamp=0 (the actual bug) 3. No crash resilience — added periodic persist_transport_data() every 15 minutes via IdentityResolutionManager Additionally adds proactive path requests as a safety net: - Startup sweep requests paths for all contacts (2s stagger) - Path requested on conversation open if missing - Path requested when adding contacts (hash-only, full-identity, announce, save-from-conversation) - Retry resolution delegates to IdentityResolutionManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR addresses two related reliability gaps in Reticulum path management: paths were not being saved on graceful shutdown (or periodically for crash resilience), and the BLE path cleanup was too aggressive — removing valid paths from prior sessions based on age rather than only the Key changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant App as App Lifecycle
participant IRM as IdentityResolutionManager
participant VM as ViewModel (Contacts/Chats/Messaging/Announce)
participant RNS as ReticulumProtocol
participant PY as reticulum_wrapper.py
App->>IRM: start(scope)
IRM->>IRM: launch resolutionJob (every 15 min)
IRM->>IRM: launch startupSweepJob (5s delay)
Note over IRM: Startup sweep (t+5s)
IRM->>RNS: hasPath(destHash) for each contact
alt path missing
IRM->>RNS: requestPath(destHash)
IRM->>IRM: delay 2s (stagger)
end
Note over IRM: Periodic loop (every 15 min)
IRM->>RNS: recallIdentity / requestPath for PENDING contacts
IRM->>RNS: persistTransportData()
RNS->>PY: persist_transport_data()
PY->>PY: RNS.Transport.persist_data()
Note over VM: User action (add contact / open conversation)
VM->>IRM: requestPathForContact(destinationHash)
IRM->>RNS: hasPath(destHash)
alt no path
IRM->>RNS: requestPath(destHash)
end
Note over App: Graceful shutdown
App->>PY: shutdown()
PY->>PY: RNS.Transport.persist_data() [Step 3.5]
PY->>PY: Clear RNS global state
|
app/src/main/java/com/lxmf/messenger/viewmodel/AnnounceStreamViewModel.kt
Show resolved
Hide resolved
…olutionManager Detekt's NoRelaxedMocks rule forbids mockk(relaxed = true). Replace with named mocks and explicit coEvery stubs for requestPathForContact and retryResolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
- AnnounceStreamViewModel: delegate to identityResolutionManager.requestPathForContact() instead of raw reticulumProtocol.requestPath(), gaining the hasPath guard - reticulum_wrapper.py: add RETICULUM_AVAILABLE guard to persist_transport_data() - IdentityResolutionManager: move delay outside try block in requestPathsForAllContacts() so stagger always applies even when requestPath throws Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…odelTest Detekt's NoRelaxedMocks rule. Use named identityResolutionManager mock with coEvery stub for requestPathForContact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ContactsViewModel.retryIdentityResolution() already calls resetContactForRetry() and checks the result before delegating to IdentityResolutionManager.retryResolution(). The second call inside retryResolution() was redundant. Remove it and document the caller contract. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The one-shot startup sweep coroutine was launched without storing its Job, so stop() couldn't cancel it. During Reticulum restarts, the sweep would continue firing path requests against a shutting-down service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline hasPath/requestPath logic with delegation to IdentityResolutionManager.requestPathForContact(), consistent with ContactsViewModel, ChatsViewModel, and AnnounceStreamViewModel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ContactsViewModel.retryIdentityResolution: add Dispatchers.IO to avoid blocking AIDL call on main thread (potential ANR) - RoutingManager.persistTransportData: inspect Python return dict so soft failures appear in logcat Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use identityInput.destinationHash consistently across all contact addition branches, and add missing path request when a contact is resolved immediately from the announce cache. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
shutdown()now callsRNS.Transport.persist_data()before clearing global state, so paths survive graceful shutdownsIdentityResolutionManagerfor crash resilienceManual Testing Checklist
Path Persistence (Part A)
IdentityResolutionMgrstartup sweep: paths should already exist (skippingmessages)Crash Resilience (Part B)
persistTransportDatacalls appear in logcat every 15 minutesStale Path Fix (Part A2)
_clear_stale_ble_paths— should only cleartimestamp=0entries, not age-based removalsProactive Path Requests — Contact Addition (Part C)
IdentityResolutionMgr: Requesting path for...)Proactive Path Requests — Conversation Open (Part C3)
Requesting path for conversation...Retry Resolution (Part C2)
retryResolutionRegression Check
./gradlew :app:testNoSentryDebugUnitTest)🤖 Generated with Claude Code