From c2acbb4ba124172782dc8ea3d2da144c43ace5da Mon Sep 17 00:00:00 2001 From: MarekWo Date: Wed, 25 Feb 2026 10:19:28 +0100 Subject: [PATCH] fix(dm): Wait for msg JSON response and fix unread markers Monitor: For .msg commands, keep waiting for expected_ack in response instead of completing on silence timeout. Waits up to half of cmd_timeout (5s) for the JSON to arrive before giving up. Unread: Apply 120s time-window dedup to outgoing messages in conversation listing, so retry messages don't inflate last_message_timestamp and cause permanent unread markers. Co-Authored-By: Claude Opus 4.6 --- app/meshcore/parser.py | 14 ++++++++++++++ meshcore-bridge/bridge.py | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/meshcore/parser.py b/app/meshcore/parser.py index 2db2c7d..336e215 100644 --- a/app/meshcore/parser.py +++ b/app/meshcore/parser.py @@ -582,6 +582,20 @@ def get_dm_conversations(days: Optional[int] = 7) -> List[Dict]: """ messages, pubkey_to_name = read_dm_messages(days=days) + # Deduplicate outgoing retry messages: collapse same text+recipient within 120s + 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 + seen_outgoing[key] = ts + deduped.append(msg) + messages = deduped + # Build reverse mapping: name -> pubkey_prefix name_to_pubkey = {name: pk for pk, name in pubkey_to_name.items()} diff --git a/meshcore-bridge/bridge.py b/meshcore-bridge/bridge.py index b8e8e0f..bf01c01 100644 --- a/meshcore-bridge/bridge.py +++ b/meshcore-bridge/bridge.py @@ -457,17 +457,16 @@ class MeshCLISession: # because meshcli may echo prompt immediately but real results come later is_slow_command = cmd_timeout >= 15 # For .msg/.m commands, JSON response (with expected_ack) may arrive after prompt echo - # with a gap > 300ms, so enforce a minimum wait to capture it + # with a gap > 300ms, so keep waiting until JSON arrives or timeout cmd_str = response_dict.get("command", "") is_msg_command = cmd_str.lstrip('.').split()[0] in ('msg', 'm') if cmd_str.strip() else False if is_slow_command: min_elapsed = cmd_timeout * 0.7 - elif is_msg_command: - min_elapsed = 1.5 # Wait at least 1.5s for JSON response else: min_elapsed = 0 - logger.info(f"Monitor [{cmd_id}] started, cmd_timeout={cmd_timeout}s, min_elapsed={min_elapsed:.1f}s") + logger.info(f"Monitor [{cmd_id}] started, cmd_timeout={cmd_timeout}s, " + f"min_elapsed={min_elapsed:.1f}s, is_msg={is_msg_command}") while not self.shutdown_flag.is_set(): time.sleep(timeout_ms / 1000.0) @@ -481,6 +480,15 @@ class MeshCLISession: time_since_last_line = time.time() - response_dict.get("last_line_time", 0) total_elapsed = time.time() - start_time has_output = len(response_dict.get("response", [])) > 0 + response_lines = response_dict.get("response", []) + + # For .msg commands: don't complete until we see expected_ack in response + # or we've waited long enough (half of cmd_timeout) + if is_msg_command and has_output: + response_text = '\n'.join(response_lines) + has_json = 'expected_ack' in response_text + if not has_json and total_elapsed < cmd_timeout * 0.5: + continue # Keep waiting for JSON response # Can only complete if: # 1. Minimum elapsed time has passed (for slow commands), AND