Newer
Older
navi-1 / docs / memory.md
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 28 Apr 4 KB Document pgvector migration in memory system

Memory System

Long-term user memory: facts extracted from conversations, stored in SQLite, injected into every session.

PostgreSQL + pgvector (semantic search)

When DATABASE_URL is set, the memory system uses PostgreSQL with pgvector for semantic search via embeddings.

Feature SQLite PostgreSQL
Storage File-based Server
Semantic search No Yes (cosine distance on vector(768))
Embeddings None Generated via Ollama (nomic-embed-text:latest)
Metadata category, key, value + source, confidence, expires_at, source_context

Schema migration

When upgrading the memory system to a new schema (e.g. adding pgvector columns), run:

.venv/bin/python navi/memory/migrate_pgvector.py

This script:

  1. Verifies the vector extension is installed in PostgreSQL
  2. Adds missing columns: embedding, source, confidence, expires_at, last_verified_at, source_context
  3. Creates indexes: hnsw(embedding), expires, source+category

Safe to run multiple times — all operations use IF NOT EXISTS.


Storage (navi/memory/store.py)

Three tables in the database:

Table Purpose
memory_facts Individual facts: (category, key, value) — unique on (category, key)
memory_summary Single-row narrative summary generated from all facts
session_memory_state Tracks which sessions have been processed (by extracted_at)

MemoryStore is initialized synchronously (creates tables), all operations are async via asyncpg (PostgreSQL) or aiosqlite (SQLite fallback).

Key operations

Method Description
upsert_fact(category, key, value) Insert or update a fact
search_facts(query, limit=15) Full-text search across category/key/value (OR across terms)
delete_fact(key, category=None) Delete by key, optionally filtered by category
get_all_facts(limit=None) All facts ordered by (category, updated_at DESC)
get_summary() Current narrative summary text
set_summary(content) Replace the summary
mark_session_extracted(session_id) Record extraction timestamp
get_extracted_at(session_id) Check if/when a session was processed

Automatic extraction (navi/memory/extractor.py)

Facts are extracted from stale sessions automatically.

Trigger: POST /sessions (create new session) fires _process_stale_sessions() as a background task.

Stale criterion: session.last_active < now - 30 minutes AND not yet extracted (or extracted before last activity).

Extraction process:

  1. Render conversation as plain text.
  2. Call LLM with an extraction prompt: "extract facts the user shared about themselves, their preferences, projects, and environment."
  3. Parse the response as category: key = value lines.
  4. Upsert each fact into memory_facts.
  5. Regenerate memory_summary from all current facts.
  6. Mark session as extracted.

Memory injection into agent context

At the start of each run_stream() / run() / run_ephemeral() call, _memory_msg() is called:

async def _memory_msg(self) -> Message | None:
    summary = await self._memory.get_summary()
    if not summary:
        return None
    return Message(role="system", content=f"## What I remember about the user\n\n{summary}")

This message is inserted after the main system message but before conversation history. The agent reads it on every turn.


Memory tools

memory_search — searches facts by keyword query. Returns matching facts with category/key/value. Agent should call this when the user mentions something personal that may already be known.

memory_forget — deletes facts matching a key (optionally filtered by category). Agent calls this when the user explicitly asks to forget something or when a fact is clearly outdated.


Memory usage guidelines (from persona)

Call memory_search when:

  • The user mentions something personal (location, project, preference, recurring task).
  • About to make an assumption about the user's environment or preferences — verify first.
  • The user asks about something helped with before.

Do NOT call memory_search reflexively at the start of every session — only when context warrants it.

Call memory_forget only when the user explicitly asks, or when a stored fact is clearly wrong or outdated.