Skip to content

Documentation

Give your assistant the memory it does not have.

PersistMemory speaks the Model Context Protocol over stdio. Point a host at it and nine tools appear: three for reading memory, one for capturing something new, three for settling what the system could not, and two for asking how the machinery itself is doing.

Running it

One process serves one person. Serving a second means a second process, because stdio has no authentication and the user id is the only thing keeping two people's memories apart.

Configure your host to launch the command below. The server writes nothing to stdout except protocol messages; a single stray log line there lands mid-message and the host disconnects, so all logging is redirected to stderr.

{
  "mcpServers": {
    "persistmemory": {
      "command": "yarn",
      "args": ["workspace", "@persistmemory/mcp", "start"],
      "env": {
        "PERSISTMEMORY_USER_ID": "usr_...",
        "EMBEDDING_BASE_URL": "https://api.openai.com/v1",
        "EMBEDDING_API_KEY": "sk-...",
        "REDIS_HOST": "127.0.0.1"
      }
    }
  }
}

The nine tools

Five of them render a view alongside their answer, so the person sees the memories the model was given rather than only the model's account of them. None of the write tools inserts a memory: extraction, entity resolution, deduplication and conflict detection run on ingestion, and a tool that wrote a row directly would skip all of it.

Reading

search_memory

Answers a question from past decisions, commitments, preferences, facts and events. Returns assembled context plus a view of every memory it drew on.

query string
The question, in the user's own words.
spaceIds string[]?
Restrict to these Spaces. Omit to search everything.
includeHistory boolean?
Include what was true and is not any more. Off by default, because superseded facts presented as current is the worst thing this can do.
memory_detail

Everything behind a single memory: what it claims, how sure the system is, which sources said it, and what it replaced.

memoryId string
The id, as search_memory reported it.
memory_timeline

The history of one memory: every version, what changed in it, and when it stopped being true.

memoryId string
The memory whose history to read.

Capturing

remember

Hands text to the ingestion pipeline. It returns immediately, so the memory is not searchable the instant the call finishes, and it may become one memory, several, or none.

text string
Up to 100,000 characters.
title string?
A short label for where this came from.
spaceIds string[]?
Spaces this belongs to, if any were named.

Deciding

review_conflicts

Memories that contradict each other and that nothing in the evidence could settle. Shows both sides with a control to decide each one.

limit number?
1 to 50. Default 10.
resolve_conflict

Records the user's decision: which memory stands, or that both hold, or that neither does. The decision is filed as theirs and outranks everything the system inferred.

conflictId string
As review_conflicts reported it.
keepMemoryId string?
The memory that stands. Must be one of the two in this conflict.
resolution "both_valid" | "neither"?
Give this or keepMemoryId, never both. They say different things.
reason string?
The user's own words for why.
confirm_memory

Marks a memory as confirmed by the user. A confirmed memory wins future conflicts, so it is only for a memory the user actually said was correct. Calling it twice changes nothing.

memoryId string
The memory the user confirmed.

Operations

usage_report

Token usage and spend broken down by model, including calls that failed, because a bill can move on a provider's error rate alone.

days number?
1 to 365. Default 30.
ingestion_status

Recent material and what became of it: read, extracted, failed, or still in flight. The answer when an expected memory is missing.

limit number?
1 to 50. Default 10.

Configuration

PERSISTMEMORY_USER_ID

required

Whose memory this process serves. Deliberately not a tool argument: every tool scopes its reads by it, and a parameter would let an untrusted string in a conversation choose whose memories come back.

EMBEDDING_API_KEY

for semantic search

With EMBEDDING_BASE_URL. Set EMBEDDING_PROVIDER to none to run without vectors; structured, temporal and graph retrieval still work, because my open tasks never needed one.

EMBEDDING_MODEL

must match

The same model the stored vectors were made with. Two models share no geometry, so a mismatch does not fail. It returns confident nonsense.

REDIS_HOST

for remember

Where ingestion work is queued, read by @persistmemory/worker. Without it remember refuses outright rather than reporting success for material nothing will process.

CONTEXT_TOKEN_BUDGET

optional

How much context a tool may return, in tokens. Defaults to 4000.

RERANK_MODEL

optional

Reranks results with a chat model. Off by default: it is a model call on the search path, so it costs latency and money on every query.

Reconnecting a client replaces its key, it does not add one.

Authorise the same account in the same client again and the previous credential is revoked in the same transaction that issues the new one. That is what people mean when they connect an account they have already connected.

Without it, connecting three times left three live keys that looked identical, and disconnecting inside the client told us nothing, so a credential handed to a client somebody had stopped using went on working forever. A partial unique index in the database enforces the same rule, so a code path that forgets cannot leave two live keys behind.

The practical consequence: if you connect the same account from a laptop and then from a desktop, the laptop stops working. Two machines that must both stay live need two accounts, or a key minted per machine.

Keys are minted only while signed in on the website. A key cannot mint another key, because that turns one leaked credential into permanent self-renewing access that survives revoking the original.

Building your own client instead?