8.4 KiB
DESIGN-0.18.0: Memory System
Version: 0.18.0 Status: Phase 1 Complete Depends on: compaction (v0.15.0), knowledge bases (v0.14.0), persona-KB binding (v0.17.0), SQLite backend (v0.17.1)
1. Overview
Memory provides long-term fact persistence across conversations. Unlike compaction (within-conversation context) or knowledge bases (static documents), memory captures preferences, facts, and context that accumulate over time from natural conversation.
Key differentiator: Persona-scoped memory. Each Persona builds its own memory independently, preventing cross-context contamination. A helpdesk Persona accumulates FAQ knowledge; a tutoring Persona tracks per-student progress; a user's personal preferences stay separate from any Persona context.
2. Scope Model
Three memory scopes with clear ownership:
┌─────────────────────────────────────────────────┐
│ user scope │
│ owner_id = user_id │
│ "Jeff prefers Go over Python" │
│ "Deployment uses Kubernetes + Traefik" │
│ Visible in ALL conversations for this user │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ persona scope │
│ owner_id = persona_id │
│ "Common question: how to reset password" │
│ "Policy: refunds within 30 days only" │
│ Shared across ALL users of this Persona │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ persona_user scope │
│ owner_id = persona_id, user_id = user_id │
│ "Student struggles with recursion" │
│ "Customer prefers email over phone" │
│ Per-user within a specific Persona │
└─────────────────────────────────────────────────┘
Scope resolution at recall time:
When a Persona is active, all three scopes are queried and merged with priority ordering: persona_user > persona > user. When no Persona is active, only user scope is queried.
3. Data Model
3.1 Table: memories
| Column | Type | Description |
|---|---|---|
id |
UUID / TEXT | Primary key |
scope |
TEXT | user, persona, persona_user |
owner_id |
UUID / TEXT | user_id (user scope) or persona_id (persona/persona_user) |
user_id |
UUID / TEXT | Only for persona_user scope — identifies which user |
key |
TEXT | Short label (2-6 words) |
value |
TEXT | The fact/detail |
source_channel_id |
UUID / TEXT | Provenance — which conversation |
confidence |
REAL | 0.0-1.0, LLM-provided |
status |
TEXT | active, pending_review, archived |
embedding |
vector(3072) / TEXT | For semantic recall (Phase 2) |
created_at |
TIMESTAMPTZ / TEXT | |
updated_at |
TIMESTAMPTZ / TEXT |
3.2 Indexes
- Primary lookup:
(scope, owner_id, status)— filtered tostatus = 'active' - Persona+user:
(owner_id, user_id)— filtered toscope = 'persona_user' - Dedup: unique on
(scope, owner_id, COALESCE(user_id, nil_uuid), key) - FTS: GIN on
to_tsvector(key || ' ' || value)(Postgres) / LIKE fallback (SQLite) - Provenance:
(source_channel_id)— "where did this memory come from?"
3.3 Model Struct
type Memory struct {
ID string `json:"id"`
Scope string `json:"scope"`
OwnerID string `json:"owner_id"`
UserID *string `json:"user_id,omitempty"`
Key string `json:"key"`
Value string `json:"value"`
SourceChannelID *string `json:"source_channel_id,omitempty"`
Confidence float64 `json:"confidence"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
4. Store Interface
type MemoryStore interface {
Upsert(ctx, *Memory) error // create or update (matched on scope+owner+user+key)
GetByID(ctx, id) (*Memory, error)
List(ctx, MemoryFilter) ([]Memory, error)
Delete(ctx, id) error // hard delete
Archive(ctx, id) error // soft delete (status → archived)
Recall(ctx, userID, *personaID, query, limit) ([]Memory, error) // scope-merged search
CountByOwner(ctx, scope, ownerID) (int, error)
}
Key design decisions:
Upsertuses the unique index for conflict resolution — same key = update valueRecalldoes a UNION across all applicable scopes with priority ordering- Both Postgres and SQLite implementations exist (same pattern as all other stores)
- SQLite falls back to LIKE for text search (no tsvector)
5. Tools
5.1 memory_save
The LLM calls this to explicitly store a fact. Scope is automatically determined by whether a Persona is active:
- No Persona →
userscope,owner_id = user_id - Persona active →
persona_userscope,owner_id = persona_id,user_id = user_id
Parameters: key (required), value (required), confidence (optional, default 1.0)
5.2 memory_recall
The LLM calls this to search stored memories. Results merge across all applicable scopes.
Parameters: query (optional), max_results (optional, default 20)
5.3 Registration
Late registration pattern (same as notes, KB search, conversation search):
// main.go
tools.RegisterMemoryTools(stores)
6. Memory Injection
At completion time, loadConversation() injects relevant memories
into the system prompt — same position as KB hints and compaction
summaries:
[admin system prompt]
[persona/channel system prompt]
[KB hint]
[memory injection] ← NEW
[compaction summary OR message history]
BuildMemoryHint() calls Recall() with an empty query (returns
top memories by confidence/recency), formats as bullet points, and
caps at ~1500 tokens (~6000 chars) to preserve context budget.
7. Implementation Phases
Phase 1: Foundation & Tools ✅ (this delivery)
- Migrations (Postgres + SQLite)
- Memory model struct
- MemoryStore interface + both implementations
- memory_save + memory_recall tools
- Memory injection in loadConversation
- 9 new files, ~5 edits to existing files
Phase 2: Automatic Extraction & Embeddings
- Background job: post-conversation analysis extracts facts
- Configurable extraction prompt per Persona
- Extracted memories start as
pending_review - Embedding generation for semantic recall
- Recall upgraded to use vector similarity + keyword hybrid
Phase 3: Review Pipeline & User Controls
- Admin review queue for pending memories
- Persona memory review for team admins
- User Settings → Memory: view, edit, delete
- Per-Persona memory toggle
- Retention policies
- Frontend memory management UI
8. Persona Memory Saves (Future: Phase 2)
Phase 1 only supports user and persona_user scopes via the tool
(the LLM saves about the user, scoped appropriately). Phase 2 adds
the ability for admins/team leads to configure Persona-scope memory
extraction — the Persona "learns" from conversations:
Persona system prompt addition (Phase 2):
"After each conversation, identify key facts, common questions,
and useful patterns. Save these as persona-scope memories for
future reference by all users."
This runs as a background job, not during the live conversation, to avoid latency impact.
9. Privacy & Security
- User memories never cross team boundaries
- Persona memories respect group access controls
- persona_user memories are only visible to the specific user + admins
- Users can view/delete their own memories (Phase 3 UI)
- Admin can disable memory system globally or per-team
- No cross-persona memory leakage — scope isolation is enforced at the query level