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
23 changes: 23 additions & 0 deletions plugins/platforms/discord/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4583,6 +4583,29 @@ async def _auto_create_thread(self, message: 'DiscordMessage') -> Optional[Any]:
thread = await message.create_thread(name=thread_name, auto_archive_duration=1440)
return thread
except Exception as direct_error:
# If another bot/client won the race and already created the thread
# from this starter message, Discord rejects a second
# message.create_thread() call. Reuse the existing thread instead
# of posting a new seed message, which would create a duplicate
# top-level post + duplicate thread.
existing_thread = getattr(message, "thread", None)
if existing_thread is not None:
return existing_thread
if self._client is not None:
try:
existing_thread = self._client.get_channel(int(message.id))
if existing_thread is None:
existing_thread = await self._client.fetch_channel(int(message.id))
if existing_thread is not None:
return existing_thread
except Exception:
logger.debug(
"[%s] Could not resolve existing Discord thread for starter message %s",
self.name,
getattr(message, "id", "unknown"),
exc_info=True,
)

display_name = getattr(getattr(message, "author", None), "display_name", None) or "unknown user"
reason = f"Auto-threaded from mention by {display_name}"
try:
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

# Auto-extracted from noreply emails + manual overrides
AUTHOR_MAP = {
"jeevesassistant00@gmail.com": "jeeves-assistant", # PR #48685 (Discord existing-thread auto-thread race)
"21178861+ScotterMonk@users.noreply.github.com": "ScotterMonk", # PR #50145 salvage (cron output truncation: adapter-aware chunking, #50126)
"rrandqua@gmail.com": "TutkuEroglu", # PR #50481 salvage (AGENTS.md stale token-lock adapter path)
"f@trycua.com": "f-trycua", # PR #50507 salvage (cross-platform computer_use; supersedes #44221/#30660)
Expand Down
35 changes: 35 additions & 0 deletions tests/gateway/test_discord_channel_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def __init__(self, channel_id: int = 1, name: str = "general", guild_name: str =
self.name = name
self.guild = SimpleNamespace(name=guild_name)
self.topic = None
self.send = AsyncMock()


class FakeThread:
Expand Down Expand Up @@ -281,6 +282,40 @@ async def test_no_thread_with_auto_thread_disabled_is_noop(adapter, monkeypatch)
adapter.handle_message.assert_awaited_once()


@pytest.mark.asyncio
async def test_auto_create_thread_reuses_existing_message_thread(adapter):
"""If another bot already created the starter thread, don't post a fallback seed."""
existing = FakeThread(channel_id=123, name="already-there")
channel = FakeTextChannel(channel_id=800)
message = make_message(channel=channel, content="Trip idea")
message.create_thread = AsyncMock(side_effect=RuntimeError("thread already exists"))
message.thread = existing

thread = await adapter._auto_create_thread(message)

assert thread is existing
channel.send.assert_not_awaited()


@pytest.mark.asyncio
async def test_auto_create_thread_fetches_existing_thread_by_starter_message_id(adapter):
"""Discord message-backed thread IDs match the starter message ID."""
existing = FakeThread(channel_id=123, name="already-there")
channel = FakeTextChannel(channel_id=800)
message = make_message(channel=channel, content="Trip idea")
message.create_thread = AsyncMock(side_effect=RuntimeError("thread already exists"))
message.thread = None
adapter._client.get_channel = MagicMock(return_value=existing)
adapter._client.fetch_channel = AsyncMock()

thread = await adapter._auto_create_thread(message)

assert thread is existing
adapter._client.get_channel.assert_called_once_with(message.id)
adapter._client.fetch_channel.assert_not_awaited()
channel.send.assert_not_awaited()


# ── config.py bridging ───────────────────────────────────────────────


Expand Down
Loading