Papez
The intelligence layer for AI memory.
Papez doesn't just remember what happened; it remembers why. A scoring engine + causal graph + lifecycle manager for AI agent memory. Speaks MCP natively.
LoCoMo benchmark (certified)
| System | Score | Protocol |
|---|---|---|
| Papez | 85.55 ± 0.37 | Frozen: gpt-4o-mini answerer + judge, temp 0, n=1,540, cats 1–4, 10 runs (July 2026) |
| Zep | 75.14 | Comparable published setup |
| Mem0 | 66.9 | Comparable published setup (Mem0 paper) |
Self-reported vendor figures above ~90 use different answerers/judges and are not comparable — the oracle retrieval ceiling under this frozen protocol is 94.9. Reproduce it yourself: Astrix-Labs/locomo-harness · full methodology · per-run results.
Hosted product: papez.ai — your personal memory for AI, carried across ChatGPT, Claude, and every MCP app · Pricing · Developer docs · Benchmark methodology (85.55 on LoCoMo, certified over 10 runs, receipts published)
What is this
Papez is a scoring engine, causal graph, and lifecycle manager for AI memory. Memories are scored by a multiplicative formula (relevance × connectivity × reactivation), connected in a causal graph, and actively forgotten when they become irrelevant.
This package (papez) is the core library: an in-memory causal graph engine with optional JSON persistence, plus a stdio MCP server. It has no database dependency and no REST API. A hosted product built on top of this library — with Postgres, additional storage backends, and a REST/HTTP MCP API — is available separately at api.papez.ai; it is not part of this package.
Why
- Flat memory doesn't scale. Dumping everything into a vector store gives you recall with zero understanding. The 500th memory buries the 5 that matter.
- No forgetting = no intelligence. Real memory systems forget. Without active pruning, your AI drowns in stale context.
- No causal reasoning. Vector similarity can't answer "why did I choose X?" — you need a graph.
Your AI remembers everything but understands nothing. Papez fixes that.
Quick Start
Requires Python 3.11 or newer.
Install the package. The base install has zero database dependencies — state lives in memory and is optionally persisted to a JSON file.
pip install papez
Optional extras:
pip install 'papez[openai]' # OpenAI embeddings
pip install 'papez[local]' # Local embeddings (sentence-transformers, no API key)
pip install 'papez[anthropic]' # LLM-based causal inference (consolidation, contradiction detection)
Run the stdio MCP server directly:
python3 -m papez
From source
git clone https://github.com/Astrix-Labs/papez.git
cd papez
pip install -e '.[dev]'
pytest tests/
Connect to your AI
Claude Code
claude mcp add papez -- python -m papez
Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"papez": {
"command": "python",
"args": ["-m", "papez"]
}
}
}
Reliability & retries
The stdio server is a single local process. Under load — or during a restart orredeploy of a hosted transport in front of it — a tool call can transiently failor the connection can briefly go unresponsive. Memory writes and reads are notworth crashing an agent turn over, so clients should degrade gracefully ratherthan treat a memory call as fatal:
- The server degrades gracefully too: a tool exception (or a missingrequired argument) is returned as a structured
{"error": "...", "retryable": bool}payload instead of a protocol-levelMCP failure, so a memory hiccup never crashes the transport. Theretryableflag encodes the guidance below:truefor the read tools, andfor amemory_store_batchcall in which every item carries anexternal_id. - Retry reads (
memory_recall,memory_search,memory_traverse,memory_explain,memory_stats,list_core_memories,space_list,space_get,space_member_list) with a short bounded backoff (e.g. 2–3attempts). These have no side effects worth worrying about beyond reactivationbookkeeping. - Retry any write that carries an
external_id.memory_storewith anexternal_id, andmemory_store_batchwhen every item has one, areidempotent: a resend returnsunchanged(oramendedif the content reallychanged) with the same node ids, never a duplicate. SeeIdempotent writes and batches. - Do not blindly retry
memory_storewithout anexternal_id,memory_amend,or a batch with any item lacking anexternal_idon an ambiguous timeout: asilent success followed by a retry creates a duplicate node. Prefer to continuethe turn and reconcile on the nextmemory_recall, or pass a stablesource_sessionso duplicates are easy to spot. (retryablestaysfalseformemory_storeeven with anexternal_id, because the flag is per tool; thecall itself is safe to resend.) - Treat memory as best-effort context, not a hard dependency. If a call fails,proceed with whatever context you already have and try again next turn ratherthan aborting. The graph is durable; a missed write is recoverable, a crashedagent turn is not.
MCP Tools
| Tool | Description |
|---|---|
memory_store |
Store a new memory. Use related for writer-specified typed edges ({id, type}); related_to is legacy and always creates caused_by. Optional category, refs, tags, space, external_id, on_existing (amend default, skip, error). May return possible_conflicts (heuristic hints). |
memory_amend |
Record a correction: creates a new memory that supersedes an existing one. The old memory is kept (decayed in recall), not deleted. Optional refs / tags (omitted: copied from the old memory; given: replace). Stays in the old memory's space. |
memory_recall |
Recall memories by natural language query (vector + keyword + graph spreading activation). Supports verbosity: "concise" or "light" for lightweight payloads. Optional tags_any, ref_type, space, spaces, include_personal, category, exclude_superseded. |
memory_search |
Filtered vector search by status, category, date (since), last-active date (active_since), entity, and now tags_any, tags_all, ref_type, ref_id. Pass an empty query to enumerate by recency instead (no embedder needed) — with since/active_since this answers "what's new since I last looked" without knowing what to query for. Optional space, spaces, include_personal. |
memory_traverse |
Walk the causal graph from a node. Returns reachable nodes and the edges of the induced subgraph (source/target/type/weight/created_by) — a superset of the BFS tree, so paths can be reconstructed. Honors edge_types. Optional space, spaces, include_personal. An invisible start node returns not_found. |
memory_explain |
Explain a memory's score. Includes a score_model block (formula + live per-force breakdown + staleness note), removal_impact, and the memory's refs, tags, space. |
memory_stats |
Get memory system statistics over what you can see. Optional space. |
pin_memory |
Pin a memory so it's never forgotten (writer+ for space memories) |
unpin_memory |
Unpin a previously pinned memory (writer+ for space memories) |
delete_memory |
Permanently delete a memory (writer+ for space memories) |
list_core_memories |
List core memories, optionally filtered by category. Optional space. |
set_core_preferences |
Set user preferences for core memory categories |
promote_to_org |
Promote a private memory to org visibility (not for space memories) |
memory_store_batch |
Store up to 100 memories into one space in one call. Validates every item first (all or nothing), supports ref_index links to earlier items, returns one result per item. Safe to retry when every item has an external_id. |
memory_retire_by_ref |
Retire the memories citing a removed source (ref_type, ref_id). mode="amend" (default) or "delete". Optional space. |
space_create |
Create a Mind Space; you become its owner. name, optional slug, org_id, description, forgetting_policy, llm_inference_enabled, show_service_names. |
space_list |
List your spaces with your role and counts. Optional include_archived. |
space_get |
One space, with member and memory counts (reader+) |
space_update |
Change name, description, forgetting_policy, llm_inference_enabled, show_service_names (owner) |
space_member_add |
Add a user or service with a role, or change an existing member's role (owner) |
space_member_remove |
Remove a member (owner; the last user owner can't be removed) |
space_member_list |
List members (reader+) |
space_archive / space_unarchive |
Make a space read-only and hidden from default recall, or undo it (owner) |
space_erase |
Permanently delete a space and everything in it (owner, confirm_slug, optional dry_run) |
promote_to_space |
Copy one of your personal memories into a space (writer+). Optional keep_original. |
Writer-specified edges & corrections
memory_store's related argument lets the writer set edge semantics instead ofguessing. Each entry is {"id": "<node-id>", "type": "<edge-type>"}, directednew_node --type--> target (so supersedes means the new node supersedes thetarget). Invalid types are rejected before the node is created — explicitwrites never half-succeed. related_to still exists but always createscaused_by; prefer related.
To correct a fact, use memory_amend(node_id, content, reason=...): it stores thenew version, links it SUPERSEDES → old, and keeps the old memory for audit.Recall automatically deprioritizes superseded hits and tags them withsuperseded_by.
When you memory_store something that lexically disagrees with an auto-linkcandidate (a changed number, a negation), the result may includepossible_conflicts — heuristic hints, not verified contradictions, and nevermaterialized as edges. Use them to decide whether to memory_amend.
References and tags
Every memory can say where it came from (refs) and carry labels (tags):
{"content": "Manual payments over $10K ship overnight via FedEx Priority.",
"tags": ["Manual Payments", "bank-a"],
"refs": [{"type": "document", "id": "doc_8812", "label": "Mail Matrix.pdf", "url": "https://docs.example.com/d/doc_8812"}]}
- Tags are normalized: trimmed, lowercased, runs of whitespace become
-("Manual Payments"->manual-payments), empty tags dropped, duplicatesremoved in order. Onlya-z 0-9 - _ . :are allowed ("q2/2026"is rejectedwithinvalid_tag). At most 20 tags of 50 characters each. - Refs are
{type, id, url?, label?}.typeis lowercase(document,meeting,crm.contact);urlmust behttp(s)://. Refs withthe same(type, id)are de-duplicated. At most 20 per memory. - Any invalid ref or tag fails the whole call and creates nothing(
invalid_ref,too_many_refs,invalid_tag,too_many_tags). - Filters:
memory_recall(tags_any=[...], ref_type=...)filters candidatesbefore ranking, sokresults still come back.memory_searchfilterstaketags_any,tags_all,ref_typeandref_id(withref_type, both must matchon the same ref). - Full recall hits, search hits and
memory_explainshowrefsandtags;concise hits showtagsandref_count.
Idempotent writes and batches
Pass your own stable id as external_id (e.g. doc_8812#kp1) and Papez keeps onecurrent memory per id per space (or per user, for personal memory):
on_existing |
Result when the id already exists |
|---|---|
amend (default) |
Identical content, refs and tags: status: "unchanged", nothing written. Different: a superseding version, status: "amended", superseded: "<old id>". |
skip |
status: "exists", nothing written |
error |
external_id_exists |
memory_store_batch(items, space=None, on_existing="amend") stores up toGENESYS_BATCH_MAX_ITEMS (100) items in one space. Every item is validatedfirst; if any is invalid, nothing is stored and batch_invalid lists eachproblem by index. An item's related may point at an earlier item with{"ref_index": i, "type": t}. When every item carries an external_id, a wholebatch is safe to resend after a timeout.
memory_retire_by_ref(ref_type, ref_id, space=None, mode="amend") handles adeleted source: memories citing only that source become [Retired] ... notestagged retired (or are deleted with mode="delete"); memories with othersources just lose the ref.
Concise recall
memory_recall(query, verbosity="concise") skips the causal-chain enrichment andreturns only id / summary / status / score / activation / is_core (plussuperseded_by when set) per hit — much cheaper on tokens for high-frequencylookups. verbosity="light" also skips the causal chains but keeps the rest of thefull hit (content, refs, category). verbosity="full" (the default) is unchanged.Reactivation writes still occur in every mode (they are governed by read_only,not verbosity).
category="note" and exclude_superseded=true narrow a recall, and k matchingresults still come back when enough of them exist among the min(k*4, 400)best candidates.
See docs/scoring.md for what activation / decay_scoreactually mean — in short, it is a retention weight that rises when a memory isrecalled, not a countdown to deletion.
Mind Spaces
A space is an isolated memory layer inside an org, for one engagement orproject (e.g. "Client A - Payments"). Personal memory works exactly as before;spaces sit beside it.
The isolation rule: a memory in a space can only be seen, linked to,compared with or changed by members of that space, and only within their role.Edges never cross spaces, auto-linking and conflict hints stay inside a space,and an unknown space looks exactly like one you aren't a member of(space_not_found).
| Action | reader | writer | owner |
|---|---|---|---|
recall, search, traverse, explain in the space; space_get, space_member_list |
yes | yes | yes |
store, batch, amend, retire, pin, unpin, delete; promote_to_space into it |
yes | yes | |
space_update, member add/remove, archive, unarchive, erase |
yes |
- Scope. Without
space, recall and search cover your personal memory plusevery non-archived space you belong to; each hit says whichspaceit camefrom.space="client-a-payments"(id or slug) orspaces=[...]limits a read;naming a space leaves personal memory out unless you passinclude_personal=true. Writes go to personal memory unless you name aspace. - Archive and erase.
space_archivemakes a space read-only and drops it fromdefault recall (members can still read it by naming it).space_erasedeletesthe space and every memory and edge in it; it needsconfirm_slugand supportsdry_run. Audit events survive the erase. - Policies.
forgetting_policy="retain_all"(default) means nothing in thespace is ever pruned;"standard"applies the normal forgetting rules inside thespace.llm_inference_enabled=falsestops every LLM call on the space's content.show_service_names=falsekeeps the writing credential's name off embed authorlines. - Attribution.
on_behalf_ofrecords the person a memory was written for({id, email, name, via}), whilecreated_bystays the credential that wroteit. Trusted host code sets it through the Python handler; it is deliberately notan MCP argument. Recall, search andmemory_explainshow both. - Audit. Every action touching a space emits an event (ids, counts andlengths only, never content) to the configured
AuditSink. - Locally, the stdio server is one user (
stdio_local_user) in one org(stdio_local_org); the space registry is saved next to the state file as<name>.spaces.json. - Service principals (API-key credentials for other systems, which never seepersonal memory and can only reach the spaces they're granted) are part of thehosted product; see papez.ai/developers.
Full reference (role matrix, visibility rule, background rules, error codes):docs/mind-spaces.md. End-to-end guide for feeding aspace from another system: docs/integration.md.
How it works
Every memory is scored by three forces multiplied together:
decay_score = relevance × connectivity × reactivation
- Relevance decays over time. Old memories fade unless reinforced.
- Connectivity rewards memories with many causal links. Hub memories survive.
- Reactivation boosts memories that keep getting recalled. Frequency matters.
Because the formula is multiplicative, a memory must score on all three axes to survive. A highly connected but never-accessed memory still decays. A frequently recalled but causally orphaned memory still fades.
decay_score (aliased activation on every hit) is a retention weight, not a deletion countdown — recalling a memory raises it, and a low score just means "resting," not "doomed." Deletion requires a low score and orphaned and unpinned and non-core and non-org and idle (not stored, recalled or reactivated for 30 days, GENESYS_FORGETTING_MIN_IDLE_DAYS), all at once. The stdio server runs the rescore-transition-prune pass every 10 minutes (GENESYS_MAINTENANCE_INTERVAL_S; 0 disables it). See docs/scoring.md for the full model and worked numbers.
STORE → ACTIVE → DORMANT → FADING → PRUNED
↑ │
└── reactivation ────┘
(only if score=0, orphan, not pinned)
Memories can also be promoted to core status — structurally important memories that are auto-pinned and never pruned.
Benchmark Results
See the certified LoCoMo results at the top of this README: 85.55 ± 0.37 over 10 runs under a frozen protocol (gpt-4o-mini answerer and judge, temperature 0, n=1,540, categories 1–4). Category 5 — adversarial questions with disputed ground truth — is excluded, matching the comparable published setups.
Every run is reproducible: the harness is at Astrix-Labs/locomo-harness, with full methodology and per-run results published. Reproduction scripts for the in-repo scenarios are in benchmarks/.
Storage backend
This package ships one storage backend: an in-memory causal graph (storage/memory.py) with optional JSON persistence via GENESYS_PERSIST_PATH. No database is required.
Additional backends — Postgres/pgvector, FalkorDB, MongoDB, and an Obsidian vault adapter — along with a REST API, OAuth, and multi-user auth, are part of the hosted product at api.papez.ai and are not included in this repo.
Want a different storage backend for the open-source library? Implement the provider protocols in storage/base.py and bring your own.
What your embedding provider sees
Every stored memory is embedded, including memories in spaces withllm_inference_enabled=false (that flag stops LLM calls, not embeddings, becauserecall needs the vectors). With the default GENESYS_EMBEDDER=openai, the textof every memory is sent to OpenAI. For sensitive spaces, useGENESYS_EMBEDDER=local (pip install 'papez[local]'), which embeds on yourmachine with sentence-transformers.
Configuration
Copy .env.example to .env and set:
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Unless GENESYS_EMBEDDER=local |
Embeddings |
ANTHROPIC_API_KEY |
No | Enables LLM-based causal inference (consolidation, contradiction detection). Off by default — without it, causal edges only come from edges the caller explicitly declares in memory_store plus cosine-similarity linking. |
GENESYS_EMBEDDER |
No | openai (default) or local (sentence-transformers, no API key). OpenAI sees memory text; see What your embedding provider sees. |
GENESYS_PERSIST_PATH |
No | JSON file path to persist state across restarts (in-memory otherwise). The space registry is saved beside it as <name>.spaces.json. |
GENESYS_USER_ID |
No | Providers.user_id for hosts that build providers with get_providers(). The stdio server ignores it: it always runs as stdio_local_user (papez.server.STDIO_LOCAL_USER) in org stdio_local_org. |
GENESYS_MAINTENANCE_INTERVAL_S |
No | Seconds between maintenance passes in the stdio server (default 600; 0 disables) |
GENESYS_FORGETTING_MIN_IDLE_DAYS |
No | Days a memory must go unused before it can be pruned (default 30) |
Mind Spaces, refs and batches
| Variable | Default | Description |
|---|---|---|
GENESYS_MAX_REFS_PER_MEMORY |
20 |
Max refs per memory, after de-duplication (too_many_refs) |
GENESYS_MAX_TAGS_PER_MEMORY |
20 |
Max tags per memory, after normalization (too_many_tags) |
GENESYS_MAX_TAG_LENGTH |
50 |
Max characters per tag (invalid_tag) |
GENESYS_BATCH_MAX_ITEMS |
100 |
Max items per memory_store_batch call |
GENESYS_AUDIT_MAX_EVENTS |
100000 |
Cap on events kept by the in-memory audit sink (oldest dropped first) |
GENESYS_AUDIT_READ_NODE_IDS_CAP |
50 |
Max node ids recorded on one read audit event (the result count is not capped) |
Auto-link tuning
Auto-linking connects a newly stored memory to semantically similar existingmemories. If it is too permissive you get a "hairball" — everything ends up ~2hops from everything, which destroys traversal scoping. Three knobs bound it:
| Variable | Default | Description |
|---|---|---|
GENESYS_AUTOLINK_MIN_SIMILARITY |
embedder-recommended | Cosine floor to create an auto-link. Explicit value wins over the embedder default. |
GENESYS_AUTOLINK_MAX_EDGES |
3 |
Max auto-links a single memory_store may create. Caps fan-out. |
GENESYS_AUTOLINK_MAX_NODE_DEGREE |
10 |
Max auto_link edges any single node may accumulate as a target. Fan-out alone still lets a hub gain one edge per store forever; this caps the hub itself. |
The floor is embedder-aware: an auto-link is permanent graph structure, so itsfloor sits above the transient recall floor. When GENESYS_AUTOLINK_MIN_SIMILARITYis unset, the effective floor is the embedder's recommendation — 0.6 for OpenAI(text-embedding-3-small, whose genuine matches cluster ~0.5+) and 0.45 forlocal sentence-transformers (whose genuine matches cluster ~0.2–0.4 but whosenoise pairs have been observed at ~0.44, so only near-duplicate contentauto-links locally). Any unknown embedder falls back to 0.45. Auto-linking alsode-dupes: if a pair is already connected by any edge (e.g. a user_explicitcaused_by), no parallel auto_link related_to is created.
The possible_conflicts hint on memory_store scans with its own, lowerfloor (GENESYS_CONFLICT_MIN_SIMILARITY, defaulting to the recall floor) overa wider window (GENESYS_CONFLICT_SCAN_K, default 8) — so tightening theauto-link floor never shrinks conflict detection.
Recall / relevance floors
The same embedder-aware pattern governs recall filtering:
| Variable | Default | Description |
|---|---|---|
GENESYS_RECALL_MIN_SIMILARITY |
embedder-recommended (OpenAI 0.5 / other 0.2) | Cosine floor below which pure vector hits are dropped from memory_recall. Keyword hits bypass it. |
GENESYS_CORE_INJECT_MIN_SIMILARITY |
embedder-recommended (OpenAI 0.45 / other 0.2) | Floor for injecting auto-promoted core memories into recall results. Pinned memories are always injected. |
Scoring knobs
The three-force scoring formula and its lifecycle thresholds are allenv-configurable (see engine/config.py anddocs/scoring.md): GENESYS_ACTR_DECAY,GENESYS_RELEVANCE_VECTOR_WEIGHT, GENESYS_RELEVANCE_KEYWORD_WEIGHT,GENESYS_MIN_CONNECTIVITY, GENESYS_FORGETTING_THRESHOLD, the GENESYS_DORMANCY_*transition thresholds, and the GENESYS_CORE_* promotion weights.
See .env.example for all options.
Built by
Papez is built by Rishi Meka at Astrix Labs. It came out of frustration with re-explaining project context to Claude every session. The goal is the intelligence layer between your LLM and your memory — fully open source.
Contributing
See CONTRIBUTING.md.
License
AGPL-3.0-or-later
Note: Papez releases prior to v0.3.6 were documented as Apache 2.0 in error. The LICENSE file has always contained the AGPLv3 text. From v0.3.6 onward, all documentation correctly references AGPL-3.0-or-later with a Contributor License Agreement.