Local Cognitive Memory Engine
Conversational memory for AI agents. Local. Trainable. Zero dependencies.
You're running a 3B-8B model locally on consumer hardware (CPU-only, 8-16 GB RAM). Your context window is 4K-8K tokens. Every token counts. You need long-term memory, but every other memory system (Mem0, Letta, Graphiti) requires a second LLM running alongside your agent — for extraction, summarization, or memory management. On hardware that's already at capacity running one model, that's not an option.
LCME gives AI agents long-term memory without requiring an LLM for memory operations. It uses regex extraction, 10 small neural networks (303K params total, <1ms inference), and multi-signal retrieval to store and recall memories at 28ms per ingest and 14ms per query. No second model. No API keys. No external servers. Just pip install and it works on the same hardware your 3B model already runs on.
Your agent (Qwen-3B, Llama-8B, Phi-3, Gemma...)
│
├── ingest() ──→ LCME (28ms, +17MB RAM, CPU)
│ └── regex extraction + neural scoring
│ └── SQLite + vectors + knowledge graph
│
└── retrieve() ←── LCME (14ms)
└── keyword + semantic + graph fusion
└── learned neural re-ranking
Every other memory system requires this:
Your agent (Qwen-3B) ← already using most of your RAM/CPU
│
├── ingest() ──→ Mem0
│ └── calls Qwen-3B AGAIN for extraction ← 12s per item
│ └── ChromaDB ← +93MB RAM
│
└── retrieve() ←── Mem0 (vector-only search)
On a machine running a 3B model, you can't afford to call that model a second time for every memory operation. LCME's 303K-param networks do the job in <1ms.
from lcme import LCME, LCMEConfig
memory = LCME(LCMEConfig(data_dir="./memory"))
memory.ingest("Alice is a backend engineer at Google.", origin="user")
memory.ingest("The auth service runs on port 8080.", origin="user")
results = memory.retrieve("Where does Alice work?")
# [{'content': 'Alice is a backend engineer at Google.', 'confidence': 0.8, ...}]
context = memory.get_context_string("Tell me about the auth service")
# "[certain] The auth service runs on port 8080."Mermaid diagram (interactive)
flowchart LR
subgraph Ingest
A[Text] --> B[Claim Extraction<br><i>regex, no LLM</i>]
B --> C[Entity Recognition]
C --> D[Neural Scoring<br><i>importance + emotion</i>]
D --> E[Interference Check]
end
subgraph Store
E --> F[(SQLite + FTS5)]
E --> G[(Vector Store<br><i>all-MiniLM-L6-v2</i>)]
E --> H[(Knowledge Graph)]
end
subgraph Retrieve
I[Query] --> J[FTS5 Keyword Search]
I --> K[Vector Similarity]
J --> L[RRF Rank Fusion]
K --> L
L --> M[Graph Expansion]
M --> N[Neural Re-ranking]
N --> O[Results]
end
subgraph Learn
P[Consolidation] --> Q[Train 10 Networks<br><i>from usage patterns</i>]
Q --> R[Maintenance<br><i>decay, prune, graduate</i>]
end
Ingestion extracts claims and entities via regex (no LLM needed), computes embeddings, and scores importance/emotion with two small neural networks. Storage writes to three layers: SQLite with FTS5 for keywords, a vector store for semantic search, and a knowledge graph for relationships. Retrieval fuses keyword and semantic rankings via Reciprocal Rank Fusion, expands through the graph, and applies learned neural weights. Consolidation periodically trains all networks from accumulated access patterns and prunes stale memories.
LCME has 10 small PyTorch networks (~303K parameters total) that run on CPU in <1ms per call. They start with rule-based defaults and gradually take over as they accumulate training data.
flowchart TB
subgraph cortex["Neural Cortex — 77K params"]
MIS["<b>MIS</b><br>Memory Importance<br><i>~1K params</i><br>Learns which memories<br>get retrieved later"]
ET["<b>ET</b><br>Emotional Tagger<br><i>~13K params</i><br>Tags valence + arousal<br>from embeddings"]
RWL["<b>RWL</b><br>Retrieval Weights<br><i>~200 params</i><br>Learns optimal signal<br>weights per query"]
AS["<b>AS</b><br>Associative Strengthener<br><i>~25K params</i><br>Hebbian learning on<br>graph edge confidence"]
CG["<b>CG</b><br>Consolidation Gate<br><i>~13K params</i><br>Decides keep / compress<br>/ forget per memory"]
ID["<b>ID</b><br>Interference Detector<br><i>~25K params</i><br>Detects contradicting<br>memory pairs"]
end
subgraph hippo["Hippocampus — 226K params"]
QUM["<b>QUM</b><br>Query Understanding<br><i>~58K params</i><br>Extracts intent, temporal<br>and emotional signals"]
AMM["<b>AMM</b><br>Associative Memory<br><i>~89K params</i><br>Hopfield network with<br>temporal binding"]
CDM["<b>CDM</b><br>Correlation Discovery<br><i>~53K params</i><br>Cross-attention<br>re-ranker"]
RCM["<b>RCM</b><br>Response Composer<br><i>~26K params</i><br>Dedup + compression<br>+ confidence calibration"]
end
Cold start behavior: All networks use a 4-phase warmup. Steps 0-50: 100% rule-based. Steps 50-150: 50/50 blend. Steps 150-300: 75% neural. Steps 300+: 95% neural. The system works from step zero and improves with use.
Training: Networks train during consolidation cycles (configurable, default every 6 hours). MIS learns from access logs (which memories get retrieved). RWL learns from retrieval feedback (which weight combinations produce useful results). Training runs in <100ms on CPU.
Note
Benchmarked for the target scenario: local agents on consumer hardware (CPU-only, 8-16GB RAM) running 3B-8B models. This is not a benchmark for cloud deployments with 70B+ models and unlimited API budgets. LCME is built for the machine that's already at capacity running your agent.
Hardware: AMD Ryzen 9 7940HS, 24.4 GB RAM, CPU-only. LCME: 30 items, 20 queries, 3 runs averaged. Mem0: 200 items, 15 queries, local Qwen-3B via llama.cpp. Graphiti/Letta: could not run (require Neo4j / Letta server).
Scalability (LCME, tested at 100 / 500 / 1000 items, 15 queries):
| Items | P@1 | P@5 | MRR | Ingest/item | Retrieval P50 | Disk |
|---|---|---|---|---|---|---|
| 100 | 1.000 | 1.000 | 1.000 | 166 ms | 12 ms | 1.4 MB |
| 500 | 0.933 | 1.000 | 0.967 | 171 ms | 12 ms | 2.4 MB |
| 1000 | 1.000 | 1.000 | 1.000 | 392 ms | 12 ms | 4.0 MB |
Concurrent access (1 writer + 2 readers): 0 errors.
Head-to-head vs Mem0 (at 1000 items):
| Metric | LCME | Mem0 v1.0.7 | Graphiti v0.28 | Letta v0.16 |
|---|---|---|---|---|
| Precision@5 | 1.000 | 0.867 | needs Neo4j | needs server |
| MRR | 1.000 | 0.800 | " | " |
| Ingest/item | 392 ms | 11,837 ms | " | " |
| Retrieval P50 | 12 ms | 11 ms | " | " |
| Needs second LLM | No | Yes | Yes | Yes |
| External deps | None | ChromaDB + LLM | Neo4j + LLM | Server + PostgreSQL |
Extended metrics (500 items, 15 queries):
| Metric | Score |
|---|---|
| nDCG@5 | 0.978 |
| Recall@1 | 1.000 |
| Recall@5 | 0.960 |
| Contradiction handling | 7/10 (70%) |
| Long conversation (200 turns, recall early facts) | P@5 = 0.875 |
| Concurrent (1 writer + 2 readers) | 0 errors |
| Write throughput | 8.8 items/sec |
| Memory growth (1000 items) | +77 MB RAM, 3.6 MB disk |
Why this matters on 3B hardware: Mem0's 11.8s per ingest means your Qwen-3B is busy doing memory extraction instead of responding to the user. On a single-model machine, memory and inference compete for the same compute. LCME's ingest runs between calls without the user noticing.
Full methodology: Benchmark Paper.
pip install lcme# Or from source
git clone https://github.com/gschaidergabriel/lcme.git
cd lcme
pip install -e .Tip
Requirements: Python 3.10+, PyTorch 2.0+, sentence-transformers 2.2+. All run on CPU.
from lcme import LCME, LCMEConfig
config = LCMEConfig(
data_dir="./memory", # Where to store everything
vector_model="all-MiniLM-L6-v2", # Embedding model
default_limit=5, # Results per query
auto_maintenance=True, # Hourly decay + pruning
auto_consolidation=False, # Set True for automatic neural training
consolidation_interval_hours=6.0,
)
memory = LCME(config)| Method | Returns | Description |
|---|---|---|
ingest(text, origin, confidence) |
dict |
Store a memory. Returns claims, entities, topics. |
retrieve(query, limit, context) |
list[dict] |
Retrieve relevant memories. |
get_context_string(query, limit) |
str |
Formatted context block for LLM injection. |
forget(node_id) |
bool |
Remove a memory (if not protected). |
protect(node_id) |
bool |
Protect from automatic pruning. |
consolidate() |
dict |
Trigger neural training + maintenance. |
get_stats() |
dict |
Node, edge, vector, claim counts. |
health_check() |
dict |
System status. |
from lcme import remember, recall, get_context, forget, protect
remember("The user prefers dark mode.") # Global singleton, ~/.lcme/data/
results = recall("user preferences")
context = get_context("What does the user like?")| Origin | Confidence | Use for |
|---|---|---|
user |
0.8 | Things the user stated directly |
code |
0.95 | Facts from code analysis |
observation |
0.7 | Behavioral observations |
inference |
0.5 | AI-generated conclusions |
memory |
0.6 | Recalled from prior memory |
Claims, not facts. Every stored item is a claim with a confidence level and origin. Nothing is treated as ground truth. Confidence decays with a 7-day half-life. Old memories fade unless they prove useful (get accessed), in which case they graduate to permanent protection.
Regex extraction, not LLM. Ingestion uses pattern matching to extract entities and subject-predicate-object claims. This is less accurate than LLM extraction but 430x faster and requires no external model. The tradeoff is intentional: for agents that ingest continuously, speed matters more than extraction quality.
Three retrieval signals, not one. FTS5 keyword search finds exact matches. Vector similarity finds semantic matches. The knowledge graph finds structural relationships. Reciprocal Rank Fusion combines all three rankings. This avoids the failure mode where vector-only search returns semantically similar but factually wrong results.
10 small networks, not 1 large one. Each network has a single job (importance scoring, emotion tagging, retrieval weighting, etc.) and trains independently from its own data source. This means each network can reach maturity on different timescales, and a failure in one doesn't affect the others.
FTS stop-word filtering, temporal detection, emotion detection, and negation detection support 8 languages:
| EN | DE | ES | FR | PT | ZH | HI | AR | |
|---|---|---|---|---|---|---|---|---|
| Stop words | ~80 | ~70 | ~55 | ~50 | ~45 | ~45 | ~35 | ~35 |
| Temporal | 25 | 22 | 19 | 16 | 16 | 26 | 26 | 24 |
| Emotion | 27 | 23 | 22 | 22 | 22 | 23 | 21 | 23 |
| Negation | 7 | 8 | 8 | 7 | 7 | 7 | 7 | 8 |
The embedding model (all-MiniLM-L6-v2) supports 100+ languages for semantic similarity.
All examples use local 3B-8B models. For 3B models, we provide an optional LoRA adapter that makes tool calling reliable (95% accuracy on Qwen 2.5 3B). For 7B+ models, system prompt injection alone is usually sufficient.
| Setup | Models | Example | Guide |
|---|---|---|---|
| LoRA + llama-server | Qwen 2.5 3B (merged GGUF) | examples/openai_compatible.py |
LoRA adapter |
| LoRA + Python | Qwen 2.5 3B (PEFT) | examples/ollama_chat.py |
LoRA adapter |
| Ollama | qwen2.5:3b, llama3.1:8b, phi3, gemma2 | examples/ollama_chat.py |
Guide |
| llama-server | Any GGUF | examples/openai_compatible.py |
Guide |
| LangChain | Ollama / LlamaCpp | examples/langchain_memory.py |
Guide |
| LlamaIndex | Ollama | -- | Guide |
| Raw Python | Any HTTP endpoint | -- | Guide |
Full guide: docs/INTEGRATIONS.md
LCME works out of the box with system prompt injection — no fine-tuning required. But for models that struggle with tool-calling syntax (especially 3B models), we provide a LoRA adapter that teaches the model when and how to call lcme.ingest() and lcme.retrieve().
A PEFT/LoRA adapter (rank 16, 120MB) trained on 788 curated examples that teaches a model to:
- Call
<tool>lcme.ingest|text|origin</tool>when the user shares personal info - Call
<tool>lcme.retrieve|query</tool>when the user asks about stored info - Not call any tool for generic questions (math, coding, trivia, greetings)
Test results: 95% accuracy (38/40) — Ingest 92%, Retrieve 100%, No-tool 92%.
- Not a personality. It teaches tool-calling only. No persona, no style, no opinion injection.
- Not a full model. It's a 120MB adapter that sits on top of an existing base model.
- Not required. LCME works without it. The adapter just makes small models more reliable at deciding when to use memory tools.
- Not model-specific. Trained on Qwen 2.5 3B but the tool-calling pattern generalizes to other Qwen sizes.
Qwen/Qwen2.5-3B-Instruct (or any Qwen 2.5 variant). The adapter was trained on Qwen2.5-3B-Instruct-abliterated-hf but works with the standard Instruct version.
# Reassemble the adapter (split due to GitHub's 100MB file limit)
cd models/lcme-lora-qwen3b-v2
bash reassemble.shfrom transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# Load base model
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-3B-Instruct",
torch_dtype="auto", device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B-Instruct")
# Load LCME LoRA adapter
model = PeftModel.from_pretrained(model, "models/lcme-lora-qwen3b-v2")
model.eval()If you want a single GGUF file for llama-server, merge the adapter into the base model first:
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-3B-Instruct", torch_dtype="auto", device_map="cpu")
model = PeftModel.from_pretrained(model, "models/lcme-lora-qwen3b-v2")
model = model.merge_and_unload()
model.save_pretrained("lcme-merged")
AutoTokenizer.from_pretrained("Qwen/Qwen2.5-3B-Instruct").save_pretrained("lcme-merged")Then convert to GGUF with llama.cpp's convert_hf_to_gguf.py and run:
llama-server --model lcme-merged.gguf --port 8080The training data generator and training script are included:
cd models/
python generate_lcme_data.py # Generates 788 train/eval examples
python train_lcme_lora.py --lr 8e-5 --epochs 4 # Requires GPU (tested on RTX 5070)~/.lcme/data/
├── lcme.db # SQLite: nodes, edges, events, claims, FTS5 index
├── lcme_vectors.npz # Compressed embeddings (384-dim per memory)
├── lcme_vector_ids.json # Node-to-vector mapping
├── hippocampus.db # Retrieval training logs
└── models/
├── lcme_cortex.pt # Cortex checkpoint (6 networks)
└── lcme_hippocampus.pt # Hippocampus checkpoint (4 networks)
MIT. See LICENSE.



