100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
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]
|