1111
1212from __future__ import annotations
1313
14+ import contextlib
1415import time
1516from pathlib import Path
1617
2829from ..session_monitor import NewWindowEvent
2930from ..thread_router import thread_router
3031from ..tmux_manager import tmux_manager
32+ from .message_sender import is_thread_gone
33+ from .topic_emoji import strip_emoji_prefix
3134
3235logger = structlog .get_logger ()
3336
@@ -192,11 +195,88 @@ async def create_topic_in_chat(
192195 )
193196
194197
198+ async def _topic_exists (bot : Bot , chat_id : int , thread_id : int ) -> bool | None :
199+ """Probe a Telegram topic. True=exists, False=gone, None=unknown."""
200+ try :
201+ msg = await bot .send_message (
202+ chat_id ,
203+ "\u200b " ,
204+ message_thread_id = thread_id ,
205+ disable_notification = True ,
206+ )
207+ except TelegramError as exc :
208+ if is_thread_gone (exc ):
209+ return False
210+ return None
211+ with contextlib .suppress (TelegramError ):
212+ await bot .delete_message (chat_id , msg .message_id )
213+ return True
214+
215+
216+ async def _rebind_existing_topic_by_name (
217+ event : NewWindowEvent , bot : Bot , topic_name : str
218+ ) -> bool :
219+ """Bind a stale same-name topic to a newly discovered manual window."""
220+ clean_topic_name = strip_emoji_prefix (topic_name )
221+ matches : list [tuple [int , int , str , int ]] = []
222+ bindings = list (thread_router .iter_thread_bindings ())
223+ for user_id , thread_id , old_window_id in bindings :
224+ if old_window_id == event .window_id :
225+ continue
226+ display_name = strip_emoji_prefix (thread_router .get_display_name (old_window_id ))
227+ if display_name != clean_topic_name :
228+ continue
229+ if await tmux_manager .find_window_by_id (old_window_id ):
230+ continue
231+ chat_id = thread_router .resolve_chat_id (user_id , thread_id )
232+ if chat_id == user_id :
233+ continue
234+ matches .append ((user_id , thread_id , old_window_id , chat_id ))
235+
236+ if len (matches ) != 1 :
237+ if len (matches ) > 1 :
238+ logger .warning (
239+ "Multiple stale same-name topics for window %s (%s); not rebinding" ,
240+ event .window_id ,
241+ clean_topic_name ,
242+ )
243+ return False
244+
245+ user_id , thread_id , old_window_id , chat_id = matches [0 ]
246+ exists = await _topic_exists (bot , chat_id , thread_id )
247+ if exists is False :
248+ thread_router .unbind_thread (user_id , thread_id )
249+ logger .info (
250+ "Dropped dead same-name topic thread %d for stale window %s" ,
251+ thread_id ,
252+ old_window_id ,
253+ )
254+ return False
255+ if exists is None :
256+ logger .info (
257+ "Could not probe same-name topic thread %d for stale window %s; not rebinding" ,
258+ thread_id ,
259+ old_window_id ,
260+ )
261+ return False
262+
263+ thread_router .bind_thread (user_id , thread_id , event .window_id , window_name = topic_name )
264+ thread_router .set_group_chat_id (user_id , thread_id , chat_id )
265+ logger .info (
266+ "Rebound existing topic thread %d from stale window %s to new window %s (%s)" ,
267+ thread_id ,
268+ old_window_id ,
269+ event .window_id ,
270+ clean_topic_name ,
271+ )
272+ return True
273+
274+
195275async def handle_new_window (event : NewWindowEvent , bot : Bot ) -> None :
196- """Create a Telegram forum topic for a newly detected tmux window.
276+ """Create or bind a Telegram forum topic for a newly detected tmux window.
197277
198- Skips if the window is already bound to a topic. Creates one topic per
199- unique group chat, binds all users in that chat.
278+ Skips if the window is already bound. Reuses one stale same-name topic when
279+ it still exists; otherwise creates one topic per unique group chat.
200280 """
201281 if _is_window_already_bound (event .window_id ):
202282 logger .debug (
@@ -207,6 +287,9 @@ async def handle_new_window(event: NewWindowEvent, bot: Bot) -> None:
207287 await _auto_detect_provider (event .window_id )
208288
209289 topic_name = event .window_name or Path (event .cwd ).name or event .window_id
290+ if await _rebind_existing_topic_by_name (event , bot , topic_name ):
291+ return
292+
210293 seen_chats = collect_target_chats (event .window_id )
211294 if not seen_chats :
212295 return
0 commit comments