fix(slack): stop consuming slash reply context outside slash sends

_pop_slash_context fell back to a channel-only scan when the
_slash_user_id ContextVar was unset (i.e. send() invoked from a
non-slash code path such as a cron delivery or a normal channel reply).
That scan could steal another user's pending slash reply context: the
normal message got swallowed into an ephemeral response_url POST that
replaces the invoker's ack, and the slash invoker's actual reply then
posted publicly. Remove the fallback — when the ContextVar is unset,
match nothing.

Surgical reapply of PR #26788 (originally against gateway/platforms/slack.py).
This commit is contained in:
Soynchux 2026-07-22 08:18:28 -07:00 committed by Teknium
parent 5c27b9b19b
commit bd707103ef
No known key found for this signature in database
2 changed files with 58 additions and 30 deletions

View file

@ -1117,9 +1117,9 @@ class SlackAdapter(BasePlatformAdapter):
Uses the ``_slash_user_id`` ContextVar (set in ``_handle_slash_command``)
to match the exact ``(channel_id, user_id)`` key. This prevents a
concurrent slash command from a different user on the same channel from
stealing another user's ephemeral context. Falls back to a
channel-only scan when the ContextVar is unset (e.g. send() called
from a non-slash code path should not match anything).
stealing another user's ephemeral context. When the ContextVar is
unset (e.g. send() called from a non-slash code path), do not match
anything otherwise normal sends can steal a pending slash reply.
"""
now = time.monotonic()
# Clean up stale entries on every lookup — dict is small.
@ -1136,16 +1136,7 @@ class SlackAdapter(BasePlatformAdapter):
if uid:
return self._slash_command_contexts.pop((chat_id, uid), None)
# Fallback: channel-only scan (only reachable when ContextVar is
# unset, i.e. send() called outside a slash-command async context).
match_key = None
for key in list(self._slash_command_contexts):
if key[0] == chat_id:
match_key = key
break
if match_key is None:
return None
return self._slash_command_contexts.pop(match_key)
return None
async def _send_slash_ephemeral(
self,