110 lines
4.9 KiB
PL/PgSQL
110 lines
4.9 KiB
PL/PgSQL
-- Phase 2: durable, per-agent memory with local pgvector embeddings.
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
|
|
ALTER TABLE bots
|
|
ADD COLUMN IF NOT EXISTS memory_enabled BOOLEAN NOT NULL DEFAULT TRUE;
|
|
|
|
-- Enables a scope-preserving foreign key without changing the existing primary key.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS bots_id_space_user_key
|
|
ON bots (id, space_id, user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS memory_items (
|
|
id UUID PRIMARY KEY,
|
|
space_id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
bot_id TEXT NOT NULL,
|
|
session_id TEXT,
|
|
source_run_id TEXT,
|
|
source_message_id TEXT,
|
|
content TEXT NOT NULL CHECK (length(btrim(content)) > 0),
|
|
importance REAL NOT NULL DEFAULT 0.5 CHECK (importance >= 0 AND importance <= 1),
|
|
embedding vector(384),
|
|
search_document TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED,
|
|
revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted_at TIMESTAMPTZ,
|
|
CONSTRAINT memory_items_bot_scope_fk
|
|
FOREIGN KEY (bot_id, space_id, user_id)
|
|
REFERENCES bots (id, space_id, user_id) ON DELETE CASCADE,
|
|
CONSTRAINT memory_items_session_fk
|
|
FOREIGN KEY (session_id) REFERENCES threads (id) ON DELETE SET NULL,
|
|
CONSTRAINT memory_items_source_run_fk
|
|
FOREIGN KEY (source_run_id) REFERENCES runs (id) ON DELETE SET NULL,
|
|
CONSTRAINT memory_items_source_message_fk
|
|
FOREIGN KEY (source_message_id) REFERENCES messages (id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS memory_revisions (
|
|
memory_id UUID NOT NULL REFERENCES memory_items (id) ON DELETE CASCADE,
|
|
revision INTEGER NOT NULL CHECK (revision > 0),
|
|
space_id TEXT NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
bot_id TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
importance REAL NOT NULL CHECK (importance >= 0 AND importance <= 1),
|
|
session_id TEXT,
|
|
source_run_id TEXT,
|
|
source_message_id TEXT,
|
|
action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete', 'restore')),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (memory_id, revision),
|
|
CONSTRAINT memory_revisions_bot_scope_fk
|
|
FOREIGN KEY (bot_id, space_id, user_id)
|
|
REFERENCES bots (id, space_id, user_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS memory_items_scope_active_idx
|
|
ON memory_items (space_id, user_id, bot_id, updated_at DESC)
|
|
WHERE deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS memory_items_session_idx
|
|
ON memory_items (session_id) WHERE session_id IS NOT NULL AND deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS memory_items_source_run_idx
|
|
ON memory_items (source_run_id) WHERE source_run_id IS NOT NULL;
|
|
CREATE INDEX IF NOT EXISTS memory_items_source_message_idx
|
|
ON memory_items (source_message_id) WHERE source_message_id IS NOT NULL;
|
|
CREATE INDEX IF NOT EXISTS memory_items_search_idx
|
|
ON memory_items USING GIN (search_document);
|
|
CREATE INDEX IF NOT EXISTS memory_items_embedding_hnsw_idx
|
|
ON memory_items USING hnsw (embedding vector_cosine_ops)
|
|
WHERE embedding IS NOT NULL AND deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS memory_revisions_scope_idx
|
|
ON memory_revisions (space_id, user_id, bot_id, memory_id, revision DESC);
|
|
|
|
-- Optional source references must belong to the same actor, bot, and (when supplied)
|
|
-- session. This prevents accidental cross-agent linkage even from future callers.
|
|
CREATE OR REPLACE FUNCTION validate_memory_item_scope() RETURNS trigger AS $$
|
|
BEGIN
|
|
IF NEW.session_id IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM threads t
|
|
WHERE t.id = NEW.session_id AND t.bot_id = NEW.bot_id
|
|
AND t.space_id = NEW.space_id AND t.user_id = NEW.user_id
|
|
) THEN
|
|
RAISE EXCEPTION 'memory session is outside agent scope';
|
|
END IF;
|
|
IF NEW.source_run_id IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM runs r
|
|
WHERE r.id = NEW.source_run_id AND r.bot_id = NEW.bot_id
|
|
AND r.space_id = NEW.space_id AND r.user_id = NEW.user_id
|
|
AND (NEW.session_id IS NULL OR r.thread_id = NEW.session_id)
|
|
) THEN
|
|
RAISE EXCEPTION 'memory run is outside agent scope';
|
|
END IF;
|
|
IF NEW.source_message_id IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM messages m
|
|
JOIN threads t ON t.id = m.thread_id
|
|
WHERE m.id = NEW.source_message_id AND t.bot_id = NEW.bot_id
|
|
AND t.space_id = NEW.space_id AND t.user_id = NEW.user_id
|
|
AND (NEW.session_id IS NULL OR t.id = NEW.session_id)
|
|
) THEN
|
|
RAISE EXCEPTION 'memory message is outside agent scope';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS memory_items_scope_guard ON memory_items;
|
|
CREATE TRIGGER memory_items_scope_guard
|
|
BEFORE INSERT OR UPDATE OF space_id, user_id, bot_id, session_id, source_run_id, source_message_id
|
|
ON memory_items FOR EACH ROW EXECUTE FUNCTION validate_memory_item_scope();
|