---
phase: 03-lifecycle-management
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- telegram/idle_timer.py
- telegram/session_manager.py
- telegram/claude_subprocess.py
autonomous: true
must_haves:
truths:
- "Per-session idle timer fires callback after configurable timeout seconds"
- "Timer resets on activity (cancel + restart)"
- "Session metadata includes idle_timeout field (default 600s)"
- "ClaudeSubprocess exposes its PID for metadata tracking"
artifacts:
- path: "telegram/idle_timer.py"
provides: "SessionIdleTimer class with asyncio-based per-session idle timers"
min_lines: 60
- path: "telegram/session_manager.py"
provides: "Session metadata with idle_timeout field, PID tracking"
contains: "idle_timeout"
- path: "telegram/claude_subprocess.py"
provides: "PID property for external access"
contains: "def pid"
key_links:
- from: "telegram/idle_timer.py"
to: "asyncio.create_task"
via: "Background sleep task with cancellation"
pattern: "asyncio\\.create_task.*_wait_for_timeout"
- from: "telegram/session_manager.py"
to: "metadata.json"
via: "idle_timeout stored in session metadata"
pattern: "idle_timeout"
---
Create the idle timer module and extend session metadata for lifecycle management.
Purpose: Foundation components needed before wiring suspend/resume into the bot. The idle timer provides per-session timeout detection, and metadata extensions store timeout configuration and subprocess PIDs.
Output: New `idle_timer.py` module, updated `session_manager.py` and `claude_subprocess.py`
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
@/home/mikkel/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-lifecycle-management/03-CONTEXT.md
@.planning/phases/03-lifecycle-management/03-RESEARCH.md
@telegram/idle_timer.py (will be created)
@telegram/session_manager.py
@telegram/claude_subprocess.py
Task 1: Create SessionIdleTimer module
telegram/idle_timer.py
Create `telegram/idle_timer.py` with a `SessionIdleTimer` class that manages per-session idle timeouts using asyncio.
Class design:
- `__init__(self, session_name: str, timeout_seconds: int, on_timeout: Callable[[str], Awaitable[None]])` -- stores config, initializes _timer_task to None, _last_activity to now (UTC)
- `reset(self)` -- updates _last_activity to now, cancels existing _timer_task if running, creates new asyncio.create_task(_wait_for_timeout())
- `async _wait_for_timeout(self)` -- awaits asyncio.sleep(self.timeout_seconds), then calls `await self.on_timeout(self.session_name)`. Catches asyncio.CancelledError silently (timer was reset).
- `cancel(self)` -- cancels _timer_task if running (used on shutdown/archive)
- `@property seconds_since_activity` -- returns float seconds since _last_activity
- `@property last_activity` -- returns the datetime of last activity (for /sessions display)
Use `datetime.now(timezone.utc)` for timestamps. Import typing for Callable, Optional, Awaitable.
Add module docstring explaining this is the idle timeout manager for session lifecycle. Log timer start/cancel/fire events at DEBUG level, timeout firing at INFO level.
`python3 -c "from idle_timer import SessionIdleTimer; print('import OK')"` run from telegram/ directory succeeds.
SessionIdleTimer class exists with reset(), cancel(), _wait_for_timeout(), seconds_since_activity, and last_activity. Imports cleanly.
Task 2: Extend session metadata and subprocess PID tracking
telegram/session_manager.py, telegram/claude_subprocess.py
**session_manager.py changes:**
1. In `create_session()`, add `"idle_timeout": 600` (10 minutes default) to the initial metadata dict (alongside existing fields like name, created, last_active, persona, pid, status).
2. Add a helper method `get_session_timeout(self, name: str) -> int` that reads metadata and returns `metadata.get('idle_timeout', 600)`. This provides a clean interface for the bot to query timeout values.
3. No changes to list_sessions() -- it already returns full metadata which will now include idle_timeout.
**claude_subprocess.py changes:**
1. Add a `@property pid(self) -> Optional[int]` that returns `self._process.pid if self._process and self._process.returncode is None else None`. This lets the bot store the PID in session metadata for orphan cleanup on restart.
2. In `start()`, after successful subprocess spawn, store the PID in a `self._pid` attribute as well (for access even after process terminates, useful for logging). Keep the property returning live PID only.
These are minimal, targeted changes. Do NOT refactor existing code. Do NOT change the terminate() method or any existing logic.
`python3 -c "from session_manager import SessionManager; sm = SessionManager(); print('SM OK')"` and `python3 -c "from claude_subprocess import ClaudeSubprocess; print('CS OK')"` both succeed from telegram/ directory.
Session metadata includes idle_timeout (default 600s). SessionManager has get_session_timeout() method. ClaudeSubprocess has pid property returning live process PID.
- `cd ~/homelab/telegram && python3 -c "from idle_timer import SessionIdleTimer; from session_manager import SessionManager; from claude_subprocess import ClaudeSubprocess; print('All imports OK')"`
- SessionIdleTimer has reset(), cancel(), seconds_since_activity, last_activity
- SessionManager.get_session_timeout() returns int
- ClaudeSubprocess.pid returns Optional[int]
- idle_timer.py exists with SessionIdleTimer class implementing asyncio-based per-session idle timeout
- session_manager.py creates sessions with idle_timeout=600 in metadata and has get_session_timeout() helper
- claude_subprocess.py exposes pid property for PID tracking
- All three modules import without errors