From 971b7742826be4209c0b8ced77e6b1b64dd26f3f Mon Sep 17 00:00:00 2001 From: Kaloyan Tenchov Date: Sat, 16 May 2026 11:30:53 -0400 Subject: [PATCH] refactor(signal): wrap top-level receive handler with _safe_handle Replace the inline try/except at the end of _handle_receive_notification with a small async context manager that swallows the exception, logs self.logger.error with the offending payload's repr (bounded to 200 chars), and attaches the traceback via logger.opt(exception=True). The previous log line only carried `e`, so diagnosing a bad envelope from production logs required correlating timestamps. The wrapper is generic so future receive/dispatch sites can adopt it; for now only this site uses it. Co-Authored-By: Claude Opus 4.7 --- nanobot/channels/signal.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/nanobot/channels/signal.py b/nanobot/channels/signal.py index 3fd23c780..fce941c74 100644 --- a/nanobot/channels/signal.py +++ b/nanobot/channels/signal.py @@ -8,7 +8,8 @@ import re import shutil import unicodedata from collections import deque -from collections.abc import Callable +from collections.abc import AsyncIterator, Callable +from contextlib import asynccontextmanager from dataclasses import dataclass, field from pathlib import Path from typing import Any @@ -557,10 +558,29 @@ class SignalChannel(BaseChannel): self.logger.error(f"Error in SSE receive loop: {e}") raise + @asynccontextmanager + async def _safe_handle( + self, action: str, payload: Any = None + ) -> AsyncIterator[None]: + """Swallow and log any exception from a top-level handler block. + + Logs `self.logger.error` with the action name, the exception, and a + bounded ``repr`` of the offending payload so the offending input is + recoverable from logs without having to correlate by timestamp. + """ + try: + yield + except Exception as e: + snippet = repr(payload)[:200] if payload is not None else "" + text = f"Error in {action}: {e}" + if snippet: + text += f" | payload={snippet}" + self.logger.opt(exception=True).error(text) + async def _handle_receive_notification(self, params: dict[str, Any]) -> None: """Handle incoming message notification from signal-cli.""" self.logger.debug(f"_handle_receive_notification called with: {params}") - try: + async with self._safe_handle("receive notification", params): # Extract envelope from SSE notification: {"envelope": {...}} envelope = params.get("envelope", {}) @@ -613,9 +633,6 @@ class SignalChannel(BaseChannel): elif typing_message: pass # Ignore typing indicators - except Exception as e: - self.logger.error(f"Error handling receive notification: {e}") - async def _handle_data_message( self, sender_id: str,