GPU-Accelerated Persistent Memory for Autonomous AI Agents
Hybrid search · RRF fusion · Dynamic reranker gate · Cross-encoder reranking · Contextual retrieval
memory-spark is a production memory substrate for OpenClaw agents: it continuously ingests workspace knowledge, indexes it in LanceDB with hybrid dense+sparse retrieval, reranks candidates with a cross-encoder, and injects high-value context before each turn. The result is materially better recall of deployment-specific facts, safety constraints, and historical incidents while staying within low-latency budgets.
Paper: See
paper/memory-spark.pdffor the full technical report.
Results from evaluation/results/ generated via scripts/run-beir-bench.ts across 36 configurations on 3 BEIR datasets.
| Metric | Best Overall (U) | GATE-A (Production) | Vector-Only |
|---|---|---|---|
| NDCG@10 | 0.7889 | 0.7802 | 0.7709 |
| MRR | 0.7572 | 0.7455 | 0.7365 |
| Recall@10 | 0.9099 | 0.9137 | 0.9037 |
| Reranker Calls | 100% | 21% | 0% |
All results are zero-shot — no dataset-specific fine-tuning. Modern benchmarks from BEIR 2.0 (2025/2026) and MTEB leaderboard.
| System | Avg NDCG@10 | Notes |
|---|---|---|
| Voyage-Large-2 | 54.8% | BEIR 2.0 leader (18 datasets) |
| Cohere Embed v4 | 53.7% | Commercial API |
| NVIDIA Nemotron-8B | ~52-54% | Open-weight (our model) |
| BGE-Large-EN | 52.3% | Open-weight alternative |
| OpenAI text-3-large | 51.9% | Commercial API |
| BM25 | 41.2% | Sparse baseline |
| memory-spark: Config U | 78.89% | SciFact (single dataset) |
| memory-spark: GATE-A | 78.02% | SciFact (production default) |
Important context: Our 78%+ scores are on SciFact only (scientific claim verification). BEIR averages include 18 datasets across diverse domains. We outperform on specialized scientific retrieval; full BEIR 2.0 comparison is planned future work.
Note: Our embedding model (NVIDIA Llama-Embed-Nemotron-8B) is competitive with 2026 SOTA. The pipeline gains come from the 15-stage retrieval architecture, not a superior base embedding model.
| Config | NDCG@10 | Recall@10 | MRR | Strategy |
|---|---|---|---|---|
| U: Logit α=0.4 | 0.7889 | 0.9099 | 0.7572 | Best NDCG — reranker on every query |
| V: Logit α=0.6 | 0.7885 | 0.9243 | 0.7527 | Best Recall — reranker on every query |
| N: Logit α=0.5 | 0.7863 | 0.9143 | 0.7522 | Balanced blend |
| MQ-C: Multi-Query | 0.7853 | 0.9177 | 0.7500 | 3 LLM reformulations |
| GATE-A ★ | 0.7802 | 0.9137 | 0.7455 | Production — 78% skip, best latency |
| GATE-D | 0.7803 | 0.8924 | 0.7525 | Soft gate + RRF k=20 |
| RRF-D | 0.7798 | 0.8924 | 0.7514 | RRF k=20 |
| P: Full Adaptive | 0.7797 | 0.9129 | 0.7440 | Adaptive RRF + conditional rerank |
| A: Vector-Only | 0.7709 | 0.9037 | 0.7365 | Baseline — no reranker |
| D: Full Pipeline | 0.7525 | 0.9101 | 0.7052 | Hybrid + reranker + MMR |
| F: Hybrid + HyDE | 0.7278 | 0.8874 | 0.6844 | HyDE hurts on short claims |
| B: FTS-Only | 0.6587 | 0.7924 | 0.6240 | BM25 keyword only |
flowchart LR
subgraph PREP["① Prepare"]
A["🔄 Agent Turn\nbefore_prompt_build"]
B["1·Query Clean\nstrip metadata"]
C["2·HyDE ⚡\nhypothetical doc"]
D["3·Multi-Query\n3 reformulations"]
E["4·Embed\nnemotron-8b 4096d"]
A --> B --> C --> D --> E
end
subgraph SEARCH["② Search + Fuse"]
F["Vector Search\nIVF_PQ index"]
G["FTS Search\ntantivy BM25"]
H["5·RRF Merge\nk=60 rank fusion"]
I["6·Source Wt\ncaptures 1.5×\nmistakes 1.6×"]
J["7·Temporal Decay\n0.8 + 0.2·e⁻ᵗ"]
K["8·Source Dedup\nJaccard > 0.85"]
F --> H
G --> H
H --> I --> J --> K
end
subgraph GATE["③ ⚡ Reranker Gate"]
L{"9·Confidence\nσ = s₁ − s₅"}
M["10·Cross-Encoder\nrerank-1b-v2\n:18096"]
N["11·RRF Blend\nvec ⊕ rerank"]
L -->|"σ ∈ [0.02, 0.08]\nFIRE 21%"| M --> N
end
subgraph OUT["④ Output"]
O["12·MMR\nλ=0.9 diversity"]
P["13·Parent Expand\nchild → parent"]
Q["14·LCM Dedup\noverlap > 40%"]
R["15·Security\ninjection filter\n≤ 2000 tokens"]
S["📦 Memories\nXML → prompt"]
O --> P --> Q --> R --> S
end
E --> F
E --> G
K --> L
L -->|"σ > 0.08 · σ < 0.02\nSKIP 78%"| O
N --> O
subgraph STORE["Storage"]
T[("LanceDB\n7 pools\nIVF_PQ + FTS")]
end
subgraph CAP["Auto-Capture"]
U["agent_end hook"]
V["Extract + classify\ndedup 0.92 cosine"]
U --> V
end
E -.->|"embed + index"| T
T -.->|query| F
T -.->|query| G
V -.->|store| T
style GATE fill:#0d1a0d,stroke:#3fb950,stroke-width:2
style L fill:#132a13,stroke:#3fb950,stroke-width:2
style M fill:#2d1117,stroke:#f85149
style S fill:#1a1f2e,stroke:#58a6ff,stroke-width:2
style T fill:#1a1a2e,stroke:#58a6ff
sequenceDiagram
participant Agent
participant Hook as before_prompt_build
participant Pipeline as 15-Stage Pipeline
participant Vector as Vector Search
participant FTS as BM25 Search
participant Gate as Reranker Gate
participant Reranker as Cross-Encoder
participant Prompt as XML Injection
Agent->>Hook: Query text
Hook->>Pipeline: recall(query)
Pipeline->>Pipeline: 1. Clean query
Pipeline->>Pipeline: 2. HyDE (optional)
Pipeline->>Pipeline: 3. Multi-query expand
Pipeline->>Pipeline: 4. Embed (4096d)
Pipeline->>Vector: IVF_PQ ANN
Vector-->>Pipeline: Top-K candidates
Pipeline->>FTS: BM25 tantivy
FTS-->>Pipeline: Top-K matches
Pipeline->>Pipeline: 5. RRF Merge
Pipeline->>Pipeline: 6. Source weighting
Pipeline->>Pipeline: 7. Temporal decay
Pipeline->>Pipeline: 8. Dedup sources
Pipeline->>Gate: Check spread = s1 - s5
alt High spread (skip reranker - 78%)
Gate->>Pipeline: SKIP reranker
else Mid spread (fire reranker - 21%)
Gate->>Reranker: Cross-encode
Reranker-->>Pipeline: Re-ranked scores
Pipeline->>Pipeline: 11. RRF blend
end
Pipeline->>Pipeline: 12. MMR diversity
Pipeline->>Pipeline: 13. Parent expand
Pipeline->>Pipeline: 14. LCM dedup
Pipeline->>Pipeline: 15. Security filter
Pipeline-->>Hook: Top-N memories
Hook->>Prompt: Inject XML context
Prompt-->>Agent: Enriched prompt
erDiagram
MEMORY_CHUNK {
string id PK "ULID"
string path "File path"
int start_line
int end_line
string content "Raw text"
float[] vector "4096d embedding"
string content_type "memory|capture|reference"
string source "agent_name"
string pool "agent_memory|mistakes|rules"
json metadata "tags, priority, version"
timestamp created_at
timestamp updated_at
}
AGENT_WORKSPACE {
string agent_id PK
string memory_pool FK
string mistakes_pool FK
string rules_pool FK
}
REFERENCE_LIBRARY {
string path PK
string content_type
json metadata
}
MEMORY_CHUNK ||--o{ AGENT_WORKSPACE : "belongs_to_pool"
REFERENCE_LIBRARY ||--o{ MEMORY_CHUNK : "indexed_as"
graph TB
subgraph OPENCLAW["OpenClaw Gateway"]
A[Agent Session]
B[Plugin Host]
end
subgraph SPARK["NVIDIA DGX Spark"]
C[Embedding Service<br/>:18091]
D[Reranker Service<br/>:18096]
E[LLM Service<br/>:18080<br/>Nemotron-120B]
F[OCR Service<br/>:18097<br/>EasyOCR]
G[GLM-OCR Service<br/>:18080<br/>vLLM vision]
H[NER Service<br/>:18112]
I[Zero-Shot Classifier<br/>:18113]
J[Summarizer Service<br/>:18110]
K[STT Service<br/>:18094]
end
subgraph STORAGE["Local Storage"]
L[(LanceDB<br/>IVF_PQ + FTS)]
M[File Watcher]
end
A --> B
B -->|"memory_search<br/>memory_store"| L
B -->|"embed()"| C
B -->|"rerank()"| D
B -->|"HyDE generate"| E
B -->|"OCR parse"| F
B -->|"GLM-OCR parse"| G
B -->|"entity extract"| H
B -->|"intent classify"| I
B -->|"summarize"| J
B -->|"speech-to-text"| K
M -->|"ingest"| L
style SPARK fill:#0d1a0d,stroke:#3fb950
style STORAGE fill:#1a1a2e,stroke:#58a6ff
| Component | Model | Port | Purpose |
|---|---|---|---|
| Embeddings | nvidia/llama-embed-nemotron-8b (4096d) | 18091 | Dense vector encoding |
| Reranker | nvidia/llama-nemotron-rerank-1b-v2 | 18096 | Cross-encoder scoring |
| LLM | Nemotron-Super-120B-A12B (NVFP4) | 18080 | HyDE + GLM-OCR |
| OCR (legacy) | EasyOCR | 18097 | Scanned PDF fallback |
| NER | configured service | 18112 | Named entity extraction |
| Zero-shot | configured service | 18113 | Intent classification |
| Summarizer | configured service | 18110 | Document summarization |
| STT | configured service | 18094 | Speech-to-text |
| Storage | LanceDB (IVF_PQ + FTS) | local | Vector + keyword index |
All ML inference runs on a local NVIDIA DGX Spark — zero cloud API calls.
git clone https://github.com/kleinpanic/memory-spark
cd memory-spark
npm ci
npm run buildIn ~/.openclaw/openclaw.json:
| Block | Purpose | Key Options |
|---|---|---|
embed |
Embedding provider | provider, spark.baseUrl, spark.model, spark.dimensions, spark.queryInstruction |
rerank |
Cross-encoder reranking | enabled, rerankerGate, blendMode, rrfK, topN, minScoreSpread |
hyde |
HyDE generation | enabled, llmUrl, model, maxTokens, timeoutMs |
fts |
Full-text search | enabled, sigmoidMidpoint |
chunk |
Document chunking | maxTokens, hierarchical, parentMaxTokens, childMaxTokens |
autoRecall |
Memory injection | enabled, agents, maxResults, mmrLambda, temporalDecay |
autoCapture |
Fact extraction | enabled, agents, minConfidence, minMessageLength, useClassifier |
spark |
Service endpoints | embed, rerank, ocr, glmOcr, ner, zeroShot, summarizer, stt |
Full Schema:
src/config.tsis the authoritative source with detailed JSDoc comments.
| Mode | Behavior | Use Case |
|---|---|---|
"off" |
Always call reranker | Baseline comparison |
"hard" |
Skip if σ > 0.08 or σ < 0.02 | Production default — ~78% skip rate |
"soft" |
Dynamically weight vector vs reranker | Experimental — adaptive blending |
Default: floor=0.8, rate=0.03 → 30-day content retains ~88% of original score.
| Source | Default Weight | Effect |
|---|---|---|
capture |
1.5× | Agent-initiated facts |
mistakes |
1.6× | Error patterns (highest priority) |
memory |
1.0× | Baseline workspace memories |
sessions |
0.5× | Lower priority ephemeral content |
reference |
1.0× | Shared knowledge base |
Full Schema:
src/config.tsis the authoritative source. The TypeScript interfaces include detailed JSDoc comments for every option.
| Category | Tools | Purpose |
|---|---|---|
| 🧠 Core Memory | search, get, store, forget, forget_by_path, bulk_ingest |
CRUD operations on agent knowledge |
| 🔍 Discovery | reference_search, temporal, related, mistakes_search, rules_search |
Specialized search across pools |
| ⚙️ Admin | mistakes_store, rules_store, inspect, reindex, index_status, recall_debug, gate_status |
Diagnostics & configuration |
| Tool | Purpose |
|---|---|
memory_search |
Vector + FTS hybrid search across all knowledge |
memory_get |
Read a file by path and line range |
memory_store |
Store a fact, preference, or decision |
memory_forget |
Remove memories matching a query |
memory_forget_by_path |
Remove all chunks for a file path |
memory_bulk_ingest |
Batch store 1–100 memories in one call |
| Tool | Purpose |
|---|---|
memory_reference_search |
Search indexed reference docs (read-only pools) |
memory_temporal |
Time-windowed search ("what did I learn last week?") |
memory_related |
Find semantically similar memories by chunk ID |
memory_mistakes_search |
Search agent mistake patterns |
memory_rules_search |
Search shared rules across agents |
| Tool | Purpose |
|---|---|
memory_mistakes_store |
Store a mistake pattern (1.6× recall boost) |
memory_rules_store |
Store a shared rule for all agents |
memory_inspect |
Simulate recall — see what would be injected |
memory_reindex |
Trigger re-index (single file or full scan) |
memory_index_status |
Health dashboard with pool/agent breakdown |
memory_recall_debug |
Full pipeline trace — see every stage's decisions |
memory_gate_status |
Show reranker gate configuration |
# Run BEIR benchmark (specific configs)
npx tsx scripts/run-beir-bench.ts --dataset scifact --config A,GATE-A
# Full benchmark suite (all configs × all datasets, ~7-8h)
bash scripts/run-full-benchmark.sh
# Generate SVG charts from results
npx tsx evaluation/generate-charts.ts
# Compile LaTeX paper
cd paper && pdflatex memory-spark.tex| Configuration | NDCG@10 | Recall@10 | Δ NDCG |
|---|---|---|---|
| GATE-A (full) | 0.7802 | 0.9137 | — |
| − Gate (unconditional rerank) | 0.7797 | 0.8924 | −0.06% |
| − Reranker entirely | 0.7709 | 0.9037 | −1.2% |
| − RRF (score blend) | 0.7525 | 0.9101 | −3.5% |
| − Source weighting | 0.7307 | 0.8764 | −6.3% |
| FTS-Only | 0.6587 | 0.7924 | −15.6% |
xychart-beta
title "NDCG@10: memory-spark vs. 2026 SOTA (SciFact)"
x-axis ["BM25", "BGE-Large", "Nemotron-8B*", "Voyage-Large-2**", "memory-spark"]
y-axis "NDCG@10" 0 --> 1
bar [0.665, 0.523, 0.540, 0.548, 0.7889]
| System | SciFact NDCG@10 | Notes |
|---|---|---|
| BM25 | 66.5% | Sparse keyword baseline |
| BGE-Large-EN | 52.3% | Open-weight embedder |
| Nemotron-8B (raw) | ~54% | Our embedding model, vector-only |
| Voyage-Large-2 (raw) | ~55% | BEIR 2.0 leader (est. SciFact) |
| memory-spark: GATE-A | 78.02% | Our pipeline with reranking gate |
| memory-spark: Config U | 78.89% | Full reranking every query |
Why our scores are higher: The pipeline adds RRF fusion, source weighting, temporal decay, and dynamic gating on top of the base embedding model. These architectural gains compound — it's not just a better embedder.
- Nemotron-8B estimated from BEIR 2.0 average + scientific domain boost ** Voyage-Large-2 estimated; BEIR 2.0 avg is 54.8% across 18 datasets
Note: BEIR 2.0 averages across 18 datasets (legal, medical, code, etc.). Our 78%+ is SciFact-specific. Full BEIR 2.0 comparison is planned.
┌─────────────────────────────────────────────────────────────────────────────┐
│ PREPARE (5 stages) │ QUERY: "What agents run loops?" │
├─────────────────────────────────────────┼─────────────────────────────────────┤
│ 1· Clean │ Strip metadata/session IDs │ "What agents run loops?" │
│ 2· HyDE │ Generate hypothetical doc │ (Optional: LLM → proxy answer) │
│ 3· Expand │ Multi-query reformulation │ → "Which agents have heartbeats?" │
│ 4· Embed │ Nemotron-8B (4096d) │ → [0.23, 0.87, ...] │
│ 5· Search │ Vector IVF_PQ + BM25 │ → Top-100 candidates │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ FUSE (4 stages) [78% SKIP] │
├───────────────────────────────────────────────────────────────────────────────┤
│ 6· RRF Merge │ k=60 rank fusion │ → Interleaved results │
│ 7· Source Wt │ mistakes=1.6×, sessions=0.5×│ → Boosted relevant pools │
│ 8· Temporal │ 0.8 + 0.2·e^(-0.03·t) │ → Recent content prioritized │
│ 9· Dedup │ Jaccard > 0.85 │ → One chunk per source │
│ 10· Gate Check │ σ = s₁ − s₅ │ → σ > 0.08 or σ < 0.02? SKIP │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ RERANK (2 stages) [21% FIRE] │
├───────────────────────────────────────────────────────────────────────────────┤
│ 11· Cross-Encoder │ Nemotron-1B-rerank │ → [0.987, 0.876, ...] │
│ 12· RRF Blend │ vec ⊕ rerank │ → Final ranking │
└─────────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ OUTPUT (4 stages) │
├───────────────────────────────────────────────────────────────────────────────┤
│ 13· MMR Diversity │ λ=0.9 relevance bias │ → Diverse top-10 │
│ 14· Parent Expand │ child → parent context │ → Full paragraphs │
│ 15· LCM Dedup │ Overlap > 40% │ → No duplicate context │
│ 16· Security │ Injection filter │ → Safe XML injection │
└─────────────────────────────────────────────────────────────────────────────┘
↓
<memory_context>
... 10 facts ...
</memory_context>
The gate analyzes vector score distribution before calling the expensive cross-encoder:
- σ > 0.08 (confident): Skip reranker → trust vector ranking
- σ < 0.02 (tied set): Skip reranker → it's gambling on noise
- 0.02 ≤ σ ≤ 0.08 (ambiguous): Fire reranker → this is where it helps
Result: 78% of queries skip reranking with no NDCG loss and +1.1% recall improvement.
Replaces score-based hybrid merging. BM25 scores (5–20+) and cosine similarities (0.2–0.6) are on incompatible scales. RRF fuses by rank position only:
Scale-invariant, no normalization needed.
src/
auto/ # Auto-recall (15-stage) + auto-capture hooks
recall.ts # Core pipeline: RRF, gate, MMR, parent expansion
capture.ts # Fact extraction + dedup (0.92 threshold)
classify/ # NER, zero-shot, quality scoring
embed/ # Provider, queue (circuit breaker), cache
hyde/ # Hypothetical Document Embeddings
ingest/ # File parsing, chunking, workspace discovery
query/ # Multi-query expansion
rerank/ # Cross-encoder + RRF blend + dynamic gate
storage/ # LanceDB backend (IVF_PQ + FTS)
security.ts # Prompt injection detection
config.ts # Full config schema
index.ts # OpenClaw plugin (18 tools + 3 hooks)
paper/ # LaTeX scientific paper
evaluation/ # BEIR benchmarks, chart generation
scripts/ # Diagnostics, migration, standalone tools
tests/ # 483 unit + integration tests
docs/ # Architecture, config, benchmarks, tuning
figures/ # SVG charts (auto-generated)
| Doc | Content |
|---|---|
| Architecture | System design, 15-stage pipeline |
| Configuration | Full config reference |
| Benchmarks | BEIR results, methodology |
| Plugin API | All 18 tools with examples |
| Tuning Guide | Threshold tuning, RRF, gate, MMR |
| Technical Report | Deep-dive engineering |
| Changelog | Version history |
| Paper (PDF) | Scientific paper |
- Cormack et al. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods (SIGIR 2009)
- Gao et al. HyDE: Precise Zero-Shot Dense Retrieval without Relevance Labels (ACL 2023)
- Thakur et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models (NeurIPS 2021)
- Anthropic. Contextual Retrieval (2024)
- Packer et al. MemGPT: Towards LLMs as Operating Systems (2023)
- Khattab & Zaharia. ColBERT: Efficient and Effective Passage Search (SIGIR 2020)
@software{memory_spark_2026,
title = {memory-spark: GPU-Accelerated Persistent Memory for Autonomous AI Agents},
author = {Panic, Klein and Contributors},
year = {2026},
url = {https://github.com/kleinpanic/memory-spark},
version = {0.4.0}
}
{ "plugins": { "slots": { "memory": "memory-spark" }, "allow": ["memory-spark"], "entries": { "memory-spark": { "enabled": true, "config": { // Storage backend (only LanceDB supported) "backend": "lancedb", "lancedbDir": "~/.openclaw/data/memory-spark/lancedb", // Embedding configuration "embed": { "provider": "spark", "spark": { "baseUrl": "http://SPARK_HOST:18091/v1", "apiKey": "${SPARK_BEARER_TOKEN}", "model": "nvidia/llama-embed-nemotron-8b", "dimensions": 4096, "queryInstruction": "Given a question, retrieve relevant passages that answer the query" } }, // Cross-encoder reranking "rerank": { "enabled": true, "rerankerGate": "hard", // "off" | "hard" | "soft" "blendMode": "rrf", // "rrf" | "score" "topN": 40, "rrfK": 60, "minScoreSpread": 0.5, "spark": { "baseUrl": "http://SPARK_HOST:18096/v1", "apiKey": "${SPARK_BEARER_TOKEN}", "model": "nvidia/llama-nemotron-rerank-1b-v2" } }, // HyDE (Hypothetical Document Embeddings) // This example disables HyDE for a lower-latency setup. // Source defaults in src/config.ts enable it by default. "hyde": { "enabled": false, "llmUrl": "http://SPARK_HOST:18080/v1/chat/completions", "model": "nvidia/nemotron-super-120b", "maxTokens": 150, "temperature": 0.7, "timeoutMs": 4000 }, // Full-text search (BM25) "fts": { "enabled": true, "sigmoidMidpoint": 10.0 }, // Document chunking "chunk": { "maxTokens": 400, "overlapTokens": 50, "minTokens": 20, "hierarchical": true, "parentMaxTokens": 2000, "childMaxTokens": 200 }, // Embedding cache "embedCache": { "enabled": true, "maxSize": 256, "ttlMs": 1800000 // 30 min }, // Vector search tuning "search": { "refineFactor": 20, "ivfPartitions": 10, "ivfSubVectors": 64 }, // Auto-recall (memory injection) // Note: minScore default is 0.75 — 0.3 shown here for broad ingestion (lower threshold) "autoRecall": { "enabled": true, "agents": ["main", "dev", "meta"], "maxResults": 10, "minScore": 0.75, "maxInjectionTokens": 2000, "mmrLambda": 0.9, "temporalDecay": { "floor": 0.8, "rate": 0.03 } }, // Auto-capture (fact extraction) "autoCapture": { "enabled": true, "agents": ["main", "dev"], "minConfidence": 0.7, "minMessageLength": 30, "useClassifier": true }, // Spark service endpoints "spark": { "embed": "http://SPARK_HOST:18091/v1", "rerank": "http://SPARK_HOST:18096/v1", "ocr": "http://SPARK_HOST:18097", // EasyOCR (legacy) "glmOcr": "http://SPARK_HOST:18080/v1", // GLM-OCR via LLM "ner": "http://SPARK_HOST:18112", "zeroShot": "http://SPARK_HOST:18113", "summarizer": "http://SPARK_HOST:18110", "stt": "http://SPARK_HOST:18094" } } } } } }