From aa2ba0a5c8429a14cdc50a0b90358de5e367544d Mon Sep 17 00:00:00 2001 From: MarekWo Date: Mon, 23 Mar 2026 20:33:36 +0100 Subject: [PATCH] fix(dm): prioritize primary (starred) path in retry rotation PATH_ROTATION now has 3 phases: 1. Exhaust retries on primary path first (initial send + retries_per_path-1) 2. Rotate through remaining non-primary paths 3. Optional FLOOD fallback (if no_auto_flood=False) Previously, retry iterated all paths in sort_order giving the primary path only the initial send attempt before switching to the first path on the list, which was often an older/worse path. Co-Authored-By: Claude Opus 4.6 --- app/device_manager.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app/device_manager.py b/app/device_manager.py index e619cca..fa8d193 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1200,8 +1200,35 @@ class DeviceManager: retries_per_path = max(1, cfg['direct_max_retries']) min_wait = float(cfg['direct_interval']) - for path_info in configured_paths: - # Switch to this path on the device + # Separate primary (starred) path from the rest + primary_path = None + other_paths = [] + for p in configured_paths: + if p.get('is_primary') and primary_path is None: + primary_path = p + else: + other_paths.append(p) + + # Phase 1: Exhaust retries on primary path first + # Initial send already used device path (assumed primary), so -1 + if primary_path: + try: + await self._change_path_async(contact, primary_path['path_hex'], primary_path['hash_size']) + logger.info(f"DM retry: retrying on primary path '{primary_path.get('label', '')}' " + f"({primary_path['path_hex']})") + except Exception as e: + logger.warning(f"DM retry: failed to set primary path: {e}") + + for _ in range(retries_per_path - 1): + attempt += 1 + if await self._dm_retry_send_and_wait( + contact, text, timestamp, attempt, dm_id, + suggested_timeout, min_wait + ): + return # Delivered on primary, no restore needed + + # Phase 2: Rotate through remaining (non-primary) paths + for path_info in other_paths: try: await self._change_path_async(contact, path_info['path_hex'], path_info['hash_size']) logger.info(f"DM retry: switched to path '{path_info.get('label', '')}' " @@ -1210,20 +1237,17 @@ class DeviceManager: logger.warning(f"DM retry: failed to switch path: {e}") continue - # Try sending on this path for _ in range(retries_per_path): attempt += 1 if await self._dm_retry_send_and_wait( contact, text, timestamp, attempt, dm_id, suggested_timeout, min_wait ): - # Delivered! Restore primary path await self._restore_primary_path(contact, contact_pubkey) return - # All configured paths exhausted + # Phase 3: Optional FLOOD fallback if not no_auto_flood: - # Fall back to FLOOD min_wait = float(cfg['flood_interval']) try: await self.mc.commands.reset_path(contact)