diff --git a/engine/core/transports.py b/engine/core/transports.py index 3df04ba..31e398c 100644 --- a/engine/core/transports.py +++ b/engine/core/transports.py @@ -69,6 +69,13 @@ class SocketTransport(BaseTransport): self.lock = asyncio.Lock() # Prevent frame interleaving self._closed = False + def _ws_disconnected(self) -> bool: + """Best-effort check for websocket disconnection state.""" + return ( + self.ws.client_state == WebSocketState.DISCONNECTED + or self.ws.application_state == WebSocketState.DISCONNECTED + ) + async def send_event(self, event: dict) -> None: """ Send a JSON event via WebSocket. @@ -76,17 +83,27 @@ class SocketTransport(BaseTransport): Args: event: Event data as dictionary """ - if self._closed: + if self._closed or self._ws_disconnected(): logger.warning("Attempted to send event on closed transport") + self._closed = True return async with self.lock: try: await self.ws.send_text(json.dumps(event)) logger.debug(f"Sent event: {event.get('event', 'unknown')}") - except Exception as e: - logger.error(f"Error sending event: {e}") + except RuntimeError as e: self._closed = True + if self._ws_disconnected() or "close message has been sent" in str(e): + logger.debug(f"Skip sending event on closed websocket: {e!r}") + return + logger.error(f"Error sending event: {e!r}") + except Exception as e: + self._closed = True + if self._ws_disconnected(): + logger.debug(f"Skip sending event on disconnected websocket: {e!r}") + return + logger.error(f"Error sending event: {e!r}") async def send_audio(self, pcm_bytes: bytes) -> None: """ @@ -95,16 +112,26 @@ class SocketTransport(BaseTransport): Args: pcm_bytes: PCM audio data (16-bit, mono, 16kHz) """ - if self._closed: + if self._closed or self._ws_disconnected(): logger.warning("Attempted to send audio on closed transport") + self._closed = True return async with self.lock: try: await self.ws.send_bytes(pcm_bytes) - except Exception as e: - logger.error(f"Error sending audio: {e}") + except RuntimeError as e: self._closed = True + if self._ws_disconnected() or "close message has been sent" in str(e): + logger.debug(f"Skip sending audio on closed websocket: {e!r}") + return + logger.error(f"Error sending audio: {e!r}") + except Exception as e: + self._closed = True + if self._ws_disconnected(): + logger.debug(f"Skip sending audio on disconnected websocket: {e!r}") + return + logger.error(f"Error sending audio: {e!r}") async def close(self) -> None: """Close the WebSocket connection.""" @@ -112,10 +139,7 @@ class SocketTransport(BaseTransport): return self._closed = True - if ( - self.ws.client_state == WebSocketState.DISCONNECTED - or self.ws.application_state == WebSocketState.DISCONNECTED - ): + if self._ws_disconnected(): return try: