fix(slack): expose shared-thread author mention target

In shared Slack threads the model saw only [sender name] prefixes, with
no verifiable current-author Slack user ID — so 'mention me again'
requests could bind to a stale or unrelated <@U...> pulled from names,
memory, or prior history (#17916).

Two cooperating changes:
- The shared-session sender prefix on Slack now carries the current
  author's ID from the event envelope:
  '[Alice | Slack user <@U123>] ...' — per-turn data, so it does not
  touch the cached system prompt.
- The Slack platform notes gain a shared-thread instruction to use the
  current turn's sender prefix as the only verified mention target and
  never guess or reuse historical mentions.

Fixes #17916.

Salvaged from #18711 by @LeonSGP43 (asdigitos), rebased over the
sender-name neutralization added on main (the ID is appended after
neutralizing the display name; the ID itself comes from the Slack
event, not user-editable text).
This commit is contained in:
LeonSGP43
2026-07-22 04:55:31 -07:00
committed by Teknium
parent 73d5c896ee
commit 503c0c0e51
4 changed files with 121 additions and 0 deletions
+10
View File
@@ -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
+7
View File
@@ -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
+43
View File
@@ -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(
@@ -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"