From 60f84c6c28bf88b15dbcd8186cd56b15769111c8 Mon Sep 17 00:00:00 2001 From: houenyang-momo <259054917+houenyang-momo@users.noreply.github.com> Date: Sat, 23 May 2026 16:32:18 +0000 Subject: [PATCH] gateway: quiet Telegram operational chatter --- cli-config.yaml.example | 15 ++++++++ gateway/display_config.py | 36 +++++++++++++++++- gateway/run.py | 40 +++++++++++++++++--- tests/gateway/test_busy_session_ack.py | 40 +++++++++++++++++++- tests/gateway/test_display_config.py | 43 ++++++++++++++++++---- tests/gateway/test_run_progress_topics.py | 5 ++- website/docs/user-guide/messaging/index.md | 16 +++++++- 7 files changed, 177 insertions(+), 18 deletions(-) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index f670f76fc26..303bdb5c13b 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -916,6 +916,11 @@ display: # Toggle at runtime with /verbose in the CLI tool_progress: all + # Per-platform defaults can be quieter than the global setting. Telegram + # defaults to final-answer-first on mobile: tool progress, interim assistant + # updates, long-running "Still working..." heartbeats, and detailed busy acks + # are off unless re-enabled under display.platforms.telegram. + # Auto-cleanup of temporary progress bubbles after the final response lands. # On platforms that support message deletion (currently Telegram), this # removes the tool-progress bubble, "⏳ Still working..." notices, and @@ -939,6 +944,16 @@ display: # false: Only send the final response interim_assistant_messages: true + # Gateway-only long-running status heartbeats. + # When false, the platform does not receive periodic "Still working..." + # notifications even if agent.gateway_notify_interval is non-zero. + # Telegram default: false. Other high-capability chat platforms default true. + long_running_notifications: true + + # Include detailed iteration/tool/status context in busy acknowledgments when + # a new user message arrives while a run is active. Telegram default: false. + busy_ack_detail: true + # What Enter does when Hermes is already busy (CLI and gateway platforms). # interrupt: Interrupt the current run and redirect Hermes (default) # queue: Queue your message for the next turn diff --git a/gateway/display_config.py b/gateway/display_config.py index eab6bebc783..e8998b55795 100644 --- a/gateway/display_config.py +++ b/gateway/display_config.py @@ -35,6 +35,11 @@ _GLOBAL_DEFAULTS: dict[str, Any] = { "show_reasoning": False, "tool_preview_length": 0, "streaming": None, # None = follow top-level streaming config + # Gateway-only assistant/status chatter controls. These default on for + # back-compat, but mobile platforms can opt down to final-answer-first. + "interim_assistant_messages": True, + "long_running_notifications": True, + "busy_ack_detail": True, # When true, delete tool-progress / "Still working..." / status bubbles # after the final response lands on platforms that support message # deletion (e.g. Telegram). Off by default — progress is still shown @@ -56,6 +61,9 @@ _TIER_HIGH = { "show_reasoning": False, "tool_preview_length": 40, "streaming": None, # follow global + "interim_assistant_messages": True, + "long_running_notifications": True, + "busy_ack_detail": True, } _TIER_MEDIUM = { @@ -63,6 +71,9 @@ _TIER_MEDIUM = { "show_reasoning": False, "tool_preview_length": 40, "streaming": None, + "interim_assistant_messages": True, + "long_running_notifications": True, + "busy_ack_detail": True, } _TIER_LOW = { @@ -70,6 +81,9 @@ _TIER_LOW = { "show_reasoning": False, "tool_preview_length": 40, "streaming": False, + "interim_assistant_messages": False, + "long_running_notifications": False, + "busy_ack_detail": False, } _TIER_MINIMAL = { @@ -77,11 +91,23 @@ _TIER_MINIMAL = { "show_reasoning": False, "tool_preview_length": 0, "streaming": False, + "interim_assistant_messages": False, + "long_running_notifications": False, + "busy_ack_detail": False, } _PLATFORM_DEFAULTS: dict[str, dict[str, Any]] = { # Tier 1 — full edit support, personal/team use - "telegram": {**_TIER_HIGH, "tool_progress": "new"}, + # Telegram is usually a mobile inbox: default to final-answer-first and + # avoid permanent operational breadcrumbs unless users opt back in with + # display.platforms.telegram.tool_progress / long_running_notifications. + "telegram": { + **_TIER_HIGH, + "tool_progress": "off", + "interim_assistant_messages": False, + "long_running_notifications": False, + "busy_ack_detail": False, + }, "discord": _TIER_HIGH, # Tier 2 — edit support, often customer/workspace channels @@ -190,7 +216,13 @@ def _normalise(setting: str, value: Any) -> Any: if value is True: return "all" return str(value).lower() - if setting in {"show_reasoning", "streaming"}: + if setting in { + "show_reasoning", + "streaming", + "interim_assistant_messages", + "long_running_notifications", + "busy_ack_detail", + }: if isinstance(value, str): return value.lower() in {"true", "1", "yes", "on"} return bool(value) diff --git a/gateway/run.py b/gateway/run.py index 4cefe4303bb..09c36aa983a 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -3223,9 +3223,22 @@ class GatewayRunner: self._busy_ack_ts[session_key] = now - # Build a status-rich acknowledgment + # Build a status-rich acknowledgment. Mobile chat defaults keep this + # terse; detailed iteration/tool state is still available in logs and + # can be opted in per platform via display.platforms..busy_ack_detail. status_parts = [] - if running_agent and running_agent is not _AGENT_PENDING_SENTINEL: + busy_ack_detail_enabled = True + try: + from gateway.display_config import resolve_display_setting as _resolve_display_setting + _user_cfg = _load_gateway_config() + _platform_key = _platform_config_key(event.source.platform) + busy_ack_detail_enabled = bool( + _resolve_display_setting(_user_cfg, _platform_key, "busy_ack_detail", True) + ) + except Exception: + busy_ack_detail_enabled = True + + if busy_ack_detail_enabled and running_agent and running_agent is not _AGENT_PENDING_SENTINEL: try: summary = running_agent.get_activity_summary() iteration = summary.get("api_call_count", 0) @@ -15874,9 +15887,13 @@ class GatewayRunner: # in chat platforms while opting into concise mid-turn updates. interim_assistant_messages_enabled = ( source.platform != Platform.WEBHOOK - and is_truthy_value( - display_config.get("interim_assistant_messages"), - default=True, + and bool( + resolve_display_setting( + user_config, + platform_key, + "interim_assistant_messages", + True, + ) ) ) @@ -17413,6 +17430,19 @@ class GatewayRunner: # 0 = disable notifications. _NOTIFY_INTERVAL_RAW = _float_env("HERMES_AGENT_NOTIFY_INTERVAL", 180) _NOTIFY_INTERVAL = _NOTIFY_INTERVAL_RAW if _NOTIFY_INTERVAL_RAW > 0 else None + try: + _notify_enabled = bool( + resolve_display_setting( + user_config, + platform_key, + "long_running_notifications", + True, + ) + ) + except Exception: + _notify_enabled = True + if not _notify_enabled: + _NOTIFY_INTERVAL = None _notify_start = time.time() async def _notify_long_running(): diff --git a/tests/gateway/test_busy_session_ack.py b/tests/gateway/test_busy_session_ack.py index f13e16961e4..798dba8462f 100644 --- a/tests/gateway/test_busy_session_ack.py +++ b/tests/gateway/test_busy_session_ack.py @@ -378,8 +378,15 @@ class TestBusySessionAck: assert adapter._send_with_retry.call_count == 2 @pytest.mark.asyncio - async def test_includes_status_detail(self): + async def test_includes_status_detail_when_opted_in(self, monkeypatch): """Ack message should include iteration and tool info when available.""" + import gateway.run as _gr + + monkeypatch.setattr( + _gr, + "_load_gateway_config", + lambda: {"display": {"platforms": {"telegram": {"busy_ack_detail": True}}}}, + ) runner, sentinel = _make_runner() runner._busy_input_mode = "interrupt" adapter = _make_adapter() @@ -408,6 +415,37 @@ class TestBusySessionAck: assert "terminal" in content # current tool assert "10 min" in content # elapsed + @pytest.mark.asyncio + async def test_telegram_omits_status_detail_by_default(self): + """Telegram busy acks stay concise unless busy_ack_detail is enabled.""" + runner, sentinel = _make_runner() + runner._busy_input_mode = "interrupt" + adapter = _make_adapter() + + event = _make_event(text="yo") + sk = build_session_key(event.source) + + agent = MagicMock() + agent.get_activity_summary.return_value = { + "api_call_count": 21, + "max_iterations": 60, + "current_tool": "terminal", + "last_activity_ts": time.time(), + "last_activity_desc": "terminal", + "seconds_since_activity": 0.5, + } + runner._running_agents[sk] = agent + runner._running_agents_ts[sk] = time.time() - 600 + runner.adapters[event.source.platform] = adapter + + await runner._handle_active_session_busy_message(event, sk) + + content = adapter._send_with_retry.call_args.kwargs.get("content", "") + assert "Interrupting current task" in content + assert "21/60" not in content + assert "terminal" not in content + assert "10 min" not in content + @pytest.mark.asyncio async def test_draining_still_works(self): """Draining case should still produce the drain-specific message.""" diff --git a/tests/gateway/test_display_config.py b/tests/gateway/test_display_config.py index 5b50ec9c9ca..06c28debdb3 100644 --- a/tests/gateway/test_display_config.py +++ b/tests/gateway/test_display_config.py @@ -41,9 +41,9 @@ class TestResolveDisplaySetting: # Empty config — should get built-in defaults config = {} - # Telegram tier_high override: "new" (not "all") to reduce edit - # pressure during streaming on Telegram's ~1 edit/s flood envelope. - assert resolve_display_setting(config, "telegram", "tool_progress") == "new" + # Telegram is a mobile inbox by default — final-answer-first unless + # explicitly configured otherwise. + assert resolve_display_setting(config, "telegram", "tool_progress") == "off" # Email defaults to tier_minimal → "off" assert resolve_display_setting(config, "email", "tool_progress") == "off" @@ -180,12 +180,11 @@ class TestPlatformDefaults: """Built-in defaults reflect platform capability tiers.""" def test_high_tier_platforms(self): - """Discord defaults to 'all' tool progress; Telegram is in tier_high - but overrides tool_progress to 'new' (less edit pressure).""" + """Discord defaults to 'all'; Telegram defaults quiet for mobile.""" from gateway.display_config import resolve_display_setting - # Telegram: tier_high member with tool_progress="new" override. - assert resolve_display_setting({}, "telegram", "tool_progress") == "new" + # Telegram: tier_high transport, but quiet mobile default. + assert resolve_display_setting({}, "telegram", "tool_progress") == "off" # Discord: pure tier_high. assert resolve_display_setting({}, "discord", "tool_progress") == "all" @@ -229,6 +228,36 @@ class TestPlatformDefaults: assert resolve_display_setting({}, "telegram", "streaming") is None + def test_telegram_mobile_chatter_defaults_off(self): + """Telegram suppresses operational chat noise unless opted in.""" + from gateway.display_config import resolve_display_setting + + assert resolve_display_setting({}, "telegram", "interim_assistant_messages") is False + assert resolve_display_setting({}, "telegram", "long_running_notifications") is False + assert resolve_display_setting({}, "telegram", "busy_ack_detail") is False + assert resolve_display_setting({}, "discord", "interim_assistant_messages") is True + assert resolve_display_setting({}, "discord", "long_running_notifications") is True + assert resolve_display_setting({}, "discord", "busy_ack_detail") is True + + def test_telegram_mobile_chatter_can_opt_in(self): + """Per-platform config can re-enable Telegram status chatter.""" + from gateway.display_config import resolve_display_setting + + config = { + "display": { + "platforms": { + "telegram": { + "interim_assistant_messages": True, + "long_running_notifications": "yes", + "busy_ack_detail": "on", + } + } + } + } + assert resolve_display_setting(config, "telegram", "interim_assistant_messages") is True + assert resolve_display_setting(config, "telegram", "long_running_notifications") is True + assert resolve_display_setting(config, "telegram", "busy_ack_detail") is True + # --------------------------------------------------------------------------- # Config migration: tool_progress_overrides → display.platforms diff --git a/tests/gateway/test_run_progress_topics.py b/tests/gateway/test_run_progress_topics.py index 5b7dfb821b0..09a6030226c 100644 --- a/tests/gateway/test_run_progress_topics.py +++ b/tests/gateway/test_run_progress_topics.py @@ -784,12 +784,13 @@ async def test_run_agent_surfaces_real_interim_commentary(monkeypatch, tmp_path) @pytest.mark.asyncio -async def test_run_agent_surfaces_interim_commentary_by_default(monkeypatch, tmp_path): +async def test_run_agent_surfaces_interim_commentary_when_globally_enabled(monkeypatch, tmp_path): adapter, result = await _run_with_agent( monkeypatch, tmp_path, CommentaryAgent, - session_id="sess-commentary-default-on", + session_id="sess-commentary-global-on", + config_data={"display": {"interim_assistant_messages": True}}, ) assert any(call["content"] == "I'll inspect the repo first." for call in adapter.sent) diff --git a/website/docs/user-guide/messaging/index.md b/website/docs/user-guide/messaging/index.md index 09c1a2d7ba0..a2c29c700fa 100644 --- a/website/docs/user-guide/messaging/index.md +++ b/website/docs/user-guide/messaging/index.md @@ -507,9 +507,23 @@ Scheduled auto-resume for N restart-interrupted session(s) No configuration is required. If you don't want the heads-up, set `gateway_restart_notification: false` on the platform. +### Mobile-friendly progress defaults + +Telegram defaults to final-answer-first output: no tool-progress stream, no periodic "still working…" heartbeat, no interim assistant status messages, and concise busy acknowledgments. Opt back into any of those per platform: + +```yaml +display: + platforms: + telegram: + tool_progress: new + interim_assistant_messages: true + long_running_notifications: true + busy_ack_detail: true +``` + ### Progress bubble cleanup (opt-in) -Tool-progress messages, the "still working…" heartbeat, and status-callback bubbles can be auto-deleted after the final response lands. Enable per-platform via `display.platforms..cleanup_progress`: +Tool-progress messages, the "still working…" heartbeat, and status-callback bubbles can also be auto-deleted after the final response lands. Enable per-platform via `display.platforms..cleanup_progress`: ```yaml display: