""" SessionDB — replay session storage. Originally in-memory only; now backed by SQLite (via backend.storage.sqlite_db). Maintains the same API for backward compatibility with session.session.BuildSession. """ import time import uuid from typing import Any, Dict, List # Optional storage backend try: from backend.storage.sqlite_db import STORAGE except Exception: STORAGE = None class SessionDB: """Replay session registry + event log. All writes go to SQLite (if available) AND to an in-memory cache. Reads hit the in-memory cache first; if missing, fall back to SQLite. """ def __init__(self): # In-memory caches (fast path) self.sessions: Dict[str, Dict[str, Any]] = {} self.events: Dict[str, List[Dict[str, Any]]] = {} self.snapshots: Dict[str, List[Dict[str, Any]]] = {} # On startup, load any persisted sessions if STORAGE: self._load_from_storage() def _load_from_storage(self): try: for s in STORAGE.list_sessions(): sid = s["session_id"] self.sessions[sid] = { "project": s.get("source", "live"), "created_at": s.get("created_at", time.time()), "status": "active", } except Exception: pass # ----------------------------- # CREATE SESSION # ----------------------------- def create_session(self, project) -> str: sid = str(uuid.uuid4()) self.sessions[sid] = { "project": project, "created_at": time.time(), "status": "active", } self.events[sid] = [] self.snapshots[sid] = [] if STORAGE: try: STORAGE.create_session(sid, source=str(project) if project else "live") except Exception: pass return sid # ----------------------------- # EVENT LOGGING (IMMUTABLE) # ----------------------------- def log_event(self, session_id, event_type, payload): event = { "ts": time.time(), "type": event_type, "data": payload, } self.events.setdefault(session_id, []).append(event) if STORAGE: try: STORAGE.append_event(event, session_id=session_id) except Exception: pass return event # ----------------------------- # SNAPSHOT (STATE CHECKPOINT) # ----------------------------- def snapshot(self, session_id, state): snap = { "ts": time.time(), "state": state, } self.snapshots.setdefault(session_id, []).append(snap) # Note: snapshots are not persisted to SQLite (they're rare and large) return snap # ----------------------------- # GET FULL TIMELINE # ----------------------------- def get_session(self, session_id): # Try in-memory first if session_id in self.sessions: return { "session": self.sessions.get(session_id), "events": self.events.get(session_id, []), "snapshots": self.snapshots.get(session_id, []), } # Fall back to storage if STORAGE: try: sess = STORAGE.get_session(session_id) if sess: events = STORAGE.get_events(session_id) return { "session": { "project": sess.get("source"), "created_at": sess.get("created_at"), "status": "active", }, "events": events, "snapshots": [], } except Exception: pass return { "session": None, "events": [], "snapshots": [], } # ----------------------------- # REPLAY ENGINE INPUT # ----------------------------- def get_replay_stream(self, session_id): # In-memory first if session_id in self.events: return sorted(self.events[session_id], key=lambda e: e["ts"]) # Storage fallback if STORAGE: try: return STORAGE.get_events(session_id) except Exception: pass return [] # ----------------------------- # LIST ALL SESSIONS # ----------------------------- def list_sessions(self) -> List[Dict[str, Any]]: seen = set() out = [] # In-memory for sid, s in self.sessions.items(): out.append({ "id": sid, "source": s.get("project", "live"), "created_at": s.get("created_at"), "events": len(self.events.get(sid, [])), }) seen.add(sid) # Storage if STORAGE: try: for s in STORAGE.list_sessions(): if s["session_id"] not in seen: out.append({ "id": s["session_id"], "source": s.get("source", "live"), "created_at": s.get("created_at"), "events": s.get("event_count", 0), }) except Exception: pass # Sort by created_at desc out.sort(key=lambda s: s.get("created_at", 0), reverse=True) return out