Memories & Recall

How long-term memory works in CopilotKit Intelligence: what a memory is, the three kinds, user and project scope, how activation is entitled, and how to read and write memories from React, Angular, REST, or MCP.

Copy a prompt that guides your coding agent through CopilotKit setup.

Build agents that get smarter with every use.

Rich Threads keep messages, generative UI, and tool activity available across sessions and devices.

Learning turns real usage into skills that improve your agent.

Build a new agent or bring one you already have. Any frontend, any backend.

Threads remember a conversation. Memories remember a person. This page explains what a memory is, how recall selects them, and what has to be true of your deployment before the memory surfaces exist at all.

If you are looking for the persistence architecture beneath a single conversation, read Threads & Persistence Architecture instead.

What is a memory?#

A memory is a short, durable statement about a user or a project, stored outside any single thread. "Prefers concise status updates" is a memory. The forty messages that revealed the preference are a thread.

The distinction matters because the two have different lifetimes. A thread is finished when the conversation is. A memory is meant to outlive it, and to be recalled into a conversation that has not happened yet.

Memories are stored as text plus a vector embedding, so recall is semantic rather than a keyword match. Asking for "how does this user like updates" can surface "prefers concise status updates" without sharing a word with it.

Key concepts#

The three kinds#

Every memory is one of three kinds. The kind is supplied by whoever saves it and is never inferred.

KindWhat it holds
topicalA durable fact about the subject matter, independent of when it was learned.
episodicSomething that happened, anchored to an occasion.
operationalA preference or working instruction about how to behave.

The kind is not cosmetic. Deduplication and supersession are same-kind operations, so a topical fact and an operational preference with identical text are two memories, not one.

User and project scope#

A memory is scoped to a single user or shared across a project.

  • user is the platform default. Omit scope and you get it. This is the only scope delivered over realtime today.
  • project is shared, and must be requested explicitly.

Scope is enforced per request against the caller's grant, so a client holding read-only access to project scope cannot write to it.

Saving a near-duplicate absorbs it#

If you save content that closely matches a live memory in the same tenant, scope, and kind, the platform does not create a second row. It absorbs the new content into the existing memory, unions the source threads, and refreshes recency.

The save still succeeds, and the result tells you which happened, so a UI or an agent can say "absorbed into an existing memory" rather than implying something new was written.

Updating is a full replacement, not a patch#

Updating a memory supersedes it: the old memory is retired and a new one is created with a new id. The change set you supply is the complete definition of the new memory.

Omitting sourceThreadIds on an update does not preserve the previous value, it resets the new memory's source threads to empty. Re-send content, kind, and any sourceThreadIds you want to keep.

Removing is a retirement, not a delete#

Removing a memory retires it rather than erasing it. Retired memories are excluded from recall and from the default list, and can be surfaced again by asking for invalidated rows explicitly.

Activating memory#

Memory is not a feature flag. There is no MEMORY_ENABLED environment variable and no memory.enabled Helm value. Access is granted by entitlement, and the embedder is configured separately at startup.

Two independent things must be true before a caller can use memory:

  1. The deployment or organization is entitled to memory.
  2. app-api has valid embedder configuration.

Entitlement resolution fails closed. An unresolved entitlement, an inactive one, or a dependency outage all deny memory rather than allowing it.

Self-hosted#

The signed deployment license is the authority. Memory is available when that license carries the memory feature, which today ships in the enterprise plan. If your license does not include it, no amount of configuration will mount the surfaces, and the correct next step is to talk to us about the license rather than to keep editing values files.

The embedder is configured through these variables, which the chart templates for you:

VariablePurpose
MEMORY_EMBEDDINGS_URLBase URL of an OpenAI-compatible embeddings endpoint. Required.
MEMORY_EMBEDDING_MODELEmbedding model id sent to that endpoint. Required.
MEMORY_EMBEDDINGS_API_KEYOptional bearer token. Leave unset for the bundled in-cluster embedder.
MEMORY_EMBEDDINGS_DIMENSIONSOptional. Must be 1024, because storage is a 1024-dimension half-precision vector.

Two consequences worth knowing before you deploy:

  • app-api refuses to start if a required variable is missing or invalid. This is deliberate. Entitlements can change while the process is running, so it must never come up with memory routes and no embedder behind them.
  • pgvector must be installed on the database server. The migration that creates the memory table also creates the extension, and the migration job fails without it.

By default the chart deploys an in-cluster text-embeddings-inference workload and points app-api at it. To use a hosted provider instead, set an external embeddings URL and model, at which point the in-cluster workload is not rendered:

values.yaml
embeddings:
  external:
    url: https://api.openai.com
    model: text-embedding-3-small
    dimensions: 1024
    apiKey:
      existingSecret: openai-embeddings
      secretKey: api-key

Changing the embedding model or provider changes the vector space. Existing memories were embedded in the old space and are not comparable in the new one, so recall quality degrades until they are re-embedded. Treat a model change as a migration, not a config tweak.

Managed#

On the managed platform, memory is resolved per organization from that organization's effective entitlement, combining its plan features with any enterprise override. Nothing needs configuring on your side.

Reading and writing memories#

Angular#

injectMemories() exposes the same surface as signals. See Threads, memory, attachments, and headless UI for a worked component, and the Angular public API for the exported surface.

REST#

The memory routes are available to any backend or script holding a runtime credential. Authentication is the runtime tuple: a project API key as a bearer token, plus the app user's id in a header.

RoutePurpose
GET /api/memoriesList the caller's memories. Pass includeInvalidated=true to include retired ones.
POST /api/memoriesSave a memory.
POST /api/memories/recallSemantic recall against a query.
PATCH /api/memories/:idSupersede a memory with a full replacement.
DELETE /api/memories/:idRetire a memory.
POST /api/memories/subscribeMint a realtime subscription for memory metadata.

A save takes the content, the kind, an optional scope, and optional provenance:

curl -X POST https://your-deployment/api/memories \
  -H "Authorization: Bearer cpk-<project>_<short>_<long>" \
  -H "X-Cpki-User-Id: <app-user-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Prefers concise status updates.",
    "kind": "operational",
    "sourceThreadIds": ["<thread-id>"]
  }'

content is capped at 8192 characters and sourceThreadIds at 100 entries. Unknown fields are rejected rather than ignored, so a typo in a key is an error instead of a silent no-op.

Recall takes a query and an optional result limit, which defaults to 5 and is capped at 20:

curl -X POST https://your-deployment/api/memories/recall \
  -H "Authorization: Bearer cpk-<project>_<short>_<long>" \
  -H "X-Cpki-User-Id: <app-user-id>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "how does this user like updates", "limit": 5 }'

Every route returns memories in the same shape: id, kind, scope, content, and sourceThreadIds. Recall results additionally carry a score, and the list route carries invalidatedAt when retired rows are requested.

Agent tools over MCP#

Three tools let an agent manage its own memory: save_memory, recall_memory, and forget_memory.

They register on the MCP server only when that surface is mounted, which is a prerequisite for MCP generally rather than a memory setting. Registration is still checked per request against the caller organization's memory entitlement, so mounting MCP does not grant memory.

How recall works#

Recall is hybrid. A query is embedded and compared against stored vectors, and the result is fused with other signals into a single score, which is what the score field on a recall result reports. Retired and superseded memories are excluded.

This is why the embedding space matters so much. Comparability is a property of the space, not of the text, so memories written under one model cannot be meaningfully ranked against a query embedded under another.

Next steps#