Technical overview

How MemHQ Works

From raw conversation turns to a cited answer — the full pipeline, with code at every step.

00
the pipeline

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.

mem.add()

Ingest & Extract

auto

Graph Write

mem.search()

Hybrid Retrieval

mem.ask()

Synthesis

01
ingest & extract

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
02
memory model

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

fact

Objective statements. "Works at Stripe."

preference

Likes, dislikes, tastes. "Prefers dark roast."

event

Time-anchored happenings. "Started job June 2026."

decision

Choices made. "Decided to move to SF."

goal

Stated intentions. "Wants to learn Rust."

relationship

Connections between entities.

belief

Opinions and perspectives.

habit

Recurring patterns. "Goes to the gym on Tuesdays."

status

Current 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.

03
hybrid retrieval

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.

BM25 keyword~5ms

Exact terms, rare words, proper nouns

Misses paraphrase and synonyms

Vector similarity~40ms

Semantic meaning, paraphrase, concepts

Misses rare entity names without context

Graph traversal~15ms

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.

04
synthesis

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.

05
connect

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

Three endpoints between you and persistent memory.