From 858aa1eee9220c4e0b98abed75b6bcb63d6b3747 Mon Sep 17 00:00:00 2001 From: "@levsky22" <56281588+LevSky22@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:08:42 +0000 Subject: [PATCH] fix(slack): preserve thread context for commands --- plugins/platforms/slack/adapter.py | 9 +++++- tests/gateway/test_slack.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index acf653af101b..aeb9f80b7aea 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -3401,6 +3401,12 @@ async def _handle_slack_message( # When entering a thread for the first time (no existing session), # fetch thread context so the agent understands the conversation. + # + # Keep recovered history separate from ``text``. Prepending it here + # moves a recognized command away from character zero, so downstream + # command routing can misclassify it as conversational text. + # ``channel_context`` is prepended only after command dispatch. + channel_context = None if is_thread_reply and not self._has_active_session_for_thread( channel_id=channel_id, thread_ts=event_thread_ts, @@ -3414,7 +3420,7 @@ async def _handle_slack_message( team_id=team_id, ) if thread_context: - text = thread_context + text + channel_context = thread_context # Determine message type msg_type = MessageType.TEXT @@ -3752,6 +3758,7 @@ async def _handle_slack_message( media_types=media_types, reply_to_message_id=thread_ts if thread_ts != ts else None, channel_prompt=_channel_prompt, + channel_context=channel_context, reply_to_text=reply_to_text, auto_skill=_auto_skill, metadata={ diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index 8a351dc7fb66..472a2aaec689 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -1332,6 +1332,55 @@ async def test_bang_works_inside_thread(self, adapter): # same thread. assert msg_event.source.thread_id == "1111111111.000001" + @pytest.mark.asyncio + async def test_bang_queue_survives_first_thread_context_backfill(self, adapter): + """Backfill stays out of command text while remaining available.""" + adapter._has_active_session_for_thread = MagicMock(return_value=False) + adapter._fetch_thread_context = AsyncMock( + return_value=( + "[Slack thread context — earlier messages]\n" + "Alice: prior request\n" + "[End of thread context]\n\n" + ) + ) + adapter._fetch_thread_parent_text = AsyncMock(return_value="prior request") + + evt = self._make_event( + "!queue follow up after the current task", + thread_ts="1111111111.000001", + ) + await adapter._handle_slack_message(evt) + + msg_event = adapter.handle_message.call_args[0][0] + assert msg_event.text == "/queue follow up after the current task" + assert msg_event.message_type == MessageType.COMMAND + assert msg_event.get_command() == "queue" + assert msg_event.get_command_args() == "follow up after the current task" + assert msg_event.channel_context.startswith("[Slack thread context") + assert "prior request" in msg_event.channel_context + + @pytest.mark.asyncio + async def test_non_command_thread_backfill_uses_channel_context(self, adapter): + """Normal thread text remains separate without losing its backfill.""" + adapter._has_active_session_for_thread = MagicMock(return_value=False) + adapter._fetch_thread_context = AsyncMock( + return_value="[Slack thread context]\nAlice: earlier note\n" + ) + adapter._fetch_thread_parent_text = AsyncMock(return_value="earlier note") + + evt = self._make_event( + "follow up", + thread_ts="1111111111.000001", + ) + await adapter._handle_slack_message(evt) + + msg_event = adapter.handle_message.call_args[0][0] + assert msg_event.text == "follow up" + assert msg_event.message_type == MessageType.TEXT + assert msg_event.channel_context == ( + "[Slack thread context]\nAlice: earlier note\n" + ) + @pytest.mark.asyncio async def test_bang_unknown_token_passes_through_unchanged(self, adapter): """``!nice work`` is just a casual message — must NOT be rewritten."""