fix(discord): preserve transport owner for thread renames

(cherry picked from commit b3e23293ed8b0f6597d442579304ea5095a9029c)
This commit is contained in:
Benjamin Rousseau
2026-09-12 19:49:47 -04:00
committed by Teknium
parent 8c58e4f976
commit fa787be2bd
2 changed files with 61 additions and 1 deletions
+8
View File
@@ -435,6 +435,14 @@ class GatewayTopicThreadsMixin:
copied_source = source
with suppress(Exception):
copied_source = dataclasses.replace(source)
# ``dataclasses.replace`` intentionally copies only declared fields.
# Preserve the in-process transport-owner ref that build_source()
# stamps on live inbound events; multiplex routed profiles need it
# for side effects such as Discord thread rename, because the
# runtime profile may not own the Discord adapter/token.
transport_ref = getattr(source, "_transport_adapter_ref", None)
if transport_ref is not None:
setattr(copied_source, "_transport_adapter_ref", transport_ref)
future = safe_schedule_threadsafe(
make_coro(copied_source), loop, logger=logger, log_message=f"{label} failed to schedule",
)
@@ -10,10 +10,12 @@ ten minutes, so the throwaway can be the one that survives.
from __future__ import annotations
import types
import weakref
import pytest
from gateway.config import Platform
from gateway.session import SessionSource
from gateway.run import GatewayRunner
from gateway.run_turn_runner import TurnRunner
@@ -56,7 +58,7 @@ def test_the_rename_waits_for_the_model_title(lane):
assert renames == ["Fix flaky auth test"]
@pytest.mark.asyncio
@pytest.mark.anyio
async def test_native_thread_rename_passes_only_the_initial_name_guard():
"""The shared rename lane must honor the strict native adapter contract."""
calls: list[tuple[str, str, str | None]] = []
@@ -102,3 +104,53 @@ async def test_native_thread_rename_passes_only_the_initial_name_guard():
)
assert calls == [("999", "Semantic Session Title", "Initial words")]
def test_title_thread_copy_preserves_transport_adapter_ref(monkeypatch):
"""Multiplex-routed sources must keep their transport owner for side effects."""
captured_sources = []
class Adapter:
pass
adapter = Adapter()
async def noop():
return None
def fake_schedule(coro, loop, logger=None, log_message=None):
# The test only inspects the source passed into make_coro; close the
# coroutine so pytest does not warn about an unawaited object.
coro.close()
return None
monkeypatch.setattr("gateway.run.safe_schedule_threadsafe", fake_schedule)
source = SessionSource(
platform=Platform.DISCORD,
chat_id="thread-1",
chat_type="thread",
thread_id="thread-1",
profile="runtime-profile",
auto_thread_created=True,
auto_thread_initial_name="Initial words",
)
source._transport_adapter_ref = weakref.ref(adapter)
runner = types.SimpleNamespace(
_gateway_loop=types.SimpleNamespace(is_closed=lambda: False),
_schedule_rename_from_title_thread=GatewayRunner._schedule_rename_from_title_thread,
)
runner._schedule_rename_from_title_thread(
runner,
source,
lambda copied: captured_sources.append(copied) or noop(),
"Discord semantic thread rename",
)
assert len(captured_sources) == 1
copied = captured_sources[0]
assert copied is not source
assert copied.profile == "runtime-profile"
assert copied._transport_adapter_ref() is adapter