Files
openclaw/src/state/openclaw-agent-schema.sql
Peter Steinberger 6fc564278d feat(agents): sessions_search full-text recall over past session transcripts (#105057)
Adds a core sessions_search agent tool backed by a SQLite FTS5 index that
lives next to the transcript rows in the per-agent database. Indexing is
transactional: user/assistant text is indexed inside the same write
transaction that persists the event, deletes drop index rows in the same
transaction, and branch rewinds mark the session dirty so the next search
rebuilds it from the same visible-path resolution sessions_history uses
(which also lazily backfills doctor-migrated databases). Search executes
gateway-side behind an additive sessions.search method with a bounded
session-key allowlist; visibility, sandbox restrictions, and snippet
redaction mirror sessions_history.

Closes #100978
2026-07-12 09:15:41 +01:00

307 lines
9.3 KiB
SQL

CREATE TABLE IF NOT EXISTS schema_meta (
meta_key TEXT NOT NULL PRIMARY KEY,
role TEXT NOT NULL,
schema_version INTEGER NOT NULL,
agent_id TEXT,
app_version TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT NOT NULL PRIMARY KEY,
session_key TEXT NOT NULL,
session_scope TEXT NOT NULL DEFAULT 'conversation' CHECK (session_scope IN ('conversation', 'shared-main', 'group', 'channel')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
transcript_updated_at INTEGER DEFAULT NULL,
transcript_observed_at INTEGER DEFAULT NULL,
started_at INTEGER,
ended_at INTEGER,
status TEXT CHECK (status IS NULL OR status IN ('running', 'done', 'failed', 'killed', 'timeout')),
chat_type TEXT CHECK (chat_type IS NULL OR chat_type IN ('direct', 'group', 'channel')),
channel TEXT,
account_id TEXT,
primary_conversation_id TEXT,
model_provider TEXT,
model TEXT,
agent_harness_id TEXT,
parent_session_key TEXT,
spawned_by TEXT,
display_name TEXT,
FOREIGN KEY (primary_conversation_id) REFERENCES conversations(conversation_id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_sessions_updated_at
ON sessions(updated_at DESC, session_id);
CREATE INDEX IF NOT EXISTS idx_agent_sessions_created_at
ON sessions(created_at DESC, session_id);
CREATE INDEX IF NOT EXISTS idx_agent_sessions_conversation
ON sessions(primary_conversation_id, updated_at DESC, session_id)
WHERE primary_conversation_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS session_routes (
session_key TEXT NOT NULL PRIMARY KEY,
session_id TEXT NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_session_routes_session_id
ON session_routes(session_id);
CREATE TABLE IF NOT EXISTS conversations (
conversation_id TEXT NOT NULL PRIMARY KEY,
channel TEXT NOT NULL,
account_id TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('direct', 'group', 'channel')),
peer_id TEXT NOT NULL,
parent_conversation_id TEXT,
thread_id TEXT,
native_channel_id TEXT,
native_direct_user_id TEXT,
label TEXT,
metadata_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_conversations_lookup
ON conversations(channel, account_id, kind, peer_id, thread_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_conversations_identity
ON conversations(
channel,
account_id,
kind,
peer_id,
IFNULL(parent_conversation_id, ''),
IFNULL(thread_id, '')
);
CREATE INDEX IF NOT EXISTS idx_agent_conversations_updated
ON conversations(updated_at DESC, conversation_id);
CREATE TABLE IF NOT EXISTS session_conversations (
session_id TEXT NOT NULL,
conversation_id TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'primary' CHECK (role IN ('primary', 'participant', 'related')),
first_seen_at INTEGER NOT NULL,
last_seen_at INTEGER NOT NULL,
PRIMARY KEY (session_id, conversation_id, role),
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_session_conversations_conversation
ON session_conversations(conversation_id, last_seen_at DESC, session_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_session_conversations_primary
ON session_conversations(session_id)
WHERE role = 'primary';
CREATE TABLE IF NOT EXISTS session_entries (
session_key TEXT NOT NULL PRIMARY KEY,
session_id TEXT NOT NULL,
entry_json TEXT NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_session_entries_updated_at
ON session_entries(updated_at DESC, session_key);
CREATE INDEX IF NOT EXISTS idx_agent_session_entries_session_updated
ON session_entries(session_id, updated_at DESC, session_key);
CREATE TABLE IF NOT EXISTS transcript_events (
session_id TEXT NOT NULL,
seq INTEGER NOT NULL,
event_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (session_id, seq),
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS trajectory_runtime_events (
session_id TEXT NOT NULL,
seq INTEGER NOT NULL,
run_id TEXT,
event_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (session_id, seq),
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_trajectory_runtime_run
ON trajectory_runtime_events(session_id, run_id, seq)
WHERE run_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS transcript_event_identities (
session_id TEXT NOT NULL,
event_id TEXT NOT NULL,
seq INTEGER NOT NULL,
event_type TEXT,
parent_id TEXT,
message_idempotency_key TEXT,
created_at INTEGER NOT NULL,
PRIMARY KEY (session_id, event_id),
FOREIGN KEY (session_id, seq) REFERENCES transcript_events(session_id, seq) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_transcript_message_idempotency
ON transcript_event_identities(session_id, message_idempotency_key)
WHERE message_idempotency_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_agent_transcript_event_parent
ON transcript_event_identities(session_id, parent_id)
WHERE parent_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_agent_transcript_event_sequence
ON transcript_event_identities(session_id, event_type, seq DESC);
CREATE TABLE IF NOT EXISTS cache_entries (
scope TEXT NOT NULL,
key TEXT NOT NULL,
value_json TEXT,
blob BLOB,
expires_at INTEGER,
updated_at INTEGER NOT NULL,
PRIMARY KEY (scope, key)
);
CREATE INDEX IF NOT EXISTS idx_agent_cache_expiry
ON cache_entries(scope, expires_at, key)
WHERE expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_agent_cache_updated
ON cache_entries(scope, updated_at DESC, key);
CREATE TABLE IF NOT EXISTS auth_profile_store (
store_key TEXT NOT NULL PRIMARY KEY,
store_json TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS auth_profile_state (
state_key TEXT NOT NULL PRIMARY KEY,
state_json TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS memory_index_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS memory_index_sources (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'memory',
hash TEXT NOT NULL,
mtime INTEGER NOT NULL,
size INTEGER NOT NULL,
UNIQUE (path, source)
);
CREATE TABLE IF NOT EXISTS memory_index_chunks (
id TEXT PRIMARY KEY,
path TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'memory',
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
hash TEXT NOT NULL,
model TEXT NOT NULL,
text TEXT NOT NULL,
embedding TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS memory_embedding_cache (
provider TEXT NOT NULL,
model TEXT NOT NULL,
provider_key TEXT NOT NULL,
hash TEXT NOT NULL,
embedding TEXT NOT NULL,
dims INTEGER,
updated_at INTEGER NOT NULL,
PRIMARY KEY (provider, model, provider_key, hash)
);
CREATE TABLE IF NOT EXISTS memory_index_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
revision INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS session_transcript_index_state (
session_id TEXT NOT NULL PRIMARY KEY,
indexed_seq INTEGER NOT NULL,
leaf_event_id TEXT,
needs_rebuild INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
CREATE VIRTUAL TABLE IF NOT EXISTS session_transcript_fts USING fts5(
text,
session_id UNINDEXED,
message_id UNINDEXED,
role UNINDEXED,
timestamp UNINDEXED,
tokenize = 'unicode61 remove_diacritics 2'
);
INSERT OR IGNORE INTO memory_index_state (id, revision) VALUES (1, 0);
CREATE TRIGGER IF NOT EXISTS memory_index_sources_revision_after_insert
AFTER INSERT ON memory_index_sources
BEGIN
UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1;
END;
CREATE TRIGGER IF NOT EXISTS memory_index_sources_revision_after_update
AFTER UPDATE ON memory_index_sources
BEGIN
UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1;
END;
CREATE TRIGGER IF NOT EXISTS memory_index_sources_revision_after_delete
AFTER DELETE ON memory_index_sources
BEGIN
UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1;
END;
CREATE TRIGGER IF NOT EXISTS memory_index_chunks_revision_after_insert
AFTER INSERT ON memory_index_chunks
BEGIN
UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1;
END;
CREATE TRIGGER IF NOT EXISTS memory_index_chunks_revision_after_update
AFTER UPDATE ON memory_index_chunks
BEGIN
UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1;
END;
CREATE TRIGGER IF NOT EXISTS memory_index_chunks_revision_after_delete
AFTER DELETE ON memory_index_chunks
BEGIN
UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1;
END;
CREATE INDEX IF NOT EXISTS idx_memory_embedding_cache_updated_at
ON memory_embedding_cache(updated_at);
CREATE INDEX IF NOT EXISTS idx_memory_index_sources_source
ON memory_index_sources(source);
CREATE INDEX IF NOT EXISTS idx_memory_index_chunks_path_source
ON memory_index_chunks(path, source);
CREATE INDEX IF NOT EXISTS idx_memory_index_chunks_path
ON memory_index_chunks(path);
CREATE INDEX IF NOT EXISTS idx_memory_index_chunks_source
ON memory_index_chunks(source);