From 49159a888c08a02ba38a43d51ed03bf5ad40d7ca Mon Sep 17 00:00:00 2001 From: MarekWo Date: Wed, 25 Feb 2026 09:58:05 +0100 Subject: [PATCH] fix(dm): Continue retry on command timeout and dedup retry messages - Don't abort retry loop when msg command fails or times out - the device may be temporarily busy (especially during flood mode) - Add 120s time-window dedup for outgoing messages with same text+recipient to prevent duplicate messages in chat when retry acks aren't tracked Co-Authored-By: Claude Opus 4.6 --- app/routes/api.py | 15 +++++++++++++++ meshcore-bridge/bridge.py | 8 ++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/routes/api.py b/app/routes/api.py index 73a25e9..2743e4c 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -1680,6 +1680,21 @@ def get_dm_messages(): except Exception as e: logger.debug(f"Retry dedup failed (non-critical): {e}") + # Secondary dedup: collapse outgoing messages with same text+recipient + # within 120s window (catches retries whose ack wasn't tracked, e.g. timeouts) + deduped = [] + seen_outgoing = {} # (recipient, text) -> earliest timestamp + for msg in messages: + if msg.get('direction') == 'outgoing': + key = (msg.get('recipient', ''), msg.get('content', '')) + ts = msg.get('timestamp', 0) + prev_ts = seen_outgoing.get(key) + if prev_ts is not None and abs(ts - prev_ts) < 120: + continue # Skip duplicate within time window + seen_outgoing[key] = ts + deduped.append(msg) + messages = deduped + # Determine display name from conversation_id or messages display_name = 'Unknown' if conversation_id.startswith('pk_'): diff --git a/meshcore-bridge/bridge.py b/meshcore-bridge/bridge.py index 1de7ff3..b8e8e0f 100644 --- a/meshcore-bridge/bridge.py +++ b/meshcore-bridge/bridge.py @@ -921,11 +921,11 @@ class MeshCLISession: # Don't break - continue retrying (message was likely sent, # just couldn't parse ack due to timing) else: - logger.error(f"Retry: msg command failed: {result.get('stderr', '')}") - break + logger.warning(f"Retry: msg command failed: {result.get('stderr', '')}") + # Don't break - continue to next attempt (device may be temporarily busy) except Exception as e: - logger.error(f"Retry: send failed: {e}") - break + logger.warning(f"Retry: send exception: {e}") + # Don't break - continue to next attempt attempt += 1 if flood_mode: