fix(messaging): time out orphaned "Sending..." messages into a retryable failure - #6630
Conversation
…ble failure
An outbound message goes ENROUTE when handed to the radio and only a routing
ACK/NAK resolves it. When that response never reaches the app — typically
because it was disconnected when the radio answered — the message showed
"Sending..." forever with no retry affordance, since ENROUTE is not retryable.
Stamp a send still ENROUTE after five minutes as ERROR + Routing.Error.TIMEOUT,
which renders through the existing vocabulary ("Failed to deliver to mesh") and
gets the standard Resend affordance. Timeouts re-arm on each connection so sends
orphaned by a disconnect or app restart resolve too, with a 30s grace for an
ACK still in the radio's phone-queue backlog.
Matches the iOS fix in meshtastic/Meshtastic-Apple#2224.
Refs meshtastic/design#43.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe change adds routing-ack timeout handling for ChangesPacket acknowledgment timeout handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/PacketHandlerImpl.kt`:
- Around line 301-308: Make scheduleSendAckTimeout use an atomic repository/DAO
conditional update that sets ERROR with Routing.Error.TIMEOUT only when the
stored packet status remains ENROUTE, rather than writing the previously read
packet copy. Add the corresponding repository and DAO operation, then test that
an ACK or NAK resolving the packet between the read and persistence prevents the
timeout update from overwriting it.
- Around line 280-292: Track each send-ack timeout job by packet ID, including
jobs created by scheduleSendAckTimeout; cancel and remove the corresponding jobs
when disconnecting, and cancel any existing job before rearming in
rearmSendAckTimeouts. Ensure reconnect scheduling uses REARM_GRACE without
allowing the original timeout to mark the packet ERROR, and add a regression
test covering send, disconnect, reconnect before the original deadline, and the
grace window.
🪄 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: 2046cee7-747f-4125-991b-2c62aadc6bab
📒 Files selected for processing (6)
core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.ktcore/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.ktcore/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.ktcore/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketRepository.kt
Address review on #6630. The timeout read the packet and wrote ERROR in two steps, so an ACK/NAK landing in between could be overwritten — showing a delivered message as failed. PacketDao.timeOutEnroutePacket now does the ENROUTE check and the write in one transaction, so a concurrently resolved packet wins. Timers are also tracked per packet id and cancelled when superseded, so repeated reconnects cannot stack timers for the same send. Timers still survive a disconnect deliberately: the ack genuinely never arrived, the resulting state is retryable, and a late ack still upgrades it, so cancelling would restore the "Sending..." forever bug for a user who never reconnects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(messaging): time out orphaned "Sending..." messages into a retryable failure
Why
An outbound message goes
ENROUTEwhen handed to the radio and only a routing ACK/NAK resolves it. If that response never reaches the app — typically because the app was disconnected when it arrived — the message showed "Sending..." forever, with no retry affordance (ENROUTEis not retryable). iOS fixed the same defect in meshtastic/Meshtastic-Apple#2224 (v2.7.18) with a 5-minute send timeout; this brings Android to parity so the shared status vocabulary on meshtastic/design#43 can be finalized.🐛 Bug Fixes
ENROUTEfive minutes after handoff to the radio (matching iOS'ssendAckTimeout) is now stampedERROR+Routing.Error.TIMEOUT. It renders through the existing vocabulary — "Failed to deliver to mesh" with the detail "No acknowledgment was received in time…" — and gets the standard Resend affordance. No new strings.PacketHandler.rearmSendAckTimeouts, called fromonRadioConfigLoaded) so messages orphaned by a disconnect or app restart resolve too. Re-arm waits at least 30 s so an ACK/NAK still sitting in the radio's phone-queue backlog can resolve the message first; a late ACK always upgrades a timed-out message via the normalhandleAckNakpath.QUEUEDmessages are deliberately untouched: they auto-send on the next connection and time out only if that send's ACK never arrives.Known limitation
iOS derives its timed-out state at render time, so it needs no timer. Android's status is persisted, so this uses a live timer plus the reconnect re-arm. If the app process is killed and the user never connects a radio again, an orphaned message keeps showing "Sending..." until the next connection. Closing that would mean deriving status at render time from the send timestamp, which ripples through the
Messagestatus APIs — left out to keep this change small.Testing Performed
PacketHandlerImplTest: unackedENROUTEsend times out to a retryableERROR/TIMEOUT; a send resolved before the deadline is left untouched; re-arm times out a stale persisted packet after the reconnect grace; re-arm gives a fresh packet its full ack window.spotlessApply spotlessCheck detekt assembleDebug test allTests kmpSmokeCompile.Refs meshtastic/design#43.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests