Files
Timmy-time-dashboard/src/timmy/nexus/persistence.py
Perplexity Computer d0b6d87eb1
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
[perplexity] feat: Nexus v2 — Cognitive Awareness & Introspection Engine (#1090) (#1348)
Co-authored-by: Perplexity Computer <perplexity@tower.local>
Co-committed-by: Perplexity Computer <perplexity@tower.local>
2026-03-24 02:50:40 +00:00

231 lines
8.4 KiB
Python

"""Nexus Session Persistence — durable conversation history.
The v1 Nexus kept conversations in a Python ``list`` that vanished on
every process restart. This module provides a SQLite-backed store so
Nexus conversations survive reboots while remaining fully local.
Schema:
nexus_messages(id, role, content, timestamp, session_tag)
Design decisions:
- One table, one DB file (``data/nexus.db``). Cheap, portable, sovereign.
- ``session_tag`` enables future per-operator sessions (#1090 deferred scope).
- Bounded history: ``MAX_MESSAGES`` rows per session tag. Oldest are pruned
automatically on insert.
- Thread-safe via SQLite WAL mode + module-level singleton.
Refs: #1090 (Nexus Epic — session persistence), architecture-v2.md §Data Layer
"""
from __future__ import annotations
import logging
import sqlite3
from contextlib import closing
from datetime import UTC, datetime
from pathlib import Path
from typing import TypedDict
logger = logging.getLogger(__name__)
# ── Defaults ─────────────────────────────────────────────────────────────────
_DEFAULT_DB_DIR = Path("data")
DB_PATH: Path = _DEFAULT_DB_DIR / "nexus.db"
MAX_MESSAGES = 500 # per session tag
DEFAULT_SESSION_TAG = "nexus"
# ── Schema ───────────────────────────────────────────────────────────────────
_SCHEMA = """\
CREATE TABLE IF NOT EXISTS nexus_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT NOT NULL,
session_tag TEXT NOT NULL DEFAULT 'nexus'
);
CREATE INDEX IF NOT EXISTS idx_nexus_session ON nexus_messages(session_tag);
CREATE INDEX IF NOT EXISTS idx_nexus_ts ON nexus_messages(timestamp);
"""
# ── Typed dict for rows ──────────────────────────────────────────────────────
class NexusMessage(TypedDict):
id: int
role: str
content: str
timestamp: str
session_tag: str
# ── Store ────────────────────────────────────────────────────────────────────
class NexusStore:
"""SQLite-backed persistence for Nexus conversations.
Usage::
store = NexusStore() # uses module-level DB_PATH
store.append("user", "hi")
msgs = store.get_history() # → list[NexusMessage]
store.clear() # wipe session
"""
def __init__(self, db_path: Path | None = None) -> None:
self._db_path = db_path or DB_PATH
self._conn: sqlite3.Connection | None = None
# ── Connection management ─────────────────────────────────────────────
def _get_conn(self) -> sqlite3.Connection:
if self._conn is None:
self._db_path.parent.mkdir(parents=True, exist_ok=True)
self._conn = sqlite3.connect(
str(self._db_path),
check_same_thread=False,
)
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.executescript(_SCHEMA)
return self._conn
def close(self) -> None:
"""Close the underlying connection (idempotent)."""
if self._conn is not None:
try:
self._conn.close()
except Exception:
pass
self._conn = None
# ── Write ─────────────────────────────────────────────────────────────
def append(
self,
role: str,
content: str,
*,
timestamp: str | None = None,
session_tag: str = DEFAULT_SESSION_TAG,
) -> int:
"""Insert a message and return its row id.
Automatically prunes oldest messages when the session exceeds
``MAX_MESSAGES``.
"""
ts = timestamp or datetime.now(UTC).strftime("%H:%M:%S")
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"INSERT INTO nexus_messages (role, content, timestamp, session_tag) "
"VALUES (?, ?, ?, ?)",
(role, content, ts, session_tag),
)
row_id: int = cur.lastrowid # type: ignore[assignment]
conn.commit()
# Prune
self._prune(session_tag)
return row_id
def _prune(self, session_tag: str) -> None:
"""Remove oldest rows that exceed MAX_MESSAGES for *session_tag*."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT COUNT(*) FROM nexus_messages WHERE session_tag = ?",
(session_tag,),
)
count = cur.fetchone()[0]
if count > MAX_MESSAGES:
excess = count - MAX_MESSAGES
cur.execute(
"DELETE FROM nexus_messages WHERE id IN ("
" SELECT id FROM nexus_messages "
" WHERE session_tag = ? ORDER BY id ASC LIMIT ?"
")",
(session_tag, excess),
)
conn.commit()
# ── Read ──────────────────────────────────────────────────────────────
def get_history(
self,
session_tag: str = DEFAULT_SESSION_TAG,
limit: int = 200,
) -> list[NexusMessage]:
"""Return the most recent *limit* messages for *session_tag*.
Results are ordered oldest-first (ascending id).
"""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT id, role, content, timestamp, session_tag "
"FROM nexus_messages "
"WHERE session_tag = ? "
"ORDER BY id DESC LIMIT ?",
(session_tag, limit),
)
rows = cur.fetchall()
# Reverse to chronological order
messages: list[NexusMessage] = [
NexusMessage(
id=r["id"],
role=r["role"],
content=r["content"],
timestamp=r["timestamp"],
session_tag=r["session_tag"],
)
for r in reversed(rows)
]
return messages
def message_count(
self, session_tag: str = DEFAULT_SESSION_TAG
) -> int:
"""Return total message count for *session_tag*."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT COUNT(*) FROM nexus_messages WHERE session_tag = ?",
(session_tag,),
)
return cur.fetchone()[0]
# ── Delete ────────────────────────────────────────────────────────────
def clear(self, session_tag: str = DEFAULT_SESSION_TAG) -> int:
"""Delete all messages for *session_tag*. Returns count deleted."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"DELETE FROM nexus_messages WHERE session_tag = ?",
(session_tag,),
)
deleted: int = cur.rowcount
conn.commit()
return deleted
def clear_all(self) -> int:
"""Delete every message across all session tags."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute("DELETE FROM nexus_messages")
deleted: int = cur.rowcount
conn.commit()
return deleted
# ── Module singleton ─────────────────────────────────────────────────────────
nexus_store = NexusStore()