← Back to Plugins
Tools

Memoria

Primo-Studio By Primo-Studio ⭐ 7 stars 👁 57 views ▲ 0 votes

Memoria — Multi-layer persistent memory plugin for OpenClaw. SQLite + FTS5 + embeddings + knowledge graph + topics + adaptive budget. 100% local via Ollama.

GitHub

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

Loading comments...