Newer
Older
navi-1 / navi / memory / _summary.py
"""Conversation summary persistence — single-row table."""

import zlib
from datetime import datetime, timezone


def _summary_id(user_id: str | None) -> int:
    """Return a deterministic primary key for the user's summary row.

    Legacy single-user mode uses id=1.  Multi-user mode generates a unique
    id per user_id so the PRIMARY KEY constraint on `id` is never violated.
    """
    if user_id is None:
        return 1
    # Deterministic, non-overlapping with legacy id=1
    # zlib.crc32 is stable across process restarts (unlike Python hash()).
    return (abs(zlib.crc32(user_id.encode())) % 2147483646) + 2


class SummaryMixin:
    """Summary storage operations.

    Expected on the composite class:
        _get_pool() -> asyncpg.Pool
    """

    async def get_summary(self, user_id: str | None = None) -> str | None:
        pool = await self._get_pool()
        sid = _summary_id(user_id)
        async with pool.acquire() as conn:
            if user_id is None:
                return await conn.fetchval(
                    "SELECT content FROM memory_summary WHERE id=$1 AND user_id IS NULL",
                    sid,
                )
            return await conn.fetchval(
                "SELECT content FROM memory_summary WHERE id=$1 AND user_id = $2",
                sid, user_id,
            )

    async def set_summary(self, content: str, user_id: str | None = None) -> None:
        now = datetime.now(timezone.utc)
        sid = _summary_id(user_id)
        pool = await self._get_pool()
        async with pool.acquire() as conn:
            await conn.execute(
                """INSERT INTO memory_summary (id, user_id, content, generated_at) VALUES ($1, $2, $3, $4)
                   ON CONFLICT(id) DO UPDATE SET
                       content      = EXCLUDED.content,
                       generated_at = EXCLUDED.generated_at""",
                sid, user_id, content, now,
            )