mnemostack
Self-hosted hybrid memory & retrieval for AI apps.
mnemostack is a durable retrieval layer over your own Qdrant (and optional Memgraph): semantic, keyword (BM25), temporal, and graph recall, fused with Reciprocal Rank Fusion and refined by an 8-stage ranking pipeline — with payload filters for multi-tenant isolation, optional LLM answer synthesis (confidence + citations), and an ingest path that enriches and projects structured fields. One recall(query) call, usable as a Python library, an HTTP service, or an MCP server.
Flagship use case — durable memory for AI agents. Long-running agents hit the same wall: context gets compacted, sessions restart, useful decisions disappear, and the next run pays the re-orientation tax again. mnemostack gives them a persistent memory layer to query when the context window is not enough — durable, searchable, scoped, and explainable, not just embedded and hoped for.
The same engine backs other retrieval-heavy work: RAG over mixed corpora, multi-tenant or per-user knowledge stores, and time-aware search backends — anywhere pure vector similarity falls short on its own.
Status: Actively developed — public API is stable; new functionality lands additively in minor releases. Breaking changes are rare and called out in CHANGELOG.md.
Quickstart: agent memory over MCP
The fastest on-ramp is the MCP server — it gives Claude Desktop, Claude Code, Cursor, ChatGPT, or another MCP-capable agent durable memory in a few commands. Building an app instead of wiring up an agent? Use the HTTP API or the Python library over the same collection.
1. Install
pip install 'mnemostack[mcp]'
Run a local Qdrant for the vector store:
docker run -p 6333:6333 qdrant/qdrant:latest
Optional: run Memgraph for graph-backed memory:
docker run -p 7687:7687 memgraph/memgraph:latest
2. Start the MCP server
export GEMINI_API_KEY=your-key-here
mnemostack mcp-serve --provider gemini --collection my-memory
Claude Desktop config example:
{
"mcpServers": {
"mnemostack": {
"command": "mnemostack",
"args": ["mcp-serve", "--provider", "gemini", "--collection", "my-memory"],
"env": {
"GEMINI_API_KEY": "your-key-here"
}
}
}
}
Claude will then be able to call mnemostack_search, mnemostack_answer, and graph tools.
3. Store memory
Index a folder of notes, docs, transcripts, or project context:
mnemostack index ./my-notes/ --provider gemini --collection my-memory --recreate
--recreate drops the existing collection, so it asks for confirmation first; pass --yes to skip the prompt (required in scripts/CI — non-interactive runs without it exit with code 2).
For a running app or assistant, use the streaming Ingestor API shown below to store messages as they arrive.
4. Recall memory
From an agent, ask a memory-style question and let the MCP tools retrieve the right facts.
From the shell, test the same collection directly:
mnemostack search "what did we decide about auth" --provider gemini --collection my-memory
mnemostack answer "what did we decide about auth" --provider gemini --collection my-memory
Why hybrid memory?
Vector search answers "what sounds similar?" Real retrieval over a growing corpus needs to answer "what actually matters for this query?" — which takes exact matches, semantic similarity, relationship tracking, recency, user/project scope, and feedback from past recalls. Mnemostack uses hybrid retrieval so recall is reliable instead of embedding roulette. (Agent memory is the most demanding version of this problem, which is why it's the flagship use case.)
Use cases
Agent and chatbot memory (flagship):
- Long-running coding agents that need to survive compaction and session restarts.
- Chat assistants and conversational bots that remember a user's earlier messages, preferences, and decisions across sessions — scope each user's memory with payload
filtersso one user never sees another's history. - Personal assistant memory for preferences, recurring tasks, and long-horizon context.
- Multi-agent context sharing through one durable memory backend.
- Session compaction recovery when the useful details no longer fit in the prompt.
Beyond agents — the same engine as a retrieval backend:
- A searchable knowledge base in memory: ingest your docs/notes/FAQ once, then serve hybrid search or grounded
answer()(with confidence and source citations) over it from the CLI, HTTP, or library. - RAG over mixed corpora (code, docs, transcripts) where exact-token and temporal recall beat pure vector similarity.
- Multi-tenant or per-user knowledge stores — payload
filtersisolate each tenant's data inside every retriever, or enable service-key auth (serve --auth) for a hard, key-resolved tenant boundary with optional per-tenant quotas (see the HTTP API). - Time-aware search and knowledge bases — "what changed last week", point-in-time graph facts, freshness-weighted ranking.
- Team or project knowledge recall across docs, notes, tickets, and chat history.
Three ways to use Mnemostack
- MCP server — for agent users. Start
mnemostack mcp-serve, connect your agent, and use memory tools from the chat/runtime you already use. - HTTP API — for app developers. Run
mnemostack serveand call/recall,/answer,/feedback,/health,/metrics, or/docsfrom any language. - Python SDK — for library users. Compose retrievers, stores, rerankers, graph tools, and the streaming ingest API inside your own Python application.
Architecture
Mental model
Think of it as a storage hierarchy for agent memory:
- Context window = RAM. Fast, limited (typically 100K–200K tokens for many agent models; larger windows exist, but usable working context is often much smaller after tools, instructions, MCP output, and context rot — ~45K usable tokens on a 200K window is a realistic working number in long-running agent sessions. Clears on session restart.
- mnemostack corpus = Disk. Persistent, searchable, grows forever — every fact the agent has ever seen, queryable on demand.
recall(query)= page fault handler. When the agent needs something that isn't in the current context, it pulls the exact fact from storage with a single hybrid query — not a grep, not a reload of the whole corpus.
The practical effect: you stop re-explaining your project to the agent after every /compact. You stop losing momentum to the re-orientation tax that shows up in any agent with session compaction. mnemostack solves it at the library level, not tied to any single agent runtime.
How it works, in one paragraph
On each recall(query): the configured retrievers (Vector and Temporal by default, with BM25 and Memgraph when configured) run in parallel and return ranked lists. Reciprocal Rank Fusion merges them. The optional 8-stage pipeline can reweight results using query classification, exact-token rescue, gravity/hub dampening, freshness, inhibition-of-return, curiosity boosts, Q-learning weights supplied through its state store, and graph resurrection. An optional LLM reranker does a final ordering pass. You get a list of RecallResult with source, score, and provenance — ready to hand to a model.

Where mnemostack fits
Most memory tools in the agent ecosystem pick one axis and optimize for it: simple vector similarity for RAG, framework-bound memory tied to a specific agent library, platform-level runtimes with audit and compliance features, or CLI wrappers over a single vendor's session store. Each makes sense for its scope.
mnemostack takes a different slice: it is a recall quality layer, offered as a plain Python package. Four retrievers (Vector + BM25 + Memgraph + Temporal), RRF fusion, an 8-stage pipeline, and an optional LLM reranker — composed to handle mixed workloads on the same corpus: exact-token lookups, semantic queries, temporal questions, and multi-hop reasoning, without forcing you to choose one mode over another.
We are not a replacement for your agent framework and not a full platform runtime. We are the piece that actually finds the right fact in a growing corpus. Drop mnemostack into your own Python agent or application, or let a higher-level service call recall() over a plain function boundary. The retrievers, pipeline, and reranker are individually composable — take only the parts you need.
Design
See ARCHITECTURE.md for detailed design: pipeline stages, Qdrant schema, Memgraph temporal model, consolidation runtime, MCP tools.
Storage, index, and retrieval layers
- Storage/index layer: Qdrant stores vector points and payloads; BM25 indexes exact-token corpora; Memgraph stores temporal graph facts; the Temporal retriever handles time-aware vector recall.
- Fusion layer: Reciprocal Rank Fusion merges ranked lists from Vector, BM25, Memgraph, and Temporal retrievers, with optional static or adaptive weights.
- Recall pipeline: the 8-stage pipeline can classify the query, rescue exact tokens, dampen gravity/hubs, blend freshness, apply inhibition-of-return, add curiosity boosts, use Q-learning state, and resurrect graph-linked memories.
- Feedback loop: HTTP and MCP recall can apply existing state; explicit
/feedbackormnemostack feedbackupdates usefulness signals without silently training on every response. - Inference layer: optional LLM reranking and answer generation sit on top of recall, so retrieval still works when the LLM is unavailable.
Pipeline state
The 8-stage pipeline can use a small state store between calls (Q-learning weights, inhibition-of-return history, per-document gravity/hub counters). FileStateStore(path) persists it to a JSON file. HTTP recall applies existing state and can record inhibition-of-return exposure with --auto-record-ior; Q-learning updates only through explicit /feedback calls. CLI/MCP recall still apply existing state but do not collect feedback automatically. For deterministic benchmarks, call build_full_pipeline(enable_stateful_stages=False) so IoR/Q-learning/curiosity state cannot affect scores. For multi-process servers, implement your own StateStore (three methods: get(), set(), update()) backed by Redis or your database.
Graceful degradation
Any retriever can fail (Memgraph down, Qdrant unreachable, BM25 corpus empty). Recaller logs and continues with the remaining sources. The LLM reranker is wrapped in try/except by convention — if the LLM is rate-limited, the pre-rerank order is returned. This is deliberate: a memory stack that goes dark because one component hiccuped is worse than a slightly degraded one.
One exception: query expansion runs before retrieval, so a misconfigured expansion step (query_expansion=True without an expansion_llm, or a provider error inside it) surfaces as an error instead of degrading silently — see ARCHITECTURE.md for the full fail-open contract. Degradations themselves are visible, not silent: every HTTP/MCP response carries degraded tags, and the full per-retriever trace is available opt-in via include_trace.
Comparison and benchmarks
On LoCoMo, Mnemostack reaches 82.9% strict accuracy in our evaluation setup. The table below includes our baseline runs and externally reported numbers for context. Results depend on dataset version, configuration, judge model, scoring rules, and query type. Treat externally reported numbers as directional unless they were run with the same harness and settings.
Benchmarks
Full LoCoMo runs use the official SNAP-Research dataset (10 samples / 1986 QA) from a clean state. Across the tables below: Strict = exact match, Combined = strict + partial. Counts in cells are correct / total.
Some LoCoMo cat_5 questions have empty ground-truth answers. Under the current scorer, these are counted as correct because there is no expected answer to match. To avoid overstating recall quality, we also report signal-only scores with those questions removed. Signal-only scores are computed on the 1,540 questions with non-empty ground-truth answers.
LoCoMo, current judge (gemini-3-flash-preview)
| Run | Strict (full) | Combined (full) | Strict (signal-only) | Combined (signal-only) |
|---|---|---|---|---|
| Baseline v0.3.0 (Vector + BM25 + 8-stage pipeline) | 76.7% (1524 / 1986) | 88.1% (1750 / 1986) | 70.0% (1078 / 1540) | 84.7% (1304 / 1540) |
Retrieval improvements (window_size=3, query expansion, top-K 25) |
82.5% (1639 / 1986) | 92.2% (1832 / 1986) | 77.5% (1193 / 1540) | 90.0% (1386 / 1540) |
| v0.4.5 + photo captions (same config as above) | 82.9% (1647 / 1986) | 92.7% (1842 / 1986) | 78.0% (1201 / 1540) | 90.6% (1396 / 1540) |
Honest numbers disclaimer.
(full)is the headline aggregate across all 1986 questions, the format vendors typically report — some publish only their strongest sub-category, we publish the full aggregate because it's what actually predicts behavior on mixed workloads.(signal-only)strips thecat_5auto-pass artifact described above, so what you read there is the real recall quality on questions that have a ground-truth answer.
Per-category breakdown (v0.4.5 + photo captions run):
| Category | Strict | Combined |
|---|---|---|
cat_1 single-hop lists |
51.4% | 88.3% |
cat_2 temporal |
79.8% | 85.7% |
cat_3 open-domain reasoning |
62.5% | 79.2% |
cat_4 multi-hop reasoning |
88.0% | 94.6% |
cat_5 adversarial open-domain |
100.0% | 100.0% |
Notes:
- Judge model matters:
gemini-3-flash-previewis more accurate than the previous Gemini Flash judge on synonyms, partial matches, and empty ground truth. cat_5questions have empty ground truth in this new run and are auto-scored as correct by the benchmark harness. That makes the newcat_5strict score (446 / 446, 100.0%) useful for aggregate harness accounting, but not directly comparable to the historicalcat_5strict score (89.7%) from the older adversarial-question evaluation.- Pipeline: Vector retrieval with Gemini embeddings + BM25 + RRF + 8-stage reranking pipeline. The LLM reranker is not part of the benchmark loop (it is a runtime/server feature), so
rerank_modedoes not affect these numbers. - The v0.4.5 run additionally ingests the photo captions (
blip_caption) that LoCoMo attaches to image-sharing turns — 697 of the 1540 signal questions cite image turns as evidence, and earlier runs silently dropped that content. Answer prompts also show the time of day of each memory since v0.4.5.
Historical LoCoMo results (gemini-2.5-flash judge)
| Metric | First full run | mnemostack 0.2.1 |
|---|---|---|
| Strict | 66.4% (1319 / 1986) | 67.8% (1346 / 1986) |
| Partial | 12.8% (254 / 1986) | 12.6% (250 / 1986) |
| Wrong | 20.8% (413 / 1986) | 19.6% (390 / 1986) |
| Combined | 79.2% (1573 / 1986) | 80.4% (1596 / 1986) |
By question category (combined not tracked for the first full run):
| Category | First run Strict | 0.2.1 Strict | 0.2.1 Combined | Δ Strict |
|---|---|---|---|---|
cat_1 single-hop lists |
34.8% | 34.4% | 74.1% | −0.4pp |
cat_2 temporal |
64.5% | 69.8% | 77.9% | +5.3pp |
cat_3 open-domain reasoning |
31.2% | 41.7% | 49.0% | +10.5pp |
cat_4 multi-hop reasoning |
69.2% | 69.6% | 82.0% | +0.4pp |
cat_5 adversarial open-domain |
90.1% | 89.7% | 89.7% | −0.4pp |
Last historical run: 2026-04-27, mnemostack 0.2.1, same dataset, judged by gemini-2.5-flash.
Comparison with reported numbers from other systems
Caveat: different judges, evaluation protocols, and in some cases category cherry-picking. Vendor numbers below are taken at face value from their published material.
| System | LoCoMo correct |
|---|---|
| Hindsight (reported range) | 78–85% |
| Memobase (temporal subset) | 85% |
| mnemostack | 82.9% |
| Letta filesystem agent | 74% |
| Mem0 graph variant | ~68.5% |
| Zep (independently replicated) | 58.4% |
Real-corpus needle benchmark
LoCoMo measures generic long-term dialogue recall. We also run a private needle-in-haystack benchmark on the production workload that drove the original design — a ~17k-point memory stack indexed from a long-running assistant. Queries mix exact tokens (IP addresses, tickers), telegram IDs, paraphrased facts, and temporal probes.
| Metric | Value |
|---|---|
| recall@1 | 90% (9/10) |
| recall@5 | 100% (10/10) |
| recall@10 | 100% (10/10) |
| Query latency p50 | 1.26 s |
| Query latency max | 1.70 s |
Honest numbers disclaimer. Reporting only
recall@5 = 100%would look impressive, but it would also hide the harder top-1 behavior.recall@1 = 90%is what an agent reading only the top hit actually experiences, and the gap between@1and@5is where reranker quality (or the lack of it) shows up. We publish all three so you can read the metric that matches your downstream usage.
Useful because LoCoMo's failure modes (list exhaustion, open-domain reasoning) are orthogonal to what production memory stacks actually spend time on (find the specific fact the user mentioned weeks ago). This benchmark is not in the public repo; its methodology is in benchmarks/synthetic_longhorizon.py, which is the closest reproducible approximation.
Reproduce LoCoMo from a fresh clone
pip install -e '.[dev]'
bash benchmarks/download_locomo.sh # fetches SNAP Research's public dataset
export GEMINI_API_KEY=...
bash benchmarks/run_locomo.sh # full 10-sample run, writes results/ts.{json,log}
Details, category definitions, and notes on the judge protocol: benchmarks/README.md.
Who is this for?
Build it in if you need:
- Long-lived agent memory that survives session restarts and doesn't drift into irrelevance as the corpus grows.
- Recall quality on mixed workloads — exact-token lookups (IDs, tickers, error strings), semantic queries, temporal questions, multi-hop reasoning — not just one of them.
- A stack you can plug into your own infrastructure: bring your own embedding model, LLM, vector store, or graph DB.
Not the best fit if you only need a single call to text-embedding-3-small + cosine similarity — something simpler will do. mnemostack earns its complexity on mixed, long-horizon workloads.
Features
- 🧠 4-source hybrid retrieval — Vector (Qdrant) + BM25 (exact tokens) + Memgraph (knowledge graph) + Temporal (time-aware vector), all fused via Reciprocal Rank Fusion. Pluggable
Retrieverabstraction — add your own sources. - ⚖️ Weighted & adaptive RRF fusion —
reciprocal_rank_fusion(weights=[...])lets you lift sources you trust more;Recaller(adaptive_weights=True)picks a per-query-shape profile (exact-token / person / temporal / general). See the honest write-up below for where this helps and where it doesn't. - 🧪 HyDE retriever (opt-in) — embeds a hypothetical answer instead of the query. Useful for query↔document vocabulary gaps in documentation-style corpora; does not reliably help on dialogue-backed memory and always costs one extra LLM roundtrip per
search(). Not included in the defaultRecaller. - 🪜 8-stage recall pipeline — ClassifyQuery → ExactTokenRescue → GravityDampen → HubDampen → FreshnessBlend → InhibitionOfReturn → CuriosityBoost → QLearningReranker. Opt-in; stateful HTTP feedback is explicit via
/feedback, and recall exposure logging is off unless--auto-record-ioris enabled. - 🔁 Reranking — Gemini Flash (or any LLM) reorders top-K by relevance, or plug a cross-encoder / hosted rerank service through the score-based
ScoringReranker. See docs/recipes.md for a runnablebge-reranker-v2-m3example. - 🔤 Pluggable BM25 analyzer — the default is lowercase + Unicode word split (great for exact tokens); pass
BM25Retriever(tokenizer=...)for stemming / lemmatization / language routing. Core stays dependency-free; docs/recipes.md has per-language recipes. - ⚡ Async API — every blocking surface has a signature-stable async mirror:
Recaller.recall_async,recall_flow_async,Ingestor.ingest_async/ingest_one_async,AnswerGenerator.generate_async,synthesize_async, plusAsyncVectorStoreover the native async Qdrant client. Retrievers dispatch in parallel; five concurrent HTTP recalls finish in roughly one single-recall wall-clock. - 🕰️ Stale-fact invalidation — mark superseded memories stale without deleting or re-embedding them:
store.invalidate(ids, valid_until=...)sets bi-temporal payload keys (invalidated_atsystem-time,valid_until/valid_fromworld-time) via a cheap merge write. Recall hides invalidated facts by default;include_invalidated=Trueshows them andas_of="<iso>"reconstructs what was valid at a past instant. The vector-side twin of the graph'svalid_untilmodel. CLImnemostack invalidate <id>...and MCPmnemostack_invalidate; HTTP stays read-only. - 🌍 Unicode-aware entity resolution — Memgraph retriever probes by
telegram_id, handle, and precomputedname_lowerso non-ASCII names match correctly (Memgraph'stoLower()lower-cases ASCII only). - 📥 Streaming
IngestorAPI — batched, idempotent, LRU-cached ingest from any Python code. Lazy iterator means large corpora ingest with bounded memory. Same(source, offset, text)→ same deterministic UUID-shaped content id, so re-runs are no-ops. - 📝 Markdown indexer —
mnemostack index-markdown <dir>indexes a folder of markdown with structure: YAML frontmatter → payload filters, header-aware chunking with heading paths, and[[wikilinks]]/[text](https://github.com/udjin-labs/mnemostack/blob/main/note.md)→File -[LINKS_TO]-> Filegraph edges (with a Memgraph URI). Generic for any markdown folder; Obsidian vaults work as a side effect. Depends only on the already-presentpyyaml. - 🌐 HTTP API (optional) —
pip install 'mnemostack[server]'gives you/recall,/answer,/health,/docs, plus/metricsin Prometheus text format. See the HTTP server section below. - 🔌 Pluggable embeddings — Gemini, Ollama, or HuggingFace (local GPU), via provider registry
- 🤖 Pluggable LLM — Gemini Flash / Ollama for answer generation and reranking
- 📚 Temporal knowledge graph — facts have
valid_from/valid_until, query point-in-time state; graph resurrection stage recovers evicted-but-relevant memories. - 💬 Answer mode — inference layer synthesizes concise factual answers with source citations and confidence. Category-aware prompts (lists / temporal / multi-hop / inference / adversarial), specificity resolver, and
cat_3inference retry with query decomposition are on by default. - 📋 Knowledge synthesis —
synthesize(entity)rolls up everything memory knows about a person, project, or topic into a structured profile (SynthesisFact/SynthesisResult, markdown or JSON). CLI:mnemostack synthesize <entity>. Optional related-entities expansion via graph and LLM summarization pass. - 📏 Progressive Tiers API —
search --tier {1,2,3}andanswer --tier {1,2,3}bound output size (~50 / ~200 / ~500 tokens) so agents can pay only for the detail they actually need. Omit--tierfor unchanged full output. - ✂️ Chunkers + sliding window — plain, fixed-size, and
MessagePairChunkerfor chat transcripts (keeps user↔assistant pairs together). The newvector.window_sizeconfig carries adjacent-turn context inside each chunk;window_size=3was worth +5.8pp strict / +4.1pp combined on LoCoMo (v0.4.0). - 🔎 Query expansion + smart retry —
Recaller(expansion_llm=...)widens recall with reformulated queries;AnswerGenerator(retry_with_expansion=True)retries low-confidence answers with the expanded query and a HyDE-style hypothetical before giving up. Opt-in via--query-expansiononmnemostack answer. - ⚙ Consolidation runtime — phase orchestrator for nightly memory lifecycle
- 🔌 MCP server — expose memory tools to Claude Desktop, ChatGPT, Cursor, etc.
- 🛡 Graceful degradation — retrieval keeps working if graph or any retriever is down
- 🔐 Multi-tenancy — soft filters or a hard auth boundary —
filters={"tenant": "a"}applies inside every retriever (exact match + ranges) on HTTP/MCP/CLI/library; results never include points outside the scope, verified by adversarial isolation tests. Filters are caller-supplied, so for a real trust boundary run the server with service-key auth (serve --auth/mcp-serve --auth): the tenant is resolved from the key (a client can't assert another's), enforced across the vector store, the knowledge graph, and per-tenant learning state. Optional per-tenant storage quotas apply at ingest, and request-rate quotas on the authenticated HTTP surface (serve --auth). Off by default. See the HTTP API section. - 🧩 Ingest enrichment + answer projection —
Ingestor(enrich=callable)extracts structured facts into payloads at ingest (fail-open,--refresh-payloadsupdates existing collections without re-embedding);context_fields=[...]shows them to the answer LLM;rewrite_followup()resolves conversational follow-ups before recall. - 🧠 Reasoning-model friendly — Ollama
thinkis off by default (reasoning models otherwise burn the whole token budget on thoughts and return empty text);options={...}passes any generation option through.
Recall tuning: fusion weights & HyDE
Some of the newer knobs help in specific workloads and do nothing (or mildly hurt) in others. Measured, not promised — both are opt-in by design, and the default Recaller stays classical equal-weight RRF over Vector + BM25 (+ Memgraph + Temporal when supplied).
Recaller(adaptive_weights=True) — picks a weight profile per query shape:
| Query shape | Detection | Profile (bm25 / memgraph / vector / temporal) |
|---|---|---|
exact_token |
IPv4 / port / version / UUID / API-style tokens | 1.4 / 1.4 / 1.0 / 0.9 |
person |
"who is", @handle, username, contact, etc. |
1.0 / 1.5 / 1.0 / 0.9 |
temporal |
"when", "yesterday", "today", dates | 1.0 / 1.0 / 1.0 / 1.4 |
general |
everything else | classical equal-weight RRF |
Measured on a real production corpus with 10 needle probes: recall@1 went 50% → 60%, recall@5 stayed at 90% (zero regression). On LoCoMo (pure dialogue questions, all classified general), adaptive weights had no effect — the profile simply isn't triggered. Rule of thumb: turn it on for production ops-style workloads (IPs, tickers, IDs, named entities); leave it off, or don't expect a lift, for dialogue benchmarks. Static retriever_weights={...} always wins over adaptive when both are set.
HyDERetriever — generates a short hypothetical answer via your LLM and embeds that instead of the raw query, then fuses alongside the other retrievers. Useful when the question and the stored answer use very different vocabulary (documentation corpora, FAQ-style content). On our LoCoMo cat_3 smoke (conv-43, 14 open-domain reasoning questions) it moved accuracy from 14.3% to 21.4% (+1 correct answer); on dialogue-backed memory overall it's roughly a wash. It always costs one extra LLM call per search(), so budget accordingly and treat it as a tool for specific workloads rather than a default.
Utilities
Agent runtimes often wrap transcript messages in metadata envelopes before the real body, which can dominate embeddings and make unrelated turns look similar. Clean messages before chunking/indexing with strip_metadata_blocks():
from mnemostack.utils import strip_metadata_blocks
clean = strip_metadata_blocks(raw_message)
Built-in profiles cover OpenClaw webchat and Telegram envelopes; pass profiles= or extra_patterns= to tune the cleanup for your runtime.
Environment
| Variable | Purpose | Required for |
|---|---|---|
GEMINI_API_KEY |
Google Generative AI key | Gemini embedding + Gemini Flash LLM |
OLLAMA_HOST |
Ollama server URL (default http://localhost:11434) |
Ollama embeddings / LLM |
MNEMOSTACK_COLLECTION |
Qdrant collection name (default mnemostack) |
CLI convenience |
MNEMOSTACK_QDRANT_URL |
Qdrant URL (default http://localhost:6333) |
Remote Qdrant |
MNEMOSTACK_GRAPH_URI / MNEMOSTACK_MEMGRAPH_URI |
Memgraph bolt URI | Graph retriever / GraphStore |
MNEMOSTACK_PROVIDER / MNEMOSTACK_EMBEDDING_PROVIDER |
Embedding provider | CLI / HTTP / MCP |
MNEMOSTACK_LLM / MNEMOSTACK_LLM_PROVIDER |
LLM provider | Answer generation / reranking |
MNEMOSTACK_BM25_PATHS |
BM25 corpus paths separated by os.pathsep (: on Unix) |
CLI / HTTP / MCP BM25 retriever |
MNEMOSTACK_AUTO_RECORD_IOR |
true/false toggle for HTTP recall exposure logging |
HTTP stateful pipeline |
MNEMOSTACK_EMBEDDING_MODEL / MNEMOSTACK_LLM_MODEL |
Override the embedding / LLM model name | CLI / HTTP / MCP |
MNEMOSTACK_VECTOR_HOST / MNEMOSTACK_VECTOR_COLLECTION |
Aliases for the Qdrant URL / collection | CLI / HTTP / MCP |
MNEMOSTACK_VECTOR_FLOOR |
Keep top-N raw vector hits in results even when fusion/rerank would drop them (0 = off) | Recall tuning |
MNEMOSTACK_RERANK_MODE |
LLM reranker mode: relevant_only (default) or full_reorder |
HTTP / MCP runtime reranker |
MNEMOSTACK_TOKEN_BUDGET |
Default recall token budget — cut results to the ranked prefix that fits (unset = off) | CLI / HTTP / MCP recall surfaces |
MNEMOSTACK_GRAPH_TIMEOUT / MNEMOSTACK_GRAPH_HEALTH_TIMEOUT |
Memgraph query / health-check timeouts in seconds | Graph retriever |
MNEMOSTACK_CONFIG |
Path to the YAML config file | All entry points |
Only the providers you actually use need their keys. HuggingFace local-GPU embeddings need no keys at all. mnemostack init writes the same settings as YAML; explicit CLI flags override config/env defaults.
Setup and usage details
Try it in 30 seconds (Docker)
Fastest way to kick the tyres. No Python install, no manual Qdrant / Memgraph setup.
git clone https://github.com/udjin-labs/mnemostack && cd mnemostack
cp README.md examples/notes/ # any markdown will do
GEMINI_API_KEY=your-key docker compose -f examples/docker-compose.yml up -d --build
# Index the notes volume and ask a question over HTTP
docker compose -f examples/docker-compose.yml exec mnemostack \
mnemostack index /data --provider gemini --collection demo
curl -s http://localhost:8000/recall \
-H 'content-type: application/json' \
-d '{"query":"what is this about","limit":5}' | jq
The mnemostack container runs the HTTP API on port 8000 by default. Interactive docs are at http://localhost:8000/docs. Use docker compose exec mnemostack mnemostack <cmd> for CLI-style operations (index, search, health) against the same stack.
Tear down with docker compose -f examples/docker-compose.yml down -v (the -v wipes Qdrant + Memgraph state).
Prefer Ollama (no cloud key needed)? Run Ollama on the host, set OLLAMA_HOST=http://host.docker.internal:11434, and pass --provider ollama everywhere instead of gemini.
Reasoning models (qwen3, deepseek-r1 and similar): mnemostack disables thinking by default (think=False in OllamaLLM) — with thinking on, these models spend the whole token budget on thoughts and return empty text, silently degrading reranking, expansion and extraction. Pass get_llm("ollama", think=None) to keep the model's own default, or think=True to force it on models that support thinking. Extra generation options go through options={...} (e.g. {"num_ctx": 8192}).
Installation
# From PyPI
pip install mnemostack
# Optional extras
pip install 'mnemostack[huggingface]' # local GPU embeddings
pip install 'mnemostack[mcp]' # MCP server
pip install 'mnemostack[dev]' # tests + linters
Run a local Qdrant for the vector store:
docker run -p 6333:6333 qdrant/qdrant:latest
Optionally a Memgraph for the knowledge graph:
docker run -p 7687:7687 memgraph/memgraph:latest
CLI quick start
# Health check
mnemostack health --provider ollama
# Index a directory of notes
mnemostack index ./my-notes/ --provider gemini --collection my-memory --recreate
# Hybrid recall
mnemostack search "what did we decide about auth" --provider gemini --collection my-memory
# Synthesize answer
mnemostack answer "what is the capital of France" --provider gemini --collection my-memory
# Record explicit feedback into the same state file used by the HTTP/MCP pipeline
mnemostack feedback <hit-id> --signal clicked --query "what did we decide about auth" \
--source-list vector --source-list bm25
# MCP server (for Claude Desktop, Cursor, etc.)
mnemostack mcp-serve --provider gemini --collection my-memory
Progressive tiers — pay only for the detail you need
search and answer accept an optional --tier {1,2,3} flag that bounds how
much output a call produces. Useful when a recall is called from a long-running
agent loop where full recall output would burn context unnecessarily.
# Tier 1 (~50 tokens) — just "is there anything in memory about this?"
# Returns id, score, source labels; no text.
mnemostack search "VPN failover" --tier 1 --provider gemini
# Tier 2 (~200 tokens) — triage with short snippets (~40 chars each)
mnemostack search "VPN failover" --tier 2 --provider gemini
# Tier 3 (~500 tokens) — full 200-char previews, up to 10 results
mnemostack search "VPN failover" --tier 3 --provider gemini
Omit --tier to get the full, uncapped output (backward compatible). Rule of
thumb for agents: tier 1 for navigation / existence checks, tier 3 only when
you actually need to read the memories. answer is already compressed, so it
needs a tier less often — use --tier 1 there to drop the SOURCES: block
when only the answer text is wanted.
Streaming ingest API
When you want to feed items into mnemostack from code — a chatbot that logs every message, a scraper, a daemon tailing a log — use the Ingestor. It handles batching, deduplication, and idempotency for you.
from mnemostack.embeddings import get_provider
from mnemostack.vector import VectorStore
from mnemostack import Ingestor, IngestItem
emb = get_provider("gemini")
store = VectorStore(collection="my-memory", dimension=emb.dimension)
store.ensure_collection()
ing = Ingestor(embedding=emb, vector_store=store, batch_size=64)
stats = ing.ingest([
IngestItem(text="alice joined acme on 2024-03-01", source="notes/alice.md",
timestamp="2024-03-01T09:00:00Z"), # event time — drives temporal recall
IngestItem(text="alice left acme on 2025-06-15", source="notes/alice.md", offset=100),
])
print(stats) # IngestStats(seen=2, embedded=2, upserted=2, skipped=0, failed=0)
Guarantees:
- Idempotent. Each item gets a deterministic UUID-shaped content id computed from
(source, offset, text). Re-running with the same input is a no-op: Qdrant upsert replaces the point onto itself, and an in-process LRU cache skips even the embedding call for items already seen in this session. - Batched. Items are embedded in batches of
batch_size, so provider HTTP overhead amortises across many items. - Dated. Every payload records
indexed_at(UTC). Passtimestamp=(ormetadata={"timestamp": ...}) to set the event time the temporal retriever filters on. Withwindow_size > 1, sliding-window chunks also carry the window's temporal range aswindow_start_ts/window_end_tspayload keys.
Images in the input (optional)
A memory stack that indexes only text answers "Not in memory" to questions whose answer lived in a photo. If your data contains images, describe them at ingest time and index the description:
from mnemostack.llm import get_llm
llm = get_llm("gemini") # or get_llm("ollama", model="llava") with a local vision model
desc = llm.describe_image(photo_bytes, mime_type="image/jpeg") # one vision call per image
caption = f" [shared a photo: {desc.text}]" if desc.ok and desc.text else ""
item = IngestItem(text=f"{message_text}{caption}", source=..., timestamp=...)
describe_image is fully opt-in — nothing in the ingest or recall paths calls it, and text-only pipelines are unaffected. It works with any provider that has vision support (Gemini; Ollama vision models such as llava, llama3.2-vision, qwen2.5-vl) — providers without it return a normal fail-open error response. The default prompt produces a dense, index-oriented description (objects, any text/signs verbatim, setting, actions); pass prompt= to customize.
- Streaming-friendly.
ing.stream(item_iter)yields per-batch stats so long feeds can be monitored without waiting for the whole stream to drain. - Graceful. If a single item fails to embed, it is counted as
failedbut the rest of the batch still lands.
# Long-lived feed (e.g. inside a FastAPI or Celery worker)
for item in your_firehose():
ing.ingest_one(IngestItem(text=item.body, source=item.channel, metadata={
"user_id": item.user_id,
"ts": item.ts.isoformat(),
}))
Python API
from mnemostack.embeddings import get_provider
from mnemostack.vector import VectorStore
from mnemostack.recall import Recaller, AnswerGenerator
from mnemostack.llm import get_llm
emb = get_provider("gemini")
store = VectorStore(collection="my-memory", dimension=emb.dimension)
store.ensure_collection()
# ... index data here ...
recaller = Recaller(embedding_provider=emb, vector_store=store)
results = recaller.recall("what did we decide", limit=10)
# Each result: .id .text .score .source ("vector" | "bm25" | "memgraph" | "temporal") .metadata
# Optional: synthesize a concise answer
gen = AnswerGenerator(llm=get_llm("gemini"))
answer = gen.generate("what did we decide", results)
print(answer.text, answer.confidence, answer.sources)
Count and "list all X" questions need set completeness, which similarity top-K does not guarantee — the model counts what it sees and undercounts, returning a subset. For those, retrieve a wide candidate pool and enable the two-pass extract-and-aggregate mode:
gen = AnswerGenerator(llm=get_llm("gemini"), list_extract_mode=True)
pool = recaller.recall("how many trips did user A take", limit=150) # wide pool, not top-10
answer = gen.generate("how many trips did user A take", pool)
list_extract_mode routes count/list questions through an extract pass (pulls every matching item as JSON) and a finalize pass (formats the list or count); other question categories are unaffected. The extract pass walks the **who
No comments yet
Be the first to share your take.