mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Implements persistent memory for agents with hybrid search: - Database-backed workspace with PostgreSQL (not filesystem) - Memory documents: MEMORY.md, daily logs, identity files - Chunked content with FTS (tsvector) + vector (pgvector) indexes - Reciprocal Rank Fusion (RRF) for hybrid search combining BM25 and semantic - Memory tools: memory_search, memory_write, memory_read - Proactive heartbeat system for periodic execution (30 min default) - OpenAI embeddings provider (text-embedding-3-small) Key patterns from OpenClaw: - "Memory is files, not RAM" - explicit persistence required - Two-tier memory: daily logs (raw) + curated MEMORY.md - Session isolation via user_id/agent_id scoping Co-Authored-By: Claude Opus 4.5 <[email protected]>
160 lines
5.8 KiB
PL/PgSQL
160 lines
5.8 KiB
PL/PgSQL
-- NEAR Agent Database Schema
|
|
-- V2: Workspace and memory system (OpenClaw-inspired)
|
|
--
|
|
-- This migration adds:
|
|
-- 1. Persistent memory documents (MEMORY.md, daily logs, identity files)
|
|
-- 2. Chunked content for hybrid search (FTS + vector)
|
|
-- 3. Heartbeat state for proactive execution
|
|
|
|
-- Enable pgvector extension for semantic search
|
|
-- NOTE: This requires pgvector to be installed on the PostgreSQL server
|
|
-- Install via: CREATE EXTENSION vector; (requires superuser or rds_superuser)
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
|
|
-- ==================== Memory Documents ====================
|
|
-- Stores full documents like MEMORY.md, daily logs, identity files
|
|
-- Think of this as the filesystem equivalent, but in PostgreSQL
|
|
|
|
CREATE TABLE memory_documents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- Ownership: who this document belongs to
|
|
user_id TEXT NOT NULL, -- User identifier (from channel)
|
|
agent_id UUID, -- NULL = shared across all agents for this user
|
|
|
|
-- Document type and content
|
|
doc_type TEXT NOT NULL, -- 'memory', 'daily_log', 'identity', 'soul', 'agents', 'user', 'heartbeat'
|
|
title TEXT, -- Optional title (e.g., date for daily logs)
|
|
content TEXT NOT NULL, -- Full document content
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Flexible metadata (tags, source, etc.)
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
|
|
-- Ensure one document per type per user (for singleton docs like MEMORY.md)
|
|
-- Daily logs use title as the date discriminator
|
|
CONSTRAINT unique_doc_per_user_type UNIQUE (user_id, agent_id, doc_type, title)
|
|
);
|
|
|
|
-- Indexes for common queries
|
|
CREATE INDEX idx_memory_documents_user ON memory_documents(user_id);
|
|
CREATE INDEX idx_memory_documents_user_type ON memory_documents(user_id, doc_type);
|
|
CREATE INDEX idx_memory_documents_updated ON memory_documents(updated_at DESC);
|
|
|
|
-- ==================== Memory Chunks ====================
|
|
-- Documents are chunked for search. Each chunk has:
|
|
-- 1. Full-text search vector (tsvector) for keyword matching
|
|
-- 2. Embedding vector for semantic similarity
|
|
--
|
|
-- Hybrid search combines both using Reciprocal Rank Fusion (RRF)
|
|
|
|
CREATE TABLE memory_chunks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
document_id UUID NOT NULL REFERENCES memory_documents(id) ON DELETE CASCADE,
|
|
|
|
-- Chunk position and content
|
|
chunk_index INT NOT NULL, -- Position in document (0-based)
|
|
content TEXT NOT NULL, -- Chunk text (~800 tokens with 15% overlap)
|
|
|
|
-- Full-text search: auto-generated tsvector
|
|
content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
|
|
|
|
-- Semantic search: embedding vector (OpenAI text-embedding-ada-002 = 1536 dims)
|
|
-- NULL until embeddings are generated
|
|
embedding VECTOR(1536),
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Each chunk index unique per document
|
|
CONSTRAINT unique_chunk_per_doc UNIQUE (document_id, chunk_index)
|
|
);
|
|
|
|
-- GIN index for full-text search
|
|
CREATE INDEX idx_memory_chunks_tsv ON memory_chunks USING GIN(content_tsv);
|
|
|
|
-- HNSW index for vector similarity (cosine distance)
|
|
-- HNSW is faster than IVFFlat for reads, slightly slower for writes
|
|
CREATE INDEX idx_memory_chunks_embedding ON memory_chunks
|
|
USING hnsw(embedding vector_cosine_ops)
|
|
WITH (m = 16, ef_construction = 64);
|
|
|
|
-- Index for document lookups
|
|
CREATE INDEX idx_memory_chunks_document ON memory_chunks(document_id);
|
|
|
|
-- ==================== Heartbeat State ====================
|
|
-- Tracks periodic heartbeat execution per user/agent
|
|
|
|
CREATE TABLE heartbeat_state (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
agent_id UUID, -- NULL = default agent for user
|
|
|
|
-- Timing
|
|
last_run TIMESTAMPTZ, -- When heartbeat last executed
|
|
next_run TIMESTAMPTZ, -- Scheduled next execution
|
|
interval_seconds INT NOT NULL DEFAULT 1800, -- 30 minutes default
|
|
|
|
-- State
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
consecutive_failures INT NOT NULL DEFAULT 0,
|
|
|
|
-- Last check timestamps (for batched monitoring)
|
|
-- e.g., {"email": "2024-01-15T10:00:00Z", "calendar": "2024-01-15T10:00:00Z"}
|
|
last_checks JSONB NOT NULL DEFAULT '{}',
|
|
|
|
-- Ensure one heartbeat config per user/agent
|
|
CONSTRAINT unique_heartbeat_per_user UNIQUE (user_id, agent_id)
|
|
);
|
|
|
|
CREATE INDEX idx_heartbeat_user ON heartbeat_state(user_id);
|
|
CREATE INDEX idx_heartbeat_next_run ON heartbeat_state(next_run) WHERE enabled = true;
|
|
|
|
-- ==================== Helper Functions ====================
|
|
|
|
-- Function to update updated_at timestamp
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ language 'plpgsql';
|
|
|
|
-- Trigger to auto-update updated_at on memory_documents
|
|
CREATE TRIGGER update_memory_documents_updated_at
|
|
BEFORE UPDATE ON memory_documents
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
-- ==================== Views ====================
|
|
|
|
-- View for documents with chunk counts (useful for debugging)
|
|
CREATE VIEW memory_documents_summary AS
|
|
SELECT
|
|
d.id,
|
|
d.user_id,
|
|
d.doc_type,
|
|
d.title,
|
|
d.created_at,
|
|
d.updated_at,
|
|
COUNT(c.id) as chunk_count,
|
|
COUNT(c.embedding) as embedded_chunk_count
|
|
FROM memory_documents d
|
|
LEFT JOIN memory_chunks c ON c.document_id = d.id
|
|
GROUP BY d.id;
|
|
|
|
-- View for pending embedding work
|
|
CREATE VIEW chunks_pending_embedding AS
|
|
SELECT
|
|
c.id as chunk_id,
|
|
c.document_id,
|
|
d.user_id,
|
|
d.doc_type,
|
|
LENGTH(c.content) as content_length
|
|
FROM memory_chunks c
|
|
JOIN memory_documents d ON d.id = c.document_id
|
|
WHERE c.embedding IS NULL;
|