From cc12ab8290158dd5ce4940e333789a032625c52d Mon Sep 17 00:00:00 2001 From: Fran Fitzpatrick Date: Thu, 9 Apr 2026 18:28:53 -0500 Subject: [PATCH] fix(matrix): remove eyes reaction on processing complete The on_processing_complete handler was never removing the eyes reaction because _send_reaction didn't return the reaction event_id. Fix: - _send_reaction returns Optional[str] event_id - on_processing_start stores it in _pending_reactions dict - on_processing_complete redacts the eyes reaction before adding completion emoji --- gateway/platforms/matrix.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index cf72d956672..ac1362cdafd 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -177,6 +177,9 @@ class MatrixAdapter(BasePlatformAdapter): self._reactions_enabled: bool = os.getenv( "MATRIX_REACTIONS", "true" ).lower() not in ("false", "0", "no") + # Tracks the reaction event_id for in-progress (eyes) reactions. + # Key: (room_id, message_event_id) → reaction_event_id (for the eyes reaction). + self._pending_reactions: dict[tuple[str, str], str] = {} # Text batching: merge rapid successive messages (Telegram-style). # Matrix clients split long messages around 4000 chars. @@ -1437,12 +1440,14 @@ class MatrixAdapter(BasePlatformAdapter): async def _send_reaction( self, room_id: str, event_id: str, emoji: str, - ) -> bool: - """Send an emoji reaction to a message in a room.""" + ) -> Optional[str]: + """Send an emoji reaction to a message in a room. + Returns the reaction event_id on success, None on failure. + """ import nio if not self._client: - return False + return None content = { "m.relates_to": { "rel_type": "m.annotation", @@ -1457,12 +1462,12 @@ class MatrixAdapter(BasePlatformAdapter): ) if isinstance(resp, nio.RoomSendResponse): logger.debug("Matrix: sent reaction %s to %s", emoji, event_id) - return True + return resp.event_id logger.debug("Matrix: reaction send failed: %s", resp) - return False + return None except Exception as exc: logger.debug("Matrix: reaction send error: %s", exc) - return False + return None async def _redact_reaction( self, room_id: str, reaction_event_id: str, reason: str = "", @@ -1477,7 +1482,9 @@ class MatrixAdapter(BasePlatformAdapter): msg_id = event.message_id room_id = event.source.chat_id if msg_id and room_id: - await self._send_reaction(room_id, msg_id, "\U0001f440") + reaction_event_id = await self._send_reaction(room_id, msg_id, "\U0001f440") + if reaction_event_id: + self._pending_reactions[(room_id, msg_id)] = reaction_event_id async def on_processing_complete( self, event: MessageEvent, outcome: ProcessingOutcome, @@ -1491,9 +1498,11 @@ class MatrixAdapter(BasePlatformAdapter): return if outcome == ProcessingOutcome.CANCELLED: return - # Note: Matrix doesn't support removing a specific reaction easily - # without tracking the reaction event_id. We send the new reaction; - # the eyes stays (acceptable UX — both are visible). + # Remove the eyes reaction first, if we tracked its event_id. + reaction_key = (room_id, msg_id) + if reaction_key in self._pending_reactions: + eyes_event_id = self._pending_reactions.pop(reaction_key) + await self._redact_reaction(room_id, eyes_event_id) await self._send_reaction( room_id, msg_id,