Skip to content

Latest commit

 

History

History
735 lines (549 loc) · 22.5 KB

File metadata and controls

735 lines (549 loc) · 22.5 KB

Multi-Memory System

Fortemi supports parallel memory archives, allowing you to maintain multiple isolated knowledge bases within a single deployment. Each memory operates as a separate PostgreSQL schema with its own notes, tags, collections, embeddings, links, and templates.

Overview

What are Memories?

A memory (formerly called "archive") is an isolated namespace for your knowledge base. Think of memories as separate workspaces or projects, each with complete data isolation:

  • Work Memory: Professional projects and documentation
  • Personal Memory: Private notes and journal entries
  • Research Memory: Academic papers and literature reviews
  • Client Memories: Separate workspace per client for data isolation

Key Features

  1. Complete Isolation: Each memory has its own PostgreSQL schema. Notes, tags, collections, embeddings, and links never cross memory boundaries
  2. Per-Request Routing: Use the X-Fortemi-Memory HTTP header to select which memory to operate on
  3. Federated Search: Search across multiple memories simultaneously with unified result ranking
  4. Memory Cloning: Deep copy entire memories including all notes, embeddings, and relationships
  5. Auto-Migration: Memories are automatically updated when new table structures are added
  6. Capacity Management: System-wide limits and per-memory statistics via overview endpoint

Architecture

Schema Isolation

Each memory operates in its own PostgreSQL schema:

Database: matric
├── public (default memory + shared tables)
│   ├── archive_registry (shared)
│   ├── oauth_clients (shared)
│   ├── api_keys (shared)
│   ├── note (default memory data)
│   ├── embedding (default memory data)
│   ├── note_links (default memory data)
│   ├── skos_concepts (default memory data)
│   └── ... (41 per-memory tables + 14 shared tables)
├── archive_work_2026 (custom memory)
│   ├── note
│   ├── note_original
│   ├── embedding
│   └── ... (41 per-memory tables)
└── archive_research (custom memory)
    ├── note
    ├── embedding
    └── ... (41 per-memory tables)

Note: The default memory uses the public schema. The seed migration (20260208000002_seed_default_archive.sql) creates a registry entry named "default" with schema_name = 'public'.

Shared vs Per-Memory Tables

Shared Tables (14 total):

  • Authentication: OAuth clients, API keys, sessions
  • System: Job queue, event subscriptions, webhooks
  • Registry: Archive metadata, embedding configurations

These tables live in the public schema and are shared across all memories.

Per-Memory Tables (41 total):

  • Notes: note, note_original, note_revision
  • Embeddings: embedding, embedding_set, embedding_set_member
  • Links: note_links
  • Tags: tag, tag_note, skos_concepts, skos_labels, skos_relations
  • Collections: collection, collection_note
  • Templates: template
  • Attachments: file_attachment, file_provenance
  • Document Types: Custom types per memory
  • Versioning: Content history tables

Deny-List Approach

The system uses a deny-list approach: all tables are per-memory except the 14 explicitly shared tables defined in SHARED_TABLES constant. This ensures zero drift when new tables are added - they automatically become per-memory unless explicitly added to the shared list.

Creating and Managing Memories

Creating a Memory

Via API:

curl -X POST http://localhost:3000/api/v1/memories \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "work-2026",
    "description": "Work-related notes for 2026"
  }'

Via MCP:

create_memory({
  name: "work-2026",
  description: "Work-related notes for 2026"
})

Memory names must be valid PostgreSQL schema identifiers (lowercase letters, numbers, underscores, hyphens).

Listing Memories

Via API:

curl http://localhost:3000/api/v1/memories \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "memories": [
    {
      "name": "default",
      "description": "Default memory",
      "created_at": "2026-01-15T10:00:00Z",
      "note_count": 1523,
      "size_bytes": 52428800,
      "schema_version": 41
    },
    {
      "name": "work-2026",
      "description": "Work-related notes for 2026",
      "created_at": "2026-02-01T12:00:00Z",
      "note_count": 245,
      "size_bytes": 8388608,
      "schema_version": 41
    }
  ]
}

Getting Memory Details

curl http://localhost:3000/api/v1/memories/work-2026 \
  -H "Authorization: Bearer $TOKEN"

Updating Memory Metadata

curl -X PATCH http://localhost:3000/api/v1/memories/work-2026 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "description": "Updated description"
  }'

Deleting a Memory

curl -X DELETE http://localhost:3000/api/v1/memories/work-2026 \
  -H "Authorization: Bearer $TOKEN"

Deletes the memory's schema and all data within it. This operation is irreversible.

Using Memories

Per-Request Memory Selection

Select which memory to operate on using the X-Fortemi-Memory HTTP header:

# Create note in work memory
curl -X POST http://localhost:3000/api/v1/notes \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Fortemi-Memory: work-2026" \
  -d '{
    "content": "# Project Documentation\n\nInternal documentation for project X."
  }'

# Search in work memory
curl "http://localhost:3000/api/v1/search?q=project+documentation" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Fortemi-Memory: work-2026"

# List notes from work memory
curl http://localhost:3000/api/v1/notes \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Fortemi-Memory: work-2026"

If no header is provided, the request operates on the default memory configured via set_default_archive API. If no default is configured, requests fall back to the public schema.

Default Archive Caching:

  • The default archive is cached for 60 seconds (configurable via DEFAULT_ARCHIVE_CACHE_TTL environment variable) to minimize database queries
  • Setting a new default via the API invalidates the cache immediately
  • This provides a balance between performance and responsiveness to configuration changes

MCP Memory Management

The MCP server provides memory management tools with session-based memory context:

select_memory

Switch the active memory for the current MCP session:

select_memory({ name: "work-2026" })
// All subsequent operations use work-2026 memory

get_active_memory

Check which memory is currently active:

get_active_memory()
// Returns: { name: "work-2026" }

list_memories

List all available memories:

list_memories()

create_memory

Create a new memory:

create_memory({
  name: "research",
  description: "Academic research notes"
})

delete_memory

Delete a memory and all its data:

delete_memory({ name: "old-project" })

Federated Search

Search across multiple memories simultaneously with unified result ranking.

Search in non-default memories uses per-schema connection pools with search_path pinned to the target archive. Standard search (GET /api/v1/search) works in any memory selected via the X-Fortemi-Memory header. For cross-archive queries, use federated search (POST /api/v1/search/federated) to search multiple memories simultaneously with unified ranking.

Search All Memories

curl -X POST http://localhost:3000/api/v1/search/federated \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "query": "machine learning",
    "memories": ["all"]
  }'

Search Specific Memories

curl -X POST http://localhost:3000/api/v1/search/federated \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "query": "project documentation",
    "memories": ["work-2026", "research"]
  }'

Response Format

{
  "results": [
    {
      "note_id": "550e8400-...",
      "memory": "work-2026",
      "score": 0.92,
      "title": "Project Documentation",
      "snippet": "...machine learning algorithms...",
      "tags": ["project", "ml"]
    },
    {
      "note_id": "660e8400-...",
      "memory": "research",
      "score": 0.85,
      "title": "ML Research Papers",
      "snippet": "...deep learning techniques...",
      "tags": ["research", "ml"]
    }
  ],
  "total": 2,
  "memories_searched": ["work-2026", "research"]
}

How Federated Search Works

  1. Parallel Execution: Search runs concurrently across all specified memories
  2. Score Normalization: Scores are normalized to [0,1] range per memory
  3. Unified Ranking: Results are merged and re-sorted by score
  4. Memory Attribution: Each result includes its source memory name

MCP Tool:

search_memories_federated({
  query: "machine learning",
  memories: ["all"]
})

Memory Cloning

Deep copy entire memories including all notes, embeddings, links, and relationships.

Clone a Memory

curl -X POST http://localhost:3000/api/v1/archives/work-2026/clone \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "new_name": "work-2026-backup",
    "description": "Backup of work memory before major refactoring"
  }'

Clone Process

  1. Schema Creation: Creates new PostgreSQL schema with new_name
  2. Table Structure Copy: Creates empty tables using CREATE TABLE ... (LIKE ... INCLUDING ALL)
  3. FK Dependency Resolution: Orders tables by foreign key dependencies using recursive CTE
  4. Data Copy: Uses INSERT INTO new.table SELECT columns FROM old.table (filtering out generated columns)
  5. Relationship Preservation: UUIDs remain identical, preserving all links and embeddings
  6. FK and Trigger Recreation: Re-creates foreign keys and triggers separately
  7. Auto-Migration: New memory is automatically at current schema version

Note: The implementation does NOT use session_replication_role = 'replica'. Instead, it copies data in FK dependency order, ensuring referential integrity without requiring superuser privileges.

Use Cases

  • Backup before major changes: Clone before bulk deletions or schema migrations
  • Testing environments: Clone production memory for testing without affecting live data
  • Client project templates: Clone a template memory structure for new clients
  • Archival: Create point-in-time snapshots of memories

MCP Tool:

clone_memory({
  source_name: "work-2026",
  new_name: "work-2026-backup",
  description: "Backup before migration"
})

Capacity Planning and Monitoring

Memory Overview

Get aggregate statistics across all memories:

curl http://localhost:3000/api/v1/memories/overview \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "capacity": {
    "max_memories": 10,
    "current_count": 3,
    "available": 7
  },
  "usage": {
    "total_notes": 1768,
    "total_size_bytes": 60817408,
    "total_size_human": "58.02 MB"
  },
  "memories": [
    {
      "name": "default",
      "note_count": 1523,
      "size_bytes": 52428800,
      "size_human": "50.00 MB",
      "schema_version": 41
    },
    {
      "name": "work-2026",
      "note_count": 245,
      "size_bytes": 8388608,
      "size_human": "8.00 MB",
      "schema_version": 41
    }
  ],
  "database": {
    "total_size_bytes": 104857600,
    "total_size_human": "100.00 MB"
  }
}

Configuring Memory Limits

MAX_MEMORIES limits the number of live memories (active schemas in the database). This is not a hard cap on total archives — you can export any memory as a shard, delete it to free a slot, and re-import it later. There is no limit on the number of archived shards stored on disk.

# .env — scale with your hardware
MAX_MEMORIES=10   # Default (Tier 1: 8GB RAM, 10GB disk)
MAX_MEMORIES=50   # Tier 2: 16GB RAM, 100GB disk
MAX_MEMORIES=200  # Tier 3: 32GB RAM, 500GB disk
MAX_MEMORIES=500  # Tier 4: 64GB+ RAM, 1TB+ disk

See Configuration Reference for the capacity formula and detailed sizing by hardware tier.

Swapping memories in and out:

# Export a memory to a shard file (frees the slot after delete)
curl -X POST http://localhost:3000/api/v1/shards/export \
  -H "X-Fortemi-Memory: old-project" -o old-project.shard

# Delete the live memory to free a slot
curl -X DELETE http://localhost:3000/api/v1/archives/old-project

# Later: re-import when needed
curl -X POST http://localhost:3000/api/v1/shards/import \
  -F "file=@old-project.shard"

Attempting to create memories beyond the live limit will fail with HTTP 400:

{
  "error": "Memory limit reached. Maximum 10 memories allowed."
}

Per-Memory Statistics

Each memory tracks:

  • note_count: Total number of notes
  • size_bytes: Estimated size on disk (all tables combined)
  • schema_version: Number of tables (for auto-migration tracking)
  • last_accessed: Timestamp of last operation (updated automatically)

MCP Tool:

get_memories_overview()

Auto-Migration

Memories are automatically migrated when new table structures are added to the system.

How Auto-Migration Works

  1. Schema Version Tracking: Each memory stores its schema_version (current table count)
  2. On Access Check: When a memory is accessed, the system compares its schema version to the expected version
  3. Missing Table Detection: If schema_version < expected, missing tables are created automatically
  4. Create Tables: Uses the same CREATE TABLE statements that initialized the default memory
  5. Version Update: schema_version is updated to reflect the new table count

Migration Trigger

Auto-migration runs when:

  • A memory is accessed via X-Fortemi-Memory header
  • MCP selects a memory via select_memory
  • Federated search includes a memory
  • Memory clone operation completes

Migration Safety

  • Non-Destructive: Only creates missing tables, never modifies existing ones
  • Idempotent: Safe to run multiple times
  • Logged: Migration events are logged for debugging
  • Fast: Typically completes in <100ms (empty table creation only)

Manual Migration Verification

Check if a memory needs migration:

curl http://localhost:3000/api/v1/memories/work-2026 \
  -H "Authorization: Bearer $TOKEN"

If schema_version < 41 (current expected version), the memory will be auto-migrated on next access.

Data Isolation Guarantees

What is Isolated

Each memory has complete isolation of:

  1. Notes: All note content, revisions, and original versions
  2. Embeddings: Vector embeddings and embedding sets
  3. Links: Semantic relationships between notes
  4. Tags: User tags and SKOS concept taxonomies
  5. Collections: Folder hierarchies
  6. Templates: Note templates and their instantiations
  7. Attachments: File attachments and provenance data
  8. Document Types: Custom document type definitions

What is Shared

The following data is shared across all memories:

  1. Authentication: OAuth clients, API keys, user sessions
  2. Job Queue: Background processing jobs (though jobs operate on specific memories)
  3. Event Subscriptions: Webhooks and event stream configuration
  4. System Configuration: Embedding configurations, backup metadata

Cross-Memory Operations

Operations cannot cross memory boundaries:

  • Notes in memory A cannot link to notes in memory B
  • Search in memory A will never return notes from memory B (unless using federated search)
  • Tags in memory A are separate from tags in memory B (even if identically named)

Exception: Federated search explicitly searches multiple memories and attributes results to their source memory.

Current Limitations

  1. Cross-archive note linking: Notes cannot link across memory boundaries. Use export/import or federated search for cross-memory workflows.
  2. Embedding generation: Background jobs use archive context from the job payload. Embedding pipelines must be triggered per-archive.
  3. Cross-archive operations: No API support for copying notes between archives. Use export/import workflow instead.

Migration from Single-Memory Deployment

Existing deployments automatically have a default memory mapped to the public schema. This happens via the seed migration (20260208000002_seed_default_archive.sql) which inserts a registry entry on first startup. No manual migration is required.

Step-by-Step Migration

  1. Create New Memories: Create memories for different projects or clients
  2. Move Notes: Use export/import or manual copying to move notes between memories
  3. Update Integrations: Add X-Fortemi-Memory header to API calls that should target specific memories
  4. Test Isolation: Verify notes don't cross memory boundaries
  5. Archive Old Data: Move historical data to archived memories for cleanup

Backward Compatibility

  • All API calls without X-Fortemi-Memory header operate on the default memory
  • Existing code continues to work without changes
  • No data loss or schema migration required

API Reference

Memory Management

Endpoint Method Description
/api/v1/memories GET List all memories
/api/v1/memories POST Create new memory
/api/v1/memories/:name GET Get memory details
/api/v1/memories/:name PATCH Update memory metadata
/api/v1/memories/:name DELETE Delete memory
/api/v1/memories/overview GET Get aggregate statistics
/api/v1/archives/:name/clone POST Clone memory (deep copy)
/api/v1/search/federated POST Search across multiple memories

Request Headers

Header Values Description
X-Fortemi-Memory Memory name Routes request to specified memory (default: configured default or "public")

MCP Tools

Tool Description
list_memories List all memories
get_active_memory Get current session's active memory
select_memory Switch active memory for session
create_memory Create new memory
delete_memory Delete memory
clone_memory Clone memory with all data
search_memories_federated Search across multiple memories
get_memories_overview Get capacity and usage statistics

Backup and Restore

Per-Memory Backup

Backups can be scoped to specific memories:

# Backup work memory only
curl http://localhost:3000/api/v1/backup/knowledge-shard \
  -H "X-Fortemi-Memory: work-2026" \
  -H "Authorization: Bearer $TOKEN" \
  -o work-2026-backup.shard

Restore to Specific Memory

# Restore to a different memory (multipart upload)
curl -X POST http://localhost:3000/api/v1/backup/knowledge-shard/upload?on_conflict=skip \
  -H "X-Fortemi-Memory: work-2026-restored" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@work-2026.shard"

Full Database Backup

Database backups include all memories and shared tables:

# Full pg_dump backup
curl http://localhost:3000/api/v1/backup/database \
  -H "Authorization: Bearer $TOKEN" \
  -o full-backup.sql

See Backup Guide for comprehensive backup strategies.

Best Practices

When to Use Multiple Memories

Use multiple memories when you need:

  • Client isolation: Separate data per client with strict isolation guarantees
  • Project separation: Isolate different projects with distinct knowledge domains
  • Personal vs professional: Keep personal notes separate from work
  • Archival: Move old projects to archived memories without deletion

Don't use multiple memories for:

  • Tagging or categorization: Use tags and collections within a single memory
  • Temporary organization: Use collections for temporary grouping
  • Search filtering: Use strict tag filtering for data isolation within a memory

Naming Conventions

  • Use lowercase with hyphens: client-acme-2026
  • Include dates for temporal organization: work-2026-q1
  • Avoid special characters: stick to letters, numbers, underscores, hyphens
  • Keep names short but descriptive: research-ml not machine-learning-research-notes

Memory Size Guidelines

  • Small memories: <10,000 notes, <100MB - Fast operations, minimal overhead
  • Medium memories: 10,000-100,000 notes, 100MB-1GB - Good performance with proper indexing
  • Large memories: >100,000 notes, >1GB - Consider splitting into multiple memories

Performance Considerations

  • Each memory adds minimal overhead (<1MB) for metadata and indexes
  • Search performance scales with memory size, not total number of memories
  • Federated search adds latency proportional to number of memories searched
  • Memory cloning time scales with source memory size (typical: 1GB/minute)

Troubleshooting

"Memory not found" Error

Symptom: HTTP 404 when accessing a memory

Causes:

  • Typo in memory name (names are case-sensitive)
  • Memory was deleted
  • Memory name not valid PostgreSQL identifier

Fix:

# List all memories
curl http://localhost:3000/api/v1/memories -H "Authorization: Bearer $TOKEN"

Memory Limit Reached

Symptom: HTTP 400 "Memory limit reached"

Cause: MAX_MEMORIES environment variable limit hit

Fix:

# Increase limit in .env
MAX_MEMORIES=200

# Restart container
docker compose -f docker-compose.bundle.yml down
docker compose -f docker-compose.bundle.yml up -d

Slow Federated Search

Symptom: Federated search takes >5 seconds

Cause: Searching too many memories or very large memories

Fix:

  • Reduce number of memories searched
  • Use specific memory names instead of ["all"]
  • Optimize individual memory search performance (add embeddings, vacuum database)

Schema Version Mismatch

Symptom: Queries fail with "relation does not exist"

Cause: Memory hasn't been auto-migrated yet

Fix: Access the memory to trigger auto-migration:

curl http://localhost:3000/api/v1/notes \
  -H "X-Fortemi-Memory: memory-name" \
  -H "Authorization: Bearer $TOKEN"

Related Documentation