diff --git a/gateway/run.py b/gateway/run.py index 354962ee62..48a0161dc9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -11818,6 +11818,16 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # (mirrors the same field's treatment in # build_session_context_prompt via _format_untrusted_prompt_value). _safe_user_name = neutralize_untrusted_inline_text(source.user_name) + # On Slack, expose the current author's verifiable user ID next to + # the display name (#17916): "mention me again" requests need a + # trusted `<@U...>` target for the CURRENT speaker — display names + # are ambiguous and historical mentions may point at someone else. + # The user_id comes from the Slack event envelope (not + # user-editable text), so it does not need neutralization. + if source.platform == Platform.SLACK and source.user_id: + _safe_user_name = ( + f"{_safe_user_name} | Slack user <@{source.user_id}>" + ) message_text = f"[{_safe_user_name}] {message_text}" # Prepend channel context from history backfill (if any). This diff --git a/gateway/session.py b/gateway/session.py index eb28a7ca14..390faf7a98 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -526,6 +526,13 @@ def build_session_context_prompt( "current message's Slack block/attachment payload when available, but " "you still cannot call Slack APIs yourself." ) + if context.shared_multi_user_session: + lines.append( + "In shared Slack threads, use the current turn's sender prefix " + "as the only verified current-author mention target. Do not " + "guess or reuse `<@U...>` mentions from names, memory, or prior " + "conversation history." + ) elif context.source.platform == Platform.DISCORD: # Inject the Discord IDs block only when the agent actually has # Discord tools loaded this session — i.e. the user opted into diff --git a/tests/gateway/test_session.py b/tests/gateway/test_session.py index 4b05c509c1..2ce5e3285a 100644 --- a/tests/gateway/test_session.py +++ b/tests/gateway/test_session.py @@ -299,6 +299,49 @@ class TestBuildSessionContextPrompt: assert "pin" in prompt.lower() assert "current message's slack block/attachment payload" in prompt.lower() + def test_shared_slack_prompt_warns_against_guessed_self_mentions(self): + """Shared Slack threads must instruct the agent to bind mention + targets to the current turn's sender prefix (#17916).""" + config = GatewayConfig( + platforms={ + Platform.SLACK: PlatformConfig(enabled=True, token="fake"), + }, + ) + source = SessionSource( + platform=Platform.SLACK, + chat_id="C123", + chat_name="team-channel", + chat_type="group", + user_id="U123", + user_name="Alice", + thread_id="171.000", + ) + ctx = build_session_context(source, config) + prompt = build_session_context_prompt(ctx) + + assert "current turn's sender prefix" in prompt + assert "Do not guess or reuse `<@U...>` mentions" in prompt + + def test_non_shared_slack_prompt_omits_self_mention_guidance(self): + """1:1 Slack DMs are single-user: the shared-thread mention guidance + must not appear.""" + config = GatewayConfig( + platforms={ + Platform.SLACK: PlatformConfig(enabled=True, token="fake"), + }, + ) + source = SessionSource( + platform=Platform.SLACK, + chat_id="D123", + chat_type="dm", + user_id="U123", + user_name="Alice", + ) + ctx = build_session_context(source, config) + prompt = build_session_context_prompt(ctx) + + assert "current turn's sender prefix" not in prompt + def test_discord_prompt_with_channel_topic(self): """Channel topic should appear in the session context prompt.""" config = GatewayConfig( diff --git a/tests/gateway/test_shared_group_sender_prefix.py b/tests/gateway/test_shared_group_sender_prefix.py index 9f0e525f64..f2bd5e6716 100644 --- a/tests/gateway/test_shared_group_sender_prefix.py +++ b/tests/gateway/test_shared_group_sender_prefix.py @@ -68,3 +68,64 @@ async def test_preprocess_keeps_plain_text_for_default_group_sessions(): ) assert result == "hello" + + +@pytest.mark.asyncio +async def test_preprocess_includes_slack_author_mention_for_shared_thread(): + """Shared Slack threads expose the current author's verifiable user ID + next to the display name so 'mention me again' requests can bind the + mention to the CURRENT speaker (#17916).""" + runner = _make_runner( + GatewayConfig( + platforms={ + Platform.SLACK: PlatformConfig(enabled=True, token="fake"), + }, + ) + ) + source = SessionSource( + platform=Platform.SLACK, + chat_id="C123", + chat_name="team-channel", + chat_type="group", + user_id="U123", + user_name="Alice", + thread_id="171.000", + ) + event = MessageEvent(text="mention me again", source=source) + + result = await runner._prepare_inbound_message_text( + event=event, + source=source, + history=[], + ) + + assert result == "[Alice | Slack user <@U123>] mention me again" + + +@pytest.mark.asyncio +async def test_preprocess_slack_shared_thread_without_user_id_keeps_name_only(): + """No user_id on the source → fall back to the plain name prefix.""" + runner = _make_runner( + GatewayConfig( + platforms={ + Platform.SLACK: PlatformConfig(enabled=True, token="fake"), + }, + ) + ) + source = SessionSource( + platform=Platform.SLACK, + chat_id="C123", + chat_name="team-channel", + chat_type="group", + user_name="Alice", + thread_id="171.000", + ) + event = MessageEvent(text="hello", source=source) + + result = await runner._prepare_inbound_message_text( + event=event, + source=source, + history=[], + ) + + assert result == "[Alice] hello"