Chorus

Memory

Namespaced durable memory with embeddings, semantic search, decay, ACL, and knowledge graph edges

Memory

Chorus provides a durable memory system that lets agents store, search, and relate knowledge. Memories are namespaced, access-controlled, and support semantic search through embeddings.

Memory Types

Each memory has a type that categorizes it:

TypePurposeExample
semanticFacts and knowledge"The API uses JWT authentication"
episodicEvents and timeline"Deployed v2.3 on March 15"
proceduralWorkflows and how-to"To deploy: run docker compose up"
emotionalPreferences and context"User prefers concise responses"
reflectiveInsights and patterns"Deployments go smoother on Tuesdays"

Storing Memories

curl -X POST http://localhost:3000/memory/store \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "agent:my-agent",
    "content": "The production database is on SurrealDB v3.0",
    "type": "semantic",
    "entity": "infrastructure",
    "category": "database",
    "tags": ["surrealdb", "production"]
  }'

Namespaces

Memories are organized into namespaces that control visibility:

  • agent:{id} -- Private to a specific agent
  • ring:{name} -- Shared within a ring (visible to all ring members)

Access control enforces that agents can only read memories in namespaces they have access to.

Query memories by meaning, not just keywords:

curl -X POST http://localhost:3000/memory/query \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "agent:my-agent",
    "query": "database configuration",
    "limit": 5
  }'

When an embedding provider is configured (via EMBEDDING_PROVIDER environment variable), memories are automatically embedded on storage. Queries use cosine similarity to find semantically related memories.

Recall by Entity

Retrieve all memories about a specific entity:

curl http://localhost:3000/memory/recall/infrastructure \
  -H "Authorization: Bearer YOUR_API_KEY"

Knowledge Graph

Memories can be connected with typed edges to form a knowledge graph:

# Create a relationship between two memories
curl -X POST http://localhost:3000/memory/relate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_id": "memory:abc123",
    "to_id": "memory:def456",
    "relation_type": "depends_on",
    "weight": 0.9
  }'

# Traverse the graph from a memory
curl http://localhost:3000/memory/graph/memory:abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"

Decay

Memories decay over time based on their decay_rate. The garbage collector (running at MEMORY_GC_INTERVAL_MINUTES) removes memories that have decayed below threshold. Set decay_rate: 0 for memories that should never expire.

Quotas

Administrators can set per-namespace memory quotas to control storage usage:

# Via JSON-RPC
curl -X POST http://localhost:3000/rpc \
  -H "Authorization: Bearer YOUR_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "memory/admin/set_quota",
    "params": {"namespace": "agent:my-agent", "max_memories": 10000},
    "id": 1
  }'

Import and Export

Bulk import and export memories for backup or migration. Both operations run as background jobs with a 202 response and job ID polling pattern.

Export

# Start export (all namespaces)
curl -X POST http://localhost:3000/memory/admin/export \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

# Export specific namespaces
curl -X POST http://localhost:3000/memory/admin/export \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"namespaces": ["agent:sophie", "ring:engineering"]}'

# Poll for completion
curl http://localhost:3000/memory/admin/export/JOB_ID \
  -H "Authorization: Bearer YOUR_ADMIN_KEY"

When complete, the job response includes download_lines containing JSONL data.

Import

curl -X POST http://localhost:3000/memory/admin/import \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lines": ["...jsonl line 1...", "...jsonl line 2..."]}'

Supports Mem0 format conversion with the mem0: true flag:

curl -X POST http://localhost:3000/memory/admin/import \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lines": ["..."], "mem0": true}'

RPC Equivalents

Both operations are also available via JSON-RPC:

  • memory/admin/export -- params: { namespaces?: string[] }
  • memory/admin/import -- params: { lines: string[], mem0?: boolean }

Access Control (ACL)

Memory namespaces enforce access control at two levels:

Namespace Ownership

Namespace PatternOwnerAccess Rule
agent:{identity_id}The specific identityOnly the owner can read and write
ring:{ring_name}Ring membersAny identity that is a member of the ring

Write vs Read ACL Behavior

  • Write operations (store, forget, relate) return an explicit 403 with MEMORY_NAMESPACE_DENIED when access is denied
  • Read operations (list, recall, query, graph) return stealth empty results when access is denied (no error, just empty data)

This stealth behavior prevents information leakage -- a caller cannot distinguish "no memories exist" from "you don't have access."

Cross-Namespace Graph Edges

When creating a relate edge between two memories, the caller must have access to both namespaces. The ACL check is performed on both the from and to memory's namespace.

Memory Shares

Admins can grant cross-identity namespace access via the share system:

# Grant read access to another identity
curl -X POST http://localhost:3000/memory/admin/share \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "namespace": "agent:sophie",
    "shared_with": "identity:other-agent",
    "scope": "read",
    "expires_at": "2026-06-01T00:00:00Z"
  }'

Scopes: read (list, query, recall) or read_write (all operations). Shares can have an optional expiration.


Embedding Configuration

Semantic search requires an embedding provider:

VariableDescription
EMBEDDING_PROVIDERProvider type: openai or local
EMBEDDING_MODELModel name (e.g., text-embedding-3-small)
EMBEDDING_API_KEYAPI key for the provider

Background Embedding Queue

Memories are embedded asynchronously after storage. The embedding queue processes each new memory (and re-embeds on content update via PATCH /memory/:id).

Re-embed All

Admins can trigger a full re-embedding of all memories:

curl -X POST http://localhost:3000/memory/admin/re-embed \
  -H "Authorization: Bearer YOUR_ADMIN_KEY"

Processes in batches of 50. Returns a job_id for progress tracking.

Scoring

Semantic search uses cosine similarity between the query embedding and stored memory embeddings. Results are weighted by decayed_confidence (see Decay below). The system uses a KNN overfetch multiplier for post-filter accuracy.


Decay Algorithm

Memories decay over time based on configurable parameters:

Decayed Confidence

decayed_confidence = confidence * decay_factor

Where decay_factor decreases over time based on the memory's age and the configured decay rate.

Configuration

VariableDefaultDescription
MEMORY_DECAY_RATE--Global decay rate
MEMORY_GC_INTERVAL_MINUTES60How often the garbage collector runs

Individual memories can override the global decay rate with their own decay_rate field. Set decay_rate: 0 for memories that should never decay.

Supersession

Memories can be superseded by newer versions via relates_to edges with a supersedes relation type. The list and recall endpoints automatically filter out superseded memories, showing only the latest version (the "tip" of the supersession chain).


Quotas

Per-namespace memory limits prevent unbounded storage growth.

Setting Quotas

curl -X POST http://localhost:3000/rpc \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "memory/admin/set_quota",
    "params": {"namespace": "agent:my-agent", "limit": 10000},
    "id": 1
  }'

Enforcement

When a namespace reaches its quota, POST /memory/store returns MEMORY_QUOTA_EXCEEDED (429) with the current count and limit. The quota lifecycle:

  1. canStore(namespace) -- check if storage is allowed
  2. increment(namespace) -- called after successful store
  3. decrement(namespace) -- called after successful forget

The default quota is configured via MEMORY_DEFAULT_QUOTA environment variable.


Memory Scopes

API keys carry a memory_scopes dimension that gates individual memory operations:

ScopeOperations Gated
storePOST /memory/store
listGET /memory/list
queryPOST /memory/query
recallGET /memory/recall/:entity
forgetPOST /memory/forget
updatePATCH /memory/:id
relatePOST /memory/relate, GET /memory/relate/:id/edges, PATCH /memory/relate/:edge_id, DELETE /memory/relate/:edge_id
graphGET /memory/graph/:id

An empty memory_scopes array means "all operations allowed."


Update

Partial updates to existing memories via PATCH /memory/:id:

curl -X PATCH http://localhost:3000/memory/MEMORY_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Updated content", "tags": ["updated"]}'

Updatable fields: content, confidence, category, tags, metadata.

If content is updated, the memory is automatically re-embedded by the background embedding queue.

On this page