diff --git a/navi/memory/_facts.py b/navi/memory/_facts.py index 2677782..e94a847 100644 --- a/navi/memory/_facts.py +++ b/navi/memory/_facts.py @@ -99,17 +99,30 @@ try: pool = await self._get_pool() async with pool.acquire() as conn: - rows = await conn.fetch( - """SELECT id, category, key, value, updated_at, - source, confidence, expires_at, source_context, - embedding <=> $1::vector AS distance - FROM memory_facts - WHERE (expires_at IS NULL OR expires_at > now()) - AND (user_id IS NOT DISTINCT FROM $3) - ORDER BY embedding <=> $1::vector - LIMIT $2""", - vec_str, limit, *user_param, - ) + if user_id is None: + rows = await conn.fetch( + """SELECT id, category, key, value, updated_at, + source, confidence, expires_at, source_context, + embedding <=> $1::vector AS distance + FROM memory_facts + WHERE (expires_at IS NULL OR expires_at > now()) + AND user_id IS NULL + ORDER BY embedding <=> $1::vector + LIMIT $2""", + vec_str, limit, + ) + else: + rows = await conn.fetch( + """SELECT id, category, key, value, updated_at, + source, confidence, expires_at, source_context, + embedding <=> $1::vector AS distance + FROM memory_facts + WHERE (expires_at IS NULL OR expires_at > now()) + AND user_id = $3 + ORDER BY embedding <=> $1::vector + LIMIT $2""", + vec_str, limit, user_id, + ) results = [ _row_to_dict(r) for r in rows diff --git a/navi/memory/_summary.py b/navi/memory/_summary.py index d6baec4..716306a 100644 --- a/navi/memory/_summary.py +++ b/navi/memory/_summary.py @@ -3,6 +3,18 @@ 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 + return (abs(hash(user_id)) % 2147483647) + 2 + + class SummaryMixin: """Summary storage operations. @@ -12,19 +24,27 @@ 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") - return await conn.fetchval("SELECT content FROM memory_summary WHERE id=1 AND user_id = $1", user_id) + 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, $1, $2, $3) - ON CONFLICT(id, user_id) DO UPDATE SET + """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""", - user_id, content, now, + sid, user_id, content, now, )