How MemHQ Works
From raw conversation turns to a cited answer — the full pipeline, with code at every step.
Three calls. Four stages under the hood.
Every call to mem.add(), mem.search(), or mem.ask() maps to a distinct pipeline stage. Here’s what actually runs.
Ingest & Extract
Graph Write
Hybrid Retrieval
Synthesis
mem.add() — turning conversation into structured memory
When you call add(), MemHQ returns an episode_id in under 100ms. The actual extraction runs asynchronously and takes 2–5 seconds depending on content length.
request
# Python
client.add(
user_id="user_42",
messages=[
{"role": "user", "content": "I just started a new job at Stripe in San Francisco."},
{"role": "assistant", "content": "Congratulations! That's exciting."},
],
)
# Returns immediately:
# { "episode_id": "ep_a1b2c3", "status": "ingesting" }1. Chunk & resolve
The conversation is split into atomic units. Pronoun co-reference is resolved so "she" becomes the named entity.
2. LLM extraction
An extraction model reads each chunk and emits typed entities (Person, Place, Preference, Event…) and the relations between them.
3. Temporal grounding
Relative dates (“last March”, “yesterday”) are resolved against the conversation timestamp and stored as absolute ISO-8601 ranges.
What gets extracted from the example above
Entities Person → "user_42" Company → "Stripe" (employer) Place → "San Francisco" (location) Memory (type: fact) content : "Works at Stripe in San Francisco" valid_from: 2026-06-19T00:00:00Z valid_until: null ← open-ended: still true until contradicted Relation user_42 —[works_at]→ Stripe Stripe —[located_in]→ San Francisco
A knowledge graph — not a vector dump
Each user gets an isolated property graph. Memories are typed, linked to entities, and carry bi-temporal validity so your agent can answer "what was true in March?" correctly.
9 memory types
factObjective statements. "Works at Stripe."
preferenceLikes, dislikes, tastes. "Prefers dark roast."
eventTime-anchored happenings. "Started job June 2026."
decisionChoices made. "Decided to move to SF."
goalStated intentions. "Wants to learn Rust."
relationshipConnections between entities.
beliefOpinions and perspectives.
habitRecurring patterns. "Goes to the gym on Tuesdays."
statusCurrent state. "Currently in onboarding."
Bi-temporal validity — how facts update without losing history
When a user says "I moved to New York", MemHQ doesn't overwrite "lives in San Francisco" — it sets valid_until on the old fact and creates a new one. Both records survive. Your agent answers "where does she live now?" and "where did she live in June?" correctly.
# Old memory — NOT deleted, validity closed
{ content: "Lives in San Francisco", valid_from: "2026-01-01", valid_until: "2026-08-15" }
# New memory — open-ended
{ content: "Lives in New York", valid_from: "2026-08-15", valid_until: null }Automatic conflict resolution
When two contradictory facts exist in the same validity window, MemHQ runs a reconciliation pass. The losing fact is superseded (not deleted), and a conflict record is written to the tamper-evident audit log — so you always know when a contradiction was detected and how it was resolved.
mem.search() — three lanes fused into one ranked list
A single search() call runs three retrieval paths in parallel and merges them with Reciprocal Rank Fusion (RRF). No single path catches everything — that’s the point.
✓ Exact terms, rare words, proper nouns
✗ Misses paraphrase and synonyms
✓ Semantic meaning, paraphrase, concepts
✗ Misses rare entity names without context
✓ Multi-hop relations, entity co-occurrence
✗ Limited to extracted graph structure
RRF fusion
Each lane produces a ranked list. RRF scores each memory as Σ 1/(k + rank_i) across all lanes that returned it. Memories appearing in multiple lanes score much higher than lane-specific results — surfacing the facts that are both semantically relevant AND textually matched AND graph-connected.
# Example: "where does she live?" retrieves "Lives in San Francisco" # BM25 : rank 4 → score 1/(60+4) = 0.0156 # Vector: rank 1 → score 1/(60+1) = 0.0164 # Graph : rank 2 → score 1/(60+2) = 0.0161 # Combined RRF score: 0.0156 + 0.0164 + 0.0161 = 0.0481 # Memory that only appeared in vector: 0.0155 — ranked lower
After RRF: reranking
The top-N RRF results are passed through a cross-encoder reranker that scores each memory against the original query. This catches cases where RRF ranks a tangentially related memory above a directly relevant one. The final list returns in 80–150ms end-to-end.
mem.ask() — retrieval + grounding + answer in one call
ask() is a convenience wrapper that runs the full retrieve → ground → synthesize pipeline server-side and returns a cited answer. You skip the LLM round-trip in your own code.
1. Retrieve
Runs the same hybrid search as mem.search(). Top-k memories are returned and ranked.
2. Ground
Memories are packed into a grounding prompt: "Answer ONLY using these memories. Cite each fact."
3. Synthesize
gpt-4o-mini generates an answer. Each claim is tagged with the memory ID it came from.
response shape
{
"answer": "You work at Stripe in San Francisco. You started in June 2026.",
"citations": [
{
"memory_id": "m_a1b2",
"content": "Works at Stripe in San Francisco",
"valid_from": "2026-06-19T00:00:00Z",
"score": 0.94
},
{
"memory_id": "m_c3d4",
"content": "Started new job in June 2026",
"valid_from": "2026-06-19T00:00:00Z",
"score": 0.87
}
],
"question_mode": "lookup" // or: aggregate | temporal
}question_mode — automatic intent classification
Before retrieval, MemHQ classifies the question into one of three modes. lookup ("who does she work for?") fetches the most relevant fact. aggregate ("how many jobs has she had?") retrieves all matching facts and counts. temporal ("what was she doing last March?") filters by valid-from/until window. The synthesis prompt changes for each mode.
Works with your existing agent stack
MemHQ never sits in the inference path. It brackets your LLM call — search before to recall, add after to remember.
Claude Code / Codex / Cursor / Windsurf
Add the MCP server to your agent config. MemHQ exposes add, search, and ask as MCP tools the agent calls automatically.
# ~/.cursor/mcp.json
{
"mcpServers": {
"memhq": {
"command": "npx",
"args": ["-y", "@memhq/mcp-server"],
"env": { "MEMHQ_API_KEY": "sk_live_…" }
}
}
}Claude Desktop / Claude Plugin
Same MCP server, same config block. Claude Desktop reads claude_desktop_config.json. The agent gets persistent memory across conversations.
# ~/Library/Application Support/Claude/
# claude_desktop_config.json
{
"mcpServers": {
"memhq": {
"command": "npx",
"args": ["-y", "@memhq/mcp-server"],
"env": { "MEMHQ_API_KEY": "sk_live_…" }
}
}
}TypeScript SDK
npm install @memhq/sdk. The client is typed end-to-end — request shapes, response shapes, error codes.
import { MemoryClient } from "@memhq/sdk";
const mem = new MemoryClient({ apiKey: process.env.MEMHQ_API_KEY });
// add, search, ask — all return typed responses
const { answer, citations } = await mem.ask({
userId: "u_42",
question: "What does this user care about?",
});Python SDK
pip install memhq. Auto-reads MEMHQ_API_KEY from the environment. Works sync or async (AsyncMemoryClient).
from memhq import MemoryClient, AsyncMemoryClient # Sync mem = MemoryClient() result = mem.ask(user_id="u_42", question="What does this user care about?") # Async async_mem = AsyncMemoryClient() result = await async_mem.ask(user_id="u_42", question="...")
ready to build