from __future__ import annotations import time from dataclasses import dataclass from typing import Any from yuxi.utils.logging_config import logger @dataclass class ThreadBinding: room_id: str root_event_id: str session_key: str = "" bound_at: float = 0.0 last_activity: float = 0.0 is_subagent: bool = False is_acp: bool = False def __post_init__(self): now = time.monotonic() if not self.bound_at: self.bound_at = now if not self.last_activity: self.last_activity = now @property def idle_seconds(self) -> float: return time.monotonic() - self.last_activity @property def age_hours(self) -> float: return (time.monotonic() - self.bound_at) / 3600 def touch(self) -> None: self.last_activity = time.monotonic() class ThreadBindingManager: def __init__(self, config: dict[str, Any]): self._config = config self._bindings: dict[str, ThreadBinding] = {} thread_cfg = config.get("threadBindings", {}) self._enabled = thread_cfg.get("enabled", True) self._idle_hours = thread_cfg.get("idleHours", 0) self._max_age_hours = thread_cfg.get("maxAgeHours", 0) self._spawn_subagent = thread_cfg.get("spawnSubagentSessions", False) self._spawn_acp = thread_cfg.get("spawnAcpSessions", False) def bind(self, room_id: str, root_event_id: str, session_key: str = "") -> ThreadBinding: binding_key = f"{room_id}:{root_event_id}" if binding_key in self._bindings: binding = self._bindings[binding_key] binding.touch() return binding binding = ThreadBinding( room_id=room_id, root_event_id=root_event_id, session_key=session_key, is_subagent=self._spawn_subagent, is_acp=self._spawn_acp, ) self._bindings[binding_key] = binding logger.debug(f"Matrix thread bound: {binding_key} -> {session_key}") return binding def unbind(self, room_id: str, root_event_id: str) -> None: binding_key = f"{room_id}:{root_event_id}" if binding_key in self._bindings: del self._bindings[binding_key] logger.debug(f"Matrix thread unbound: {binding_key}") def get(self, room_id: str, root_event_id: str) -> ThreadBinding | None: binding_key = f"{room_id}:{root_event_id}" return self._bindings.get(binding_key) def check_timeouts(self) -> list[ThreadBinding]: expired: list[ThreadBinding] = [] for key, binding in list(self._bindings.items()): if self._idle_hours > 0 and binding.idle_seconds > self._idle_hours * 3600: expired.append(binding) del self._bindings[key] logger.debug(f"Matrix thread binding expired (idle): {key}") elif self._max_age_hours > 0 and binding.age_hours > self._max_age_hours: expired.append(binding) del self._bindings[key] logger.debug(f"Matrix thread binding expired (max age): {key}") return expired @property def binding_count(self) -> int: return len(self._bindings) def list_bindings(self) -> list[ThreadBinding]: return list(self._bindings.values()) def get_room_bindings(self, room_id: str) -> list[ThreadBinding]: return [b for b in self._bindings.values() if b.room_id == room_id]