diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 0bb6f26351d9..ded7b40891e7 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -3825,6 +3825,12 @@ class SlackAdapter(BasePlatformAdapter): # 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, @@ -3838,7 +3844,7 @@ class SlackAdapter(BasePlatformAdapter): team_id=team_id, ) if thread_context: - text = thread_context + text + channel_context = thread_context # Determine message type msg_type = MessageType.TEXT @@ -4176,6 +4182,7 @@ class SlackAdapter(BasePlatformAdapter): 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 b5946e8b968f..5bf9dc5e4ab5 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -1483,6 +1483,55 @@ class TestBangPrefixCommands: # 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."""