"""In-process preview cache for FlowPilot resolution-note / escalation-package previews. Phase 3 implementation per FLOWPILOT-MIGRATION.md Section 5.5: - Cache key: `(kind, session_id, state_version)` — no TTL needed, state_version is the source of truth. - Invalidation: any write to session_facts, session_suggested_fixes, or script_generations bumps `ai_sessions.state_version`. Old entries simply stop being looked up and leak harmlessly until process restart. - Storage: plain dict, single-process. When Session Sharing brings Redis, swap the storage without changing the call sites. Bound: best-effort soft cap of 5000 entries. When exceeded we drop the oldest insertion. Not a TTL — at current scale, the cap is more about resident-memory hygiene than correctness. """ from __future__ import annotations from collections import OrderedDict from typing import Any from uuid import UUID _MAX_ENTRIES = 5000 class _PreviewCache: def __init__(self) -> None: self._store: OrderedDict[tuple[str, UUID, int], Any] = OrderedDict() def get(self, kind: str, session_id: UUID, state_version: int) -> Any | None: key = (kind, session_id, state_version) if key not in self._store: return None # Touch on access so LRU eviction is meaningful. self._store.move_to_end(key) return self._store[key] def set(self, kind: str, session_id: UUID, state_version: int, value: Any) -> None: key = (kind, session_id, state_version) self._store[key] = value self._store.move_to_end(key) # Evict oldest if over cap. OrderedDict.popitem(last=False) is O(1). while len(self._store) > _MAX_ENTRIES: self._store.popitem(last=False) def invalidate_session(self, session_id: UUID) -> None: """Drop all entries for a session — used when the session is deleted.""" keys = [k for k in self._store if k[1] == session_id] for k in keys: del self._store[k] preview_cache = _PreviewCache()