From ee559ccb3b337f3443d9e7628d8e3e185c163ea6 Mon Sep 17 00:00:00 2001 From: Angello Picasso <24831723+anpicasso@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:51:49 +0000 Subject: [PATCH] fix(discord): preserve title retry thread rename Fixes #127667. --- gateway/run_turn_runner.py | 29 +++++++++ .../gateway/test_session_title_rename_lane.py | 60 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/gateway/run_turn_runner.py b/gateway/run_turn_runner.py index 9c1a8ade5e..19045769fb 100644 --- a/gateway/run_turn_runner.py +++ b/gateway/run_turn_runner.py @@ -871,6 +871,35 @@ class TurnRunner: session_id = getattr(agent, "session_id", None) source = ctx.source runner = self._runner + # Native Discord only stamps auto-thread markers on the opening parent-channel event. A + # title retry arrives as an ordinary in-thread event; when the agent was rebuilt, there + # is no first-turn callback left to rename it. Recover only those immutable markers from + # this same thread's persisted origin, while keeping the live source's transport owner. + if ( + getattr(source, "platform", None) == Platform.DISCORD + and getattr(source, "chat_type", None) == "thread" + and getattr(source, "thread_id", None) + and getattr(source, "delivered_via_upstream_relay", False) is not True + and not runner._is_discord_auto_thread_lane(source) + ): + with suppress(Exception): + row = agent._session_db.get_session(session_id) + origin = json.loads((row or {}).get("origin_json") or "{}") + initial_name = origin.get("auto_thread_initial_name") + if ( + origin.get("platform") == Platform.DISCORD.value + and origin.get("chat_type") == "thread" + and str(origin.get("thread_id") or "") == str(source.thread_id) + and origin.get("auto_thread_created") is True + and isinstance(initial_name, str) + and initial_name.strip() + ): + from gateway.session_identity import replace_source + source = replace_source( + source, + auto_thread_created=True, + auto_thread_initial_name=initial_name, + ) # Both lanes spend a rate-limited platform call per title, so they use the model's title # only (TitleCallback); renaming twice burns Discord's 2-per-10-min budget on a throwaway. # Relay Discord predicate is shape-only: whether the connector auto-threaded our reply is diff --git a/tests/gateway/test_session_title_rename_lane.py b/tests/gateway/test_session_title_rename_lane.py index 683d36bd93..5d2a463820 100644 --- a/tests/gateway/test_session_title_rename_lane.py +++ b/tests/gateway/test_session_title_rename_lane.py @@ -9,6 +9,7 @@ ten minutes, so the throwaway can be the one that survives. from __future__ import annotations +import json import types import weakref @@ -58,6 +59,65 @@ def test_the_rename_waits_for_the_model_title(lane): assert renames == ["Fix flaky auth test"] +def test_discord_title_retry_recovers_auto_thread_origin(): + """A fresh agent on a later thread turn still wires the semantic rename.""" + scheduled = [] + current = SessionSource( + platform=Platform.DISCORD, + chat_id="thread-1", + chat_type="thread", + thread_id="thread-1", + message_id="follow-up", + ) + origin = SessionSource( + platform=Platform.DISCORD, + chat_id="thread-1", + chat_type="thread", + thread_id="thread-1", + message_id="opening-message", + auto_thread_created=True, + auto_thread_initial_name="Opening words", + ) + session_db = types.SimpleNamespace( + get_session=lambda session_id: { + "origin_json": json.dumps(origin.to_dict()), + } + ) + runner = types.SimpleNamespace( + _is_telegram_topic_lane=lambda source: False, + _is_discord_auto_thread_lane=lambda source: ( + source.platform == Platform.DISCORD + and source.chat_type == "thread" + and source.auto_thread_created is True + and bool(source.thread_id) + and bool(source.auto_thread_initial_name) + ), + _is_relay_discord_channel_lane=lambda source: False, + _schedule_discord_semantic_thread_rename=( + lambda source, session_id, title: scheduled.append( + (source, session_id, title) + ) + ), + ) + holder = types.SimpleNamespace( + _runner=runner, + _attach_session_title_callback=TurnRunner._attach_session_title_callback, + ) + agent = types.SimpleNamespace(session_id="sess-1", _session_db=session_db) + + holder._attach_session_title_callback( + holder, agent, types.SimpleNamespace(source=current) + ) + agent._on_session_title("Recovered semantic title", "llm") + + recovered, session_id, title = scheduled[0] + assert recovered.message_id == "follow-up" + assert recovered.auto_thread_created is True + assert recovered.auto_thread_initial_name == "Opening words" + assert session_id == "sess-1" + assert title == "Recovered semantic title" + + @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."""