feat(slack): thread engagement — auto-respond in bot-started and mentioned threads (#5897)

When the bot sends a message in a thread, track its ts in _bot_message_ts.
When the bot is @mentioned in a thread, register it in _mentioned_threads.
Both sets enable auto-responding to future messages in those threads
without requiring repeated @mentions — making the bot behave like a
team member that stays engaged once a conversation starts.

Channel message gating now checks 4 signals (in order):
  1. @mention in this message
  2. Reply in a thread the bot started/participated in (_bot_message_ts)
  3. Message in a thread where the bot was previously @mentioned (_mentioned_threads)
  4. Existing session for this thread (_has_active_session_for_thread — survives restarts)

Thread context fetching now triggers on ANY first-entry path (not just
@mention), so the agent gets context whether it's entering via a mention,
a bot-thread reply, or a mentioned-thread auto-trigger.

Both tracking sets are bounded (5000 cap with prune-oldest-half) to prevent
unbounded memory growth in long-running deployments.

Salvaged from PR #5754 by @hhhonzik. Preserves our existing approval buttons,
thread context fetching, and session key fix. Does NOT include the
edit_message format_message() removal (that was a regression in the original PR).

Tests: 4 new tests for bot-ts tracking and mentioned-thread bounds.
This commit is contained in:
Teknium
2026-04-07 11:12:08 -07:00
committed by GitHub
parent ab0c1e58f1
commit e49c8bbbbb
2 changed files with 120 additions and 33 deletions

View File

@@ -371,3 +371,56 @@ class TestSessionKeyFix:
channel_id="C1", thread_ts="1000.0", user_id="U123"
)
assert result is False
# ===========================================================================
# Thread engagement — bot-started threads & mentioned threads
# ===========================================================================
class TestThreadEngagement:
"""Test _bot_message_ts and _mentioned_threads tracking."""
@pytest.mark.asyncio
async def test_send_tracks_bot_message_ts(self):
"""Bot's sent messages are tracked so thread replies work without @mention."""
adapter = _make_adapter()
mock_client = adapter._team_clients["T1"]
mock_client.chat_postMessage = AsyncMock(return_value={"ts": "9000.1"})
await adapter.send(chat_id="C1", content="Hello!", metadata={"thread_id": "8000.0"})
assert "9000.1" in adapter._bot_message_ts
# Thread root should also be tracked
assert "8000.0" in adapter._bot_message_ts
@pytest.mark.asyncio
async def test_bot_message_ts_cap(self):
"""Verify memory is bounded when many messages are sent."""
adapter = _make_adapter()
adapter._BOT_TS_MAX = 10 # low cap for testing
mock_client = adapter._team_clients["T1"]
for i in range(20):
mock_client.chat_postMessage = AsyncMock(return_value={"ts": f"{i}.0"})
await adapter.send(chat_id="C1", content=f"msg {i}")
assert len(adapter._bot_message_ts) <= 10
def test_mentioned_threads_populated_on_mention(self):
"""When bot is @mentioned in a thread, that thread is tracked."""
adapter = _make_adapter()
# Simulate what _handle_slack_message does on mention
adapter._mentioned_threads.add("1000.0")
assert "1000.0" in adapter._mentioned_threads
def test_mentioned_threads_cap(self):
"""Verify _mentioned_threads is bounded."""
adapter = _make_adapter()
adapter._MENTIONED_THREADS_MAX = 10
for i in range(15):
adapter._mentioned_threads.add(f"{i}.0")
if len(adapter._mentioned_threads) > adapter._MENTIONED_THREADS_MAX:
to_remove = list(adapter._mentioned_threads)[:adapter._MENTIONED_THREADS_MAX // 2]
for t in to_remove:
adapter._mentioned_threads.discard(t)
assert len(adapter._mentioned_threads) <= 10