fix: ACK-accelerated delivery — immediate on ACK, retry once, abort on double fail

Delays 1.5-2.5s (was 3-5s, only for broadcasts now).
DMs: send → ACK → next immediately. No ACK → retry once → abort.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-06 14:10:16 +00:00
commit 34f894ea79
2 changed files with 95 additions and 87 deletions

View file

@ -35,8 +35,8 @@ class ConnectionConfig:
class ResponseConfig: class ResponseConfig:
"""Response behavior settings.""" """Response behavior settings."""
delay_min: float = 3.0 delay_min: float = 1.5
delay_max: float = 5.0 delay_max: float = 2.5
max_length: int = 200 max_length: int = 200
max_messages: int = 3 max_messages: int = 3

View file

@ -1,85 +1,93 @@
"""Response handling - delays and message delivery.""" """Response handling - delays and message delivery."""
import asyncio import asyncio
import logging import logging
import random import random
from typing import Optional from typing import Optional
from .config import ResponseConfig from .config import ResponseConfig
from .connector import MeshConnector from .connector import MeshConnector
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Responder: class Responder:
"""Handles response delivery with pacing.""" """Handles response delivery with pacing."""
def __init__(self, config: ResponseConfig, connector: MeshConnector): def __init__(self, config: ResponseConfig, connector: MeshConnector):
self.config = config self.config = config
self.connector = connector self.connector = connector
async def send_response( async def send_response(
self, self,
messages: list[str] | str, messages: list[str] | str,
destination: Optional[str] = None, destination: Optional[str] = None,
channel: int = 0, channel: int = 0,
) -> bool: ) -> bool:
"""Send response messages with ACK waiting and retry. """Send response messages with ACK-accelerated delivery.
For DMs: waits for ACK before sending next message, retries once on failure. DMs: Send -> wait for ACK -> if ACK, send next immediately.
For broadcasts: uses delay-based pacing (no ACK for broadcasts). If no ACK, retry once -> if still no ACK, abort.
""" Broadcasts: delay-based pacing (no ACK available).
if isinstance(messages, str): """
messages = [messages] if isinstance(messages, str):
messages = [messages]
if not messages:
return True if not messages:
return True
success = True
is_dm = destination is not None success = True
is_dm = destination is not None
for i, msg in enumerate(messages):
# Randomized delay before sending (except first message) for i, msg in enumerate(messages):
if i > 0: if is_dm and hasattr(self.connector, 'send_and_wait_ack'):
delay = random.uniform(self.config.delay_min, self.config.delay_max) # Send and wait for ACK
await asyncio.sleep(delay) ack = await asyncio.get_event_loop().run_in_executor(
None,
if is_dm and hasattr(self.connector, 'send_and_wait_ack'): self.connector.send_and_wait_ack,
# DMs: send and wait for ACK msg, destination, channel, 30.0,
ack = await asyncio.get_event_loop().run_in_executor( )
None,
self.connector.send_and_wait_ack, if ack:
msg, destination, channel, 30.0, # ACK received - next message sends immediately (no delay)
) logger.debug(f"ACK msg {i+1}/{len(messages)}: {msg[:50]}...")
continue
if not ack:
# Retry once # No ACK - retry same message once after short pause
logger.warning(f"No ACK for msg {i+1}/{len(messages)}, retrying...") logger.warning(f"No ACK for msg {i+1}/{len(messages)}, retrying...")
await asyncio.sleep(random.uniform(3.0, 5.0)) await asyncio.sleep(2.0)
ack = await asyncio.get_event_loop().run_in_executor(
None, ack = await asyncio.get_event_loop().run_in_executor(
self.connector.send_and_wait_ack, None,
msg, destination, channel, 30.0, self.connector.send_and_wait_ack,
) msg, destination, channel, 30.0,
if not ack: )
logger.error(f"No ACK after retry for msg {i+1}/{len(messages)}, skipping remaining")
success = False if ack:
break logger.debug(f"ACK on retry msg {i+1}/{len(messages)}")
continue
logger.debug(f"Sent+ACK msg {i+1}/{len(messages)}: {msg[:50]}...")
else: # Double failure - abort
# Broadcasts or fallback: fire and delay logger.error(f"No ACK after retry msg {i+1}/{len(messages)}, aborting remaining")
sent = self.connector.send_message( success = False
text=msg, break
destination=destination,
channel=channel, else:
) # Broadcasts or fallback: delay-based pacing
if not sent: if i > 0:
logger.error(f"Failed to send message {i+1}/{len(messages)}") delay = random.uniform(self.config.delay_min, self.config.delay_max)
success = False await asyncio.sleep(delay)
break
sent = self.connector.send_message(
logger.debug(f"Sent msg {i+1}/{len(messages)}: {msg[:50]}...") text=msg,
destination=destination,
return success channel=channel,
)
if not sent:
logger.error(f"Failed to send message {i+1}/{len(messages)}")
success = False
break
logger.debug(f"Sent msg {i+1}/{len(messages)}: {msg[:50]}...")
return success