Astrix-Labs

Papez

Community Astrix-Labs
Updated

Open-source causal graph memory for AI agents. 85.55 on LoCoMo under a frozen, reproducible protocol. MCP server with ACT-R scoring, spreading activation, and active forgetting.

PyPIPyPI DownloadsCILicense: AGPL v3

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. Theretryable flag encodes the guidance below: true for the read tools, andfor a memory_store_batch call in which every item carries an external_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_store with anexternal_id, and memory_store_batch when every item has one, areidempotent: a resend returns unchanged (or amended if the content reallychanged) with the same node ids, never a duplicate. SeeIdempotent writes and batches.
  • Do not blindly retry memory_store without an external_id, memory_amend,or a batch with any item lacking an external_id on an ambiguous timeout: asilent success followed by a retry creates a duplicate node. Prefer to continuethe turn and reconcile on the next memory_recall, or pass a stablesource_session so duplicates are easy to spot. (retryable stays false formemory_store even with an external_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. Only a-z 0-9 - _ . : are allowed ("q2/2026" is rejectedwith invalid_tag). At most 20 tags of 50 characters each.
  • Refs are {type, id, url?, label?}. type is lowercase(document, meeting, crm.contact); url must be http(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, so k results still come back. memory_search filters taketags_any, tags_all, ref_type and ref_id (with ref_type, both must matchon the same ref).
  • Full recall hits, search hits and memory_explain show refs and tags;concise hits show tags and ref_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 which space it camefrom. space="client-a-payments" (id or slug) or spaces=[...] limits a read;naming a space leaves personal memory out unless you passinclude_personal=true. Writes go to personal memory unless you name a space.
  • Archive and erase. space_archive makes a space read-only and drops it fromdefault recall (members can still read it by naming it). space_erase deletesthe space and every memory and edge in it; it needs confirm_slug and 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=false stops every LLM call on the space's content.show_service_names=false keeps the writing credential's name off embed authorlines.
  • Attribution. on_behalf_of records the person a memory was written for({id, email, name, via}), while created_by stays the credential that wroteit. Trusted host code sets it through the Python handler; it is deliberately notan MCP argument. Recall, search and memory_explain show 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.

MCP Server · Populars

MCP Server · New

    alessandro-afloarei

    ShadowGit MCP server

    A Model Context Protocol (MCP) server that provides AI assistants with secure, read-only access to your ShadowGit repositories. This enables powerful debugging and code analysis capabilities by giving AI access to your project's fine-grained git history.

    Astrix-Labs

    Papez

    Open-source causal graph memory for AI agents. 85.55 on LoCoMo under a frozen, reproducible protocol. MCP server with ACT-R scoring, spreading activation, and active forgetting.

    Community Astrix-Labs
    FROWNINGdev

    ORM Lens

    ER diagrams, N+1 detection, schema drift & migration-risk linting for Django and SQLAlchemy (Alembic) — VS Code, CLI and MCP server. Static analysis: no database, no app boot. Free & MIT. Formerly django-orm-lens.

    Community FROWNINGdev
    M-Pineapple

    Dev Project Coordinator

    Local desk for AI agents: track coding and non-coding projects as JSON, with Analytics and Kanban views. MCP server, Mac-first, Docker for Linux.

    Community M-Pineapple
    gimp-mcp

    GIMP MCP

    GIMP MCP server

    Community gimp-mcp