""" Session idle timer management for Claude Code Telegram bot. This module provides the SessionIdleTimer class that manages per-session idle timeouts using asyncio. Each session has its own timer that fires a callback after a configurable timeout period. Timers reset on activity and can be cancelled on session shutdown/archive. Example: async def handle_timeout(session_name: str): print(f"Session {session_name} timed out") timer = SessionIdleTimer("my-session", timeout_seconds=600, on_timeout=handle_timeout) timer.reset() # Start the timer # ... activity occurs ... timer.reset() # Reset timer on activity # ... no activity for 600 seconds ... # handle_timeout("my-session") called automatically """ import asyncio import logging from datetime import datetime, timezone from typing import Awaitable, Callable, Optional logger = logging.getLogger(__name__) class SessionIdleTimer: """ Manages idle timeout for a single session. Provides per-session timeout detection with automatic callback firing after inactivity. Timer can be reset on activity and cancelled on shutdown. Attributes: session_name: Name of the session this timer tracks timeout_seconds: Idle timeout in seconds on_timeout: Async callback to invoke when timeout fires last_activity: UTC timestamp of last activity seconds_since_activity: Float seconds since last activity """ def __init__( self, session_name: str, timeout_seconds: int, on_timeout: Callable[[str], Awaitable[None]] ): """ Initialize SessionIdleTimer. Args: session_name: Name of the session to track timeout_seconds: Idle timeout in seconds on_timeout: Async callback(session_name) to invoke when timeout fires """ self.session_name = session_name self.timeout_seconds = timeout_seconds self.on_timeout = on_timeout self._timer_task: Optional[asyncio.Task] = None self._last_activity = datetime.now(timezone.utc) logger.debug( f"SessionIdleTimer initialized: session={session_name}, " f"timeout={timeout_seconds}s" ) def reset(self) -> None: """ Reset the idle timer. Updates last activity timestamp to now, cancels any existing timer task, and creates a new background task that will fire the timeout callback after timeout_seconds of inactivity. Call this whenever activity occurs on the session. """ # Update last activity timestamp self._last_activity = datetime.now(timezone.utc) # Cancel existing timer if running if self._timer_task and not self._timer_task.done(): self._timer_task.cancel() logger.debug(f"Cancelled existing timer for session '{self.session_name}'") # Create new timer task self._timer_task = asyncio.create_task(self._wait_for_timeout()) logger.debug( f"Started idle timer for session '{self.session_name}': " f"{self.timeout_seconds}s" ) async def _wait_for_timeout(self) -> None: """ Background task that waits for timeout then fires callback. Sleeps for timeout_seconds, then invokes on_timeout callback with session name. Catches asyncio.CancelledError silently (timer was reset). """ try: await asyncio.sleep(self.timeout_seconds) # Timeout fired - call callback logger.info( f"Session '{self.session_name}' idle timeout fired " f"({self.timeout_seconds}s)" ) await self.on_timeout(self.session_name) except asyncio.CancelledError: # Timer was reset or cancelled - this is normal logger.debug(f"Timer cancelled for session '{self.session_name}'") def cancel(self) -> None: """ Cancel the idle timer. Stops the background timer task if running. Used on session shutdown or archive to prevent timeout callback from firing. """ if self._timer_task and not self._timer_task.done(): self._timer_task.cancel() logger.debug(f"Cancelled timer for session '{self.session_name}'") @property def seconds_since_activity(self) -> float: """ Get seconds since last activity. Returns: Float seconds elapsed since last reset() call """ now = datetime.now(timezone.utc) delta = now - self._last_activity return delta.total_seconds() @property def last_activity(self) -> datetime: """ Get timestamp of last activity. Returns: UTC datetime of last reset() call """ return self._last_activity