@@ -128,8 +128,20 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
128128 }
129129
130130 // For direct packets with no relay hashes (zero-hop, addressed straight to us),
131- // no further path-based processing applies
132- if (pkt->getPathHashCount () == 0 ) goto direct_path_done;
131+ // no further path-based processing applies.
132+ // A destination's receipt-ACK for a TXT_MSG we delivered as the final relay hop arrives
133+ // exactly this way: zero-path direct, because the destination ACKs its immediate
134+ // neighbour (it has no usable multi-hop return path to the originator, so the ACK only
135+ // travels one hop). The ACK-relay branch below is gated on is_next_hop, which requires
136+ // getPathHashCount() > 0 — unreachable for such an ACK. Cancel any pending final-hop
137+ // resend here, before the early-exit, so the resend does not fire after a delivery the
138+ // destination has already acknowledged.
139+ if (pkt->getPathHashCount () == 0 ) {
140+ if (pkt->getPayloadType () == PAYLOAD_TYPE_ACK ) {
141+ cancelPendingFinalHopResend ();
142+ }
143+ goto direct_path_done;
144+ }
133145
134146 // check for 'early received' ACK
135147 if (pkt->getPayloadType () == PAYLOAD_TYPE_ACK ) {
@@ -145,6 +157,12 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
145157 if (pkt->getPayloadType () == PAYLOAD_TYPE_MULTIPART ) {
146158 return forwardMultipartDirect (pkt);
147159 } else if (pkt->getPayloadType () == PAYLOAD_TYPE_ACK ) {
160+ // This ACK is addressed back along the return path with us as the next hop, i.e. it
161+ // transits back through the very relay that delivered the original message. That is
162+ // proof the destination received it: cancel any pending final-hop resend so we do not
163+ // needlessly retransmit to the destination.
164+ cancelPendingFinalHopResend ();
165+
148166 if (!_tables->wasSeen (pkt)) { // don't retransmit!
149167 _tables->markSeen (pkt);
150168 removeSelfFromPath (pkt);
@@ -155,12 +173,25 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
155173
156174 if (!_tables->wasSeen (pkt)) {
157175 _tables->markSeen (pkt);
176+
177+ // Detect the final relay hop: only our own hash remained in the path, so after
178+ // removeSelfFromPath() the destination is reached implicitly (it lives in the payload
179+ // dest_hash, not as a relay hash) and getPathHashCount() becomes 0. The normal resend
180+ // guard (getPathHashCount() > 0) would then suppress any retransmit, leaving a lost
181+ // final-hop TX unrecoverable at the relay layer. TXT_MSG destinations ACK receipt, so
182+ // mark this packet to allow exactly one ACK-cancellable resend (see resendPacket() and
183+ // cancelPendingFinalHopResend()): the resend self-cancels on the returning ACK, so the
184+ // destination is not flooded.
185+ if (pkt->getPathHashCount () == 1 && pkt->getPayloadType () == PAYLOAD_TYPE_TXT_MSG ) {
186+ pkt->final_hop_ack_resend = true ;
187+ }
188+
158189 removeSelfFromPath (pkt);
159190
160191 MESH_DEBUG_PRINTLN (" Mesh::onRecvPacket(): prepare to repeat packet %s" , pkt->getHashHex ());
161192
162193 uint32_t d = getDirectRetransmitDelay (pkt);
163- return ACTION_RETRANSMIT_DELAYED (0 , d); // Routed traffic is HIGHEST priority
194+ return ACTION_RETRANSMIT_DELAYED (0 , d); // Routed traffic is HIGHEST priority
164195 }
165196 }
166197 return ACTION_RELEASE ; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
@@ -399,6 +430,34 @@ void Mesh::removeSelfFromPath(Packet* pkt) {
399430 }
400431}
401432
433+ void Mesh::cancelPendingFinalHopResend () {
434+ // The destination has ACKed receipt. ACKs are produced in delivery order and (for the
435+ // direct zero-path ACKs a companion sends) travel only one hop back to us, so the OLDEST
436+ // pending final-hop resend corresponds to the message this ACK acknowledges (FIFO). Cancel
437+ // exactly one — the earliest still-queued resend.
438+ //
439+ // The sending_attempts > 0 guard is essential: it selects only actual resends (the attempt
440+ // counter is bumped in resendPacket() before re-queueing) and never the original final-hop
441+ // forward, which is also flagged final_hop_ack_resend while it sits in the queue waiting to
442+ // be TXed. Without it, an ACK arriving in that brief pre-TX window would cancel the delivery
443+ // itself. (send_queue.add() appends, so index 0 is the oldest entry.)
444+ //
445+ // An in-flight resend (the 'outbound' packet, already on-air) is deliberately NOT touched:
446+ // aborting a mid-TX send risks radio state, and the duplicate it delivers is harmless (the
447+ // destination dedups via wasSeen). Only queued, not-yet-sent resends are cancelled.
448+ for (int i = 0 ; i < _mgr->getOutboundTotal (); i++) {
449+ Packet* queued = _mgr->getOutboundByIdx (i);
450+ if (queued && queued->final_hop_ack_resend && queued->sending_attempts > 0 &&
451+ queued->isRouteDirect () && queued->getPayloadType () == PAYLOAD_TYPE_TXT_MSG ) {
452+ MESH_DEBUG_PRINTLN (" %s Mesh::cancelPendingFinalHopResend(): ACK heard, canceling oldest "
453+ " final-hop resend (queued) %s" , getLogDateTime (), queued->getHashHex ());
454+ Packet* removed = _mgr->removeOutboundByIdx (i);
455+ if (removed) _mgr->free (removed);
456+ return ;
457+ }
458+ }
459+ }
460+
402461DispatcherAction Mesh::routeRecvPacket (Packet* packet) {
403462 uint8_t n = packet->getPathHashCount ();
404463 if (packet->isRouteFlood () && !packet->isMarkedDoNotRetransmit ()
0 commit comments