
Eryx Memory System: Entity Graphs, Ebisu Curves & Intelligent Retrieval
A deep dive into how Eryx implements entity knowledge graphs, Ebisu forget curves, adaptive retrieval, and serendipity injection for AI memory
Introduction
Eryx is an Enterprise AI Collaboration Platform that features a sophisticated multi-layered memory system. Unlike simple RAG (Retrieval Augmented Generation) systems that just chunk and search documents, Eryx implements:
- Entity Knowledge Graph — facts linked to canonical entities with temporal validity
- Episodic Memory — per-chat session summaries with proper tree branching
- Adaptive Forget Curve — Ebisu-style memory decay based on actual access patterns
- Intelligent Retrieval — query decomposition, RRF fusion, and intent-based routing
This post dissects every major component, the formulas powering it, and how they all connect.
Architecture Overview
+------------------------------------------------------------------+
| User Query |
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Query Intent Classification |
| +---------+ +----------+ +------------+ +---------------------+|
| | DECISION| | FACTUAL | | EXPLORATORY| | TEMPORAL ||
| | vs | | "what is"| | "tell me | | "last week" ||
| | "decided"| | | | about" | | ||
| +---------+ +----------+ +------------+ +---------------------+|
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Query Decomposition |
| "what did I decide about auth0" |
| --> ["auth0 decision", "auth0 choice", "auth0 conclusion"] |
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Qdrant Vector Search |
| +--------------+ +---------------+ +--------------------+ |
| | entity_facts | | episodic_ | | file_chunks + | |
| | | | summaries | | memory_embeddings | |
| +--------------+ +---------------+ +--------------------+ |
| | |
| | RRF Fusion (k=60) |
| v |
| +--------------------------------------------------------------+ |
| | Reciprocal Rank Fusion | |
| | score(d) = SUM 1/(k + rank(d)) for each sub-query | |
| +--------------------------------------------------------------+ |
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Cohere Rerank (optional) |
| Reranks top results using semantic understanding |
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Ebisu Forget Curve Filter |
| +--------------------------------------------------------------+ |
| | P(t) = stability x 2^(-(t/halfLife)^difficulty) | |
| | | |
| | Reinforcement on access: | |
| | * halfLife *= 1.5 (capped at 365 days) | |
| | * difficulty -= 0.1 (floored at 1.0) | |
| | * stability += 0.1 (capped at 2.0) | |
| +--------------------------------------------------------------+ |
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Serendipity Injection |
| Thompson-sampled high-confidence facts from unrelated topics |
| (prevents echo chambers) |
+-----------------------------+------------------------------------+
|
v
+------------------------------------------------------------------+
| Memory Context Assembly |
| |
| ## KNOWLEDGE ABOUT USER: |
| [Sarah] -- confidence: 85% |
| - Works at Anthropic as ML Engineer |
| - Prefers React over Vue |
| - Currently learning RL |
| |
| ## RELATIONSHIPS: |
| - Anthropic -- company_of Sarah |
| - React -- skilled_in Sarah |
| |
| ## MEMORY SPRINKLE: |
| - [Birthday]: Sarah's birthday is March 15 |
| |
| ## RECENT DISCUSSIONS: |
| - 2026-07-18: Discussed auth0 migration... |
+------------------------------------------------------------------+
1. Vector Database: Qdrant
1.1 Collections
Eryx uses 6 Qdrant collections, each optimized for a specific content type:
| Collection | Dimensions | Purpose |
|---|---|---|
entity_facts | 768 | Individual facts extracted from chat/file |
episodic_summaries | 768 | Chat session summaries |
entity_context | 768 | Aggregated entity context (all facts for one entity) |
entity_relations | 768 | Semantic relationship vectors |
file_chunks | 768 | Uploaded file content chunks |
memory_embeddings | 768 | User memory notes |
1.2 HNSW Index Configuration
hnsw_config: {
m: 16, // Connections per node — higher = better recall, more memory
ef_construct: 128, // Build-time accuracy — higher = more accurate index
full_scan_threshold: 10000, // Use exact search for small datasets (<10k vectors)
on_disk: true, // Keep HNSW index on disk to reduce RAM
}- m=16: Each node connects to 16 nearest neighbors. This is high — typical values are 8-16. More connections improve recall at the cost of memory.
- ef_construct=128: During index construction, the algorithm explores 128 nearest neighbors. Higher = more accurate index but slower build.
- on_disk=true: Qdrant stores the HNSW graph on disk instead of RAM. Slower queries but enables running on cheaper instances.
1.3 Cosine Similarity
All collections use Cosine distance:
vectors: {
size: 768, // nomic-embed-text produces 768-dim vectors
distance: "Cosine", // 1 - (A·B) / (||A|| × ||B||)
on_disk: true,
}Cosine similarity is ideal for text embeddings because it measures direction rather than magnitude. Two documents with similar topics but different lengths will have high cosine similarity even if their Euclidean distance is large.
1.4 Recency Boost Formula
When searching, results can be boosted by how recently they were created:
// In applyRecencyBoost():
const recencyFactor = Math.exp(-daysOld / 30) * recencyWeight;
const boostedScore = result.score * (1 + recencyFactor);Formula:
recencyFactor = e^(-daysOld / 30) × recencyWeight
boostedScore = originalScore × (1 + recencyFactor)
- 30-day half-life: After 30 days, recencyFactor drops to ~0.37 of its initial value
- recencyWeight (default 0.3): Controls how much recency affects final score
- Capped at 1.0 so scores don't exceed maximum
1.5 Freshness Score
Each search result gets a freshnessScore (0-1) indicating how recent it is:
// In computeFreshnessScore():
export function computeFreshnessScore(result, halfLifeDays = 90): number {
const daysSinceUpdate = (now - updatedAt) / (1000 * 60 * 60 * 24);
return Math.exp(-daysSinceUpdate / halfLifeDays);
}Formula:
freshness = e^(-daysSinceUpdate / halfLifeDays)
| Days Since Update | Freshness (halfLife=90) |
|---|---|
| 0 | 1.00 |
| 30 | 0.72 |
| 90 | 0.37 |
| 180 | 0.14 |
| 270 | 0.05 |
Freshness is applied as a minor boost to the final score:
boostedScore = result.score * (1 + freshness * freshnessWeight);
// freshnessWeight default: 0.12. Ebisu Forget Curve: Adaptive Memory Decay
2.1 The Ebisu Formula
Eryx implements the Ebisu memory model for fact decay and reinforcement. Unlike simple TTL-based expiration, Ebisu models actual human memory — facts you access frequently become stronger, unused facts decay.
Core Recall Probability Formula:
// In recallProbability():
export function recallProbability(params: EbisuParams, tDays: number): number {
const { stability, difficulty, halfLife } = params;
const tHalf = tDays / halfLife;
const exponent = tHalf ** difficulty;
const raw = 2 ** -exponent;
const result = raw * stability;
return Math.max(0, Math.min(1, result));
}Formula:
P(t) = stability × 2^(-(t/halfLife)^difficulty)
Where:
- stability (default 1.0): Resistance to decay. Higher = more durable memory
- difficulty (default 2.5): Steepness of decay curve. Higher = faster initial drop
- halfLife (default 30 days): Time until recall drops to ~50% (before stability scaling)
2.2 Parameter Effects
Effect of difficulty on decay:
| Difficulty | Curve Shape | Description |
|---|---|---|
| 1.0 | Slow decay | Nearly flat retention |
| 2.5 | Medium decay | Steep initial drop, then flattens |
| 4.0 | Fast decay | Very steep drop |
Effect of stability on recall:
| Stability | Base Recall | Description |
|---|---|---|
| 0.5 | 50% | Low resistance to decay |
| 1.0 | 100% | Normal recall |
| 2.0 | 200% | High resistance to decay |
Effect of halfLife on retention:
| HalfLife | Retention Speed | Description |
|---|---|---|
| 7 days | Fast decay | Quick drop-off |
| 30 days | Medium decay | Standard retention |
| 90 days | Slow decay | Long-term memory |
2.3 Reinforcement on Access
When a fact is accessed (retrieved in context), it gets reinforced:
// In reinforce():
const newStability = Math.min(stability + 0.1, 2.0); // +10%, cap 2.0
const newDifficulty = Math.max(difficulty - 0.1, 1.0); // -10%, floor 1.0
const newHalfLife = Math.min(halfLife * 1.5, 365); // ×1.5, cap 365 daysReinforcement effects:
- Accessed facts become more durable (increased halfLife)
- The decay curve becomes shallower (decreased difficulty)
- Base recall increases (increased stability)
This models the real-world effect of spaced repetition — facts you use often are remembered longer.
2.4 Nightly Decay Job
A background job runs nightly to decay all facts:
// In decayParams():
export function decayParams(params: EbisuParams, daysElapsed = 1): EbisuParams {
const decayFactor = 0.995 ** daysElapsed; // 0.5% per day
return {
stability: Math.max(params.stability * decayFactor, 0.5),
difficulty: Math.max(params.difficulty * decayFactor, 1.0),
halfLife: Math.max(params.halfLife * decayFactor, 7),
};
}Decay Factor Formula:
decayFactor = 0.995^daysElapsed
With 0.5% daily decay, after 30 days:
- stability ≈ 0.86 (14% weaker)
- difficulty ≈ 0.86 (14% shallower)
- halfLife ≈ 86% of original
2.5 Soft Delete Threshold
Facts are soft-deleted when:
- Confidence < 0.25 AND
- Last accessed > 180 days
if (daysSinceAccess > 365 || newConfidence < 0.25) {
// Soft delete — resurrectable
await prisma.entityFact.update({
where: { id: fact.id },
data: { deletedAt: now },
});
}2.6 Resurrection
Soft-deleted facts can be resurrected when a user asks about them:
// In resurrectSoftDeletedFacts():
// If search returns < 3 results, check soft-deleted facts
// that match query keywords and re-activate themThis handles the "I haven't thought about X in months but I'm asking about it now" case — the user clearly cares, so bring it back.
3. Query Intent Classification
3.1 Intent Types
Eryx classifies queries into 6 types to optimize retrieval:
| Intent | Patterns | Strategy |
|---|---|---|
| DECISION | "what did I decide", "chose", "agreed" | High-confidence facts only, quality weighting |
| FACTUAL | "what is X", "who is Y" | Entity-based, direct facts |
| EXPLORATORY | "tell me about", "what do you know" | Broad search, all collections |
| TEMPORAL | "last week", "in 2024" | Time filters, strong recency boost |
| COMPARATIVE | "X vs Y", "difference between" | Multi-entity, expanded limit |
| RELATIONSHIP | "works at", "related to" | Graph traversal |
3.2 Classification Algorithm
Pattern matching with LLM fallback:
// Pattern scoring
const scores: Map<QueryIntent, number> = new Map();
for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
let matchCount = 0;
for (const pattern of patterns) {
if (pattern.test(trimmed)) {
matchCount++;
}
}
if (matchCount > 0) {
scores.set(intent, matchCount);
}
}
// Best intent
const bestIntent = Array.from(scores.entries()).reduce((a, b) =>
a[1] > b[1] ? a : b
)[0];
const confidence = Math.min(0.9, 0.4 + matchCount * 0.15);If no pattern matches, falls back to LLM classification with gpt-4.1-mini.
3.3 Intent-Based Retrieval Strategies
// In getRetrievalStrategy():
switch (intent) {
case QueryIntent.DECISION:
return {
collections: ["entity_facts"],
scoreThreshold: 0.2, // Low threshold — confidence matters more
limit: 5, // Top facts only
applyQualityWeighting: true,
rerankAfterFusion: true,
};
case QueryIntent.EXPLORATORY:
return {
collections: ["entity_facts", "file_chunks", "memory_embeddings"],
scoreThreshold: 0.1, // Lower threshold — cast wide net
limit: 20, // More results
applyQualityWeighting: false,
rerankAfterFusion: true,
};
case QueryIntent.TEMPORAL:
return {
collections: ["entity_facts", "file_chunks", "memory_embeddings"],
scoreThreshold: 0.15,
limit: 10,
applyTemporalFilter: true,
scoreBoost: { recency: 0.8 }, // Strong recency boost
};
}4. Query Decomposition & RRF Fusion
4.1 When to Decompose
Queries are decomposed when they contain:
- Decision patterns: "decided", "chose", "agreed", "concluded"
- Comparison patterns: "vs", "versus", "compared", "difference"
- Multiple entities: "X and Y"
4.2 Decomposition Examples
Input: "what did I decide about the auth0 migration"
Output: ["auth0 migration decision", "auth0 migration choice", "auth0 migration conclusion"]
Input: "how does react compare to vue"
Output: ["react", "vue", "react vs vue comparison"]
Input: "tell me about Anthropic and OpenAI"
Output: ["Anthropic", "OpenAI"]
4.3 Reciprocal Rank Fusion (RRF)
When multiple sub-queries return results, they're fused using RRF:
// In fuseResultsWithRRF():
const RRF_K = 60;
for (const [subQuery, results] of subQueryResults) {
for (const { item, rank } of results) {
const rrfScore = 1 / (RRF_K + rank);
if (existing) {
existing.fusedScore += rrfScore; // Accumulate across sub-queries
} else {
itemScores.set(item, { fusedScore: rrfScore, subQueryScores });
}
}
}RRF Formula:
score(d) = Σ 1/(k + rank(d)) for each sub-query
| k value | Effect |
|---|---|
| k=0 | Pure rank — first position gets 1.0, second gets 0.5 |
| k=60 | Smoothed — first gets 0.016, tenth gets 0.014 |
| k=infinity | Ignores rank, pure frequency |
Why k=60? It provides enough smoothing to not over-weight the first position, but enough discrimination to still prefer higher-ranked results.
4.4 RRF Visualization
Query: "auth0 migration decision"
Sub-query results:
SubQ1: [A, B, C, D, E]
SubQ2: [B, F, A, G, H]
SubQ3: [A, C, B, I, J]
Item A: rank in Q1=0, Q2=2, Q3=0 -> 1/(60+0) + 1/(60+2) + 1/(60+0) = 0.0331
Item B: rank in Q1=1, Q2=0, Q3=2 -> 1/(60+1) + 1/(60+0) + 1/(60+2) = 0.0330
Item C: rank in Q1=2, Q2=N/A, Q3=1 -> 1/(60+2) + 0 + 1/(60+1) = 0.0327
Final order: A > B > C > D > E > F > ...
5. Cohere Rerank Integration
After RRF fusion, Eryx can apply Cohere Rerank 3 for semantic reordering:
// In rerankEntityFacts():
const response = await fetch("https://api.cohere.ai/v2/rerank", {
method: "POST",
headers: { Authorization: `Bearer ${ragConfig.cohereKey}` },
body: JSON.stringify({
query,
documents: facts.map(f => ({ text: f.content })),
topN: facts.length,
model: "rerank-english-v3.0",
return_documents: false,
}),
});Flow:
- Get initial results from Qdrant (fast vector search)
- Reorder with Cohere (semantic understanding)
- Take top results
Caching: Results cached for 60 seconds per userId + queryHash to avoid redundant API calls.
6. Serendipity Injection
6.1 The Echo Chamber Problem
Pure relevance retrieval tends to return the same types of facts, creating an echo chamber. Eryx injects "serendipity facts" — high-confidence facts from unrelated topics — to surface forgotten knowledge.
6.2 Thompson Sampling for Injection Probability
The injection probability is learnable per user via Thompson sampling:
// In learnable-serendipity.service:
// Maintain Beta(alpha, beta) distribution per user
// Sample to decide whether to inject serendipity facts
// Update distribution based on user engagement6.3 Serendipity Selection
// In getSerendipityFacts():
const oldFacts = await prisma.entityFact.findMany({
where: {
entityId: { notIn: recentEntityIds }, // NOT recently discussed
confidence: { gte: 0.7 }, // Only high-confidence
deletedAt: null,
},
orderBy: { confidence: "desc" },
take: 2, // Max 2 injections
});Rules:
- Only for exploratory intents (not decision/factual/comparative)
- Minimum 3 entities already retrieved
- Max 2 facts injected
- Only from entities NOT in current conversation
7. Adaptive Spatial Index
7.1 The Problem with Fixed Partitions
Traditional vector DBs use fixed partitions. Eryx implements learnable routing — the system learns which entity types are relevant to which queries.
7.2 How It Works
// In adaptive-spatial-index.service:
// 1. Record query + entity types that matched
// 2. Build usage statistics over time
// 3. On new query, predict likely entity types
// 4. Only search relevant partitions
const likelyTypes = await getSearchPartitions(query);
// → ["PERSON", "SKILL"] for "who works on ML"
if (likelyTypes.length > 0 && likelyTypes.length < 8) {
const entityIds = await prisma.entity.findMany({
where: { userId, type: { in: likelyTypes } },
select: { id: true },
take: 500,
});
// Search only these entity partitions
}Query → Entity Type Examples:
"who knows Rust" → [PERSON, SKILL]
"Auth0 migration" → [PROJECT, COMPANY]
"my presentation" → [EVENT, PROJECT]
8. Memory Context Assembly
8.1 Token Budget Architecture
Total context budget: 16,000 tokens
| Component | Max Tokens |
|---|---|
| System prompt | Adaptive |
| Memory RAG | 1,500 |
| Project RAG | 3,000 |
| File content | 4,000 |
| User-selected memories | 1,500 |
8.2 Adaptive Context Windows by Intent
const CONTEXT_WINDOW_TOKENS = {
decision: { maxTokens: 2000, priorityOrder: ["entityFacts", "relations", "episodic", "chunks"] },
exploratory: { maxTokens: 3000, priorityOrder: ["entityFacts", "relations", "chunks", "serendipity"] },
temporal: { maxTokens: 2000, priorityOrder: ["episodic", "entityFacts", "relations", "chunks"] },
};8.3 Prompt Format
## KNOWLEDGE ABOUT USER:
[Sarah] — confidence: 85%
- Works at Anthropic as ML Engineer
- Prefers React over Vue
- Currently learning reinforcement learning
## RELATIONSHIPS:
- Anthropic — company_of Sarah
- React — skilled_in Sarah
- Anthropic — uses Claude
## MEMORY SPRINKLE (from older topics):
- [Birthday]: Sarah's birthday is March 15
- [Preference]: Sarah prefers dark mode IDE theme
## RECENT DISCUSSIONS:
- 2026-07-18: Discussed auth0 migration decision...
Topics: auth0, migration, authentication
- 2026-07-15: Talked about new ML project...
Topics: ML, project, research
## FROM FILES:
- (architecture.md) The system uses a microservices architecture with...
## FROM NOTES:
- (Project Ideas) Consider using vector database for semantic search...9. Effective Confidence Calculation
9.1 Formula
// In effectiveConfidence():
export function effectiveConfidence(
baseConfidence: number, // Source quality (0-1)
params: EbisuParams, // Ebisu parameters
lastAccessedAt: Date
): number {
const tDays = (Date.now() - lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24);
const recall = recallProbability(params, tDays);
return baseConfidence * recall;
}Formula:
effectiveConfidence = baseConfidence × P(t)
Where P(t) is the Ebisu recall probability.
9.2 Example Calculation
baseConfidence = 0.8 (high-quality file source)
stability = 1.0
difficulty = 2.5
halfLife = 30 days
lastAccessed = 10 days ago
tDays = 10
recall = 1.0 × 2^(-(10/30)^2.5)
= 1.0 × 2^(-0.163)
= 1.0 × 0.893
= 0.893
effectiveConfidence = 0.8 × 0.893 = 0.714
10. Data Flow: End-to-End
10.1 Chat Request Flow
User Message
│
▼
app/api/chat/route.ts (POST)
│
├──► Rate limit check
├──► Authenticate
│
├──► buildMessages()
│ │
│ ├──► getChatContext() → summarize.service.ts
│ │ └──► Incremental summarization if needed
│ │
│ ├──► retrieveContext() → rag.service.ts
│ │
│ └──► getMemoryContext() → memory-v2.service.ts
│ │
│ ├──► classifyQueryIntent() → query-intent.service.ts
│ │ └──► Returns: DECISION, FACTUAL, EXPLORATORY, etc.
│ │
│ ├──► decomposeQuery() → query-decompose.service.ts
│ │ └──► ["sub1", "sub2", "sub3"] or single query
│ │
│ ├──► embedText() → nomic-embed-text
│ │
│ ├──► getSearchPartitions() → adaptive-spatial-index.service.ts
│ │ └──► Returns likely entity types
│ │
│ ├──► searchEntityFacts() → Qdrant
│ │ ├──► RRF fusion of sub-query results
│ │ ├──► Cohere Rerank (optional)
│ │ ├──► Keyword hybrid boost
│ │ └──► Temporal validity filter
│ │
│ ├──► resurrectSoftDeletedFacts() if sparse results
│ │
│ ├──► reinforce() → forget-curve.service.ts (fire-and-forget)
│ │
│ ├──► getSerendipityFacts() (Thompson sampling)
│ │
│ ├──► getRelationsForEntities() → Prisma
│ │
│ ├──► searchFileChunks() + searchMemoryEmbeddings() → Qdrant
│ │
│ └──► assembleMemoryPrompt()
│ └──► Token-budget-aware assembly by intent
│
├──► streamText() → Anthropic/OpenAI
│
├──► Process tool calls → MCP executor
│
└──► Queue (fire-and-forget):
├──► queueFactExtraction() → entity-fact.service.ts
└──► deductCredits()
10.2 Fact Extraction Pipeline
After Chat Response:
│
▼
queueFactExtraction() → BullMQ job queue
│
▼
Worker: extractEntities()
├──► GPT-4.1-mini extracts entities
├──► resolveEntities() (name dedup via nameHash)
└──► Jaccard similarity on aliases (threshold 0.5)
│
▼
Worker: extractFacts()
├──► GPT-4.1-mini extracts facts per entity
└──► Check for conflicts
│
├──► No conflict → saveEntityFact() → Prisma
│ │
│ ├──► indexEntityFact() → Qdrant
│ └──► reinforce() → Ebisu params initialized
│
└──► Conflict → logMemoryConflict() → user resolves
│
▼
updateEntityBaseConfidence()
└──► Avg of all fact confidences
11. Key Differentiators from Simple RAG
| Feature | Simple RAG | Eryx |
|---|---|---|
| Memory model | Flat chunks | Entity knowledge graph |
| Forget curve | TTL expiration | Ebisu adaptive decay |
| Query handling | Single query | Decomposition + RRF |
| Result fusion | Single search | Multi-collection RRF |
| Echo chamber | No protection | Serendipity injection |
| Entity resolution | None | Name hashing + fuzzy match |
| Implicit relations | None | DERIVES inference |
| Spatial indexing | Fixed | Learnable routing |
12. Configuration Reference
12.1 Ebisu Parameters
const DECAY_CONFIG = {
initialHalfLife: 30, // days
initialDifficulty: 2.5,
initialStability: 1.0,
minStability: 0.5,
maxStability: 2.0,
minConfidenceThreshold: 0.25,
reinforcementMultiplier: 1.5,
difficultyDecayRate: 0.1,
stabilityBoostRate: 0.1,
minDifficulty: 1.0,
maxDifficulty: 4.0,
maxHalfLife: 365, // cap at 1 year
softDeleteAfterDays: 365,
};12.2 Vector Search Parameters
const HNSW_CONFIG = {
m: 16,
ef_construct: 128,
full_scan_threshold: 10000,
};
const SEARCH_CONFIG = {
scoreThreshold: 0.7, // cosine similarity threshold
recencyWeight: 0.3, // 30% recency boost
freshnessWeight: 0.1, // 10% freshness boost
freshnessHalfLife: 90, // days
};12.3 RRF Parameters
const RRF_K = 60; // smoothing parameter
const MAX_SUB_QUERIES = 5;
const MIN_QUERY_LENGTH_FOR_DECOMPOSITION = 15;Conclusion
Eryx's memory system goes far beyond simple document retrieval. By combining:
- Entity knowledge graphs with temporal validity and conflict detection
- Ebisu forget curves that model actual human memory decay
- Query intelligence with decomposition, intent routing, and RRF fusion
- Serendipity injection to prevent echo chambers
The system delivers context that's both relevant (accessed recently, high confidence) and surprising (forgotten facts from unrelated topics).
All the formulas, thresholds, and parameters are tunable — the architecture is built for production at scale while remaining flexible for different use cases.