From a724c3b9cf5f01e28365322ae5ae3a9579567806 Mon Sep 17 00:00:00 2001 From: Indigo Karasu Date: Fri, 15 May 2026 16:05:23 -0700 Subject: [PATCH] feat(telegram): pin incoming user message for duration of agent turn When a user sends a message on Telegram, the incoming message is now automatically pinned at the start of processing and unpinned when the agent finishes its turn. This gives the user a visual indicator that their message is being worked on, and keeps the conversation anchored. Changes: - telegram.py: Added pinChatMessage in on_processing_start and unpinChatMessage in on_processing_complete. Restructured both hooks so pin/unpin runs independently of the reactions feature (reactions are optional; pinning is always on). - telegram.py: Pass message_id through SessionSource so it's available in the session context. - session_context.py: Added HERMES_SESSION_MESSAGE_ID context var. - run.py: Pass source.message_id through set_session_vars. Pinning is silent (disable_notification=True) and failures are logged at debug level without interrupting message processing. Only the user's incoming message is pinned -- never the agent's replies. Auto-resume events (which have no message_id) are correctly skipped. --- gateway/platforms/telegram.py | 47 ++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 62619a08739..35c8d386c89 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -5416,16 +5416,25 @@ class TelegramAdapter(BasePlatformAdapter): return False async def on_processing_start(self, event: MessageEvent) -> None: - """Add an in-progress reaction when message processing begins.""" - if not self._reactions_enabled(): - return + """Add an in-progress reaction and pin the message when processing begins.""" chat_id = getattr(event.source, "chat_id", None) message_id = getattr(event, "message_id", None) if chat_id and message_id: - await self._set_reaction(chat_id, message_id, "\U0001f440") + if self._reactions_enabled(): + await self._set_reaction(chat_id, message_id, "\U0001f440") + # Pin the incoming message for the duration of the turn + if self._bot: + try: + await self._bot.pin_chat_message( + chat_id=int(chat_id), + message_id=int(message_id), + disable_notification=True, + ) + except Exception: + logger.debug("[Telegram] Failed to pin message %s in chat %s", message_id, chat_id) async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingOutcome) -> None: - """Swap the in-progress reaction for a final success/failure reaction. + """Swap the in-progress reaction for a final success/failure reaction and unpin. Unlike Discord (additive reactions), Telegram's set_message_reaction replaces all existing reactions in one call — no remove step needed. @@ -5437,17 +5446,25 @@ class TelegramAdapter(BasePlatformAdapter): another agent run to swap it to 👍/👎 — which never happens if the cancellation was the last activity in the chat. """ - if not self._reactions_enabled(): - return chat_id = getattr(event.source, "chat_id", None) message_id = getattr(event, "message_id", None) if not (chat_id and message_id): return - if outcome == ProcessingOutcome.CANCELLED: - await self._clear_reactions(chat_id, message_id) - else: - await self._set_reaction( - chat_id, - message_id, - "\U0001f44d" if outcome == ProcessingOutcome.SUCCESS else "\U0001f44e", - ) + if self._reactions_enabled(): + if outcome == ProcessingOutcome.CANCELLED: + await self._clear_reactions(chat_id, message_id) + else: + await self._set_reaction( + chat_id, + message_id, + "\U0001f44d" if outcome == ProcessingOutcome.SUCCESS else "\U0001f44e", + ) + # Unpin the message when processing is complete + if self._bot: + try: + await self._bot.unpin_chat_message( + chat_id=int(chat_id), + message_id=int(message_id), + ) + except Exception: + logger.debug("[Telegram] Failed to unpin message %s in chat %s", message_id, chat_id)