Tiered storage
Three storage layers, three roles. Every memory entry lives in some combination of them, and search reads from all three in parallel.
Warm — Postgres full-text search
Job: low-latency literal recall over active entries.
- Storage:
memory_entriestable with atsvectorshadow column maintained by a trigger oncontent. - Query path:
SELECT … WHERE tsv @@ plainto_tsquery($1)ranked byts_rank_cd. - Lifespan: an entry stays warm as long as
(now - last_hit) < effectiveDays(hits). The synaptic-decay sweep (every 6 h by default) demotes anything past that threshold.
Cold — Qdrant vectors
Job: semantic recall over older / less-frequently-touched entries.
- Storage: one Qdrant collection per
(tenant × project × namespace)triple. Naming:novamem_<tenantId>_<namespace>for tenant-wide,novamem_p_<projectId>_<namespace>for project entries. - Vector dim:
NOVAMEM_COLD_VECTOR_SIZE(default 384, matching the localall-MiniLM-L6-v2embedder). Must matchNOVAMEM_EMBEDDINGS_DIM. - Reactive promotion: a cold entry whose accumulated lifespan now exceeds its idle time is moved back to warm on the same call that hit it. Without this, useful entries would slowly disappear forever.
Graph — FalkorDB
Job: surface adjacent context — "what was related to this hit that I forgot to ask about."
- Storage:
Memory {id, user, project}nodes withRELATES {kind, strength}edges. - Auto-link: every
remember()callslinkVectorNeighbors()which connects the new node to its top-N vector neighbours (defaultgraphLinkFanout=3). One round-trip via the batchaddEdgesBatch. - Traversal: depth 1 (hot path), 2, or 3 — controlled by an allowlist so cost is bounded. Cypher path-product aggregates strengths along multi-hop walks.
- Optional:
NOVAMEM_GRAPH_ENABLED=falseskips graph entirely; engine emitsdegraded: trueon every search.
Why three?
Each tier alone has a failure mode:
| Tier | Strength | Weakness |
|---|---|---|
| Warm only | Exact ids, function names, hashes | Misses paraphrases ("I want to eat" vs "I'm hungry") |
| Cold only | Semantic similarity | Misses literals; a single typo'd identifier can fail to match |
| Graph only | Adjacent context | No initial seed; needs an entry to walk from |
Hybrid fusion picks the strengths from each, normalises (min-max) to a 0..1 scale, then weighted-sums with defaults keyword: 0.3, vector: 0.6, graph: 0.1. Override per call when you have a specific reason — {keyword:1, vector:0} for exact-id lookups, {vector:1} for pure semantic.
Decay maths
effectiveDays = NOVAMEM_DECAY_DAYS · log₂(hits + 1)A fresh entry (1 hit) lives 7 days. After 7 hits it lives 7 · log₂(8) = 21 days. After 31 hits, 35 days. The shape is sub-linear — popular entries persist longer but you don't need millions of hits to keep something around.
See also
- Hybrid search internals
- Decay & dream cycle
- System shape — how tiers fit together with the engine layer