{"repo":"NousResearch/hermes-agent","public_id":"22771","display_label":"Telegram notification mode to suppress intermediate pushes","summary":"These members all concern adding a notification mode to the Telegram gateway integration that suppresses intermediate push notifications. The work appears across a feature request and at least two related implementation attempts.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":22793,"merged_at":"2026-05-09T20:38:25+00:00"},"resolution":{"state":"resolved","by":"pr-22793"},"freshness":{"state":"verified","last_verified_at":"2026-05-09T20:38:25+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":22771,"pr_number":22793,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-09T20:38:25+00:00"}],"members":[{"kind":"issue","number":22771,"title":"Telegram: add notification mode to suppress intermediate push notifications","url":null,"state":"closed"},{"kind":"pr","number":22772,"title":"feat(gateway): add Telegram notification mode to suppress intermediate push notifications","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22793","number":22793}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":22793,"title":"feat(gateway): add Telegram notification mode (salvage #22772)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/base.py","gateway/platforms/telegram.py","gateway/run.py","scripts/release.py","tests/gateway/test_base_topic_sessions.py"],"fix_hunks":[{"file":"gateway/platforms/base.py","hunks":["@@ -2950,6 +2950,18 @@ async def _stop_typing_task() -> None:\n                 if text_content:\n                     logger.info(\"[%s] Sending response (%d chars) to %s\", self.name, len(text_content), event.source.chat_id)\n                     _reply_anchor = _reply_anchor_for_event(event)\n+                    # Mark final response messages for notification delivery.\n+                    # Platform adapters that support per-message notification\n+                    # control (e.g. Telegram's disable_notification) use this\n+                    # flag to override silent-mode and ensure the final\n+                    # response triggers a push notification.\n+                    # Clone to avoid mutating the metadata shared with the\n+                    # typing-indicator task (which must remain unmarked).\n+                    if _thread_metadata is not None:\n+                        _thread_metadata = dict(_thread_metadata)\n+                        _thread_metadata[\"notify\"] = True\n+                    else:\n+                        _thread_metadata = {\"notify\": True}\n                     result = await self._send_with_retry(\n                         chat_id=event.source.chat_id,\n                         content=text_content,"]},{"file":"gateway/platforms/telegram.py","hunks":["@@ -319,6 +319,30 @@ def __init__(self, config: PlatformConfig):\n         # Slash-confirm button state: confirm_id → session_key (for /reload-mcp\n         # and any other slash-confirm prompts; see GatewayRunner._request_slash_confirm).\n         self._slash_confirm_state: Dict[str, str] = {}\n+        # Notification mode for message sends.\n+        # \"important\" — only final responses, approvals, and slash confirmations\n+        #               trigger notifications; tool progress, streaming, status\n+        #               messages are delivered silently via disable_notification.\n+        #               This is the default — Telegram users found per-tool-call\n+        #               push notifications too noisy.\n+        # \"all\"       — every message triggers a push notification (legacy\n+        #               behavior; opt-in via display.platforms.telegram.notifications).\n+        self._notifications_mode: str = \"important\"\n+\n+    def _notification_kwargs(\n+        self, metadata: Optional[Dict[str, Any]]\n+    ) -> Dict[str, Any]:\n+        \"\"\"Return disable_notification kwargs when the adapter is in silent mode.\n+\n+        In \"important\" mode, all message sends are silently delivered\n+        (disable_notification=True) unless the caller explicitly requests a\n+        notification by setting ``metadata[\"notify\"] = True``.\n+        \"\"\"\n+        if getattr(self, \"_notifications_mode\", \"important\") != \"important\":\n+            return {}\n+        if (metadata or {}).get(\"notify\"):\n+            return {}\n+        return {\"disable_notification\": True}\n \n     def _is_callback_user_authorized(\n         self,","@@ -1414,6 +1438,7 @@ async def send(\n                                 reply_to_message_id=reply_to_id,\n                                 **thread_kwargs,\n                                 **self._link_preview_kwargs(),\n+                                **self._notification_kwargs(metadata),\n                             )\n                         except Exception as md_error:\n                             # Markdown parsing failed, try plain text","@@ -1427,6 +1452,7 @@ async def send(\n                                     reply_to_message_id=reply_to_id,\n                                     **thread_kwargs,\n                                     **self._link_preview_kwargs(),\n+                                    **self._notification_kwargs(metadata),\n                                 )\n                             else:\n                                 raise","@@ -2374,6 +2400,7 @@ async def send_voice(\n                             \"caption\": caption[:1024] if caption else None,\n                             \"reply_to_message_id\": reply_to_id,\n                             **voice_thread_kwargs,\n+                            **self._notification_kwargs(metadata),\n                         },\n                         metadata,\n                         reply_to_id,","@@ -2398,6 +2425,7 @@ async def send_voice(\n                             \"caption\": caption[:1024] if caption else None,\n                             \"reply_to_message_id\": reply_to_id,\n                             **audio_thread_kwargs,\n+                            **self._notification_kwargs(metadata),\n                         },\n                         metadata,\n                         reply_to_id,","@@ -2534,6 +2562,7 @@ def _reset_opened_files() -> None:\n                         \"media\": media,\n                         \"reply_to_message_id\": reply_to_id,\n                         **thread_kwargs,\n+                        **self._notification_kwargs(metadata),\n                     },\n                     metadata,\n                     reply_to_id,","@@ -2591,6 +2620,7 @@ async def send_image_file(\n                         \"caption\": caption[:1024] if caption else None,\n                         \"reply_to_message_id\": reply_to_id,\n                         **thread_kwargs,\n+                        **self._notification_kwargs(metadata),\n                     },\n                     metadata,\n                     reply_to_id,","@@ -2686,6 +2716,7 @@ async def send_document(\n                         \"caption\": caption[:1024] if caption else None,\n                         \"reply_to_message_id\": reply_to_id,\n                         **thread_kwargs,\n+                        **self._notification_kwargs(metadata),\n                     },\n                     metadata,\n                     reply_to_id,","@@ -2731,6 +2762,7 @@ async def send_video(\n                         \"caption\": caption[:1024] if caption else None,\n                         \"reply_to_message_id\": reply_to_id,\n                         **thread_kwargs,\n+                        **self._notification_kwargs(metadata),\n                     },\n                     metadata,\n                     reply_to_id,","@@ -2781,6 +2813,7 @@ async def send_image(\n                     \"caption\": caption[:1024] if caption else None,\n                     \"reply_to_message_id\": reply_to_id,\n                     **photo_thread_kwargs,\n+                    **self._notification_kwargs(metadata),\n                 },\n                 metadata,\n                 reply_to_id,","@@ -2816,6 +2849,7 @@ async def send_image(\n                         \"caption\": caption[:1024] if caption else None,\n                         \"reply_to_message_id\": reply_to_id,\n                         **upload_thread_kwargs,\n+                        **self._notification_kwargs(metadata),\n                     },\n                     metadata,\n                     reply_to_id,","@@ -2861,6 +2895,7 @@ async def send_animation(\n                     \"caption\": caption[:1024] if caption else None,\n                     \"reply_to_message_id\": reply_to_id,\n                     **animation_thread_kwargs,\n+                    **self._notification_kwargs(metadata),\n                 },\n                 metadata,\n                 reply_to_id,"]},{"file":"gateway/run.py","hunks":["@@ -4649,7 +4649,29 @@ def _create_adapter(\n             if not check_telegram_requirements():\n                 logger.warning(\"Telegram: python-telegram-bot not installed\")\n                 return None\n-            return TelegramAdapter(config)\n+            adapter = TelegramAdapter(config)\n+            # Apply Telegram notification mode from config.  Controls whether\n+            # intermediate messages (tool progress, streaming, status) trigger\n+            # push notifications.  Supports ENV override for quick testing.\n+            _notify_mode = os.getenv(\"HERMES_TELEGRAM_NOTIFICATIONS\", \"\")\n+            if not _notify_mode:\n+                try:\n+                    _gw_cfg = _load_gateway_config()\n+                    _raw = cfg_get(_gw_cfg, \"display\", \"platforms\", \"telegram\", \"notifications\")\n+                    if _raw not in (None, \"\"):\n+                        _notify_mode = str(_raw).strip().lower()\n+                except Exception:\n+                    pass\n+            _notify_mode = _notify_mode or \"important\"\n+            if _notify_mode not in (\"all\", \"important\"):\n+                logger.warning(\n+                    \"Unknown telegram notifications mode '%s', \"\n+                    \"defaulting to 'important' (valid: all, important)\",\n+                    _notify_mode,\n+                )\n+                _notify_mode = \"important\"\n+            adapter._notifications_mode = _notify_mode\n+            return adapter\n         \n         elif platform == Platform.DISCORD:\n             from gateway.platforms.discord import DiscordAdapter, check_discord_requirements"]},{"file":"scripts/release.py","hunks":["@@ -65,6 +65,7 @@\n     \"[redacted:email]\": \"subtract0\",\n     \"[redacted:email]\": \"qWaitCrypto\",\n     \"[redacted:email]\": \"Qwinty\",\n+    \"[redacted:email]\": \"CalmProton\",\n     \"[redacted:email]\": \"wesleysimplicio\",\n     \"[redacted:email]\": \"SiliconID\",\n     \"[redacted:email]\": \"xieNniu\","]},{"file":"tests/gateway/test_base_topic_sessions.py","hunks":["@@ -130,8 +130,8 @@ async def hold_typing(_chat_id, interval=2.0, metadata=None):\n             {\n                 \"chat_id\": \"-1001\",\n                 \"content\": \"ack\",\n-                \"reply_to\": \"1\",\n-                \"metadata\": {\"thread_id\": \"17585\"},\n+                \"reply_to\": None,\n+                \"metadata\": {\"thread_id\": \"17585\", \"notify\": True},\n             }\n         ]\n         assert typing_calls == [\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}