memory-v2

by haustorium12

Not rated
GitHub

About

Brain-inspired persistent memory MCP server with hybrid BM25+vector search, ACT-R activation scoring, FadeMem decay, and knowledge graphs — 17 tools, fully local via Ollama, zero API keys.

Details

Author
haustorium12
Categories
Database, Other, Knowledge Base, Search

Setup

Install memory-v2 in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/haustorium12/memory-v2

Follow the installation instructions in the repository README, then restart your MCP client.

memory-v2
Brain-Inspired Persistent Memory for AI Coding Assistants

Most AI memory systems are just databases with a search API. memory-v2 is acognitive architecture-- it models how human memory actually works.

# Install pip install memory-v2 # Start the MCP server memory-v2-server --db ~/memory.db # Or add to Claude Code settings.json
{ "mcpServers": { "memory": { "command": "memory-v2-server", "args": ["--db", "~/memory.db"] } } }

Your AI assistant now has persistent memory that survives compactions, decays gracefully, and builds a knowledge graph as you work.

memory-v2is a cognitively-grounded persistent memory system designed for AI coding assistants operating over long-lived, multi-session workflows. It replaces the first-generationclaude-memorysystem with a ground-up rewrite that fuses techniques from cognitive psychology (ACT-R activation theory, power-law forgetting), information retrieval (BM25/vector hybrid search with Reciprocal Rank Fusion), and graph-based knowledge representation (Leiden community detection, Personalized PageRank retrieval) into a single SQLite-backed store.

The system serves as a Model Context Protocol (MCP) server exposing 17 tools, allowing any MCP-compatible AI client -- Claude Code, Claude Desktop, or custom agents -- to store, search, decay, compress, and share memories without external API calls. All embedding and LLM inference runs locally through Ollama, requiring zero API keys and transmitting zero data off-machine.

memory-v2 was built by a practitioner who ran 59 compaction cycles on the v1 system and decided the architecture needed to be rethought from first principles. The result is a system where memories compete for survival through activation scores, where protected content is exempt from decay, where multiple agents coordinate through authority chains, and where a knowledge graph discovers connections the flat search index cannot.

- Why memory-v2?
-
Abstract
-
Architecture Overview
-
Theoretical Foundations

- ACT-R Activation Model
-
FadeMem Decay System
-
Hybrid Search with Reciprocal Rank Fusion
-
Knowledge Graph and Personalized PageRank

- Embedding Layer
-
Vault Indexer
-
Auto-Extraction Pipeline
-
CogCanvas Compaction Pipeline
-
Multi-Agent Coordination
-
Security Module

- ACT-R Activation Walkthrough
-
FadeMem Decay Walkthrough
-
Hybrid Search Walkthrough

- As an MCP Server
-
As a CLI Tool
-
Building the Knowledge Graph

+-----------------------------+ | MCP Client Layer | | (Claude Code / Desktop / | | any MCP-compatible agent) | +-------------+---------------+ | FastMCP Protocol (stdio) | +-------------v---------------+ | server.py (17 tools) | | add | search | graph_search | | extract | compact | decay | | agent_sync | check_integrity | +---+-----+-----+-----+------+ | | | | +---------------+ | | +---------------+ | | | | +----------v---------+ +------v-----v------+ +----------v---------+ | db.py | | scoring.py | | knowledge_graph.py | | SQLite + sqlite-vec | | ACT-R activation | | NetworkX DiGraph | | + FTS5 | | FadeMem decay | | Leiden communities | | | | Cosine similarity | | PPR retrieval | | memories (rows) | | | | | | memory_fts (BM25) | | base_level_act() | | extract_entities() | | memory_vec (768-d) | | spreading_act() | | detect_communities | | graveyard | | importance_score()| | ppr_search() | | agent_offsets | | decay_value() | | visualize_graph() | | conflicts | | retrieval_prob() | | | | compaction_receipts | | | | | +----------+----------+ +-------------------+ +----------+---------+ | | | +-------------------+ | +--------------+ embeddings.py +---------------+ | Ollama | | nomic-embed-text | | 768-dim vectors | | Local file cache | +-------------------+ | +-----------------------+-----------------------+ | | | +----------v---------+ +---------v----------+ +---------v----------+ | extraction.py | | compaction.py | | multi_agent.py | | 2-pass LLM pipeline| | CogCanvas 6-step | | Authority chain | | Fact extraction | | Protected extract | | Consumer offsets | | Novelty checking | | Selective deletion | | Conflict detection | | Action decision | | Summary + verify | | Kafka-style sync | +--------------------+ +--------------------+ +--------------------+ | +----------v---------+ +--------------------+ | vault_indexer.py | | security.py | | Markdown chunking | | 13 credential pats | | SHA-256 delta detect| | 6 injection pats | | Frontmatter parsing | | SHA-256 manifests | | Incremental indexing| | Allowlist filtering | +--------------------+ +--------------------+

The architecture follows a layered design where the MCP server (server.py) acts as the sole entry point for AI clients. All 17 tools delegate to specialized subsystems. The database layer (db.py) owns the single SQLite file, which contains three co-located indexes: relational rows inmemories, BM25 full-text search inmemory_fts(FTS5), and 768-dimensional vector embeddings inmemory_vec(sqlite-vec). The scoring layer applies cognitive activation formulas on top of search results. The knowledge graph lives in a separate NetworkX pickle file and provides multi-hop discovery that the flat index cannot.

Every embedding and LLM call routes through Ollama, which runs locally. The system never phones home.

The Adaptive Control of Thought -- Rational (ACT-R) framework, developed by John Anderson and colleagues at Carnegie Mellon, provides the core theory for how memories compete for retrieval. The central claim is that human memory retrieval is a rational adaptation to the statistical structure of the environment: items that have been used recently and frequently are more likely to be needed again (Anderson & Schooler, 1991).

memory-v2 implements the ACT-R activation equation as a scoring overlay on search results. Each memory has anactivationvalue that determines its probability of being retrieved. Activation has three components: base-level activation (how often and how recently the memory was accessed), spreading activation (contextual priming from the current query), and noise (stochastic variation that prevents deterministic behavior).

The base-level activation approximates the full rational analysis using the optimized closed-form:

- n= total access count for the memory
- d= decay parameter (fixed at0.5, the canonical ACT-R value)
- L= lifetime of the memory in hours (time since creation)

This approximation avoids storing the full access history while preserving the key property: activation rises with frequency and falls with time, following a power law.

Context tags from the current query prime associated memories:

S_i = SUM_j [ W_j  (S_max - ln(fan_j)) ]

- W_j = 1 / |context_tags|(attention weight, divided equally among context sources)
- S_max = 1.6(maximum associative strength)
- fan_j= number of memories sharing tagj(the "fan" of the source)

The key insight: tags that appear on many memories provide less activation (they are less discriminating), while rare tags provide more. This is the ACT-R equivalent of IDF weighting.

Activation includes a stochastic noise term drawn from the logistic distribution:

epsilon = s  ln(u / (1 - u)) where u ~ Uniform(0, 1)

This noise prevents the system from becoming deterministic and allows occasionally surprising retrievals -- a property that mirrors human memory.

Full Activation and Retrieval Probability

The probability of successful retrieval given activation:

P(retrieve) = 1 / (1 + exp(-(A_i - tau) / s))

- tau = -0.5(retrieval threshold)
- s = 0.25(noise scale, same as the noise parameter)

This is a sigmoid function centered at the threshold. Memories with activation well above the threshold are almost certainly retrieved; memories well below are almost certainly forgotten.

Protected floor: Memories marked as protected receive an activation floor oftau + 1.0 = 0.5, ensuring they are always retrievable regardless of age or access pattern.

After hybrid search produces an initial ranked list, ACT-R activation is used to rerank:

final_score = 0.6  hybrid_rrf_score + 0.4  (activation / 10.0)

The activation is divided by 10 to normalize it into the same range as the RRF score (typically 0.0 to 0.03). The 60/40 split weights lexical+semantic relevance above cognitive activation, while still allowing frequently-accessed, contextually-primed memories to rise.

While ACT-R handles retrieval competition at query time, FadeMem handles the background lifecycle of memories. It implements a dual-layer memory architecture (Short-Term Memory and Long-Term Memory) with power-law decay, importance-based promotion/demotion, and archival.

Each memory's importance is a weighted combination of three signals:

I(t) = 0.4  relevance + 0.3  frequency + 0.3  recency

- relevance= contextual relevance score (0.5 during background sweeps when no query context is available)
- frequency = log(access_count + 1) / log(max_access_count + 1)(log-normalized access frequency)
- recency = exp(-decay_rate
hours_since_access)(exponential recency decay)

The weights (0.4, 0.3, 0.3) reflect a design choice: what a memory is about matters slightly more than how often or how recently it was accessed.

- v(0)= initial memory strength
- lambda= per-memory decay rate (default0.1)
- t= time since last access in hours
- beta= layer-dependent exponent:

- beta_LTM = 0.8(sub-linear -- LTM memories decayslowerthan exponential)
- beta_STM = 1.2(super-linear -- STM memories decayfasterthan exponential)

The beta parameter is the key innovation over standard exponential decay. Atbeta < 1, the decay curve bends upward relative to exponential, meaning old memories decay more slowly the older they get -- they are "hardened" by time. Atbeta > 1, the curve bends downward, meaning new memories that fail to consolidate decay acceleratingly.

Memories move between layers based on importance thresholds:

Promotion: STM --> LTM when importance >= 0.7 Demotion: LTM --> STM when importance <= 0.3 Archive: STM --> grave when importance < 0.1 AND age > 30 days

The gap between 0.3 and 0.7 is ahysteresis zone: memories in this range stay in their current layer. This prevents oscillation at the boundary.

Memories tagged with any of the following are immune to decay and archival:

correction, decision, identity, emotional_anchor, commitment, exact_value, chain_of_command, person

These represent categories of information where loss would be harmful regardless of access frequency.

Hybrid Search with Reciprocal Rank Fusion

memory-v2 runs two independent search algorithms and merges them:
- BM25 keyword searchvia SQLite FTS5 -- excels at exact term matching, file names, error codes
- Vector similarity searchvia sqlite-vec (768-dim, cosine distance) -- excels at semantic similarity

The merge usesReciprocal Rank Fusion(Cormack et al., 2009), which has been shown to outperform individual ranking methods and Condorcet fusion:

score(d) = SUM_i [ 1 / (k + rank_i(d)) ]

- k = 60(the RRF constant; higher values reduce the influence of top-ranked results)
- rank_i(d)= position of documentdin thei-th ranked list (0-indexed)
- The sum runs over both BM25 and vector result lists

Documents appearing in both lists receive scores from both. Documents appearing in only one list receive a score from that list alone (the other term is 0). The result is a fused ranking that captures both lexical precision and semantic breadth.

Each search retrieveslimit * 3candidates before fusion to ensure adequate recall. The fused results are then passed to the ACT-R scoring layer for final reranking.

Knowledge Graph and Personalized PageRank

The knowledge graph is a NetworkX directed graph (DiGraph) that represents entities and relationships extracted from vault documents. It provides a complementary retrieval path: while flat search finds documents containing similar words or vectors, graph traversal discoversstructurally relatedconcepts even when they share no lexical or embedding similarity.

person, project, concept, decision, tool, event, emotion, conversation, chunk, community
discussed_in, decided, built, uses, part_of, related_to, preceded_by, caused, felt, evolved_from, member_of

Entities are normalized to lowercase. Edges carry weight (incremented on repeated observation), temporal metadata (valid_from,valid_until), confidence scores, and source file provenance.

Entity and relationship extraction uses a local LLM (default:qwen2.5:3bvia Ollama) with a structured prompt that produces JSON output. The model is instructed to normalize names, skip trivial relationships, and use canonical forms. The first 4000 characters of each document are processed (respecting the small model's context window).

The graph is partitioned using the Leiden algorithm (Traag et al., 2019), which guarantees well-connected communities -- an improvement over the earlier Louvain method that could produce arbitrarily badly connected communities. Implementation usespython-igraphandleidenalg(optional dependencies).

Community assignments are stored as node attributes and surfaced through thelist_topicsMCP tool, providing an automatic clustering of the knowledge base without manual taxonomy.

HippoRAG-Style Personalized PageRank Retrieval

Thegraph_searchtool implements Personalized PageRank (PPR) retrieval inspired by HippoRAG (2024):
- Seed identification: Extract entities from the query; match them to graph nodes by word overlap. If no direct match, fall back to embedding similarity against node names.
- Personalization vector: Construct a uniform distribution over seed nodes (all others get weight 0).
- PPR computation:nx.pagerank(G_undirected, alpha=0.85, personalization=p)
- Result extraction: Return top-K nodes by PPR score, including community membership, mention count, and source provenance.

The teleport probabilityalpha = 0.85means 85% of the random walk follows edges and 15% teleports back to seed nodes. This strikes the standard balance between exploration and relevance.

The key advantage over flat search: PPR discovers nodes reachable by multi-hop traversal from the query entities, even if those nodes share no embedding or lexical similarity with the query. This enables "what else is connected to this?" reasoning.

All persistent state (except the knowledge graph pickle) lives in a single SQLite database file.

erDiagram memories { INTEGER id PK TEXT content TEXT content_type TEXT source_file INTEGER source_line TEXT author INTEGER authority_level REAL confidence INTEGER protected TEXT tags TEXT created_at TEXT updated_at TEXT last_accessed_at INTEGER access_count REAL activation_score REAL importance_score REAL decay_rate INTEGER archived INTEGER supersedes FK TEXT content_hash } memory_fts { TEXT content TEXT tags TEXT source_file } memory_vec { INTEGER id PK BLOB embedding } graveyard { INTEGER id PK INTEGER memory_id FK TEXT content TEXT metadata TEXT archived_at TEXT reason REAL last_activation_score } agent_offsets { TEXT agent_id PK INTEGER last_read_line TEXT last_read_time } file_hashes { TEXT file_path PK TEXT content_hash TEXT indexed_at INTEGER chunk_count } conflicts { INTEGER id PK INTEGER memory_id_a FK INTEGER memory_id_b FK TEXT agent_a TEXT agent_b TEXT description INTEGER resolved TEXT resolved_by TEXT created_at } compaction_receipts { INTEGER id PK TEXT timestamp INTEGER original_tokens INTEGER compressed_tokens REAL ratio INTEGER protected_items_extracted REAL verification_score TEXT vault_files_updated TEXT receipt_data } memories ||--o{ graveyard : "archived to" memories ||--|| memory_fts : "FTS5 index" memories ||--|| memory_vec : "vector index" memories ||--o{ conflicts : "involved in" memories ||--o| memories : "supersedes"
PRAGMA journal_mode = WAL; -- Write-Ahead Logging for concurrent reads PRAGMA foreign_keys = ON; -- Enforce referential integrity

Thesqlite-vecextension is loaded at connection time viasqlite_vec.load(conn). The database file is protected by a 10-second timeout for lock contention.

memory-v2 exposes17 toolsthrough the Model Context Protocol viaFastMCP. The server runs over stdio (standard MCP transport) and can be registered with any MCP-compatible client.
- Registers all 17 tools with FastMCP
- Spawns a background thread to pre-warm the Ollama embedding model (avoids ~55-second cold-start penalty on first query)
- Lazily initializes the SQLite connection on first tool call
- Runs over stdio withmcp.run(show_banner=False)

The embedding layer is intentionally thin: four functions (embed_text,embed_batch,embed_with_cache,get_client). Embedding model selection is configurable viaMEMORY_V2_EMBED_MODELfor users who want to swap in a different Ollama model.

Theget_embedder()function in__init__.pyprovides lazy initialization with a throwaway warmup call to avoid cold-start latency on the first real query.

The vault indexer converts a directory of markdown files into searchable memory chunks.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.