"""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,
)