Skip to content

fix(lifecycle): harden packet admission and transport ownership - #6598

Open
jeremiah-k wants to merge 5 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/command-transport-lifecycle
Open

fix(lifecycle): harden packet admission and transport ownership#6598
jeremiah-k wants to merge 5 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/command-transport-lifecycle

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Overview

This makes outbound packet admission explicit across the queue, service, and transport layers and keeps admitted work bound to the connection or transport generation that owns it.

Rejected or stale sends no longer create dispatch evidence, response timers, optimistic state, or follow-on work. Accepted sends retain their ownership through acknowledgements, cancellation, reconnects, and teardown. The same lifecycle boundaries are applied to settings transactions, post-handshake requests, remote-admin session refresh, and discovery home restoration.

Review Guide

The five commits are intended to be reviewed in order:

  1. Packet ownership and connection lifecycle — reserves packet ownership through terminal completion, correlates connection epochs, and applies those boundaries to settings commits.
  2. Transport admission — makes transport handoff explicit, binds queued writes to active sessions, and rejects work that cannot be admitted.
  3. Remote-admin latency — allows a realistic response window for multi-hop metadata/session refresh without changing late-response persistence.
  4. Lifecycle hardening — closes teardown/callback races, binds post-handshake work to its connection generation, and serializes discovery cleanup and restoration.
  5. Review-identified state races — closes remaining request, transport, and discovery edge cases found during review and aligns supporting test contracts with production lifecycle semantics.

Key Changes

Packet admission and command outcomes

  • Reserve packet IDs from admission through terminal completion so duplicate sends cannot take ownership of acknowledgements.
  • Distinguish queue rejection, radio rejection, timeout, transport departure, and successful response outcomes.
  • Publish dispatch evidence only after transport admission and ignore acknowledgements that arrive before dispatch ownership exists.
  • Avoid optimistic status, timer, position, or request side effects when the underlying packet was not admitted.
  • Treat terminal routing/admin failures as the end of the current request flow so stale request timers cannot replace the specific failure later.
  • Reject requests that require a local node identity when that identity is unavailable instead of substituting node number 0.

Transport lifecycle

  • Require transport handoff to report synchronous admission across BLE, serial, TCP, replay, mock, and inert transports.
  • Bind queued writes and callbacks to the transport generation that admitted them so replacement connections cannot inherit stale work.
  • Bound pending stream sends and drain or cancel admitted work before physical teardown.
  • Keep connect/disconnect callbacks inside lifecycle admission and reject stale serial-device selection before transport creation.
  • Treat bounded teardown timeouts as operational failures to report rather than exceptions that can abort the remaining shutdown sequence.
  • Preserve legacy single-device USB serial address recovery while rejecting ambiguous stale selections.
  • Release Android serial operation ownership before wake-failure cleanup begins so teardown cannot depend on draining the operation that initiated it.

Admin and connection lifecycle

  • Stage local settings projections behind accepted commit boundaries and preserve the original edit failure when commit cleanup also fails.
  • Capture local-destination ownership consistently for the lifetime of a settings transaction.
  • Bind post-handshake passkey and telemetry requests to the connected lifecycle generation that scheduled them while keeping independent requests isolated from sibling failures.
  • Give remote-admin metadata/session refreshes a 30-second user-facing deadline so multi-hop responses beyond the previous 10-second window can complete normally.
  • Keep late remote-admin responses eligible to refresh durable session state.

Discovery recovery

  • Serialize terminal session persistence and home-radio restoration.
  • Keep interrupted-session recovery bound to the selected device and prevent recovery from reconfiguring the radio underneath a newly admitted scan.
  • Wake pending restoration when either connection state or the selected device changes.
  • Prevent overlapping starts or resets from displacing active scan/cleanup ownership.
  • Bound initial radio-configuration snapshot acquisition so scan startup cannot suspend indefinitely before publishing a result.
  • Keep optional NeighborInfo requests best-effort so a request failure cannot terminate the scan loop before home-radio restoration.
  • Preserve the intended terminal status for completed sessions that are still awaiting restoration, including legacy rows without a usable restore plan.
  • Preserve terminal status when a late background restore completes after another terminal path has already finalized the session.

User-facing rejection handling

  • Surface rejected node and configuration requests without starting success timers or applying local success state.
  • Keep queue/session rejection typed through command APIs so callers can distinguish admission failure from a later response timeout.
  • Resolve asynchronous node-request feedback without blocking a coroutine while loading string resources.

Testing

Added or extended coverage for:

  • packet-ID reservation, duplicate admission, dispatch ordering, acknowledgement handling, timeout, cancellation, and reconnect generations;
  • unavailable local-node identity, NeighborInfo request-ID normalization, and terminal request cleanup;
  • unavailable/opening transports, bounded send queues, completion ownership, close/send races, stale callbacks, replacement sessions, legacy USB selection, and wake-failure cleanup;
  • edit-settings begin/commit boundaries, failure preservation, cancellation cleanup, local departure evidence, and transaction-scoped destination ownership;
  • remote-admin timeout, cancellation, in-flight request sharing, routing errors, and responses arriving after the former 10-second deadline;
  • discovery restoration ordering, interrupted and legacy pending sessions, selected-device changes, overlapping cleanup, bounded startup, rejected restore writes, and active-scan ownership;
  • rejected node requests and optimistic side-effect suppression;
  • lifecycle-aware fakes and test fixtures so repeated connection states and configured command failures match production behavior.

On-device F-Droid validation exercised BLE, Wi-Fi/TCP, and USB serial transport paths, including transport switching, handshake, packet admission, node and configuration exchange, telemetry, admin-session refresh, and remote-admin traffic. All three transports completed normal connection setup and handshake, carried admitted outbound traffic, and continued exchanging expected queue, configuration, telemetry, and session data. Transitions between transports also cleared the outgoing session and established a new connection generation without stale work being carried forward.

Scope

  • No changes to Meshtastic protocol behavior, message formats, or firmware-level communication are introduced.
  • No database schema or user-data migration is required.
  • Internal transport and command-admission contracts are updated consistently across in-repo implementations and callers.
  • Existing reconnect policy and transport selection behavior are unchanged except where required to reject stale or unowned work or preserve legacy single-device serial recovery.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR introduces boolean transport admission and structured send outcomes, adds lifecycle and connection-epoch tracking, coordinates radio teardown, strengthens transactional settings and discovery restoration flows, and surfaces expected request failures through localized feedback.

Changes

Transport and request resilience

Layer / File(s) Summary
Packet admission and awaited results
core/data/..., core/repository/...
Packet sending now reports admission and detailed outcomes. Command requests validate local state, use nonzero request IDs, and propagate queue rejection.
Connection lifecycle state
core/model/..., core/repository/..., core/service/...
Connection snapshots and epochs track departures and completed handshakes across repositories and controllers.
Radio transport lifecycle
core/network/...
BLE, serial, TCP, stream, mock, and replay transports use lifecycle gates, admission results, bounded operations, and coordinated teardown.
Transactional settings
core/service/..., core/testing/...
Settings edits stage local projections and require accepted begin and commit boundaries. Connection departures can confirm dispatched commits.
Discovery restoration
core/database/..., feature/discovery/...
Discovery statuses, recoverable-session queries, terminal cleanup, and reconnect-aware home restoration were added.
Feature failure handling
feature/node/..., feature/widget/..., feature/settings/..., androidApp/..., core/resources/...
Expected queue and local-node failures now use localized feedback or best-effort handling. Crashlytics access is guarded when Firebase is unavailable.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Tests Prove The Path, Not The End State ⚠️ Warning The added resetAndNewScanCannotDisplaceActiveTerminalCleanup test asserts only discoveryDao.sessions.size == 1; it does not verify which session survived or that the new scan issued no retune request. Assert the original session ID and state, and verify no MEDIUM_FAST configuration write or new-session request occurred while terminal cleanup was blocked.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Sibling Call Sites And Presence Semantics ✅ Passed The PR does not change metric representation; both NodeItem and NodeItemCompact use nullable temperature presence, with tests for absent and 0°C, and no new physical-metric field defaults to 0.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's main changes to packet admission and transport ownership lifecycle handling.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added bugfix PR tag desktop Desktop target labels Aug 8, 2026
@jeremiah-k
jeremiah-k force-pushed the bugfix/command-transport-lifecycle branch from ee1a32b to 6839180 Compare August 8, 2026 23:10
@jeremiah-k
jeremiah-k marked this pull request as ready for review August 8, 2026 23:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt (1)

86-98: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Start the transport so this test still exercises the decode path.

handleSendToRadio now returns false immediately when started.value is false. This transport is never started, so the fuzzed bytes never reach ToRadio.ADAPTER.decode. The test would still pass if the runCatching tolerance in ReplayRadioTransport.handleSendToRadio were removed, so it no longer proves the behavior in its name. Call start() first.

💚 Proposed fix
     fun `handleSendToRadio tolerates arbitrary outbound bytes`() = runTest {
         val transport =
             ReplayRadioTransport(Sink(), backgroundScope, address = "", frames = validAsset(), packetDelayMs = 0)
+        transport.start()
         ReplayFuzz.forSeeds { random, seed ->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt`
around lines 86 - 98, Start the ReplayRadioTransport before the ReplayFuzz loop
in handleSendToRadio tolerates arbitrary outbound bytes, ensuring started.value
is true and both fuzzed inputs reach the decode path while preserving the
existing assertions and scheduler handling.

Source: Coding guidelines

🧹 Nitpick comments (7)
core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt (1)

96-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tie OPERATION_TIMEOUT to the lifecycle drain bound.

close() relies on every admitted operation finishing within TransportLifecycleGate.OPERATION_DRAIN_TIMEOUT (15 s). Line 160 bounds each operation at OPERATION_TIMEOUT (10 s), so the invariant holds today. The two values live in separate files, so a later increase of OPERATION_TIMEOUT past 15 s would silently make close() stop the transport while I/O is still in flight.

Consider passing an explicit operationDrainTimeout derived from OPERATION_TIMEOUT when constructing the gate, as BleRadioTransport already does.

♻️ Suggested change
-    private val lifecycle = TransportLifecycleGate("TCP")
+    private val lifecycle = TransportLifecycleGate("TCP", operationDrainTimeout = OPERATION_TIMEOUT * 2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt`
around lines 96 - 99, Update TcpRadioTransport’s TransportLifecycleGate
construction to pass an explicit operationDrainTimeout derived from
OPERATION_TIMEOUT, matching the BleRadioTransport pattern. Ensure the gate’s
drain bound remains synchronized with the per-operation timeout used in
close()’s operation handling, without changing unrelated lifecycle behavior.
core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt (1)

181-190: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider logging instead of throwing when the lifecycle close times out.

close() throws IllegalStateException when lifecycle.close returns false. lifecycle.close returns false for a drain timeout or a teardown timeout, which are bounded, non-fatal outcomes. A throw from a teardown API can abort the caller's remaining shutdown work. MockRadioTransport and ReplayRadioTransport are exercised in the debug/replay path only, so the blast radius is small, but an error log keeps teardown total.

♻️ Optional: report the timeout without failing teardown
     override suspend fun close() {
-        check(
-            lifecycle.close {
-                handshakeQueue.close()
-                transportJob.cancelAndJoin()
-            },
-        ) {
-            "Replay transport teardown did not complete within its lifecycle bounds"
+        val completed =
+            lifecycle.close {
+                handshakeQueue.close()
+                transportJob.cancelAndJoin()
+            }
+        if (!completed) {
+            Logger.e { "Replay transport teardown did not complete within its lifecycle bounds" }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt`
around lines 181 - 190, Update ReplayRadioTransport.close() to stop throwing
when lifecycle.close returns false; instead, log the bounded teardown timeout
and allow close() to complete so callers can continue shutdown work. Preserve
the existing handshakeQueue.close(), transportJob.cancelAndJoin(), and lifecycle
close behavior.
feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt (1)

337-372: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider folding this fake into SharedInMemoryDiscoveryDao.

This PR added SharedInMemoryDiscoveryDao in the same package and migrated DiscoveryHistoryBehaviorTest and DiscoveryMapFilterTest to it. This file keeps a second, near-identical fake, and both copies needed the same two new overrides in this PR. The only structural difference is that InMemoryDiscoveryDao exposes sessions, presetResults, and discoveredNodes as public properties.

Expose read-only accessors on SharedInMemoryDiscoveryDao and delete this copy. That removes the next divergence when DiscoveryDao gains a method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt`
around lines 337 - 372, Replace the local InMemoryDiscoveryDao in
DiscoveryPacketCollectionTest with SharedInMemoryDiscoveryDao, adding read-only
accessors there for sessions, presetResults, and discoveredNodes so existing
test usage remains supported. Remove the duplicate fake and update any
references to use the shared implementation.
feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt (1)

290-290: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the named settle-delay constant.

The sibling test at Line 242 advances by DiscoveryHomeRestorer.POST_RESTORE_SETTLE_DELAY_MS. This line hard-codes the same duration. If the constant grows, the restore does not complete and result.await() at Line 293 blocks until the runTest timeout instead of failing clearly.

♻️ Proposed change
-        advanceTimeBy(3_000L)
+        advanceTimeBy(DiscoveryHomeRestorer.POST_RESTORE_SETTLE_DELAY_MS)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt`
at line 290, Replace the hard-coded 3_000L delay in the affected test with
DiscoveryHomeRestorer.POST_RESTORE_SETTLE_DELAY_MS, matching the sibling test
and keeping the restore timing tied to the named production constant.
core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt (1)

270-336: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving isLocalDestination once per transaction.

EditSettingsSession computes isLocalDestination at line 273. requireCommitBoundaryAccepted recomputes the same predicate at line 351 from nodeManager.myNodeNum.value. If the local node identity changes between the block and the commit, the write-staging decision and the commit-departure decision disagree for the same transaction.

Extract the value once in editSettings and pass it to both the session and the commit boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt`
around lines 270 - 336, Derive the local-destination boolean once in
editSettings and pass that captured value to both EditSettingsSession and
requireCommitBoundaryAccepted. Remove the session-level recomputation from
EditSettingsSession and update the commit-boundary call to use the same
transaction-scoped value, preserving consistent behavior if
nodeManager.myNodeNum changes during the transaction.
core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionStatus.kt (1)

37-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Build RECOVERABLE from the constants instead of parsing the SQL literal.

Line 38 recovers the status values by splitting RECOVERABLE_SQL_LIST on ", " and stripping quotes. The parse depends on the exact spacing of the SQL literal. A future edit that adds a newline, changes the separator spacing, or introduces a status value containing ", " makes the derived Set silently disagree with the SQL filter. That is the drift the comment aims to prevent.

Room requires RECOVERABLE_SQL_LIST to stay a const val, so it cannot be derived from a Set. Declare both from the same constants and add a test that asserts they agree.

♻️ Proposed refactor
-    /** In-memory representation derived from [RECOVERABLE_SQL_LIST] so fakes cannot drift from Room's filters. */
-    val RECOVERABLE: Set<String> = RECOVERABLE_SQL_LIST.split(", ").mapTo(linkedSetOf()) { it.removeSurrounding("'") }
+    /** In-memory counterpart of [RECOVERABLE_SQL_LIST]. A unit test asserts both lists stay in sync. */
+    val RECOVERABLE: Set<String> =
+        linkedSetOf(IN_PROGRESS, INTERRUPTED, RESTORE_PENDING_COMPLETE, RESTORE_PENDING_FAILED, RESTORE_PENDING_STOPPED)

Add the guard test in core/database/src/commonTest:

`@Test`
fun recoverableSetMatchesSqlList() {
    val fromSql = DiscoverySessionStatus.RECOVERABLE_SQL_LIST.split(",").map { it.trim().removeSurrounding("'") }
    assertEquals(fromSql.toSet(), DiscoverySessionStatus.RECOVERABLE)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionStatus.kt`
around lines 37 - 38, Update DiscoverySessionStatus.RECOVERABLE and
RECOVERABLE_SQL_LIST to be declared independently from shared status constants
rather than parsing the SQL literal; keep RECOVERABLE_SQL_LIST as a const val
for Room. Add a common test named recoverableSetMatchesSqlList that trims
comma-separated SQL values and asserts they equal
DiscoverySessionStatus.RECOVERABLE.
feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt (1)

201-256: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the duplicated refusal blocks and the redundant null check.

Lines 205-210 and 213-218 repeat the same mutex.withLock { if (!isActive) _scanState.value = Failed(...) } shape. Line 242 tests initialLoraConfig != null again although refusal == null already implies it, and line 252 then needs refusal ?: "Scan cannot start" for a branch that cannot occur.

Extract a small refusal helper and bind the non-null config to a local value so the unreachable fallback disappears.

♻️ Proposed refactor sketch
+    private suspend fun refuseScan(reason: String) {
+        mutex.withLock { if (!isActive) _scanState.value = DiscoveryScanState.Failed(reason) }
+    }

Then replace each guard block with refuseScan("..."), and inside the mutex use:

val homeLora = initialLoraConfig
when {
    homeLora == null -> _scanState.value = DiscoveryScanState.Failed("Home LoRa configuration is not available")
    initialPrimaryChannel == null && requiresPrimaryRestore ->
        _scanState.value = DiscoveryScanState.Failed("Primary channel is not available for a custom-channel scan")
    else -> prepareScanLocked(targets, dwellDurationSeconds, homeLora, initialPrimaryChannel, requiresPrimaryRestore)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt`
around lines 201 - 256, In the scan-start flow, extract a local refusal helper
that acquires mutex and sets DiscoveryScanState.Failed only when !isActive, then
replace both duplicated refusal blocks with refuseScan calls. In the mutex body,
bind initialLoraConfig to a local nullable value and use a when branch for the
missing-config and missing-primary-channel cases; pass the non-null value
directly to prepareScanLocked, removing the redundant refusal == null &&
initialLoraConfig != null check and unreachable fallback message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt`:
- Around line 353-355: Update requestNeighborInfo to throw
LocalNodeUnavailableException when nodeManager.myNodeNum.value is null, matching
the other request paths in CommandSenderImpl. Remove the ?: 0 fallback so
self-directed NeighborInfo responses are never constructed with node ID 0.

In
`@core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioTransportFactory.kt`:
- Around line 68-72: The exact serialDevices lookup removed legacy path-based
USB address recovery. In AndroidRadioTransportFactory.kt lines 68-72, restore
the single-device fallback in isPlatformAddressValid or resolve and persist the
stable USB key; apply the same resolution in SerialRadioTransport.kt lines
217-234 before connect() logs “Serial device not found,” preserving generation
safety and legacy-address self-healing at both sites.

In
`@core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt`:
- Around line 791-813: Update BleRadioTransport.kt lines 791-813 in the close()
teardown flow to replace check(sessionClosed) and check(completed) with warning
logs, allowing expected lifecycle timeouts without throwing or poisoning the
shared gate. Update TcpRadioTransport.kt lines 127-140 to store the
lifecycle.close result and log a warning when it is false instead of asserting
it. Preserve the existing cleanup and fallback behavior in both transports.

In
`@core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt`:
- Around line 147-153: Update detectSerialGroup to bound the stat subprocess
wait using a 2-second timeout and TimeUnit.MILLISECONDS, preserving the
"dialout" fallback when it fails or times out. Ensure the process is destroyed
in a finally block so the child process and its streams are released regardless
of the outcome, and add the SERIAL_GROUP_LOOKUP_TIMEOUT_MS constant and required
import.

In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt`:
- Around line 247-256: Update awaitConnected and awaitReconnect to combine
serviceRepository.connectionState with meshPrefs.deviceAddress so the wait
predicate is re-evaluated when either source changes. Add the
kotlinx.coroutines.flow.combine import, preserve the existing connected-state
and plan-matching conditions, and keep returning false when the selected device
no longer matches.

In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery.kt`:
- Around line 72-75: The recovery flow in DiscoveryInterruptedSessionRecovery.kt
(lines 72-75) must map completionStatus through finalStatusForPendingRestore,
using UNRESTORABLE only as the fallback; update DiscoveryDao.kt (lines 76-87)
KDoc to note RECOVERABLE_SQL_LIST may return terminal sessions; and update
DiscoveryTerminalCoordinator.kt (lines 120-149) to persist the mapped terminal
status when request.restorePlan is null instead of leaving RESTORE_PENDING_*.
- Around line 48-52: Change the retryable failure log in
DiscoveryInterruptedSessionRecovery’s restoreIfAny handling from Logger.e to
Logger.w, preserving the existing failure details and retry message. Keep
Logger.e reserved for non-retried failures.

In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt`:
- Around line 529-538: Update requestNeighborInfoAtDwellBoundary to make the
optional neighbor-info request fully best-effort: catch
LocalNodeUnavailableException as well as PacketQueueRejectedException, and
ensure any other request failure is contained so it cannot terminate
runScanLoop. Preserve the existing diagnostic logging and allow the scan loop to
continue without propagating exceptions.
- Around line 221-228: Move the initial home-restore wait in startScanTargets to
before acquiring the engine mutex, removing the pre-schedule await from the
mutex-held path. Retain the second awaitBeforeScan check inside the mutex after
terminalCoordinator.resetForScan so last-minute restore conflicts remain
covered, while stop/reset/packet-collector operations are not blocked by the
bounded wait.

In
`@feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt`:
- Line 86: Update UiText.resolve(), particularly the UiText.Resource branch, to
use getStringSuspend instead of the blocking getString implementation. Ensure
nested StringResource values are resolved through the suspend/async path as
well, while preserving the existing resolveUiText suspend lambda and safeLaunch
behavior.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`:
- Around line 1083-1110: Update the test around setResponseStateLoading and
radioConfigUseCase.getOwner to verify the request is actually initiated after
runCurrent(), for example by asserting the configured request callback or
invocation. Keep the existing timeout and failure-state assertions unchanged so
the test proves the timeout originates from the intended request path.

---

Outside diff comments:
In
`@core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt`:
- Around line 86-98: Start the ReplayRadioTransport before the ReplayFuzz loop
in handleSendToRadio tolerates arbitrary outbound bytes, ensuring started.value
is true and both fuzzed inputs reach the decode path while preserving the
existing assertions and scheduler handling.

---

Nitpick comments:
In
`@core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionStatus.kt`:
- Around line 37-38: Update DiscoverySessionStatus.RECOVERABLE and
RECOVERABLE_SQL_LIST to be declared independently from shared status constants
rather than parsing the SQL literal; keep RECOVERABLE_SQL_LIST as a const val
for Room. Add a common test named recoverableSetMatchesSqlList that trims
comma-separated SQL values and asserts they equal
DiscoverySessionStatus.RECOVERABLE.

In
`@core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt`:
- Around line 181-190: Update ReplayRadioTransport.close() to stop throwing when
lifecycle.close returns false; instead, log the bounded teardown timeout and
allow close() to complete so callers can continue shutdown work. Preserve the
existing handshakeQueue.close(), transportJob.cancelAndJoin(), and lifecycle
close behavior.

In
`@core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt`:
- Around line 96-99: Update TcpRadioTransport’s TransportLifecycleGate
construction to pass an explicit operationDrainTimeout derived from
OPERATION_TIMEOUT, matching the BleRadioTransport pattern. Ensure the gate’s
drain bound remains synchronized with the per-operation timeout used in
close()’s operation handling, without changing unrelated lifecycle behavior.

In
`@core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt`:
- Around line 270-336: Derive the local-destination boolean once in editSettings
and pass that captured value to both EditSettingsSession and
requireCommitBoundaryAccepted. Remove the session-level recomputation from
EditSettingsSession and update the commit-boundary call to use the same
transaction-scoped value, preserving consistent behavior if
nodeManager.myNodeNum changes during the transaction.

In
`@feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt`:
- Around line 201-256: In the scan-start flow, extract a local refusal helper
that acquires mutex and sets DiscoveryScanState.Failed only when !isActive, then
replace both duplicated refusal blocks with refuseScan calls. In the mutex body,
bind initialLoraConfig to a local nullable value and use a when branch for the
missing-config and missing-primary-channel cases; pass the non-null value
directly to prepareScanLocked, removing the redundant refusal == null &&
initialLoraConfig != null check and unreachable fallback message.

In
`@feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt`:
- Line 290: Replace the hard-coded 3_000L delay in the affected test with
DiscoveryHomeRestorer.POST_RESTORE_SETTLE_DELAY_MS, matching the sibling test
and keeping the restore timing tied to the named production constant.

In
`@feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt`:
- Around line 337-372: Replace the local InMemoryDiscoveryDao in
DiscoveryPacketCollectionTest with SharedInMemoryDiscoveryDao, adding read-only
accessors there for sessions, presetResults, and discoveredNodes so existing
test usage remains supported. Remove the duplicate fake and update any
references to use the shared implementation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eed564e7-074a-41e3-8019-c952b1a68c77

📥 Commits

Reviewing files that changed from the base of the PR and between a885127 and 6839180.

📒 Files selected for processing (104)
  • .skills/compose-ui/strings-index.txt
  • androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/DataPacketPersistence.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/CommandSenderImplTest.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImplTest.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DiscoveryDao.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/SwitchingDiscoveryDao.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionEntity.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionStatus.kt
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonDiscoveryDaoTest.kt
  • core/domain/README.md
  • core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCase.kt
  • core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/ConnectionState.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/MeshActivity.kt
  • core/model/src/commonMain/kotlin/org/meshtastic/core/model/Position.kt
  • core/network/src/androidHostTest/kotlin/org/meshtastic/core/network/radio/SerialRadioTransportTest.kt
  • core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioTransportFactory.kt
  • core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialRadioTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopRadioTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt
  • core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/TransportLifecycleGate.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/MockRadioTransportTest.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayRadioTransportTest.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/StreamTransportTest.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/TcpRadioTransportTest.kt
  • core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/TransportLifecycleGateTest.kt
  • core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt
  • core/repository/README.md
  • core/repository/build.gradle.kts
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AdminController.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AwaitedSendResult.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ConnectionStateHolder.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/ConnectionStateProvider.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/EditSettingsTransactionException.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/FixedPositionAdminMessage.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LocalNodeUnavailableException.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketQueueRejectedException.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioInterfaceService.kt
  • core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/RadioTransport.kt
  • core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/AwaitedSendResultTest.kt
  • core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/ConnectionStateHolderTest.kt
  • core/repository/src/commonTest/kotlin/org/meshtastic/core/repository/RadioTransportTest.kt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/AdminControllerImpl.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/MessagingControllerImpl.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/NodeControllerImpl.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/RadioControllerImpl.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/ServiceRepositoryImpl.kt
  • core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt
  • core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt
  • core/service/src/commonTest/kotlin/org/meshtastic/core/service/ServiceRepositoryImplTest.kt
  • core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt
  • core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKMeshIntegrationTest.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeBle.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeCommandSender.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioInterfaceService.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioTransport.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeServiceRepository.kt
  • core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeRadioInterfaceServiceSessionTest.kt
  • core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
  • desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt
  • desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorer.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryInterruptedSessionRecovery.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngine.kt
  • feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/DiscoveryTerminalCoordinator.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHistoryBehaviorTest.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryHomeRestorerTest.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryMapFilterTest.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryPacketCollectionTest.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt
  • feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/SharedInMemoryDiscoveryDao.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/CommonNodeRequestActions.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestRejectionFeedback.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
  • feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
  • feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/CommonNodeRequestActionsTest.kt
  • feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt
  • feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt
  • feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/RecordingSnackbarManager.kt
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
  • feature/widget/src/main/kotlin/org/meshtastic/feature/widget/RefreshLocalStatsAction.kt
  • feature/widget/src/test/kotlin/org/meshtastic/feature/widget/RefreshLocalStatsActionTest.kt

@jeremiah-k
jeremiah-k marked this pull request as draft August 8, 2026 23:48
Reserve packet IDs from admission through terminal completion so duplicate sends cannot steal acknowledgements and IDs become reusable only after ownership is released. Serialize dispatch publication with response completion, drain reservations during teardown, and distinguish rejection, timeout, transport departure, and send failure.

Publish connection state and epochs from one committed lifecycle snapshot so queued work, acknowledgements, and reconnect handling cannot combine evidence from different sessions. Keep compatibility views correlated under concurrent transitions.

Apply the same evidence to edit-settings transactions: await begin and commit boundaries, finalize accepted transactions under NonCancellable cleanup, accept a local commit only after dispatch and a post-baseline departure, and keep remote or pre-dispatch failures fail-closed. Cover queue ownership, cancellation, reconnect, and commit-boundary races across production and test fakes.
Require every transport handoff to report synchronous admission rather than imply delivery. Gate packet and heartbeat scheduling through the active session lease so teardown drains accepted work, rejects late writes, and never publishes dispatch or mesh activity for bytes the transport refused.

Enforce the contract across BLE, serial, stream, TCP, replay, mock, and inert transports. Admit serial writes only after the current connection reports ready, reject disconnected TCP and stopped scopes promptly, and isolate replay startup under atomic ownership.

Surface queue rejection through command APIs before persisting optimistic side effects, translate routing NAKs into RADIO_REJECTED results, preserve command-stamped reaction status, and present node-request rejection without starting success timers. Keep diagnostics privacy-safe and document the admission boundary.

Cover unavailable and opening transports, stopped scopes, teardown races, late heartbeats, realistic fakes, coroutine exception recovery, routing responses, reaction persistence, and rejected request side effects.
Give multi-hop remote-admin metadata refreshes thirty seconds to produce a user-facing result instead of treating valid responses after ten seconds as failures. The request remains asynchronous and late responses still update durable session state.

Document the deadline and cover a response arriving after the former timeout so slow mesh paths can establish a session without weakening disconnected or no-response handling.
Linearize transport startup, admission, callback publication, and teardown across BLE, serial, TCP, mock, replay, and stream transports. Bind queued work to the exact transport generation, bound pending sends, drain or cancel owned work before physical teardown, and keep terminal close idempotent for every caller.

Preserve packet and admin ownership across queue rejection, persistence, cancellation, and connection departure. Stage local edit projections behind accepted commit boundaries, fail closed when local identity is unavailable, and bind post-handshake passkey and telemetry retries to the connected lifecycle generation that requested them.

Serialize discovery terminal persistence and home-radio restoration so aggregate/session state is written before restoration can finalize status. Keep interrupted recovery device-bound, preserve terminal outcomes across cancellation, and refuse overlapping cleanup or unrestorable scans.

Add deterministic coverage for queue bounds, replacement generations, close/send races, stale callbacks, recovery ordering, transaction boundaries, and best-effort UI request failures.
Make dispatch evidence and queued channel-batch ownership single-source and cancellation-safe. Keep discovery preparation bound to captured radio identity, preserve terminalization while backing off process-lifetime restore retries, and align direct dependencies, lifecycle fakes, documentation, and regression coverage with production contracts.

Preserve routing-error identity and bounded ownership for late remote settings reads without relaxing write, batch, or destructive-action failure semantics.
@jeremiah-k
jeremiah-k force-pushed the bugfix/command-transport-lifecycle branch from 6839180 to 0b5307d Compare August 9, 2026 15:29
@jeremiah-k

Copy link
Copy Markdown
Contributor Author

@coderabbitai I consolidated the follow-up here rather than posting a series of small inline replies.

The issues raised in this CodeRabbit review have been addressed across the current five-commit series:

  • CommandSenderImpl.requestNeighborInfo now rejects an unavailable local identity instead of constructing a request with node 0. The discovery caller treats the optional NeighborInfo request as best effort, contains request failures, and still propagates cancellation.
  • Legacy Android USB serial selections retain the single-device recovery path in both address validation and connection resolution, while ambiguous stale selections remain rejected.
  • BLE and TCP lifecycle timeouts are reported without throwing from teardown. Replay teardown follows the same rule, and TCP now derives its operation-drain bound from its operation timeout. The JVM serial stat lookup is bounded and always releases its subprocess.
  • Restore waits observe both connection state and selected-device address, so a device change can wake an otherwise stationary connection wait. Retryable interrupted-session restore failures are logged as warnings rather than reporting expected reconnect behavior as defects.
  • Recoverable discovery rows distinguish unfinished scans from terminal sessions awaiting restoration. restore_pending_* states are mapped back to their intended terminal status, including the no-restore-plan path, and the DAO contract documents that recoverable rows are not necessarily unfinished scans.
  • The bounded home-restore wait runs outside the scan mutex. The final race check under the mutex is non-suspending and verifies pending-restore ownership before scan admission, so stop/reset/packet collection are not held behind the restore timeout.
  • The scan-start refusal paths were consolidated so the failure state is applied consistently without duplicating the mutex-protected logic.
  • UiText.resolve() uses the suspend resource path, including nested StringResource and UiText arguments, rather than blocking through the synchronous resolver.
  • The routing-error timeout regression test now verifies that the underlying request was actually initiated before virtual time is advanced.
  • ReplayFuzzTest starts the replay transport before sending arbitrary outbound data, so the test continues to exercise the decode/tolerance path rather than succeeding on pre-start rejection.
  • The discovery test support was consolidated where the fakes had the same contract: packet-collection coverage now uses SharedInMemoryDiscoveryDao, and restore timing uses the named production settle-delay constant.
  • DiscoverySessionStatus.RECOVERABLE is declared from the status constants rather than parsed from the Room SQL literal, with a common test asserting that the in-memory set and SQL filter stay synchronized.
  • Settings transactions capture whether the destination is local once and use that same value for staging and commit-boundary handling, so a changing local-node identity cannot make the two halves of one transaction disagree.

While applying those fixes, I also tightened a few adjacent lifecycle cases for consistency:

  • Discovery startup now bounds acquisition of its initial radio-configuration snapshot rather than allowing the start call to suspend indefinitely before publishing a result.
  • Scan preparation carries the selected-device identity and connection-lifecycle version across the suspending persistence step and revalidates them before publishing the session; stale provisional state is removed if ownership changed meanwhile.
  • Home restoration uses capped exponential retry backoff while it still owns the selected device, avoiding a hard retry limit without retrying at a fixed high rate during a prolonged admission outage.
  • Android serial wake-failure cleanup begins only after the admitted operation lease is released, avoiding teardown depending on the operation that initiated it.
  • Supporting lifecycle fakes and tests were aligned with production version/admission semantics, including completion ownership and zero-request-ID coverage.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

echo "=== CommandSenderImpl.requestNeighborInfo ==="
rg -n "requestNeighborInfo" -A 15 core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt | head -60

echo "=== DiscoverySessionStatus.RECOVERABLE ==="
cat core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionStatus.kt

echo "=== UiText.resolve() ==="
fd UiText.kt | xargs -I{} sh -c 'echo ---{}---; cat {}'

Length of output: 6605


@jeremiah-k Thanks for the consolidated summary. I spot-checked three representative items:

  • CommandSenderImpl.requestNeighborInfo now throws LocalNodeUnavailableException when nodeManager.myNodeNum.value is null, instead of building a request with node 0. Confirmed in core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt.
  • DiscoverySessionStatus.RECOVERABLE is declared as a linkedSetOf built from the same named constants used in RECOVERABLE_SQL_LIST. This keeps the in-memory set and the Room SQL literal on one source of truth. Confirmed in core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DiscoverySessionStatus.kt.
  • UiText.resolve() uses getStringSuspend for StringResource values and recurses through nested UiText arguments, matching the suspend resource path you described. Confirmed in core/resources/src/commonMain/kotlin/org/meshtastic/core/resources/UiText.kt.

These three checks match your description. Given the scope of this consolidated response, I will treat the full list as addressed based on this verification and the consistency of the surrounding changes. I appreciate the detailed write-up; it made review much easier to follow.

(=^・ω・^=) ノ

You are interacting with an AI system.

@jeremiah-k
jeremiah-k marked this pull request as ready for review August 9, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag desktop Desktop target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant