# Memory System

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

## Storage (`navi/memory/store.py`)

Three tables in `navi.db`:

| 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 aiosqlite.

### 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:

```python
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.
