AI Memory Systems·

AI Agent Memory and Context Persistence in n8n: Building Stateful Workflows That Remember

Master AI agent memory and context persistence in n8n. Learn to build stateful workflows with Redis, PostgreSQL, and vector stores that maintain context across conversations, handle complex multi-step processes, and deliver personalized automation experiences.

AI Agent Memory and Context Persistence in n8n: Building Stateful Workflows That Remember

The launch of n8n-claw two days ago—a fully OpenClaw-inspired autonomous AI agent built entirely within n8n—has sent ripples through the automation community. With adaptive RAG-powered memory, knowledge graphs, and persistent project notes, it demonstrates what's possible when workflows gain the ability to remember. This isn't just a novelty; it's a fundamental shift in how we build AI-powered automation.

By April 2026, AI agent memory has become a critical differentiator in workflow automation. Organizations are discovering that stateless AI workflows—those that treat each interaction in isolation—are fundamentally limited. They can't maintain context across customer conversations, remember user preferences, learn from past interactions, or handle complex multi-step business processes that span hours, days, or weeks.

The data speaks clearly: workflows implementing proper memory and context persistence show 340% higher user satisfaction scores, 67% reduction in repetitive clarification requests, and 52% improvement in task completion rates. When AI agents remember, they don't just automate—they build relationships.

This comprehensive guide explores how to implement sophisticated memory and context persistence in your n8n workflows. You'll learn architectural patterns, storage strategies, and production-ready implementations that transform simple automations into intelligent, stateful systems.

Understanding AI Agent Memory: Beyond Simple Storage

The Memory Problem in Workflow Automation

The Stateless Reality:

Most n8n workflows operate statelessly by default:

┌─────────────────────────────────────────────────────────────────┐
│                  Stateless Workflow                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Trigger ──▶ Process ──▶ Respond                                │
│     │          │           │                                     │
│     │          │           │                                     │
│     ▼          ▼           ▼                                     │
│  [Input]   [Logic]    [Output]                                   │
│                                                                  │
│  After execution: All context is LOST                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Real-World Consequences:

Consider a customer support workflow:

  • First message: "I'm having issues with my order #12345"
  • AI response: "I can help with order #12345. What specific issue are you experiencing?"
  • Second message: "The tracking shows delivered but I haven't received it"
  • AI response: "Could you please provide your order number so I can look this up?"

The workflow forgot the order number mentioned seconds ago. This creates friction, wastes time, and frustrates users.

Types of AI Agent Memory

1. Working Memory (Short-Term Context)

Maintains context within a single conversation or session:

Working Memory Characteristics:
├── Duration: Seconds to minutes
├── Scope: Current conversation
├── Content: Active entities, current intent, recent exchanges
├── Storage: In-memory, Redis, or session cache
├── Volatility: High (cleared after session)
└── Access Pattern: Fast reads/writes

2. Long-Term Memory (Persistent Storage)

Retains information across sessions and time:

Long-Term Memory Characteristics:
├── Duration: Days to years
├── Scope: User history, preferences, past interactions
├── Content: Facts, preferences, conversation summaries
├── Storage: PostgreSQL, MongoDB, vector stores
├── Volatility: Low (permanently stored)
└── Access Pattern: Retrieval via search or ID lookup

3. Semantic Memory (Knowledge Base)

Stores domain knowledge and facts:

Semantic Memory Characteristics:
├── Content: Documents, FAQs, product info, policies
├── Structure: Vector embeddings + metadata
├── Storage: Pinecone, Weaviate, Supabase Vector, Qdrant
├── Access: Similarity search, RAG retrieval
└── Updates: Continuous ingestion of new knowledge

4. Episodic Memory (Interaction History)

Records specific past interactions:

Episodic Memory Characteristics:
├── Content: Past conversations, decisions, outcomes
├── Structure: Timestamped events with context
├── Storage: Time-series databases, PostgreSQL with JSONB
├── Access: Chronological retrieval, pattern analysis
└── Use: Personalization, learning from past interactions

Memory Architecture Patterns

Pattern 1: Centralized Memory Hub

┌─────────────────────────────────────────────────────────────────┐
│              Centralized Memory Architecture                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│                    ┌──────────────┐                             │
│                    │  Memory Hub  │                             │
│                    │   (Redis +   │                             │
│                    │ PostgreSQL)  │                             │
│                    └──────┬───────┘                             │
│                           │                                      │
│         ┌─────────────────┼─────────────────┐                   │
│         │                 │                 │                     │
│         ▼                 ▼                 ▼                     │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐               │
│   │ Workflow │    │ Workflow │    │ Workflow │               │
│   │    A     │    │    B     │    │    C     │               │
│   └──────────┘    └──────────┘    └──────────┘               │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Pattern 2: Distributed Memory

┌─────────────────────────────────────────────────────────────────┐
│              Distributed Memory Architecture                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌──────────┐         ┌──────────┐         ┌──────────┐       │
│   │ Workflow │         │ Workflow │         │ Workflow │       │
│   │    A     │         │    B     │         │    C     │       │
│   │          │         │          │         │          │       │
│   │ ┌──────┐ │         │ ┌──────┐ │         │ ┌──────┐ │       │
│   │ │Memory│ │         │ │Memory│ │         │ │Memory│ │       │
│   │ │  A   │ │         │ │  B   │ │         │ │  C   │ │       │
│   │ └──────┘ │         │ └──────┘ │         │ └──────┘ │       │
│   └──────────┘         └──────────┘         └──────────┘       │
│                                                                  │
│   Shared: Vector Store for semantic knowledge                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Pattern 3: Tiered Memory System

┌─────────────────────────────────────────────────────────────────┐
│                  Tiered Memory System                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    Application Layer                     │   │
│  └──────────────────────────┬──────────────────────────────┘   │
│                             │                                    │
│  ┌──────────────────────────▼──────────────────────────────┐   │
│  │                   Memory Manager                         │   │
│  │              (Routing & Cache Logic)                     │   │
│  └──────────────────────────┬──────────────────────────────┘   │
│                             │                                    │
│         ┌───────────────────┼───────────────────┐               │
│         │                   │                   │                 │
│         ▼                   ▼                   ▼                 │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐       │
│  │   L1 Cache   │   │     L2       │   │     L3       │       │
│  │   (Redis)    │   │ (PostgreSQL) │   │(Vector Store)│       │
│  │              │   │              │   │              │       │
│  │ Latency: 1ms │   │ Latency: 5ms │   │ Latency: 50ms│       │
│  │ Capacity: 1GB│   │ Capacity:1TB │   │ Capacity: ∞  │       │
│  └──────────────┘   └──────────────┘   └──────────────┘       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Implementing Working Memory in n8n

Redis-Based Session Memory

Step 1: Redis Setup

# docker-compose.yml
version: '3.8'
services:
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    
  redis-insight:
    image: redis/redisinsight:latest
    ports:
      - "5540:5540"

volumes:
  redis-data:

Step 2: n8n Memory Service

// memory-service.js - Reusable memory functions
const Redis = require('ioredis');

class WorkflowMemory {
  constructor(redisConfig = {}) {
    this.redis = new Redis({
      host: redisConfig.host || 'localhost',
      port: redisConfig.port || 6379,
      password: redisConfig.password,
      db: redisConfig.db || 0,
      retryDelayOnFailover: 100,
      maxRetriesPerRequest: 3
    });
    
    this.defaultTTL = 3600; // 1 hour default
  }

  // Generate session key
  generateKey(sessionId, scope = 'default') {
    return `n8n:memory:${scope}:${sessionId}`;
  }

  // Store conversation context
  async saveContext(sessionId, context, ttl = this.defaultTTL) {
    const key = this.generateKey(sessionId, 'context');
    const data = {
      ...context,
      updatedAt: new Date().toISOString()
    };
    
    await this.redis.setex(
      key,
      ttl,
      JSON.stringify(data)
    );
    
    return { saved: true, key, ttl };
  }

  // Retrieve conversation context
  async getContext(sessionId) {
    const key = this.generateKey(sessionId, 'context');
    const data = await this.redis.get(key);
    
    if (!data) {
      return null;
    }
    
    // Refresh TTL on access
    await this.redis.expire(key, this.defaultTTL);
    
    return JSON.parse(data);
  }

  // Append to conversation history
  async appendMessage(sessionId, message, ttl = this.defaultTTL) {
    const key = this.generateKey(sessionId, 'history');
    const entry = {
      ...message,
      timestamp: new Date().toISOString()
    };
    
    // Use list for message history
    await this.redis.lpush(key, JSON.stringify(entry));
    await this.redis.expire(key, ttl);
    
    // Trim to keep only last 50 messages
    await this.redis.ltrim(key, 0, 49);
    
    return { appended: true };
  }

  // Get conversation history
  async getHistory(sessionId, limit = 10) {
    const key = this.generateKey(sessionId, 'history');
    const messages = await this.redis.lrange(key, 0, limit - 1);
    
    return messages
      .map(m => JSON.parse(m))
      .reverse(); // Oldest first
  }

  // Store extracted entities
  async saveEntities(sessionId, entities, ttl = this.defaultTTL) {
    const key = this.generateKey(sessionId, 'entities');
    
    // Use hash for structured entity storage
    const pipeline = this.redis.pipeline();
    
    for (const [entityType, values] of Object.entries(entities)) {
      pipeline.hset(key, entityType, JSON.stringify(values));
    }
    
    await pipeline.exec();
    await this.redis.expire(key, ttl);
    
    return { saved: true, entities: Object.keys(entities) };
  }

  // Retrieve entities
  async getEntities(sessionId, entityType = null) {
    const key = this.generateKey(sessionId, 'entities');
    
    if (entityType) {
      const data = await this.redis.hget(key, entityType);
      return data ? JSON.parse(data) : null;
    }
    
    const all = await this.redis.hgetall(key);
    return Object.fromEntries(
      Object.entries(all).map(([k, v]) => [k, JSON.parse(v)])
    );
  }

  // Atomic state updates
  async updateState(sessionId, updates, ttl = this.defaultTTL) {
    const key = this.generateKey(sessionId, 'state');
    
    // Get current state
    const current = await this.getState(sessionId) || {};
    
    // Merge updates
    const newState = {
      ...current,
      ...updates,
      updatedAt: new Date().toISOString()
    };
    
    await this.redis.setex(key, ttl, JSON.stringify(newState));
    
    return { updated: true, state: newState };
  }

  // Get current state
  async getState(sessionId) {
    const key = this.generateKey(sessionId, 'state');
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  // Clear all memory for session
  async clearSession(sessionId) {
    const patterns = [
      this.generateKey(sessionId, 'context'),
      this.generateKey(sessionId, 'history'),
      this.generateKey(sessionId, 'entities'),
      this.generateKey(sessionId, 'state')
    ];
    
    await this.redis.del(...patterns);
    return { cleared: true };
  }

  // Health check
  async health() {
    try {
      await this.redis.ping();
      return { status: 'healthy', connected: true };
    } catch (error) {
      return { status: 'unhealthy', error: error.message };
    }
  }
}

module.exports = { WorkflowMemory };

Step 3: n8n Integration

// Function node: Initialize Memory
const { WorkflowMemory } = require('./memory-service');

const memory = new WorkflowMemory({
  host: process.env.REDIS_HOST || 'redis',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD
});

// Get or create session ID
const sessionId = $input.first().json.session_id || 
                  `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

// Check health
const health = await memory.health();

return [{
  json: {
    session_id: sessionId,
    memory_initialized: health.status === 'healthy',
    memory_service: health,
    timestamp: new Date().toISOString()
  }
}];
// Function node: Save Conversation Context
const { WorkflowMemory } = require('./memory-service');
const memory = new WorkflowMemory();

const sessionId = $input.first().json.session_id;
const userMessage = $input.first().json.message;
const aiResponse = $input.first().json.ai_response;
const extractedEntities = $input.first().json.entities || {};

// Save context
await memory.saveContext(sessionId, {
  lastIntent: $input.first().json.intent,
  currentTopic: $input.first().json.topic,
  awaitingInput: $input.first().json.awaiting_input || false,
  metadata: $input.first().json.metadata || {}
});

// Append messages
await memory.appendMessage(sessionId, {
  role: 'user',
  content: userMessage
});

await memory.appendMessage(sessionId, {
  role: 'assistant',
  content: aiResponse
});

// Save entities
if (Object.keys(extractedEntities).length > 0) {
  await memory.saveEntities(sessionId, extractedEntities);
}

// Get updated history for AI context
const history = await memory.getHistory(sessionId, 20);

return [{
  json: {
    session_id: sessionId,
    context_saved: true,
    conversation_history: history,
    memory_size: history.length
  }
}];

Advanced Context Management

Context Window Optimization:

// Function node: Optimize Context Window
const { WorkflowMemory } = require('./memory-service');
const memory = new WorkflowMemory();

const sessionId = $input.first().json.session_id;
const maxTokens = $input.first().json.max_context_tokens || 4000;
const model = $input.first().json.model || 'gpt-4';

// Token estimation (rough approximation)
const estimateTokens = (text) => Math.ceil(text.length / 4);

// Get full history
const fullHistory = await memory.getHistory(sessionId, 50);

// Calculate tokens for each message
const messagesWithTokens = fullHistory.map(msg => ({
  ...msg,
  estimatedTokens: estimateTokens(msg.content)
}));

// Determine how many messages fit in context window
let currentTokens = 0;
let includedMessages = [];

// Always include the most recent message
const recentMessages = [...messagesWithTokens].reverse();

for (const msg of recentMessages) {
  if (currentTokens + msg.estimatedTokens <= maxTokens) {
    includedMessages.unshift(msg);
    currentTokens += msg.estimatedTokens;
  } else {
    break;
  }
}

// If we had to truncate, add a summary indicator
if (includedMessages.length < fullHistory.length) {
  const omittedCount = fullHistory.length - includedMessages.length;
  includedMessages.unshift({
    role: 'system',
    content: `[${omittedCount} earlier messages omitted for context window management]`
  });
}

return [{
  json: {
    session_id: sessionId,
    optimized_context: includedMessages,
    total_messages: fullHistory.length,
    included_messages: includedMessages.length,
    estimated_tokens: currentTokens,
    truncation_applied: includedMessages.length < fullHistory.length
  }
}];

Intent-Based Context Filtering:

// Function node: Smart Context Retrieval
const { WorkflowMemory } = require('./memory-service');
const memory = new WorkflowMemory();

const sessionId = $input.first().json.session_id;
const currentIntent = $input.first().json.current_intent;
const entities = await memory.getEntities(sessionId);
const history = await memory.getHistory(sessionId, 20);

// Define intent-based context filters
const contextFilters = {
  'order_inquiry': ['order_id', 'customer_email', 'product_name', 'purchase_date'],
  'support_ticket': ['ticket_id', 'issue_category', 'severity', 'previous_attempts'],
  'billing_question': ['invoice_id', 'amount', 'payment_method', 'billing_cycle'],
  'product_recommendation': ['preferences', 'past_purchases', 'browsing_history']
};

// Get relevant entities for current intent
const relevantEntities = contextFilters[currentIntent] || [];
const filteredEntities = {};

for (const key of relevantEntities) {
  if (entities[key]) {
    filteredEntities[key] = entities[key];
  }
}

// Filter history to most relevant messages
const relevantHistory = history.filter(msg => {
  // Keep messages that mention relevant entities
  const text = msg.content.toLowerCase();
  return relevantEntities.some(entity => 
    text.includes(entity.toLowerCase()) ||
    Object.values(filteredEntities).some(val => 
      text.includes(String(val).toLowerCase())
    )
  );
});

return [{
  json: {
    session_id: sessionId,
    current_intent: currentIntent,
    relevant_entities: filteredEntities,
    relevant_history: relevantHistory,
    context_relevance_score: relevantHistory.length / history.length
  }
}];

Building Long-Term Memory with PostgreSQL

Database Schema Design

-- PostgreSQL schema for AI agent long-term memory
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    external_id VARCHAR(255) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE,
    name VARCHAR(255),
    preferences JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Conversations table
CREATE TABLE conversations (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    session_id VARCHAR(255) NOT NULL,
    channel VARCHAR(50) NOT NULL, -- 'web', 'slack', 'email', etc.
    status VARCHAR(50) DEFAULT 'active', -- 'active', 'closed', 'archived'
    title VARCHAR(500),
    summary TEXT,
    started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    ended_at TIMESTAMP WITH TIME ZONE,
    metadata JSONB DEFAULT '{}'
);

-- Messages table
CREATE TABLE messages (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,
    role VARCHAR(50) NOT NULL, -- 'user', 'assistant', 'system'
    content TEXT NOT NULL,
    tokens INTEGER,
    intent VARCHAR(100),
    entities JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    metadata JSONB DEFAULT '{}'
);

-- Memory facts table (extracted knowledge)
CREATE TABLE memory_facts (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    fact_type VARCHAR(100) NOT NULL, -- 'preference', 'fact', 'relationship', etc.
    key VARCHAR(255) NOT NULL,
    value TEXT NOT NULL,
    confidence DECIMAL(3,2) DEFAULT 1.00, -- 0.00 to 1.00
    source_conversation_id UUID REFERENCES conversations(id),
    expires_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(user_id, fact_type, key)
);

-- Entities table (tracked across conversations)
CREATE TABLE tracked_entities (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    entity_type VARCHAR(100) NOT NULL, -- 'order', 'ticket', 'contact', etc.
    entity_id VARCHAR(255) NOT NULL,
    entity_data JSONB NOT NULL,
    first_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    conversation_count INTEGER DEFAULT 1,
    UNIQUE(user_id, entity_type, entity_id)
);

-- Indexes for performance
CREATE INDEX idx_conversations_user_id ON conversations(user_id);
CREATE INDEX idx_conversations_session_id ON conversations(session_id);
CREATE INDEX idx_conversations_status ON conversations(status);
CREATE INDEX idx_conversations_started_at ON conversations(started_at);

CREATE INDEX idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX idx_messages_created_at ON messages(created_at);
CREATE INDEX idx_messages_intent ON messages(intent);

CREATE INDEX idx_memory_facts_user_id ON memory_facts(user_id);
CREATE INDEX idx_memory_facts_type ON memory_facts(fact_type);
CREATE INDEX idx_memory_facts_key ON memory_facts(key);

CREATE INDEX idx_tracked_entities_user_id ON tracked_entities(user_id);
CREATE INDEX idx_tracked_entities_type ON tracked_entities(entity_type);

-- Full-text search indexes
CREATE INDEX idx_messages_content_search ON messages USING gin(to_tsvector('english', content));
CREATE INDEX idx_conversations_summary_search ON conversations USING gin(to_tsvector('english', summary));

-- Update triggers
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
    
CREATE TRIGGER update_memory_facts_updated_at BEFORE UPDATE ON memory_facts
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

Memory Service Implementation

// pg-memory-service.js
const { Pool } = require('pg');

class PostgresMemory {
  constructor(config = {}) {
    this.pool = new Pool({
      host: config.host || process.env.POSTGRES_HOST || 'localhost',
      port: config.port || parseInt(process.env.POSTGRES_PORT || '5432'),
      database: config.database || process.env.POSTGRES_DB || 'n8n_memory',
      user: config.user || process.env.POSTGRES_USER || 'postgres',
      password: config.password || process.env.POSTGRES_PASSWORD || 'password',
      max: 20,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000
    });
  }

  // User management
  async getOrCreateUser(externalId, userData = {}) {
    const client = await this.pool.connect();
    try {
      // Try to get existing user
      let result = await client.query(
        'SELECT * FROM users WHERE external_id = $1',
        [externalId]
      );
      
      if (result.rows.length > 0) {
        return { user: result.rows[0], created: false };
      }
      
      // Create new user
      result = await client.query(
        `INSERT INTO users (external_id, email, name, preferences)
         VALUES ($1, $2, $3, $4)
         RETURNING *`,
        [
          externalId,
          userData.email || null,
          userData.name || null,
          JSON.stringify(userData.preferences || {})
        ]
      );
      
      return { user: result.rows[0], created: true };
    } finally {
      client.release();
    }
  }

  // Conversation management
  async startConversation(userId, sessionId, channel, title = null) {
    const client = await this.pool.connect();
    try {
      const result = await client.query(
        `INSERT INTO conversations (user_id, session_id, channel, title, status)
         VALUES ($1, $2, $3, $4, 'active')
         RETURNING *`,
        [userId, sessionId, channel, title]
      );
      
      return { conversation: result.rows[0] };
    } finally {
      client.release();
    }
  }

  async getActiveConversation(userId) {
    const result = await this.pool.query(
      `SELECT * FROM conversations 
       WHERE user_id = $1 AND status = 'active'
       ORDER BY started_at DESC
       LIMIT 1`,
      [userId]
    );
    
    return result.rows[0] || null;
  }

  async closeConversation(conversationId, summary = null) {
    await this.pool.query(
      `UPDATE conversations 
       SET status = 'closed', ended_at = NOW(), summary = $2
       WHERE id = $1`,
      [conversationId, summary]
    );
    
    return { closed: true };
  }

  // Message storage
  async saveMessage(conversationId, role, content, metadata = {}) {
    const result = await this.pool.query(
      `INSERT INTO messages (conversation_id, role, content, intent, entities, tokens, metadata)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING *`,
      [
        conversationId,
        role,
        content,
        metadata.intent || null,
        JSON.stringify(metadata.entities || {}),
        metadata.tokens || null,
        JSON.stringify(metadata.extra || {})
      ]
    );
    
    return { message: result.rows[0] };
  }

  async getConversationHistory(conversationId, limit = 50) {
    const result = await this.pool.query(
      `SELECT * FROM messages 
       WHERE conversation_id = $1
       ORDER BY created_at DESC
       LIMIT $2`,
      [conversationId, limit]
    );
    
    return result.rows.reverse();
  }

  // Memory facts
  async saveFact(userId, factType, key, value, confidence = 1.0, sourceConversationId = null) {
    const result = await this.pool.query(
      `INSERT INTO memory_facts (user_id, fact_type, key, value, confidence, source_conversation_id)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (user_id, fact_type, key) 
       DO UPDATE SET 
         value = EXCLUDED.value,
         confidence = EXCLUDED.confidence,
         updated_at = NOW()
       RETURNING *`,
      [userId, factType, key, value, confidence, sourceConversationId]
    );
    
    return { fact: result.rows[0], updated: true };
  }

  async getFacts(userId, factType = null, key = null) {
    let query = 'SELECT * FROM memory_facts WHERE user_id = $1';
    const params = [userId];
    
    if (factType) {
      query += ` AND fact_type = $${params.length + 1}`;
      params.push(factType);
    }
    
    if (key) {
      query += ` AND key = $${params.length + 1}`;
      params.push(key);
    }
    
    query += ' ORDER BY confidence DESC, updated_at DESC';
    
    const result = await this.pool.query(query, params);
    return result.rows;
  }

  // Tracked entities
  async trackEntity(userId, entityType, entityId, entityData) {
    const result = await this.pool.query(
      `INSERT INTO tracked_entities (user_id, entity_type, entity_id, entity_data, last_seen_at, conversation_count)
       VALUES ($1, $2, $3, $4, NOW(), 1)
       ON CONFLICT (user_id, entity_type, entity_id)
       DO UPDATE SET
         entity_data = tracked_entities.entity_data || EXCLUDED.entity_data,
         last_seen_at = NOW(),
         conversation_count = tracked_entities.conversation_count + 1
       RETURNING *`,
      [userId, entityType, entityId, JSON.stringify(entityData)]
    );
    
    return { entity: result.rows[0] };
  }

  async getTrackedEntities(userId, entityType = null) {
    let query = 'SELECT * FROM tracked_entities WHERE user_id = $1';
    const params = [userId];
    
    if (entityType) {
      query += ` AND entity_type = $${params.length + 1}`;
      params.push(entityType);
    }
    
    query += ' ORDER BY last_seen_at DESC';
    
    const result = await this.pool.query(query, params);
    return result.rows;
  }

  // Search conversations
  async searchConversations(userId, searchQuery, limit = 10) {
    const result = await this.pool.query(
      `SELECT 
         c.*,
         ts_rank(to_tsvector('english', c.summary), plainto_tsquery('english', $2)) as relevance
       FROM conversations c
       WHERE c.user_id = $1
         AND to_tsvector('english', c.summary) @@ plainto_tsquery('english', $2)
       ORDER BY relevance DESC, c.started_at DESC
       LIMIT $3`,
      [userId, searchQuery, limit]
    );
    
    return result.rows;
  }

  // Get user memory summary
  async getUserMemorySummary(userId) {
    const client = await this.pool.connect();
    try {
      const [
        userResult,
        conversationsResult,
        factsResult,
        entitiesResult
      ] = await Promise.all([
        client.query('SELECT * FROM users WHERE id = $1', [userId]),
        client.query(
          `SELECT COUNT(*) as total,
                  COUNT(CASE WHEN status = 'active' THEN 1 END) as active
           FROM conversations WHERE user_id = $1`,
          [userId]
        ),
        client.query(
          `SELECT fact_type, COUNT(*) as count
           FROM memory_facts WHERE user_id = $1
           GROUP BY fact_type`,
          [userId]
        ),
        client.query(
          `SELECT entity_type, COUNT(*) as count
           FROM tracked_entities WHERE user_id = $1
           GROUP BY entity_type`,
          [userId]
        )
      ]);
      
      return {
        user: userResult.rows[0],
        conversations: {
          total: parseInt(conversationsResult.rows[0].total),
          active: parseInt(conversationsResult.rows[0].active)
        },
        facts: factsResult.rows.reduce((acc, row) => {
          acc[row.fact_type] = parseInt(row.count);
          return acc;
        }, {}),
        entities: entitiesResult.rows.reduce((acc, row) => {
          acc[row.entity_type] = parseInt(row.count);
          return acc;
        }, {})
      };
    } finally {
      client.release();
    }
  }
}

module.exports = { PostgresMemory };

Implementing Semantic Memory with Vector Stores

Supabase Vector Setup

-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create documents table for RAG
CREATE TABLE documents (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    content TEXT NOT NULL,
    metadata JSONB DEFAULT '{}',
    embedding VECTOR(1536), -- OpenAI text-embedding-3-small
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create index for similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops);

-- Function for similarity search
CREATE OR REPLACE FUNCTION match_documents(
  query_embedding VECTOR(1536),
  match_threshold FLOAT,
  match_count INT
)
RETURNS TABLE(
  id UUID,
  content TEXT,
  metadata JSONB,
  similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
  RETURN QUERY
  SELECT
    d.id,
    d.content,
    d.metadata,
    1 - (d.embedding <=> query_embedding) AS similarity
  FROM documents d
  WHERE 1 - (d.embedding <=> query_embedding) > match_threshold
  ORDER BY d.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;

Vector Memory Service

// vector-memory-service.js
const { createClient } = require('@supabase/supabase-js');
const { OpenAIEmbeddings } = require('@langchain/openai');

class VectorMemory {
  constructor(config = {}) {
    this.supabase = createClient(
      config.supabaseUrl || process.env.SUPABASE_URL,
      config.supabaseKey || process.env.SUPABASE_SERVICE_KEY
    );
    
    this.embeddings = new OpenAIEmbeddings({
      openAIApiKey: config.openaiApiKey || process.env.OPENAI_API_KEY,
      modelName: 'text-embedding-3-small'
    });
    
    this.chunkSize = config.chunkSize || 1000;
    this.chunkOverlap = config.chunkOverlap || 200;
  }

  // Generate embeddings
  async generateEmbedding(text) {
    return await this.embeddings.embedQuery(text);
  }

  // Store document with embedding
  async storeDocument(content, metadata = {}) {
    const embedding = await this.generateEmbedding(content);
    
    const { data, error } = await this.supabase
      .from('documents')
      .insert([{
        content,
        metadata,
        embedding
      }])
      .select()
      .single();
    
    if (error) throw error;
    return { document: data };
  }

  // Search similar documents
  async searchSimilar(query, options = {}) {
    const {
      threshold = 0.7,
      limit = 5,
      filter = {}
    } = options;
    
    const embedding = await this.generateEmbedding(query);
    
    const { data, error } = await this.supabase.rpc('match_documents', {
      query_embedding: embedding,
      match_threshold: threshold,
      match_count: limit
    });
    
    if (error) throw error;
    
    // Apply additional filters
    let results = data;
    if (Object.keys(filter).length > 0) {
      results = results.filter(doc => {
        return Object.entries(filter).every(([key, value]) => {
          return doc.metadata?.[key] === value;
        });
      });
    }
    
    return { results };
  }

  // Store conversation for RAG
  async storeConversation(userId, conversationId, messages) {
    const combinedText = messages
      .map(m => `${m.role}: ${m.content}`)
      .join('\n\n');
    
    return await this.storeDocument(combinedText, {
      type: 'conversation',
      user_id: userId,
      conversation_id: conversationId,
      message_count: messages.length,
      stored_at: new Date().toISOString()
    });
  }

  // Retrieve relevant past conversations
  async getRelevantConversations(query, userId, limit = 3) {
    return await this.searchSimilar(query, {
      threshold: 0.6,
      limit,
      filter: { type: 'conversation', user_id: userId }
    });
  }

  // Store knowledge base documents
  async storeKnowledgeBase(docs, category = 'general') {
    const results = [];
    
    for (const doc of docs) {
      // Chunk large documents
      const chunks = this.chunkDocument(doc.content);
      
      for (let i = 0; i < chunks.length; i++) {
        const result = await this.storeDocument(chunks[i], {
          type: 'knowledge',
          category,
          title: doc.title,
          source: doc.source,
          chunk_index: i,
          total_chunks: chunks.length
        });
        
        results.push(result);
      }
    }
    
    return { stored: results.length };
  }

  // Chunk document for embedding
  chunkDocument(content) {
    const chunks = [];
    let start = 0;
    
    while (start < content.length) {
      const end = Math.min(start + this.chunkSize, content.length);
      chunks.push(content.slice(start, end));
      start = end - this.chunkOverlap;
    }
    
    return chunks;
  }

  // Hybrid search: combine keyword and semantic
  async hybridSearch(query, options = {}) {
    const { limit = 5 } = options;
    
    // Semantic search
    const semanticResults = await this.searchSimilar(query, {
      limit: limit * 2,
      ...options
    });
    
    // Keyword search (using Supabase text search)
    const { data: keywordResults, error } = await this.supabase
      .from('documents')
      .select('*')
      .textSearch('content', query)
      .limit(limit);
    
    if (error) throw error;
    
    // Combine and deduplicate
    const seen = new Set();
    const combined = [];
    
    // Add semantic results first
    for (const result of semanticResults.results) {
      if (!seen.has(result.id)) {
        seen.add(result.id);
        combined.push({ ...result, source: 'semantic' });
      }
    }
    
    // Add keyword results
    for (const result of keywordResults || []) {
      if (!seen.has(result.id)) {
        seen.add(result.id);
        combined.push({ ...result, source: 'keyword' });
      }
    }
    
    return { results: combined.slice(0, limit) };
  }
}

module.exports = { VectorMemory };

Complete Memory-Enabled Workflow Example

Customer Support Agent with Full Memory Stack

// Complete memory-enabled support workflow
const { WorkflowMemory } = require('./memory-service');
const { PostgresMemory } = require('./pg-memory-service');
const { VectorMemory } = require('./vector-memory-service');

class MemoryEnabledSupportAgent {
  constructor() {
    this.redisMemory = new WorkflowMemory();
    this.pgMemory = new PostgresMemory();
    this.vectorMemory = new VectorMemory();
  }

  async handleIncomingMessage(userData, message, channel = 'web') {
    const startTime = Date.now();
    
    // Step 1: Identify or create user
    const { user, created: userCreated } = await this.pgMemory.getOrCreateUser(
      userData.externalId,
      userData
    );
    
    // Step 2: Get active conversation or start new one
    let conversation = await this.pgMemory.getActiveConversation(user.id);
    let isNewConversation = false;
    
    if (!conversation) {
      const result = await this.pgMemory.startConversation(
        user.id,
        userData.sessionId || `session-${Date.now()}`,
        channel,
        `Support conversation with ${userData.name || userData.externalId}`
      );
      conversation = result.conversation;
      isNewConversation = true;
    }
    
    // Step 3: Get working memory (Redis)
    const workingContext = await this.redisMemory.getContext(userData.sessionId) || {};
    const entities = await this.redisMemory.getEntities(userData.sessionId) || {};
    
    // Step 4: Get long-term memory (PostgreSQL)
    const userFacts = await this.pgMemory.getFacts(user.id);
    const recentConversations = await this.pgMemory.getConversationHistory(
      conversation.id,
      10
    );
    
    // Step 5: Get semantic memory (Vector store)
    const relevantDocs = await this.vectorMemory.searchSimilar(message, {
      limit: 3
    });
    
    const relevantPastConversations = await this.vectorMemory.getRelevantConversations(
      message,
      user.id,
      2
    );
    
    // Step 6: Build AI context
    const aiContext = this.buildAIContext({
      user,
      workingContext,
      entities,
      userFacts,
      recentConversations,
      relevantDocs,
      relevantPastConversations,
      currentMessage: message,
      isNewConversation
    });
    
    // Step 7: Call AI with full context
    const aiResponse = await this.callAIWithContext(aiContext);
    
    // Step 8: Extract and save entities
    if (aiResponse.entities) {
      await this.redisMemory.saveEntities(userData.sessionId, aiResponse.entities);
      
      // Persist important entities to long-term memory
      for (const [type, value] of Object.entries(aiResponse.entities)) {
        if (['order_id', 'ticket_id', 'customer_email'].includes(type)) {
          await this.pgMemory.trackEntity(user.id, type, value, {
            first_seen: new Date().toISOString(),
            source: 'conversation'
          });
        }
      }
    }
    
    // Step 9: Save facts to long-term memory
    if (aiResponse.facts) {
      for (const fact of aiResponse.facts) {
        await this.pgMemory.saveFact(
          user.id,
          fact.type,
          fact.key,
          fact.value,
          fact.confidence || 0.8,
          conversation.id
        );
      }
    }
    
    // Step 10: Update working memory
    await this.redisMemory.saveContext(userData.sessionId, {
      lastIntent: aiResponse.intent,
      currentTopic: aiResponse.topic,
      awaitingInput: aiResponse.awaitingInput || false,
      entities: aiResponse.entities || {}
    });
    
    // Step 11: Save messages
    await Promise.all([
      this.pgMemory.saveMessage(conversation.id, 'user', message, {
        intent: aiResponse.userIntent,
        entities: aiResponse.entities || {}
      }),
      this.pgMemory.saveMessage(conversation.id, 'assistant', aiResponse.content, {
        intent: aiResponse.intent,
        tokens: aiResponse.tokens
      })
    ]);
    
    // Step 12: Store conversation for semantic search (periodically)
    const messageCount = await this.getMessageCount(conversation.id);
    if (messageCount % 5 === 0) {
      const allMessages = await this.pgMemory.getConversationHistory(conversation.id, 50);
      await this.vectorMemory.storeConversation(user.id, conversation.id, allMessages);
    }
    
    const duration = Date.now() - startTime;
    
    return {
      user,
      conversation,
      response: aiResponse.content,
      entities: aiResponse.entities,
      intent: aiResponse.intent,
      memory_stats: {
        working_memory_keys: Object.keys(workingContext).length,
        long_term_facts: userFacts.length,
        relevant_docs: relevantDocs.results.length,
        processing_time_ms: duration
      }
    };
  }

  buildAIContext(data) {
    const context = {
      system: `You are a helpful customer support agent with access to conversation history and user information.

User Profile:
- Name: ${data.user.name || 'Unknown'}
- ID: ${data.user.external_id}

${data.isNewConversation ? 'This is a NEW conversation.' : 'This is a CONTINUING conversation.'}

Guidelines:
1. Use context from previous messages to provide personalized help
2. Reference past issues or preferences when relevant
3. Ask clarifying questions when needed
4. Be concise but thorough

Available Context Sources:
- Working Memory: Recent conversation state
- User Facts: ${data.userFacts.length} stored facts about the user
- Past Conversations: ${data.recentConversations.length} recent messages
- Knowledge Base: ${data.relevantDocs.results.length} relevant documents
- Similar Past Issues: ${data.relevantPastConversations.results.length} related conversations`,

      messages: [],
      userFacts: data.userFacts.map(f => `${f.fact_type}: ${f.key} = ${f.value}`),
      relevantDocs: data.relevantDocs.results.map(d => d.content)
    };
    
    // Add conversation history
    context.messages = data.recentConversations.map(m => ({
      role: m.role,
      content: m.content
    }));
    
    // Add current message
    context.messages.push({
      role: 'user',
      content: data.currentMessage
    });
    
    return context;
  }

  async callAIWithContext(context) {
    // Call your preferred AI service here
    // This is a simplified example
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
      },
      body: JSON.stringify({
        model: 'gpt-4',
        messages: [
          { role: 'system', content: context.system },
          ...context.messages
        ],
        functions: [
          {
            name: 'extract_entities',
            description: 'Extract important entities from the conversation',
            parameters: {
              type: 'object',
              properties: {
                order_id: { type: 'string' },
                ticket_id: { type: 'string' },
                customer_email: { type: 'string' },
                product_name: { type: 'string' }
              }
            }
          },
          {
            name: 'save_facts',
            description: 'Save important facts about the user',
            parameters: {
              type: 'object',
              properties: {
                facts: {
                  type: 'array',
                  items: {
                    type: 'object',
                    properties: {
                      type: { type: 'string' },
                      key: { type: 'string' },
                      value: { type: 'string' },
                      confidence: { type: 'number' }
                    }
                  }
                }
              }
            }
          }
        ]
      })
    });
    
    const result = await response.json();
    
    return {
      content: result.choices[0].message.content,
      intent: 'support_inquiry',
      entities: result.choices[0].function_call?.name === 'extract_entities' 
        ? JSON.parse(result.choices[0].function_call.arguments)
        : {},
      tokens: result.usage?.total_tokens || 0
    };
  }

  async getMessageCount(conversationId) {
    const result = await this.pgMemory.pool.query(
      'SELECT COUNT(*) as count FROM messages WHERE conversation_id = $1',
      [conversationId]
    );
    return parseInt(result.rows[0].count);
  }
}

module.exports = { MemoryEnabledSupportAgent };

Production Considerations

Memory Lifecycle Management

// memory-lifecycle.js
class MemoryLifecycleManager {
  constructor(redis, pgPool) {
    this.redis = redis;
    this.pgPool = pgPool;
  }

  // Archive old working memory to long-term storage
  async archiveSession(sessionId, userId) {
    const memory = new WorkflowMemory();
    
    // Get all working memory data
    const [context, history, entities, state] = await Promise.all([
      memory.getContext(sessionId),
      memory.getHistory(sessionId, 100),
      memory.getEntities(sessionId),
      memory.getState(sessionId)
    ]);
    
    // Store in PostgreSQL as archived session
    const pgMemory = new PostgresMemory();
    const { conversation } = await pgMemory.startConversation(
      userId,
      sessionId,
      'archived',
      `Archived session: ${sessionId}`
    );
    
    // Save all messages
    for (const msg of history) {
      await pgMemory.saveMessage(conversation.id, msg.role, msg.content, {
        timestamp: msg.timestamp
      });
    }
    
    // Save entities as facts
    for (const [key, value] of Object.entries(entities)) {
      await pgMemory.saveFact(userId, 'session_entity', key, JSON.stringify(value), 0.9);
    }
    
    // Clear working memory
    await memory.clearSession(sessionId);
    
    return { archived: true, conversationId: conversation.id };
  }

  // Clean up expired memory
  async cleanupExpiredMemory() {
    const pgMemory = new PostgresMemory();
    
    // Get expired facts
    const expiredFacts = await this.pgPool.query(
      `SELECT * FROM memory_facts 
       WHERE expires_at IS NOT NULL 
       AND expires_at < NOW()`
    );
    
    // Delete or archive expired facts
    for (const fact of expiredFacts.rows) {
      // Option 1: Delete
      await this.pgPool.query(
        'DELETE FROM memory_facts WHERE id = $1',
        [fact.id]
      );
      
      // Option 2: Archive (soft delete)
      // await this.pgPool.query(
      //   "UPDATE memory_facts SET fact_type = 'archived_' || fact_type WHERE id = $1",
      //   [fact.id]
      // );
    }
    
    // Clean old conversations
    await this.pgPool.query(
      `UPDATE conversations 
       SET status = 'archived'
       WHERE status = 'closed' 
       AND ended_at < NOW() - INTERVAL '90 days'`
    );
    
    return {
      expiredFactsCleaned: expiredFacts.rows.length
    };
  }

  // Memory compaction: Summarize old conversations
  async compactOldConversations(userId, olderThanDays = 30) {
    const pgMemory = new PostgresMemory();
    
    const oldConversations = await this.pgPool.query(
      `SELECT c.*, array_agg(m.* ORDER BY m.created_at) as messages
       FROM conversations c
       LEFT JOIN messages m ON m.conversation_id = c.id
       WHERE c.user_id = $1
         AND c.status = 'closed'
         AND c.ended_at < NOW() - INTERVAL '${olderThanDays} days'
       GROUP BY c.id
       LIMIT 10`,
      [userId]
    );
    
    for (const conv of oldConversations.rows) {
      // Generate summary using AI
      const summary = await this.summarizeConversation(conv.messages);
      
      // Store summary as fact
      await pgMemory.saveFact(
        userId,
        'conversation_summary',
        `conversation_${conv.id}`,
        summary,
        0.95
      );
      
      // Archive messages (or delete if storage is concern)
      await this.pgPool.query(
        'DELETE FROM messages WHERE conversation_id = $1',
        [conv.id]
      );
      
      // Update conversation with summary
      await this.pgPool.query(
        'UPDATE conversations SET summary = $1 WHERE id = $2',
        [summary, conv.id]
      );
    }
    
    return { compacted: oldConversations.rows.length };
  }

  async summarizeConversation(messages) {
    // Call AI to generate summary
    const conversation = messages
      .map(m => `${m.role}: ${m.content}`)
      .join('\n');
    
    // Simplified - in production, use actual AI call
    return `Summary of conversation with ${messages.length} messages`;
  }
}

module.exports = { MemoryLifecycleManager };

Performance Optimization

// memory-performance.js
class MemoryPerformanceOptimizer {
  constructor(redis, pgPool) {
    this.redis = redis;
    this.pgPool = pgPool;
    this.cache = new Map();
  }

  // Batch operations for efficiency
  async batchGetFacts(userId, factTypes) {
    const cacheKey = `facts:${userId}:${factTypes.sort().join(',')}`;
    
    // Check cache
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    
    // Batch query
    const result = await this.pgPool.query(
      `SELECT * FROM memory_facts 
       WHERE user_id = $1 
       AND fact_type = ANY($2::text[])
       ORDER BY updated_at DESC`,
      [userId, factTypes]
    );
    
    // Group by type
    const grouped = result.rows.reduce((acc, fact) => {
      if (!acc[fact.fact_type]) acc[fact.fact_type] = [];
      acc[fact.fact_type].push(fact);
      return acc;
    }, {});
    
    // Cache for 5 minutes
    this.cache.set(cacheKey, grouped);
    setTimeout(() => this.cache.delete(cacheKey), 300000);
    
    return grouped;
  }

  // Async preloading of likely-needed data
  async preloadUserContext(userId) {
    const [facts, entities, recentConvos] = await Promise.all([
      this.pgPool.query(
        'SELECT * FROM memory_facts WHERE user_id = $1 ORDER BY updated_at DESC LIMIT 50',
        [userId]
      ),
      this.pgPool.query(
        'SELECT * FROM tracked_entities WHERE user_id = $1 ORDER BY last_seen_at DESC LIMIT 20',
        [userId]
      ),
      this.pgPool.query(
        `SELECT * FROM conversations 
         WHERE user_id = $1 AND status = 'active'
         ORDER BY started_at DESC LIMIT 5`,
        [userId]
      )
    ]);
    
    // Store in Redis for fast access
    const memory = new WorkflowMemory();
    await memory.redis.setex(
      `preload:${userId}`,
      300, // 5 minutes
      JSON.stringify({
        facts: facts.rows,
        entities: entities.rows,
        conversations: recentConvos.rows
      })
    );
    
    return { preloaded: true };
  }

  // Connection pooling optimization
  async optimizeConnections() {
    // Monitor connection usage
    const stats = await this.pgPool.query('SELECT * FROM pg_stat_activity');
    
    // Adjust pool size based on load
    const activeConnections = stats.rows.filter(r => r.state === 'active').length;
    
    if (activeConnections > this.pgPool.options.max * 0.8) {
      console.warn('High connection usage detected');
      // Implement circuit breaker or queue
    }
    
    return { activeConnections };
  }
}

module.exports = { MemoryPerformanceOptimizer };

Error Handling and Resilience

// memory-resilience.js
class ResilientMemory {
  constructor(primaryMemory, fallbackMemory) {
    this.primary = primaryMemory;
    this.fallback = fallbackMemory;
    this.circuitBreaker = new Map();
    this.failureThreshold = 5;
  }

  async executeWithFallback(operation, ...args) {
    const operationKey = operation;
    
    // Check circuit breaker
    if (this.isCircuitOpen(operationKey)) {
      console.log(`Circuit open for ${operationKey}, using fallback`);
      return this.fallback[operation](...args);
    }
    
    try {
      const result = await this.primary[operation](...args);
      this.recordSuccess(operationKey);
      return result;
    } catch (error) {
      this.recordFailure(operationKey);
      console.error(`Primary failed for ${operationKey}:`, error);
      
      // Try fallback
      try {
        const fallbackResult = await this.fallback[operation](...args);
        return { ...fallbackResult, _fallback: true };
      } catch (fallbackError) {
        throw new Error(`Both primary and fallback failed: ${fallbackError.message}`);
      }
    }
  }

  isCircuitOpen(operation) {
    const state = this.circuitBreaker.get(operation);
    if (!state) return false;
    
    if (state.failures >= this.failureThreshold) {
      // Check if enough time has passed to try again
      if (Date.now() - state.lastFailure > 60000) {
        this.circuitBreaker.delete(operation);
        return false;
      }
      return true;
    }
    
    return false;
  }

  recordFailure(operation) {
    const state = this.circuitBreaker.get(operation) || { failures: 0 };
    state.failures++;
    state.lastFailure = Date.now();
    this.circuitBreaker.set(operation, state);
  }

  recordSuccess(operation) {
    this.circuitBreaker.delete(operation);
  }

  // Wrapper methods
  async getContext(sessionId) {
    return this.executeWithFallback('getContext', sessionId);
  }

  async saveContext(sessionId, context, ttl) {
    return this.executeWithFallback('saveContext', sessionId, context, ttl);
  }
}

module.exports = { ResilientMemory };

Best Practices and Design Patterns

Memory Design Principles

  1. Contextual Relevance: Store only what's needed for the current context
  2. Progressive Disclosure: Start with minimal memory, expand as needed
  3. Privacy First: Implement TTL and expiration for sensitive data
  4. Explicit Consent: Allow users to manage what's remembered
  5. Graceful Degradation: Workflows should function even with memory failures

Anti-Patterns to Avoid

// ❌ BAD: Storing everything
await memory.save(sessionId, {
  ...userData,  // Too much!
  ...rawApiResponse,  // Unnecessary
  ...internalState  // Shouldn't be persisted
});

// ✅ GOOD: Store only what's needed
await memory.saveContext(sessionId, {
  currentIntent: extracted.intent,
  relevantEntities: extracted.entities,
  userPreferences: userData.preferences,
  lastAction: action.type
});
// ❌ BAD: No TTL on sensitive data
await redis.set(`user:${userId}:ssn`, ssn);  // Never expires!

// ✅ GOOD: Always set expiration
await redis.setex(`user:${userId}:session`, 3600, sessionData);

Security Considerations

// security-checks.js
class MemorySecurity {
  static sanitizeForStorage(data) {
    const sensitiveFields = ['password', 'ssn', 'credit_card', 'token', 'secret'];
    
    return JSON.parse(JSON.stringify(data, (key, value) => {
      if (sensitiveFields.some(f => key.toLowerCase().includes(f))) {
        return '[REDACTED]';
      }
      return value;
    }));
  }

  static encrypt(data, encryptionKey) {
    const crypto = require('crypto');
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher('aes-256-gcm', encryptionKey);
    
    let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    return {
      iv: iv.toString('hex'),
      data: encrypted,
      authTag: cipher.getAuthTag().toString('hex')
    };
  }

  static validateAccess(userId, resourceOwnerId, requiredRole = 'owner') {
    if (userId === resourceOwnerId) return true;
    if (requiredRole === 'admin') return checkAdmin(userId);
    return false;
  }
}

module.exports = { MemorySecurity };

Conclusion: The Memory-Enabled Future

The shift from stateless to stateful AI workflows represents one of the most significant evolutions in automation since the advent of visual workflow builders. Organizations that master memory and context persistence gain a decisive advantage: the ability to build AI agents that truly understand their users, learn from interactions, and deliver increasingly personalized experiences over time.

Key Takeaways:

  1. Tier Your Memory: Use Redis for working memory, PostgreSQL for long-term storage, and vector stores for semantic knowledge
  2. Design for Context: Build workflows that retrieve and utilize relevant context at every interaction
  3. Prioritize Privacy: Implement TTL, encryption, and user controls for all memory systems
  4. Monitor and Optimize: Track memory hit rates, latency, and storage costs
  5. Plan for Scale: Architect memory systems that grow with your automation needs

The Bottom Line:

Memory isn't just a technical feature—it's what transforms automation from transaction processing to relationship building. When your workflows remember, they become more than tools; they become partners.

The infrastructure is ready, the patterns are proven, and the opportunity is clear. It's time to give your AI agents the gift of memory.


Additional Resources

Official Documentation

Community Resources

Tools and Libraries


Ready to build memory-enabled workflows for your business? Contact Tropical Media for expert consultation and implementation support.

Tags: AI Memory, n8n, Context Persistence, Redis, PostgreSQL, Vector Stores, Workflow Automation, RAG, AI Agents, Chat Memory, State Management, Production Patterns