Tools
Memoria
Memoria — Multi-layer persistent memory plugin for OpenClaw. SQLite + FTS5 + embeddings + knowledge graph + topics + adaptive budget. 100% local via Ollama.
Configuration Example
{
"autoRecall": true,
"autoCapture": true,
"recallLimit": 12,
"captureMaxFacts": 8,
"defaultAgent": "koda",
"contextWindow": 200000,
"workspacePath": "~/.openclaw/workspace",
"syncMd": true,
"llm": {
"provider": "ollama",
"baseUrl": "http://localhost:11434",
"model": "gemma3:4b",
"apiKey": "",
"overrides": {
"extract": { "provider": "ollama", "model": "gemma3:4b" },
"contradiction": { "provider": "openai", "model": "gpt-5.4-nano", "apiKey": "sk-..." },
"graph": { "provider": "ollama", "model": "gemma3:4b" },
"topics": { "provider": "lmstudio", "model": "glm-4.7-flash" },
}
},
"embed": {
"provider": "ollama",
"baseUrl": "http://localhost:11434",
"model": "nomic-embed-text-v2-moe",
"dimensions": 768,
"apiKey": ""
},
"fallback": [
{ "name": "ollama", "type": "ollama", "model": "gemma3:4b", "baseUrl": "http://localhost:11434", "timeoutMs": 12000, "embedModel": "nomic-embed-text-v2-moe", "embedDimensions": 768 },
{ "name": "openai", "type": "openai", "model": "gpt-5.4-nano", "baseUrl": "https://api.openai.com/v1", "apiKey": "sk-...", "timeoutMs": 15000 },
{ "name": "lmstudio", "type": "lmstudio", "model": "auto", "baseUrl": "http://localhost:1234/v1", "timeoutMs": 12000 }
],
"topics": {
"emergenceThreshold": 3,
"mergeOverlap": 0.7,
"subtopicThreshold": 5,
"decayDays": 30,
"scanInterval": 15
},
"mdRegen": {
"recentDays": 30,
"maxFactsPerFile": 150,
"archiveNotice": true
}
}
README
# 🧠 Memoria v2.5.0 — Multi-layer Memory Plugin for OpenClaw
Brain-inspired persistent memory for AI agents. SQLite-backed, fully local, zero cloud dependency.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ MEMORIA v2.5.0 │
│ │
│ Hooks: before_prompt_build │ agent_end │ after_compaction │
├─────────────────────────────────────────────────────────────┤
│ │
│ RECALL PIPELINE (before_prompt_build): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │🔥 Hot │→│ Hybrid │→│ Graph │→│ Topics │ │
│ │ Tier │ │ Search │ │ Enrich │ │ Enrich │ │
│ │ access≥5 │ │ FTS5+cos │ │ BFS 2hop │ │ keyword │ │
│ │ always │ │ +scoring │ │ hebbian │ │ +cosine │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↓ ↓ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Context │ ← merge all ─────────────── │ Adaptive │ │
│ │ Tree │ │ Budget │ │
│ │ heuristic│ │ 2-12 │ │
│ │ NO LLM │ │ facts │ │
│ └──────────┘ └──────────┘ │
│ ↓ │
│ formatRecall() → inject into system prompt │
│ │
│ CAPTURE PIPELINE (agent_end / after_compaction): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Extract │→│ Selective│→│ Store │→│ Post- │ │
│ │ via LLM │ │ Filter │ │ to DB │ │ process │ │
│ │(extract │ │ dedup+ │ │ │ │ embed+ │ │
│ │ Chain) │ │contradict│ │ │ │ graph+ │ │
│ │ │ │(contradict│ │ │ │ topics+ │ │
│ │ │ │ Chain) │ │ │ │ sync .md │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ Per-layer LLM: extract │ contradiction │ graph │ topics │
│ Default: FallbackChain (Ollama → OpenAI → LM Studio) │
│ Override: llm.overrides.{layer} → provider/model au choix │
├─────────────────────────────────────────────────────────────┤
│ SQLite memoria.db (FTS5 + vectors) │
│ Tables: facts, facts_fts, embeddings, entities, │
│ relations, topics, fact_topics │
└─────────────────────────────────────────────────────────────┘
```
---
## Layers — Détail par couche
### Layer 1: SQLite Core + FTS5 (`db.ts` ~446 lignes)
- **DB**: `~/.openclaw/workspace/memory/memoria.db` (WAL mode)
- **Tables**: `facts` (main), `facts_fts` (FTS5 virtual table), `embeddings`, `entities`, `relations`, `topics`, `fact_topics`
- **CRUD**: `storeFact()`, `getFact()`, `searchFacts()`, `recentFacts()`, `hotFacts()`, `supersedeFact()`, `enrichFact()`, `trackAccess()`
- **FTS5**: Index via triggers (INSERT/UPDATE/DELETE). Queries sanitisées (hyphens, unicode-safe)
- **LLM**: Aucun
- **Provider**: Aucun
- **Fallback**: N/A
### Layer 2: Temporal Scoring + Hot Tier (`scoring.ts`)
- **Rôle**: Score chaque fait par fraîcheur + catégorie + fréquence d'accès
- **Formule**: `score = confidence × decayFactor × recencyBoost × accessBoost × freshnessBoost × stalePenalty`
- Decay exponentiel: demi-vie par catégorie (erreur=∞, savoir/preference=90j, outil=30j, chronologie=14j)
- Access boost: **`0.3 × log(accessCount + 1)`** — un fait accédé 50x score 2.2x plus (v2.5.0: 3x plus fort qu'avant)
- Recency boost: <24h = ×1.3, <7j = ×1.1
- Freshness bonus: mis à jour <48h = ×1.2
- Stale penalty: >90j + faible confiance = ×0.7
- **Hot Tier** (NEW v2.5.0): faits accédés ≥5x = **toujours injectés** en recall, comme un numéro de téléphone appris par cœur
- `getHotFacts()` → top 3 par access_count (configurable: `minAccessCount`, `maxHotFacts`, `staleAfterDays`)
- Hot facts exclus du search normal pour éviter les doublons
- Slots réservés : `searchLimit = recallLimit - hotCount`
- **API**: `scoreAndRank(facts)`, `scoreFact(fact)`, `getHotFacts(facts, config)`
- **LLM**: Aucun
- **Provider**: Aucun
- **Fallback**: N/A
### Layer 3: Selective Memory (`selective.ts` ~361 lignes)
- **Rôle**: Filtre avant stockage — dedup, contradiction, enrichment
- **Pipeline**:
1. Longueur < 10 chars → skip (too_short)
2. Noise patterns (salutations, confirmations) → skip
3. Importance scoring (mots-clés techniques, catégorie) → threshold
4. FTS5 candidates → Levenshtein > 0.85 → skip (duplicate)
5. FTS5 candidates → Jaccard keyword overlap → skip (duplicate)
6. Si similaire mais pas identique → **LLM contradiction check** → supersede/enrich
- **API**: `process(fact, category, confidence)`, `processAndApply(...)`
- **LLM**: ✅ Contradiction check uniquement
- **Provider**: `this.llm` = `contradictionLlm` (configurable via `llm.overrides.contradiction`)
- **Fallback**: Override provider → puis chain par défaut (Ollama → OpenAI → LM Studio)
- **Safety**: try/catch → si LLM fail, le fait est stocké quand même (conservative)
### Layer 4: Embeddings + Hybrid Search (`embeddings.ts` ~247 lignes)
- **Rôle**: Vecteurs + recherche sémantique
- **Modèle embed**: configurable, défaut `nomic-embed-text-v2-moe` (768 dims)
- **Stockage**: Table `embeddings` (fact_id, vector BLOB, model, dimensions, created_at)
- **Hybrid search**: FTS5 score (60%) + cosine similarity (40%) + temporal scoring → merged ranking
- **API**: `hybridSearch(query, limit)`, `embedFact(factId)`, `embedAllMissing()`, `embedBatch()`, `embeddedCount()`
- **LLM**: Aucun
- **Provider**: `this.provider` = `EmbedProvider` (configuré via `embed.provider`)
- Ollama: POST `/api/embed` (`OllamaEmbed`)
- LM Studio: POST `/embeddings` (`lmStudioEmbed`)
- OpenAI: POST `/embeddings` (`openaiEmbed`)
- OpenRouter: POST `/embeddings` (`openrouterEmbed`)
- **Fallback embed**: Aucun (single provider). Si embed fail → fait stocké sans vecteur
### Layer 5: Knowledge Graph + Hebbian (`graph.ts` ~390 lignes)
- **Rôle**: Entités extraites, relations pondérées, traversal BFS
- **Extraction**: LLM parse le fait → extrait entités (nom, type) + relations (source, target, relation)
- **Hebbian**: Co-accès renforce les poids des relations (weight += 0.1 par co-recall)
- **Traversal**: `getRelatedFacts(entityNames, maxHops=2, maxFacts=10)` — BFS, fuzzy entity matching
- **Tables**: `entities` (id, name, type, attributes), `relations` (source_id, target_id, relation, weight, context)
- **API**: `extractAndStore(factId, factText)`, `getRelatedFacts()`, `findEntitiesInText()`, `hebbianReinforce()`, `stats()`
- **LLM**: ✅ Extraction entités/relations (1 appel par fait capturé)
- **Provider**: `this.llm` = `graphLlm` (configurable via `llm.overrides.graph`)
- **Fallback**: Override provider → puis chain par défaut
- **Safety**: try/catch → si LLM fail, le fait est stocké mais pas indexé dans le graph
### Layer 6: Context Tree (`context-tree.ts` ~340 lignes)
- **Rôle**: Organise les faits candidats en arbre hiérarchique, pondère par query
- **Algorithme**:
1. Cluster faits par catégorie
2. Sous-cluster par mots-clés si > 5 faits
3. Pondérer chaque branche par overlap query ↔ labels
4. Retourner les faits triés par poids de branche
- **Extraction keywords**: ⚠️ **Heuristique locale** (regex + patterns), PAS de LLM
- **API**: `build(facts, query)` → `ContextTree`, `extractFacts(tree, limit)`, `renderTree(tree, depth)`
- **LLM**: ❌ Aucun — extraction keywords = regex/heuristique locale
- **Provider**: Aucun
- **Fallback**: N/A
### Layer 7: Adaptive Budget (`budget.ts` ~121 lignes)
- **Rôle**: Limite dynamique du nombre de faits injectés selon l'espace contexte
- **Courbe quadratique** (v2.3.0):
- Light (< 30%): 10 faits max
- Medium (30-70%): 10 → 4 (courbe t² — lent au début, rapide à la fin)
- Heavy (70-85%): 4 → 2
- Critical (> 85%): 2 faits (minimum)
- **Config**: contextWindow (200K défaut, nous=1M), maxFacts=12 (défaut), minFacts=2, thresholds configurables
- **API**: `compute(messagesTokenEstimate, systemTokenEstimate)` → `BudgetResult { limit, usage, zone }`
- **LLM**: Aucun
- **Provider**: Aucun
- **Fallback**: N/A
### Layer 8: Topics Émergents (`topics.ts` ~688 lignes)
- **Rôle**: Clustering automatique de faits par keywords partagés
- **Processus**:
1. **LLM** extrait 3-5 keywords par fait → stockés dans `facts.tags` (JSON array)
2. Scan orphans: si ≥ 3 faits partagent un keyword → créer topic
3. Si ≥ 5 faits partagent un keyword spécifique dans un topic → créer subtopic
4. Topics avec > 70% overlap → fusionner
5. Topic embedding = moyenne des embeddings des faits membres (via `this.embedder`)
6. **LLM** nomme chaque topic (prompt → 1-3 mots)
- **Tables**: `topics` (id, name, keywords, parent_id, score, embedding), `fact_topics` (fact_id, topic_id)
- **Scoring**: score = fact_count × (1 + recency_boost), decay si inactif > 30j
- **API**: `findRelevantTopics(query, limit)`, `onFactCaptured(factId, factText, category)`, `scanAndEmerge()`, `stats()`
- **LLM**: ✅ 2 usages — keyword extraction + topic naming
- **Provider**: `this.llm` = `topicsLlm` (configurable via `llm.overrides.topics`) + `this.embedder` (config `embed`)
- **Fallback**: Override provider → puis chain par défaut
- **Safety**: try/catch → si LLM fail, fait non taggé (reste orphelin jusqu'au prochain scan)
### Layer 9: .md Sync + Regen (`sync.ts` ~258 lignes, `md-regen.ts` ~277 lignes)
**Sync** (`sync.ts`):
- Après capture, append nouveaux faits aux fichiers .md du workspace
- Mapping catégorie → fichier :
| Catégorie | Fichier cible |
|-----------|------
... (truncated)
tools
Comments
Sign in to leave a comment