From 290c71a707e18f766a372129e9c2826ec9d268cb Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:37:53 -0700 Subject: [PATCH] fix(gateway): scope progress thread fallback to Slack only (salvage #3414) (#3488) * test(gateway): map fixture adapter by platform in progress threading tests * fix(gateway): scope progress thread fallback to Slack only --------- Co-authored-by: EmpireOperating <258363005+EmpireOperating@users.noreply.github.com> --- gateway/run.py | 17 +++-- tests/gateway/test_run_progress_topics.py | 90 ++++++++++++++++++++++- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 13043b97c..e2a2211dd 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -4974,12 +4974,17 @@ class GatewayRunner: progress_queue.put(msg) # Background task to send progress messages - # Accumulates tool lines into a single message that gets edited - # For DM top-level Slack messages, source.thread_id is None but the - # final reply will be threaded under the original message via reply_to. - # Use event_message_id as fallback so progress messages land in the - # same thread as the final response instead of going to the DM root. - _progress_thread_id = source.thread_id or event_message_id + # Accumulates tool lines into a single message that gets edited. + # + # Threading metadata is platform-specific: + # - Slack DM threading needs event_message_id fallback (reply thread) + # - Telegram uses message_thread_id only for forum topics; passing a + # normal DM/group message id as thread_id causes send failures + # - Other platforms should use explicit source.thread_id only + if source.platform == Platform.SLACK: + _progress_thread_id = source.thread_id or event_message_id + else: + _progress_thread_id = source.thread_id _progress_metadata = {"thread_id": _progress_thread_id} if _progress_thread_id else None async def send_progress_messages(): diff --git a/tests/gateway/test_run_progress_topics.py b/tests/gateway/test_run_progress_topics.py index c4839133c..95ad2fba7 100644 --- a/tests/gateway/test_run_progress_topics.py +++ b/tests/gateway/test_run_progress_topics.py @@ -14,8 +14,8 @@ from gateway.session import SessionSource class ProgressCaptureAdapter(BasePlatformAdapter): - def __init__(self): - super().__init__(PlatformConfig(enabled=True, token="fake-token"), Platform.TELEGRAM) + def __init__(self, platform=Platform.TELEGRAM): + super().__init__(PlatformConfig(enabled=True, token="***"), platform) self.sent = [] self.edits = [] self.typing = [] @@ -76,7 +76,7 @@ def _make_runner(adapter): GatewayRunner = gateway_run.GatewayRunner runner = object.__new__(GatewayRunner) - runner.adapters = {Platform.TELEGRAM: adapter} + runner.adapters = {adapter.platform: adapter} runner._voice_mode = {} runner._prefill_messages = [] runner._ephemeral_system_prompt = "" @@ -133,3 +133,87 @@ async def test_run_agent_progress_stays_in_originating_topic(monkeypatch, tmp_pa ] assert adapter.edits assert all(call["metadata"] == {"thread_id": "17585"} for call in adapter.typing) + + +@pytest.mark.asyncio +async def test_run_agent_progress_does_not_use_event_message_id_for_telegram_dm(monkeypatch, tmp_path): + """Telegram DM progress must not reuse event message id as thread metadata.""" + monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "all") + + fake_dotenv = types.ModuleType("dotenv") + fake_dotenv.load_dotenv = lambda *args, **kwargs: None + monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv) + + fake_run_agent = types.ModuleType("run_agent") + fake_run_agent.AIAgent = FakeAgent + monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) + + adapter = ProgressCaptureAdapter(platform=Platform.TELEGRAM) + runner = _make_runner(adapter) + gateway_run = importlib.import_module("gateway.run") + monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path) + monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}) + + source = SessionSource( + platform=Platform.TELEGRAM, + chat_id="12345", + chat_type="dm", + thread_id=None, + ) + + result = await runner._run_agent( + message="hello", + context_prompt="", + history=[], + source=source, + session_id="sess-2", + session_key="agent:main:telegram:dm:12345", + event_message_id="777", + ) + + assert result["final_response"] == "done" + assert adapter.sent + assert adapter.sent[0]["metadata"] is None + assert all(call["metadata"] is None for call in adapter.typing) + + +@pytest.mark.asyncio +async def test_run_agent_progress_uses_event_message_id_for_slack_dm(monkeypatch, tmp_path): + """Slack DM progress should keep event ts fallback threading.""" + monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "all") + + fake_dotenv = types.ModuleType("dotenv") + fake_dotenv.load_dotenv = lambda *args, **kwargs: None + monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv) + + fake_run_agent = types.ModuleType("run_agent") + fake_run_agent.AIAgent = FakeAgent + monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) + + adapter = ProgressCaptureAdapter(platform=Platform.SLACK) + runner = _make_runner(adapter) + gateway_run = importlib.import_module("gateway.run") + monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path) + monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}) + + source = SessionSource( + platform=Platform.SLACK, + chat_id="D123", + chat_type="dm", + thread_id=None, + ) + + result = await runner._run_agent( + message="hello", + context_prompt="", + history=[], + source=source, + session_id="sess-3", + session_key="agent:main:slack:dm:D123", + event_message_id="1234567890.000001", + ) + + assert result["final_response"] == "done" + assert adapter.sent + assert adapter.sent[0]["metadata"] == {"thread_id": "1234567890.000001"} + assert all(call["metadata"] == {"thread_id": "1234567890.000001"} for call in adapter.typing)