mnemostack
The mnemostack server provides tools for managing durable, hybrid memory for AI agents and applications through MCP (Model Context Protocol).
mnemostack_health: Verify that all backend components (embedding provider, Qdrant vector store, optional Memgraph graph database) are reachable and healthy before issuing queries.mnemostack_search: Search indexed memories using hybrid recall (BM25 exact-token + semantic vector + graph + temporal retrieval, fused via Reciprocal Rank Fusion). Returns ranked results with id, text, score, sources, and payload. Supports configurable result limits, payload filters for multi-tenant isolation (exact match or gte/lte ranges), and optional per-retriever debug traces (include_trace).mnemostack_answer: Ask a natural language question and receive a concise, synthesized factual answer generated from retrieved memories, complete with a confidence score (0.0–1.0), source citations, degradation status, and fallback recommendations. Supports payload filters to scope answers to specific tenants or time ranges.mnemostack_feedback: Record explicit user feedback signals (useful,clicked,irrelevant) on specific recalled memories to drive stateful Q-learning weight updates and inhibition-of-return tracking, improving future recall quality over time.
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:
# tag rule: match your installed qdrant-client - same major, minor within 1
docker run -p 6333:6333 qdrant/qdrant:v1.18.3Optional: run Memgraph for graph-backed memory:
docker run -p 7687:7687 memgraph/memgraph:latest2. Start the MCP server
export GEMINI_API_KEY=your-key-here
mnemostack mcp-serve --provider gemini --collection my-memoryClaude 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-memoryRelated MCP server: Cortex
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, the memory write/lifecycle endpoints (/memories,/invalidate,/triples),/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. The list order is authoritative: score has no single scale — many stages and fallback paths write it, and a rerank changes the order without rewriting the numbers — so re-sorting by them undoes it. It is not a similarity, not a confidence, and not comparable across queries; see what score is not.

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 ( | 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 |
| 51.4% | 88.3% |
| 79.8% | 85.7% |
| 62.5% | 79.2% |
| 88.0% | 94.6% |
| 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 |
| 34.8% | 34.4% | 74.1% | −0.4pp |
| 64.5% | 69.8% | 77.9% | +5.3pp |
| 31.2% | 41.7% | 49.0% | +10.5pp |
| 69.2% | 69.6% | 82.0% | +0.4pp |
| 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 fromvalid_from/valid_until— each optional, andinvalidated_atis not read there at all, so a point-in-time view can be a superset of the default one (contract). The vector-side twin of the graph'svalid_untilmodel. CLImnemostack invalidate <id>..., MCPmnemostack_invalidate, and — since 2.2 — HTTPPOST /invalidate(withDELETE /memoriesfor irreversible erasure), selecting either an id list or a wholesource.🌍 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](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, the write/lifecycle surface (POST/GET/DELETE /memories,/invalidate,/triples),/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) |
| IPv4 / port / version / UUID / API-style tokens | 1.4 / 1.4 / 1.0 / 0.9 |
| "who is", | 1.0 / 1.5 / 1.0 / 0.9 |
| "when", "yesterday", "today", dates | 1.0 / 1.0 / 1.0 / 1.4 |
| 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 |
| Google Generative AI key | Gemini embedding + Gemini Flash LLM |
| Ollama server URL (default | Ollama embeddings / LLM |
| Qdrant collection name (default | CLI convenience |
| Qdrant URL (default | Remote Qdrant |
| Memgraph bolt URI | Graph retriever / GraphStore |
| LLM endpoint (ollama: default inherits the embedding | Answer / reranker / expansion LLM |
| Bearer token for the | Answer / reranker / expansion LLM |
| Embedding provider | CLI / HTTP / MCP |
| LLM provider | Answer generation / reranking |
| BM25 corpus paths separated by | CLI / HTTP / MCP BM25 retriever |
|
| HTTP stateful pipeline |
| Override the embedding / LLM model name | CLI / HTTP / MCP |
| Aliases for the Qdrant URL / collection | CLI / HTTP / MCP |
| Keep top-N raw vector hits in results even when fusion/rerank would drop them (0 = off) | Recall tuning |
| LLM reranker mode: | HTTP / MCP runtime reranker |
| Default recall token budget — cut results to the ranked prefix that fits (unset = off) | CLI / HTTP / MCP recall surfaces |
| Memgraph query / health-check timeouts in seconds | Graph retriever |
| 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}' | jqThe 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 and pass --provider ollama everywhere instead of gemini. The endpoint resolves as: --ollama-host flag > MNEMOSTACK_OLLAMA_HOST env / embedding.ollama_host config > the native OLLAMA_HOST variable > http://localhost:11434 — so a client running in a container or VM can reach a remote Ollama daemon directly. An ollama LLM follows the same chain and inherits the embedding host by default; set llm.host / MNEMOSTACK_LLM_HOST only when generation lives on a different box:
mnemostack index-markdown memory/ \
--provider ollama \
--embedding-model qwen3-embedding:8b \
--ollama-host http://192.0.2.10:11434 \
--embedding-timeout 180 \
--embedding-batch-size 64Embedding uses the batch POST /api/embed endpoint (one request per batch; servers too old for it are detected once and served per-item with a loud warning). The embedding timeout (--embedding-timeout / MNEMOSTACK_EMBEDDING_TIMEOUT, default 180s) is independent of the short Qdrant liveness timeout — cold loads of larger local models are legitimately slow. Vector dimensions come from the model tables (quantization-suffix aware) or, for unknown models, a one-shot probe of the live model — there is no blind fallback dimension, so a wrong-size collection can't be created.
Behind an OpenAI-compatible endpoint (LiteLLM proxy, vLLM, llama.cpp server, an API gateway)? Use the openai LLM provider — it speaks POST {base}/v1/chat/completions, which all of them accept:
MNEMOSTACK_LLM_API_KEY=sk-... mnemostack serve \
--llm openai --llm-model team-llmwith llm.host: http://gateway:4000 in the config (or MNEMOSTACK_LLM_HOST). Both the base URL and the model name are required — gateways have no meaningful defaults, so a missing one is a loud, actionable error (serve logs it and disables /answer) instead of a silent dial to the wrong place. A base URL already ending in /v1 (the OpenAI SDK convention) works too. Leave the key unset (or set it to none) for keyless vLLM / llama.cpp deployments; embeddings are unaffected and keep their own provider. Redirects are refused outright — a gateway 3xx becomes a normal error instead of carrying the bearer token to another origin. Reasoning models pointed straight at the cloud OpenAI endpoint (o1 family) reject the classic fields; via the SDK, get_llm("openai", token_param="max_completion_tokens", options={"temperature": None}) renames the budget field and drops the fields they refuse (gateways normally translate this themselves).
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 + lintersRun a local Qdrant for the vector store:
# tag rule: match your installed qdrant-client - same major, minor within 1
docker run -p 6333:6333 qdrant/qdrant:v1.18.3Optionally a Memgraph for the knowledge graph:
docker run -p 7687:7687 memgraph/memgraph:latestCLI 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-memoryProgressive 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 geminiOmit --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
# The list order is authoritative — .score need not follow it, and is not a
# confidence; re-sorting by it undoes reranking. docs/api-stability.md#what-score-is-not
# 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 whole pool you pass in, in batches of list_extract_batch_size (default 40), merging items across batches — so pool order does not decide whether a memory is seen, and the cost is one LLM call per batch plus finalize. An empty extract over a non-empty pool is retried once before abstaining. For guaranteed exhaustiveness on a bounded slice, build the pool from a full scan (VectorStore.scroll) filtered to the relevant slice. To evaluate it on your own data, the benchmark harness exposes the same knobs: benchmarks/locomo_single.py --list-extract --pool 150.
list_finalize="verbatim" skips the finalize LLM pass and assembles the answer deterministically from the extracted items (the count for count questions, the comma-joined items otherwise). Recommended for non-English corpora: an LLM finalize pass can paraphrase or distort items instead of repeating them verbatim. The default "llm" keeps the formatting pass.
Enriching payloads at ingest. Ingestor(enrich=callable) calls your function for every final item (including assembled window chunks) and merges the returned dict into the chunk payload — the mechanism is core, the extractor is yours (content extraction is corpus- and language-specific, so mnemostack ships none). Fail-open: a raising hook logs a warning and the item is indexed without enrichment; text/source/offset and an explicit item timestamp can't be overridden. From the CLI: mnemostack index docs/ --enrich mypkg.extractors:invoice_fields. Enriched fields combine with the rest of the stack: scope recall with filters={"amount": {"gte": 100}} and show them to the answer LLM with context_fields=["amount"].
def invoice_fields(item): # yours — any language, any domain
amounts = AMOUNT_RE.findall(item.text)
return {"amounts": amounts} if amounts else {}
ing = Ingestor(embedding=emb, vector_store=store, enrich=invoice_fields)Already-indexed collections don't need re-embedding to pick up enrichment: mnemostack index docs/ --enrich ... --refresh-payloads rewrites the payloads of existing chunks in place (Qdrant set_payload, vectors untouched) — only genuinely new chunks pay for embedding.
Structured payload fields in the answer prompt. By default the answer context shows each memory's timestamp, source and text. AnswerGenerator(context_fields=["author", "amount"]) additionally projects the named payload fields into each memory's context line (author=…, lists comma-joined, long values truncated; memories without the field render without it). Use it for structured facts the answer needs — who said it, amounts, your own ingest-time enrichments. Note the boundary: projection only changes what the answer prompt shows — retrieval ranks by text, so content that must be findable (image captions and similar) belongs in the text itself, not in a payload field.
Conversational follow-ups. "And who wrote that?" carries none of the conversation, so recall misses. rewrite_followup(query, history, llm) resolves pronouns and ellipses into a standalone question before recall — mnemostack holds no dialog state, you pass the history ((question, answer) pairs or plain lines, oldest first). One LLM call; the prompt instructs the model to return a self-contained question unchanged, and any failure falls back to the original query. To skip the call entirely for queries you already know are standalone, pass needs_rewrite=callable — that trigger heuristic is language-dependent, so core ships none (same boundary as question_classifier).
from mnemostack.recall import rewrite_followup
standalone = rewrite_followup("а кто это написал?", history, llm)
results = recall_flow(recaller, standalone, limit=10, pipeline=pipeline)Non-English corpora. The built-in answer prompts and the question classifier are English; on other languages the extract/finalize passes degrade instead of helping. Both are pluggable:
gen = AnswerGenerator(
llm=llm,
list_extract_mode=True,
prompt_overrides={ # any subset; templates in YOUR corpus language
"list_extract": MY_EXTRACT_TEMPLATE, # must contain {context} and {query}
"list_finalize": MY_FINALIZE_TEMPLATE, # must contain {query} and {items}
"temporal": MY_TEMPORAL_TEMPLATE, # category prompts: {context} and {query}
},
)
# the classifier's patterns are English too — route question classes yourself:
answer = gen.generate(query, pool, category="count")Override names: the seven category prompts (general, list, count, temporal, multihop, inference, adversarial) plus list_extract / list_finalize. Required placeholders are validated at construction. mnemostack ships no translations by design — prompt quality is corpus- and domain-specific, so you own the templates.
Full stack: 4-source retrieval + 8-stage pipeline + reranker
This is the full runtime configuration. The LoCoMo numbers above are produced by a subset of it: the benchmark loop runs Vector + BM25 retrieval, the 8-stage pipeline, window_size=3, query expansion, and top-K 25 — the LLM reranker and the graph retriever are runtime-only features and are not part of the benchmark methodology (see benchmarks/run_locomo.sh for the exact reproduction path).
from mnemostack.embeddings import get_provider
from mnemostack.llm import get_llm
from mnemostack.vector import VectorStore
from mnemostack.recall import (
Recaller, Reranker,
VectorRetriever, BM25Retriever,
MemgraphRetriever, TemporalRetriever,
build_full_pipeline,
)
from mnemostack.recall.pipeline import FileStateStore, default_state_path
emb = get_provider("gemini")
store = VectorStore(collection="my-memory", dimension=emb.dimension)
retrievers = [
VectorRetriever(embedding=emb, vector_store=store),
BM25Retriever(docs=bm25_docs), # see "Building a BM25 corpus" below
MemgraphRetriever(uri="bolt://localhost:7687"), # optional
TemporalRetriever(embedding=emb, vector_store=store),
]
recaller = Recaller(retrievers=retrievers)
raw = recaller.recall("what did we decide", limit=30)
pipeline = build_full_pipeline(state_store=FileStateStore(default_state_path()))
reranked = pipeline.apply("what did we decide", raw)
reranker = Reranker(llm=get_llm("gemini"), max_items=20)
final = reranker.rerank("what did we decide", reranked)[:10]Reranker is generative: it asks an LLM to return candidate IDs. If you have
a backend that returns numeric relevance scores instead (a local cross-encoder
or a hosted rerank service), use ScoringReranker:
from mnemostack.recall import ScoringReranker
scoring_reranker = ScoringReranker(scorer=my_relevance_scorer, max_items=100)
final = scoring_reranker.rerank("retention policy", reranked)[:10]The scorer object only needs score(query, documents) -> Iterable[float].
Scores are relative; no absolute threshold is applied by default. Generative
LLMs can be wrapped as scorers, but dedicated rerank models/services are the
more stable default because they avoid ID-format parsing.
Building a BM25 corpus
BM25Retriever needs a list of BM25Doc. Each doc is the atomic unit BM25 will rank — typically a paragraph or chunk of one of your source files:
from mnemostack.recall import BM25Doc
from pathlib import Path
docs = []
for i, path in enumerate(Path("my-notes/").rglob("*.md")):
text = path.read_text()
# chunk however you like — here: 800-char windows
for j in range(0, len(text), 800):
chunk = text[j : j + 800]
if chunk.strip():
docs.append(BM25Doc(
id=f"{path.name}:{j}",
text=chunk,
payload={"source": str(path), "offset": j},
))For transcript-like inputs (adjacent user and assistant turns), prefer MessagePairChunker so related turns stay in the same chunk. See mnemostack.chunking.
If your canonical memory corpus is already stored in Qdrant payloads, build the BM25 corpus from the same collection instead of maintaining a separate markdown export. This keeps exact-token lookup aligned with vector search (IDs, commit hashes, filenames, quoted phrases):
from qdrant_client import QdrantClient
from qdrant_client.models import FieldCondition, Filter, MatchValue
from mnemostack.recall import BM25Retriever
client = QdrantClient(host="localhost", port=6333)
bm25 = BM25Retriever.from_qdrant(
client,
"memory",
scroll_filter=Filter(
must=[FieldCondition(key="chunk_type", match=MatchValue(value="transcript"))]
),
limit=40_000,
)
hits = bm25.search("api_key_rotation", limit=5)You can also call bm25_docs_from_qdrant(...) directly if you want to combine Qdrant payload chunks with local BM25Docs before constructing BM25Retriever.
For morphologically rich languages or domain-specific normalization, pass a custom tokenizer/analyzer. The same analyzer is applied to corpus and query text; the default exact-token behavior is unchanged when omitted.
from mnemostack.recall import BM25Retriever
def analyzer(text: str) -> list[str]:
# Normalize only what your corpus needs; preserve IDs, hashes and paths.
...
bm25 = BM25Retriever.from_qdrant(client, "memory", tokenizer=analyzer)If you pre-tokenize BM25Doc objects yourself, pass retokenize=False when
constructing BM25/BM25Retriever with the same analyzer. The
BM25Retriever.from_qdrant(...) helper does this automatically.
HTTP server (optional)
If you want mnemostack available to callers that aren't Python — any service written in Node, Go, Rust, or a plain curl from a shell script — install the server extra and expose it over HTTP:
pip install 'mnemostack[server]'
export GEMINI_API_KEY=...
mnemostack serve --provider gemini --collection memory --port 8000mnemostack serve binds to 127.0.0.1 by default. Use
--host 0.0.0.0 only behind your own auth/rate-limit layer.
Endpoints:
Method | Path | Purpose |
|
| Qdrant + Memgraph reachability + config summary |
|
| Liveness probe — |
|
| Readiness probe — |
|
| Operator snapshot — config, live dependency reachability, headline counters |
|
| Hybrid recall with optional 8-stage pipeline |
|
| Recall + LLM answer synthesis with citations |
|
| Verify a citation — resolve a chunk id back to its source document — |
|
| Explicit click/usefulness feedback for stateful learning |
|
| Create memories (server-side embedding, store-backed dedup) — |
|
| List what the tenant holds from one |
|
| Irreversible erasure by id list or |
|
| Non-destructive retraction by id list or |
|
| Write knowledge-graph facts — |
|
| Prometheus scrape endpoint (counters + summary histograms) |
|
| Interactive OpenAPI UI |
curl -s http://localhost:8000/recall \
-H 'content-type: application/json' \
-d '{"query": "what did we decide about auth", "limit": 10}' | jqResponse shape (abridged):
{
"query": "what did we decide about auth",
"results": [
{ "id": "...", "text": "...", "score": 0.72, "source": "notes/...md", "metadata": {} }
],
"degraded": [], // components that ACTUALLY fell back, e.g. "retriever:bm25:failed", "reranker:fallback"; empty when healthy
"notes": [], // routine signals for stages that did not apply, e.g. "temporal:no_parse" on a date-less query; never a fault
"tokens_estimate": 512 // estimated text tokens of the returned results
}The order of results is authoritative — do not re-sort by score: many stages and fallback paths write that number on different scales, and a rerank changes the order without rewriting it. See what score is not.
Pass "include_trace": true in the request body to additionally get a trace object with per-retriever ranked lists, the fused order, and the post-rerank order — useful when debugging why a memory did or didn't surface.
Pass "token_budget": 2000 to cap how much prompt space the results may occupy: the final ranking is cut to the prefix whose total text tokens fit the budget (a hard cap — never overshot, so an oversized top hit yields an empty list rather than a blown prompt). tokens_estimate in the response is the value the budget is enforced against; counting uses a dependency-free heuristic (≈4 chars/token for ASCII, ≈2 for non-ASCII scripts), so leave yourself margin rather than budgeting to the exact context limit. A server-wide default can be set with recall.token_budget in the config file (or MNEMOSTACK_TOKEN_BUDGET); per-request values override it. The same parameter is available on /answer (caps the memories fed to the LLM), on MCP mnemostack_search / mnemostack_answer, on the CLI as --token-budget, and in the library as recall_flow(..., token_budget=...) — where you can also pass an exact token_counter= (e.g. a tiktoken encoder) instead of the heuristic.
Pass "filters": {...} to scope recall by payload fields — exact match ({"tenant": "a"}) or inclusive ranges ({"timestamp": {"gte": "2026-01-01"}}). Filters apply inside every retriever, not as a post-filter on the output: the candidate pool itself is restricted, so top-K stays full and results never include points outside the scope — this is the isolation contract for multi-tenant and per-user memory. Sources that cannot attribute their results to the scope contribute nothing rather than leak. The knowledge-graph retriever attributes its hits where it can: a filter key the hit's own node metadata carries (e.g. index_root) is checked in place, the rest is proven through the hit's vector chunks — a graph file hit (with a recorded root, pinning the probe to its exact document) passes the filter exactly when at least one of its chunks does; entity nodes and anything else without pinnable chunks are still excluded, never leaked. The same filters parameter is available on /answer (the answer is generated only from in-scope memories, including retry sub-recalls), on MCP mnemostack_search / mnemostack_answer, on the CLI as --filters '{"tenant": "a"}', and in the library as recaller.recall(query, filters=...) / recall_flow(..., filters=...).
The /answer endpoint adds { answer, confidence, sources } alongside the memories and carries the same degraded / notes / opt-in trace fields, plus tokens_used — the LLM provider's reported token usage for the generation call that produced the answer (provider-specific semantics; null when the provider reports nothing). If the LLM isn't configured, /answer returns 503 and /recall still works — graceful degradation applies at the HTTP layer too.
Start the server with --retry-on-weak if you want a recall that comes back nearly empty to be paraphrased by the answer LLM and asked again, fusing the rounds by reciprocal rank — so a memory that two phrasings both find outranks one that only a single phrasing did, and a later paraphrase can beat an earlier one. What counts as "weak" is a COUNT (--retry-weak-below, default 1 — only a recall that returned nothing), not a score: fused scores are RRF values encoding rank, not confidence, so a threshold on them would measure nothing. One extra round, at most two paraphrases, and every retry carries the caller's tenant, filters, validity view and budget unchanged. Budget for it accordingly: each variant repeats your recall in full, reranker included, so a weak recall costs up to three LLM calls (one paraphrase plus one rerank per variant) and two extra retrieval rounds — not the single call the name suggests. Two consequences worth knowing before you switch it on: a retried response fuses over several rounds where an unretried one fuses over a single one, so the rank basis behind score differs and the two are never comparable — threshold on rank, not on the number, and see what score is not for what the number holds on each path; and a retry never returns fewer memories than it was given — if the fused list plus your token budget would hand back less than the recall already had, the original results come back untouched. That is a guarantee about the size of the response, not its membership: at a fixed limit a hit that a paraphrase ranks first will take the slot of one your original phrasing ranked last, which is what asking again is for. Off by default, and a request can only opt out of it — the server pays for the LLM call, so enabling it is the operator's call.
Stateful learning is explicit. Start the server with --auto-record-ior if you want /recall and /answer responses to update inhibition-of-return state, and with --record-access (or MNEMOSTACK_RECORD_ACCESS) if you want them to stamp access_count/last_accessed on every point they return — the reinforcement the freshness stage reads, recorded where the retrieval actually happens instead of in each client. Both are off by default: they turn reads into writes. Access recording is fail-open (a failed write is logged and counted, never raised), best-effort on the count (no atomic increment exists; concurrent recalls of one point can record one increment, and the reader clamps reinforcement at 10 anyway), and scoped to the caller's tenant.
It also changes what ranking means, so know which direction it moves things: recording these keys makes the freshness stage's access term live, and that term is a bonus, not a decay. A memory that has been retrieved gets a multiplier in [1.0, 1 + access_bonus_max] (0.25 by default, reached at 10 accesses) which fades back toward 1.0 as the access ages and stops there — use can raise a memory's rank, never lower it. A memory nothing has ever retrieved sits at exactly 1.0, so a deployment that records no accesses ranks exactly as it did before. Set --access-bonus-max 0 (or MNEMOSTACK_ACCESS_BONUS_MAX=0, honoured by serve, search, answer and mcp-serve alike) to take the access signal out of ranking entirely — the switch to reach for if your clients stamp last_accessed themselves, since leaving --record-access off does not help there: the stage reads those keys whoever wrote them. Values are clamped to [0, 1]. Because this is the one term a recall's own output feeds back into, it is bounded on purpose — small ceiling, saturating counter, and only the points actually handed to the caller are recorded. Send user actions to /feedback to update Q-learning:
curl -s http://localhost:8000/feedback \
-H 'content-type: application/json' \
-d '{"hit_id":"...","signal":"clicked","query":"what did we decide about auth","sources":["vector","bm25"]}' | jqsignal is one of useful, clicked, or irrelevant; pass the retrievers list returned by /recall as sources so Q-learning can update the right source weights.
The same state update is available from CLI as mnemostack feedback ... and from MCP as mnemostack_feedback.
Multi-tenant auth. By default the server is unauthenticated (single-tenant; put it behind your own auth layer). For a hard, per-tenant boundary, start it with --auth and issue service keys:
mnemostack keys add --tenant acme --scopes read,write # prints the key once
mnemostack serve --auth # default-deny on every data endpoint
curl -s http://localhost:8000/recall \
-H 'X-API-Key: msk_...' -H 'content-type: application/json' \
-d '{"query":"..."}' # or: Authorization: Bearer msk_...The tenant is resolved from the key (a client can't assert another's), enforced across the vector store, the tenant-scoped knowledge graph, and per-tenant learning state — so it's a real authorization boundary, unlike the caller-supplied filters above. /recall, /answer and GET /memories require read; /feedback, POST/DELETE /memories, /invalidate and /triples require write; a missing/invalid key is 401, insufficient scope 403. Cap each tenant with mnemostack quota set --tenant <id> --max-points N --max-rps R (storage enforced at ingest, rate on the HTTP surface → 429). The operator endpoints (/health, /healthz, /readyz, /status, /metrics) stay unauthenticated — protect them at your proxy if sensitive. Auth is off by default; you can still front the server with your own reverse proxy (nginx, Caddy, Traefik) either way. See docs/deployment.md and docs/api-stability.md.
Knowledge graph (optional)
from mnemostack.graph import GraphStore
graph = GraphStore(uri="bolt://localhost:7687")
graph.add_triple("alice", "works_on", "project-x", valid_from="2024-01-01")
graph.add_triple("alice", "works_on", "project-y", valid_from="2024-07-01")
# Who was alice working on in March?
march_facts = graph.query_triples(subject="alice", as_of="2024-03-15")Current graph facts use the explicit valid_until="current" marker. If you
created graph data with an older release, run
mnemostack graph-migrate-current --dry-run first, then
mnemostack graph-migrate-current to backfill legacy NULL markers.
MCP server for Claude Desktop
Add the entry below to your Claude Desktop config file:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.jsonLinux:
~/.config/Claude/claude_desktop_config.json
{
"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.
Custom embedding provider
from mnemostack.embeddings import EmbeddingProvider, register_provider
class MyProvider(EmbeddingProvider):
@property
def name(self): return "my-provider"
@property
def dimension(self): return 512
def embed(self, text): ...
def embed_batch(self, texts): ...
register_provider("my-provider", MyProvider)embed/embed_batch are the neutral primitives and stay the required
surface. Ingestion and retrieval call the inherited role methods —
embed_document(s) for chunks, embed_query/embed_queries for queries —
which apply the model family's embedding profile exactly once before
delegating to your primitives. Built-in profiles cover asymmetric families
(Qwen3-Embedding's query instruction; E5's query: /passage: prefixes;
the multilingual-E5-instruct variant's instruction format);
symmetric models resolve to the identity profile and behave exactly as
before. Transforms touch only inference input — stored text, chunk ids,
lexical search and citations keep the original document text. To teach
mnemostack a new family without patching it:
from mnemostack.embeddings import EmbeddingProfile, register_embedding_profile
register_embedding_profile("ollama", EmbeddingProfile(
name="my-family",
version=1,
model_patterns=("my-family-embed:*",),
query_transform={"kind": "prefix", "prefix": "query: "},
))Indexed points are stamped with a document-space fingerprint (provider +
model + document transform + dimension + provider inference knobs —
profile name/version are metadata and never hashed; upgrading from 1.x? see
docs/migration-1.x-to-2.0.md); mnemostack index /
index-markdown refuse a collection embedded under a different space, and
doctor reports the active profile and fingerprint.
Install for an OpenClaw-style agent
If you run an OpenClaw-style assistant with its own scripts/recall.sh or similar recall entry point, you don't have to wire mnemostack in by hand. Ask the assistant to do it and it can follow the steps below verbatim.
(These steps are written against the markdown-backed memory layout popularised by OpenClaw / OpenClaw — the reasoning applies to any assistant whose memory lives in MEMORY.md + daily notes.)
Assistant, read this section and follow the steps if your user asks you to install mnemostack.
Install the package into whichever Python env your recall scripts use.
pip install mnemostackConfirm with
python3 -c "import mnemostack; print(mnemostack.__version__)".Make sure Qdrant and (optionally) Memgraph are reachable. If either is missing, start them:
# tag rule: match your installed qdrant-client - same major, minor within 1 docker run -d -p 6333:6333 --name qdrant qdrant/qdrant:v1.18.3 docker run -d -p 7687:7687 --name memgraph memgraph/memgraph:latest # optionalCheck with
curl -sf http://localhost:6333/collectionsandecho 'RETURN 1;' | cypher-shell -a bolt://localhost:7687.Populate the vector store with the assistant's existing memory. Almost every OpenClaw-style assistant stores memory as markdown files —
MEMORY.md,memory/*.mddaily notes, maybeUSER.md/TOOLS.md. Those files are the ground truth; Qdrant is just a searchable index on top of them. So "migration" here really means a first-time ingest.Two cases:
Case A — user has no Qdrant yet (default OpenClaw setup, markdown-only memory).
Stand up Qdrant (see step 2), then index the workspace in one command:
mnemostack index ~/.openclaw/workspace/ \ --provider gemini \ --collection memory \ --chunk-size 800This walks every
*.md/*.txtunder the workspace, chunks them, embeds each chunk with the chosen provider, and upserts into Qdrant. For a typical workspace (a few megabytes of notes) this finishes in a minute or two and stays well inside free-tier embedding quotas.Re-running is safe.
mnemostack indexuses a deterministic id per(source, offset, content)chunk, so a second run skips unchanged chunks (no duplicates, no wasted embedding calls) and only re-embeds chunks whose content changed or moved. Use--recreateif you deliberately want to wipe and rebuild from scratch.Memgraph is optional — skip it for a pure markdown-backed memory and
MemgraphRetrieverjust won't be part of the retriever list.Case B — user already has Qdrant / Memgraph populated by another plugin or a prior assistant run. Don't re-index. mnemostack reads existing collections and graphs as-is; payload like
chat_id,memory_class,valid_fromis preserved. Just pointVectorStore(collection=...)at the existing collection name andMemgraphRetriever(uri=...)at the existing graph.Critical for both cases: use the same embedding model for the query that was used for the stored points. Mixing providers/models across ingest and query silently returns garbage — no error, just bad results. Indexing commands now enforce this for documents: points carry a document-space fingerprint and
index/index-markdownrefuse a collection embedded under a different provider/model/profile.Point your recall entry at mnemostack. Swap your search helper to use the 4-retriever
Recallerfrom the "Full stack" example above. If your recall script has a--unifiedflag, route to mnemostack first and fall back to the legacy code path on any exception.Keep the fallback wired. Don't delete the existing recall implementation. Route mnemostack as the primary, fall back to the old recall on any exception. A bad upgrade becomes a one-minute rollback, not an outage.
Smoke-test on real queries from the user's memory, not toy strings. Ask at least one exact-token query (an IP, a ticker, an error string), one semantic query, one temporal query ("what did we decide last March"), and one list query. If any regresses vs the old recall, keep both running in parallel until you understand why.
Write a short note in the user's daily memory describing what changed: which command was used to ingest, which embedding provider, which collection name, and where the fallback lives. Future sessions need this to avoid re-indexing with a different model and trashing recall quality.
If you're a human operator and you'd rather do this yourself, the same steps apply; treat them as a checklist.
OpenClaw auto-recall companion plugin
This repository also includes a self-contained OpenClaw companion plugin in openclaw-plugin/. mnemostack remains the recall backend — vector + BM25 + graph + temporal retrieval, fusion, reranking, and answer synthesis — while the plugin connects that backend to OpenClaw's before_prompt_build hook.
Zero-config path: install mnemostack, run the daemon on the default local port, install/enable the plugin, and OpenClaw will automatically inject bounded recall answers for recall-style questions:
mnemostack serve --host 127.0.0.1 --port 18793
cd openclaw-plugin
npm testThe plugin defaults to http://127.0.0.1:18793/answer, supports English/Russian trigger defaults with extensible language-agnostic trigger lists, and can fall back to a Script backend such as recall-selfeval.sh when you are not running the daemon.
Roadmap
Embedding provider registry (Gemini / Ollama / HuggingFace)
LLM provider registry (Gemini Flash / Ollama)
Qdrant wrapper
BM25 + RRF recall pipeline
Answer mode with confidence + citations
LLM-based reranker
Memgraph wrapper with temporal validity
Consolidation runtime (phase orchestrator)
CLI (
mnemostack health/doctor/inspect/search/answer/index/mcp-serve)MCP server (Model Context Protocol)
Text → graph triple extractor helpers (
mnemostack.graph.TripleExtractor)Config file support YAML/JSON (
mnemostack.config,mnemostack init/configCLI)Async variants for high-throughput servers (
mnemostack.vector.AsyncQdrantStore)Docker compose examples (
examples/docker-compose.yml)Reproducible LoCoMo benchmark harness in-tree (
benchmarks/run_locomo.sh)First-class FastAPI/Starlette service wrapper (
pip install 'mnemostack[server]',mnemostack serve)Async
Recaller.recall_asyncand parallel retriever dispatch (proven: 5 concurrent HTTP recalls complete in ~1x single-request wall-clock)Benchmarks on longer-horizon synthetic corpora (
benchmarks/synthetic_longhorizon.py)Streaming
IngestorAPI (mnemostack.ingest)Prometheus
/metricsendpoint on the HTTP serverUnicode-aware
MemgraphRetrieverprobes (telegram_id, handle,name_lower)Community health: Code of Conduct, Security policy, issue/PR templates
Per-retriever latency in
/metrics(mnemostack_recall_<name>_latency_ms)Weighted RRF fusion (
reciprocal_rank_fusion(weights=[...]))Adaptive per-query-shape weights in
Recaller(adaptive_weights=True)HyDERetriever(opt-in, not in defaultRecaller)Two-pass graph extraction (full + detail) in the agent's graph-sync pipeline
Progressive Tiers API on
search/answer(--tier {1,2,3}, backward-compatible)MCP integration guides for Claude Desktop/Code, Cursor, OpenClaw (
integrations/)Silent-zero fix in
TemporalRetriever+ dispatch-by-type filter builder (0.2.0a1)Canonical
recall_flow()— CLI/HTTP/MCP rank identically (0.5.0)Chunk lifecycle:
index --prune(root-scoped) +--refresh-payloadswithout re-embeddingRecall
filters=on every surface with adversarially-tested tenant isolationIngest-time enrichment hook (
Ingestor(enrich=...)) +context_fieldsanswer projectionOllama
thinkcontrol (off by default) + generationoptionspassthroughFollow-up question rewriting (
rewrite_followup)
Contributing
Issues and PRs are welcome. Public APIs are intended to remain stable; new functionality should land additively where possible.
License
Apache 2.0 — see LICENSE.
Available Tools
7 toolsmnemostack_answerA
Answer a question using retrieved memories.
Read-only, no side effects, no authentication required. Use this when you want a concise factual answer synthesized from memory search results instead of the raw matches returned by mnemostack_search. Returns a JSON object with ok, query, answer text, confidence (0.0-1.0), sources, notes (the AUTHORITATIVE routine signals — e.g. temporal:no_parse on any query without a date; NOT a fault), degraded (components that actually fell back, plus a deprecated back-compat duplicate of the routine tags until the next major), fallback_recommended, tokens_estimate (estimated text tokens of the context memories), tokens_used (LLM-provider-reported usage for the answer call; null when unreported), and error. Stale facts are hidden by default; use include_invalidated or as_of to see them.
| Name | Required | Description | Default |
|---|---|---|---|
| as_of | No | Point-in-time recall (ISO-8601); same contract as mnemostack_search. | |
| limit | No | Maximum number of results to return (default 10) | |
| query | Yes | Natural language question or keyword to search memories for | |
| filters | No | Payload filters applied inside every retriever (exact match or gte/lte ranges); the answer is generated only from memories inside the filtered scope. | |
| token_budget | No | Hard cap on the total (estimated) text tokens of the memories fed to the answer LLM (same contract as mnemostack_search). Unset uses the server-wide default. | |
| include_invalidated | No | Include facts marked stale (default false; same as mnemostack_search). |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description fully carries the burden. It states 'Read-only, no side effects, no authentication required,' and thoroughly describes return fields including the 'notes' routine signals, 'degraded' field behavior, and null token_used cases. It also discloses the default hiding of stale facts, which is a behavioral trait beyond what the schema conveys.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the purpose and usage, followed by a detailed return-value specification. It is long but every sentence carries substantive information; the use of parentheses and semicolons keeps it structured. Not as concise as the ideal two-sentence example, but appropriately sized for the tool's complexity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is comprehensive: it explains the tool's purpose, usage context, return schema, safety profile, and edge cases (e.g., stale facts, degraded flags, token reporting). Given the tool's complexity and the absence of annotations, this description covers all necessary context for an agent to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3. The description adds value by referencing 'same contract as mnemostack_search' for as_of and token_budget, clarifying filter behavior, and noting the default for include_invalidated. These cross-tool and behavioral details go beyond the schema's property descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool answers a question using retrieved memories and distinguishes it from mnemostack_search by producing a synthesized answer rather than raw matches. The verb 'answer' plus the resource ('retrieved memories') is specific, and the explicit mention of the sibling tool clarifies the differentiation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit usage guidance: 'Use this when you want a concise factual answer synthesized from memory search results instead of the raw matches returned by mnemostack_search.' It also notes when to use include_invalidated or as_of to see stale facts, providing clear context for this tool over alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mnemostack_feedbackA
Record explicit feedback for stateful recall learning.
Use signal='clicked' to also record inhibition-of-return exposure. Pass retriever labels from mnemostack_search results as sources so Q-learning can update source weights.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Natural language question or keyword associated with the feedback | |
| hit_id | Yes | ||
| reward | No | ||
| signal | Yes | ||
| source | No | ||
| sources | No | ||
| query_type | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations; description mentions recording and Q-learning update but omits side effects, permissions, or idempotency. Adequate but not thorough.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three concise sentences, front-loaded with purpose, no fluff. Each sentence adds value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Has output schema, so return values covered. Covers core usage with sibling integration, but could elaborate on parameter behavior.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
14% schema coverage; description adds meaning for signal and sources but leaves hit_id, query_type, reward, source unexplained. Partially compensates.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clear verb-resource pair 'Record explicit feedback' with specific use case for signal='clicked'. Distinct from siblings like search and answer.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides guidance on when to use (feedback recording) and how to connect with mnemostack_search. Lacks explicit exclusions but context is sufficient.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mnemostack_healthA
Check health of all mnemostack components.
Read-only, no side effects, no authentication required. Returns a JSON object with ok (bool) and per-component status for the embedding provider, Qdrant vector store, and optional Memgraph graph database. Use this to verify the memory backend is reachable before issuing recall queries.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries full burden. It discloses the tool is read-only, has no side effects, and requires no authentication. It also describes the return format. This goes beyond minimal requirements, though it omits potential error conditions or rate limits.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences, front-loaded with the primary purpose, and includes all essential details without any filler. Every sentence adds value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's simplicity (no parameters, presence of an output schema), the description fully covers the agent's needs: purpose, behavior, return fields, and usage guidance. No gaps remain.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and the input schema is fully described (100% coverage). The description correctly indicates no inputs are needed. Baseline for zero parameters is 4, and the description adds no extra parameter semantics, which is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb 'check' and clearly identifies the resource 'health of all mnemostack components'. It distinguishes itself from siblings by directing users to use this tool before recall queries, implying other tools (answer, feedback, search) are for different tasks.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states when to use it ('before issuing recall queries') and clarifies characteristics ('Read-only, no side effects, no authentication required'). It does not explicitly state when not to use it or name alternatives, but the context makes those clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mnemostack_invalidateA
Mark memories stale by id, non-destructively.
A write tool (parallel to mnemostack_graph_add_triple). Sets invalidated_at (and optionally valid_until) on each point's payload without deleting or re-embedding it; invalidated facts drop out of default recall but stay reachable via include_invalidated / as_of. Points that do not exist are skipped. Pass index_root in multi-root collections to avoid marking another root's chunks stale. Returns ok, requested, and invalidated (the number of points actually updated).
| Name | Required | Description | Default |
|---|---|---|---|
| ids | Yes | Point id(s) to mark stale (string or integer) | |
| index_root | No | Owner guard: when set, points owned by a different index_root are skipped, so one root cannot invalidate another's chunks in a shared collection. | |
| valid_until | No | World-time the fact stopped being true (ISO-8601); optional, separate from the system-time invalidation stamp. | |
| invalidated_at | No | System-time stamp (ISO-8601); default: now (UTC) |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description fully discloses behavior: non-destructive, sets timestamps without deletion/re-embedding, skips non-existent points, guard logic for index_root, and return fields. No contradictions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Five sentences, no filler. The first sentence states the core purpose. Every subsequent sentence adds necessary detail about behavior, edge cases, and return values. Structure is compact and well-organized.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has 4 parameters (1 required) and no output schema shown, the description fully covers return values, side effects, and parameter behavior (e.g., skip logic, index_root guard). No gaps remain for an agent to misunderstand.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, providing baseline 3. The description adds semantic value beyond schema by explaining the owner guard ('so one root cannot invalidate another's chunks'), the distinction between system-time and world-time stamps ('optional, separate from the system-time invalidation stamp'), and the effect on recall.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Mark memories stale by id, non-destructively,' giving a specific verb and resource. It also positions itself as a write tool parallel to a sibling, distinguishing its purpose from the other siblings (e.g., search, answer).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explains when to use this tool versus alternatives: 'invalidated facts drop out of default recall but stay reachable via include_invalidated / as_of.' It also gives instructions for multi-root collections ('Pass index_root...'). However, it does not explicitly state when NOT to use this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mnemostack_rememberA
Store a memory for the caller's tenant (the write counterpart of mnemostack_search).
A write tool: requires the write scope, embeds server-side, and
stamps the process key's tenant so the memory lands in — and is
recallable from — exactly this tenant's scope. Ids are deterministic
from (source, offset, text): retries and repeated content return
duplicate without a second embedding call. Long documents pass
chunk=true and yield one result per chunk. Use
mnemostack_invalidate to retract a stored memory.
| Name | Required | Description | Default |
|---|---|---|---|
| tags | No | Optional tags stored in the payload. | |
| text | Yes | The memory content to store. Embedded server-side. Plain items are capped at 32768 characters; longer documents need chunk=true. | |
| chunk | No | Split a long document server-side into the same fixed character windows `mnemostack index` uses (identical chunk ids). Requires a non-empty source. | |
| offset | No | Position within `source` for multi-part documents. | |
| source | No | Logical origin (e.g. 'chat/2026-08-19'). With `offset` it forms the deterministic id: re-sending the same content is a no-cost duplicate, never a second copy. | |
| metadata | No | Free payload fields, filterable at recall. Server-reserved keys (underscore-prefixed, structural ones like tenant_id/source, tags/timestamp — use their dedicated parameters — and the lifecycle marker invalidated_at, settable only via mnemostack_invalidate) are rejected. | |
| timestamp | No | Event time of the content (ISO-8601); drives temporal recall. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are present, so the description carries the full behavioral burden. It discloses the required write scope, server-side embedding, tenant stamping, deterministic duplicate behavior without a second embedding call, and chunked output behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Every sentence delivers distinct high-value information: scope, tenancy, idempotency, chunking, and the invalidation alternative. The primary action is front-loaded and there is no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a state-changing tool with seven parameters and no annotations, the description supplies crucial context: tenant isolation, permissions, duplicate semantics, chunking behavior, and the companion invalidation tool. Along with the schema, it is sufficient for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema already documents each parameter, but the description adds deeper semantics: source, offset, and text combine into a deterministic id, and chunk=true maps to one result per fixed character window. This is beyond the schema's parameter descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description leads with a specific verb and resource: 'Store a memory for the caller's tenant,' and immediately labels it as the write counterpart of mnemostack_search. That alone lets an agent distinguish it from the search and invalidation siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly tells the agent to use mnemostack_search for recall and mnemostack_invalidate for retraction, positioning this tool as the write path. It also provides a conditional usage rule for long documents via chunk=true.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mnemostack_resolveA
Verify a citation: resolve a chunk id back to its source document.
Re-reads the CURRENT source and returns an honest verdict: intact / source_changed / moved (citation still supported), changed / missing (not supported by the current source), or unresolvable (cannot be verified from this process). Includes the snapshot-hash comparison and the fragment when locatable. Read-only; never mutates stored memory; runs outside the recall path. Resolution is confined to the corpus root recorded at ingest — there is deliberately no way for a caller to point it at another directory.
| Name | Required | Description | Default |
|---|---|---|---|
| chunk_id | Yes | The [id:...] value from a recall result or answer citation |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description fully discloses behavioral traits: 'Read-only; never mutates stored memory' explicitly states safety, and 'runs outside the recall path' plus 'Resolution is confined to the corpus root recorded at ingest — there is deliberately no way for a caller to point it at another directory' provides important scope limitations. It also lists the verdict categories and mentions 'snapshot-hash comparison,' giving rich behavioral detail beyond what structured data could convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the purpose in the first sentence and each subsequent sentence adds distinct value: verdict categories, read-only safety, and root-confined scope. It is concise enough at roughly 90 words and avoids redundancy or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter tool, the description covers the core purpose, explicit safety guarantees, scope restrictions, and the nature of the return value (verdicts and optional fragment). The presence of an output schema means return details are not required in the description, and the tool's simplicity makes this fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already documents the sole parameter `chunk_id` with the description 'The [id:...] value from a recall result or answer citation,' achieving 100% schema coverage. The tool description adds only that it resolves the id back to a source document, which is purpose-oriented rather than parameter-specific. This meets the baseline but does not elevate it.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with 'Verify a citation: resolve a chunk id back to its source document,' giving a specific verb ('verify'/'resolve') and resource ('chunk id back to source document'). This clearly distinguishes it from siblings like search, answer, invalidate, and health by focusing on citation resolution and verification.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description clearly establishes context: 'Verify a citation' and 'Re-reads the CURRENT source' imply it is for verifying existing citations against the current source. It also notes it 'runs outside the recall path,' distinguishing it from retrieval tools. However, it does not explicitly name sibling alternatives or state when not to use it, so it stops short of full usage guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mnemostack_searchA
Search indexed memories with hybrid recall.
Read-only, no side effects, no authentication required. Use this when you need raw memory matches rather than a synthesized answer. Returns a JSON object with ok, query, count, results, tokens_estimate (estimated total text tokens of the results), notes (the AUTHORITATIVE list of routine signals for stages that did not apply — e.g. temporal:no_parse on any query without a date; NOT a fault), and degraded (components that actually fell back, plus a deprecated back-compat duplicate of the routine tags until the next major; an entry absent from notes is a real fault). Results are ranked by reciprocal rank fusion of BM25, semantic, graph, and temporal retrievers when configured; each result includes id, text, score, sources, and payload. Stale facts (invalidated_at set) are hidden by default; use include_invalidated or as_of to see them.
| Name | Required | Description | Default |
|---|---|---|---|
| as_of | No | Point-in-time recall (ISO-8601): return facts valid at this world-time instant, ignoring later invalidation. | |
| limit | No | Maximum number of results to return (default 10) | |
| query | Yes | Natural language question or keyword to search memories for | |
| filters | No | Payload filters applied inside every retriever: exact match ({"tenant": "a"}) or gte/lte range ({"timestamp": {"gte": "2026-01-01"}}). Results never include points outside the filtered scope. | |
| token_budget | No | Hard cap on the total (estimated) text tokens of the returned results; the final ranking is cut to the prefix that fits. Unset uses the server-wide default, if any. | |
| include_trace | No | Include the per-retriever recall trace (debug; verbose) | |
| include_invalidated | No | Include facts marked stale. Default false: memories with an invalidated_at marker are hidden from recall. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full transparency burden and meets it comprehensively. It discloses read-only/no side effects/no auth, ranking algorithm (RRF of BM25, semantic, graph, temporal), stale-fact defaults, and explains the semantics of notes and degraded fields, including deprecated back-compat behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but well-organized, with every sentence adding meaningful detail. It flows from purpose to usage to return semantics to behavior, with no filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
This is a complex tool with 7 parameters and nuanced return fields, yet the description covers when to use it, safety profile, ranking methodology, stale handling, and how to interpret notes/degraded signals. Even though an output schema exists, the description adds necessary context about deprecation and non-fault indicators, making it fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, setting a baseline of 3. The description adds value beyond the schema by relating include_invalidated and as_of to stale-fact visibility and explaining how filters interact with retrievers. While not exhaustive per-parameter, it enhances understanding of parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description opens with a specific verb+resource ('Search indexed memories with hybrid recall') and explicitly differentiates from the sibling answer tool: 'raw memory matches rather than a synthesized answer.' This clearly states what it does and distinguishes it from alternatives.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when-to-use context ('Use this when you need raw memory matches rather than a synthesized answer') and indicates a read-only, no-authentication need. The synthesized-answer contrast implicitly names the mnemostack_answer sibling as the alternative.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v2.3.1- Added
mnemostack_remember
1 tool update
v2.0.0- Added
mnemostack_resolve
3 tool updates
v0.8.0- Changed
mnemostack_answer3 fields changed- added
Input schema / properties / as_ofAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Point-in-time recall (ISO-8601); same contract as mnemostack_search." +} - added
Input schema / properties / include_invalidatedAdded value: +{ + "default": false, + "description": "Include facts marked stale (default false; same as mnemostack_search).", + "type": "boolean" +} - added
Input schema / properties / token_budgetAdded value: +{ + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hard cap on the total (estimated) text tokens of the memories fed to the answer LLM (same contract as mnemostack_search). Unset uses the server-wide default." +}
- Added
mnemostack_invalidate - Changed
mnemostack_search3 fields changed- added
Input schema / properties / as_ofAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Point-in-time recall (ISO-8601): return facts valid at this world-time instant, ignoring later invalidation." +} - added
Input schema / properties / include_invalidatedAdded value: +{ + "default": false, + "description": "Include facts marked stale. Default false: memories with an invalidated_at marker are hidden from recall.", + "type": "boolean" +} - added
Input schema / properties / token_budgetAdded value: +{ + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hard cap on the total (estimated) text tokens of the returned results; the final ranking is cut to the prefix that fits. Unset uses the server-wide default, if any." +}
2 tool updates
v0.6.0- Changed
mnemostack_answer1 field changed- added
Input schema / properties / filtersAdded value: +{ + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Payload filters applied inside every retriever (exact match or gte/lte ranges); the answer is generated only from memories inside the filtered scope." +}
- Changed
mnemostack_search2 fields changed- added
Input schema / properties / filtersAdded value: +{ + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Payload filters applied inside every retriever: exact match ({\"tenant\": \"a\"}) or gte/lte range ({\"timestamp\": {\"gte\": \"2026-01-01\"}}). Results never include points outside the filtered scope." +} - added
Input schema / properties / include_traceAdded value: +{ + "default": false, + "description": "Include the per-retriever recall trace (debug; verbose)", + "type": "boolean" +}
3 tool updates
v0.4.3- Changed
mnemostack_answer2 fields changed- added
Input schema / properties / limit / descriptionAdded value: +"Maximum number of results to return (default 10)" - added
Input schema / properties / query / descriptionAdded value: +"Natural language question or keyword to search memories for"
- Changed
mnemostack_feedback1 field changed- added
Input schema / properties / query / descriptionAdded value: +"Natural language question or keyword associated with the feedback"
- Changed
mnemostack_search2 fields changed- added
Input schema / properties / limit / descriptionAdded value: +"Maximum number of results to return (default 10)" - added
Input schema / properties / query / descriptionAdded value: +"Natural language question or keyword to search memories for"
4 tool updates
v0.4.1- First observed
mnemostack_answer - First observed
mnemostack_feedback - First observed
mnemostack_health - First observed
mnemostack_search
TDQS
Scored across 7 tools
Each tool has a clearly distinct role: health checks backend status, search returns raw matches, answer synthesizes an answer, resolve verifies citations, invalidate retracts memories, remember stores new memories, and feedback records learning signals. The only potentially confusing pair is search vs. answer, but the descriptions explicitly separate raw retrieval from synthesized response.
All tools share the consistent mnemostack_ prefix and mostly use verb-like action names such as search, answer, resolve, invalidate, and remember. health and feedback are noun-style rather than verb-style, which is a minor deviation from an otherwise uniform pattern.
Seven tools is a well-scoped size for a memory server covering health, ingestion, retrieval, synthesis, retraction, citation verification, and feedback. Each tool earns its place without redundancy or bloat.
The core memory lifecycle is covered: store with remember, retrieve with search/answer, retract with invalidate, verify with resolve, and learn with feedback. However, there is no permanent delete operation, and the invalidate description references a mnemostack_graph_add_triple write tool that is not present in the set, leaving graph-write coverage unclear.
Maintenance
Related MCP Connectors
Persistent memory and knowledge graphs for AI agents. Hybrid search, context checkpoints, and more.
- memnodeOAuthdev.memnode
Persistent, inspectable memory for AI agents with lineage, correction, and a hosted MCP endpoint.
Persistent memory and knowledge management for AI agents with semantic search and 50+ tools.
Persistent memory for AI agents. EU-hosted, privacy-first, hybrid recall, contradiction detection.
Related MCP Servers
- AlicenseAqualityAmaintenancePersistent memory MCP server for AI coding agents (Claude Code, Codex, Gemini CLI). Hybrid retrieval (vector + BM25), cross-encoder reranking, knowledge graph, session checkpoint/resume, and multi-scope isolation. Local-first with LanceDB.30200 npm15MIT
- AlicenseNot gradedqualityDmaintenanceLocal-first AI memory layer with hybrid retrieval and brain-inspired namespaces. Enables agents to save, search, and manage memories directly via MCP tools.5 npmMIT

Mnemo MCPofficial
AlicenseNot gradedqualityBmaintenancePersistent AI memory server with hybrid search and embedded sync. Enables AI agents to store, retrieve, and manage information across sessions with temporal knowledge graph support.MIT- AlicenseNot gradedqualityCmaintenancePersistent, semantically-searchable memory for AI agents using local PostgreSQL, pgvector, and Ollama embeddings, exposed via MCP with hybrid retrieval, knowledge graph, and auto-recall hook.3 npmMIT