Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion plugins/platforms/slack/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -3414,7 +3420,7 @@ async def _handle_slack_message(
team_id=team_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping thread history separate is necessary, but it does not protect commands from prior Slack enrichment: Block Kit processing mutates text at :3163-3185 and text-file injection can prepend at :3640-3644. Preserve a canonical recognized command through those paths as well, or !queue can still gain unintended arguments or lose its leading slash.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the remaining Block Kit/file-enrichment and queued-event context propagation concerns appear to be addressed by #66310, which explicitly consolidates this PR’s thread-backfill fix with the broader command-integrity path. Since the implementations now overlap substantially, should #66069 remain as the focused alternative, or should it be closed in favor of #66310?

)
if thread_context:
text = thread_context + text
channel_context = thread_context

# Determine message type
msg_type = MessageType.TEXT
Expand Down Expand Up @@ -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={
Expand Down
49 changes: 49 additions & 0 deletions tests/gateway/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down