File size: 24,302 Bytes
729bfb8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 | -- sibyl-memory-client SQLite schema v1
--
-- Port of the canonical sibyl_memory.* Postgres schema (scripts/sibyl-memory-schema.sql,
-- applied to Neon 2026-05-01) to SQLite for the local-first plugin v1.
--
-- Dialect translations:
-- UUID → TEXT (Python uuid.uuid4() at write-time)
-- JSONB → TEXT with CHECK(json_valid(col)) using SQLite json1
-- TIMESTAMPTZ + now() → TEXT ISO 8601 UTC via strftime('%Y-%m-%dT%H:%M:%fZ','now')
-- gin jsonb_path_ops → SQLite json_extract expression indexes where useful
-- NUMERIC → REAL (sufficient precision for plugin v1 use)
-- tsvector → FTS5 virtual tables for text search
--
-- Multi-tenant: every table carries tenant_id. Local-first means typically
-- one tenant per machine, but the schema accepts N tenants (paid Team-tier
-- federation forward-compatible).
--
-- Idempotent. Apply via CREATE TABLE IF NOT EXISTS. Schema version recorded
-- in sibyl_memory_schema_version for future migrations.
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
-- ============================================================================
-- WARM tier: entities (single source of truth per rule 43)
-- ============================================================================
CREATE TABLE IF NOT EXISTS entities (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
category TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT,
body TEXT NOT NULL CHECK (json_valid(body)),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (tenant_id, category, name)
);
CREATE INDEX IF NOT EXISTS entities_tenant_cat_status
ON entities (tenant_id, category, status);
CREATE INDEX IF NOT EXISTS entities_updated_at
ON entities (tenant_id, updated_at DESC);
-- ============================================================================
-- Cross-references: typed relations between entities
-- ============================================================================
CREATE TABLE IF NOT EXISTS entity_relations (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
from_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
to_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
relation_type TEXT NOT NULL,
metadata TEXT CHECK (metadata IS NULL OR json_valid(metadata)),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS entity_relations_from
ON entity_relations (tenant_id, from_id, relation_type);
CREATE INDEX IF NOT EXISTS entity_relations_to
ON entity_relations (tenant_id, to_id, relation_type);
-- ============================================================================
-- HOT tier: state documents (treasury, priorities, session, index analogs)
-- ============================================================================
CREATE TABLE IF NOT EXISTS state_documents (
tenant_id TEXT NOT NULL,
document_key TEXT NOT NULL,
body TEXT NOT NULL CHECK (json_valid(body)),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (tenant_id, document_key)
);
-- ============================================================================
-- COLD tier: append-only journal of events
-- ============================================================================
CREATE TABLE IF NOT EXISTS journal_events (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
ts TEXT NOT NULL,
evaluated TEXT CHECK (evaluated IS NULL OR json_valid(evaluated)),
acted TEXT CHECK (acted IS NULL OR json_valid(acted)),
forward TEXT CHECK (forward IS NULL OR json_valid(forward)),
extra TEXT CHECK (extra IS NULL OR json_valid(extra))
);
CREATE INDEX IF NOT EXISTS journal_events_tenant_ts
ON journal_events (tenant_id, ts DESC);
-- ============================================================================
-- COLD tier: revenue events with optional entity ref
-- ============================================================================
CREATE TABLE IF NOT EXISTS revenue_events (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
ts TEXT NOT NULL,
event_type TEXT,
gross_usd REAL,
operator_share_usd REAL,
source TEXT,
tx TEXT,
entity_id TEXT REFERENCES entities(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS revenue_events_tenant_ts
ON revenue_events (tenant_id, ts DESC);
-- ============================================================================
-- COLD tier: error events
-- ============================================================================
CREATE TABLE IF NOT EXISTS error_events (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
ts TEXT NOT NULL,
code TEXT,
message TEXT,
context TEXT CHECK (context IS NULL OR json_valid(context))
);
CREATE INDEX IF NOT EXISTS error_events_tenant_ts
ON error_events (tenant_id, ts DESC);
-- ============================================================================
-- REFERENCE tier: static documents (markdown bodies, lookup-only)
-- ============================================================================
CREATE TABLE IF NOT EXISTS reference_documents (
tenant_id TEXT NOT NULL,
doc_key TEXT NOT NULL,
body TEXT,
metadata TEXT CHECK (metadata IS NULL OR json_valid(metadata)),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (tenant_id, doc_key)
);
-- ============================================================================
-- ARCHIVE tier: frozen entities (out of working set, retrievable)
-- ============================================================================
CREATE TABLE IF NOT EXISTS archived_entities (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
original_entity_id TEXT,
category TEXT,
name TEXT,
body TEXT CHECK (body IS NULL OR json_valid(body)),
archived_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
archive_reason TEXT
);
CREATE INDEX IF NOT EXISTS archived_entities_tenant_cat
ON archived_entities (tenant_id, category, name);
-- ============================================================================
-- FLAGGED tier: actors flagged for social-engineering / fraud (rule 13/14/15)
-- ============================================================================
CREATE TABLE IF NOT EXISTS flagged_actors (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
actor_handle TEXT,
actor_address TEXT,
flagged_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
reason TEXT,
evidence TEXT CHECK (evidence IS NULL OR json_valid(evidence))
);
CREATE INDEX IF NOT EXISTS flagged_actors_tenant
ON flagged_actors (tenant_id);
-- ============================================================================
-- FTS5 virtual tables for full-text retrieval (the tsvector port)
-- ============================================================================
-- v3 (2026-05-18): all FTS5 tables now use external-content (or contentless
-- for journal). Body lives in the base table, FTS5 stores only the index.
-- Triggers fire transparently. Disk footprint stays flat (vs v2's 2x dup).
-- Cross-tier search lands here: entities + state + reference + journal.
-- v2 → v3 migration is handled in storage.py:_migrate_to_v3.
-- ENTITIES: external-content FTS5 over entities table
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
name, category, body, tenant_id UNINDEXED,
content='entities', content_rowid='rowid',
tokenize = 'porter unicode61'
);
CREATE TRIGGER IF NOT EXISTS entities_ai_fts
AFTER INSERT ON entities BEGIN
INSERT INTO entities_fts(rowid, name, category, body, tenant_id)
VALUES (new.rowid, new.name, new.category, new.body, new.tenant_id);
END;
CREATE TRIGGER IF NOT EXISTS entities_ad_fts
AFTER DELETE ON entities BEGIN
INSERT INTO entities_fts(entities_fts, rowid, name, category, body, tenant_id)
VALUES ('delete', old.rowid, old.name, old.category, old.body, old.tenant_id);
END;
CREATE TRIGGER IF NOT EXISTS entities_au_fts
AFTER UPDATE ON entities BEGIN
INSERT INTO entities_fts(entities_fts, rowid, name, category, body, tenant_id)
VALUES ('delete', old.rowid, old.name, old.category, old.body, old.tenant_id);
INSERT INTO entities_fts(rowid, name, category, body, tenant_id)
VALUES (new.rowid, new.name, new.category, new.body, new.tenant_id);
END;
-- STATE: external-content FTS5 over state_documents
CREATE VIRTUAL TABLE IF NOT EXISTS state_documents_fts USING fts5(
document_key, body, tenant_id UNINDEXED,
content='state_documents', content_rowid='rowid',
tokenize = 'porter unicode61'
);
CREATE TRIGGER IF NOT EXISTS state_documents_ai_fts
AFTER INSERT ON state_documents BEGIN
INSERT INTO state_documents_fts(rowid, document_key, body, tenant_id)
VALUES (new.rowid, new.document_key, new.body, new.tenant_id);
END;
CREATE TRIGGER IF NOT EXISTS state_documents_ad_fts
AFTER DELETE ON state_documents BEGIN
INSERT INTO state_documents_fts(state_documents_fts, rowid, document_key, body, tenant_id)
VALUES ('delete', old.rowid, old.document_key, old.body, old.tenant_id);
END;
CREATE TRIGGER IF NOT EXISTS state_documents_au_fts
AFTER UPDATE ON state_documents BEGIN
INSERT INTO state_documents_fts(state_documents_fts, rowid, document_key, body, tenant_id)
VALUES ('delete', old.rowid, old.document_key, old.body, old.tenant_id);
INSERT INTO state_documents_fts(rowid, document_key, body, tenant_id)
VALUES (new.rowid, new.document_key, new.body, new.tenant_id);
END;
-- REFERENCE: external-content FTS5 over reference_documents
CREATE VIRTUAL TABLE IF NOT EXISTS reference_documents_fts USING fts5(
doc_key, body, tenant_id UNINDEXED,
content='reference_documents', content_rowid='rowid',
tokenize = 'porter unicode61'
);
CREATE TRIGGER IF NOT EXISTS reference_ai_fts
AFTER INSERT ON reference_documents BEGIN
INSERT INTO reference_documents_fts(rowid, doc_key, body, tenant_id)
VALUES (new.rowid, new.doc_key, new.body, new.tenant_id);
END;
CREATE TRIGGER IF NOT EXISTS reference_ad_fts
AFTER DELETE ON reference_documents BEGIN
INSERT INTO reference_documents_fts(reference_documents_fts, rowid, doc_key, body, tenant_id)
VALUES ('delete', old.rowid, old.doc_key, old.body, old.tenant_id);
END;
CREATE TRIGGER IF NOT EXISTS reference_au_fts
AFTER UPDATE ON reference_documents BEGIN
INSERT INTO reference_documents_fts(reference_documents_fts, rowid, doc_key, body, tenant_id)
VALUES ('delete', old.rowid, old.doc_key, old.body, old.tenant_id);
INSERT INTO reference_documents_fts(rowid, doc_key, body, tenant_id)
VALUES (new.rowid, new.doc_key, new.body, new.tenant_id);
END;
-- JOURNAL: standalone FTS5 over journal_events (concatenated payload).
-- Standalone (not external-content) because journal_events has 4 separate
-- JSON payload columns we want searchable as one concatenated field, and
-- there's no single base-table column we could external-content against.
-- Acceptable cost: journal is append-only (no updates), so the body
-- duplication doesn't compound on edits like it would on warm entities.
CREATE VIRTUAL TABLE IF NOT EXISTS journal_events_fts USING fts5(
ts UNINDEXED, payload, tenant_id UNINDEXED, event_id UNINDEXED,
tokenize = 'porter unicode61'
);
CREATE TRIGGER IF NOT EXISTS journal_events_ai_fts
AFTER INSERT ON journal_events BEGIN
INSERT INTO journal_events_fts(rowid, ts, payload, tenant_id, event_id)
VALUES (
new.rowid, new.ts,
COALESCE(new.evaluated, '') || ' ' || COALESCE(new.acted, '') || ' ' ||
COALESCE(new.forward, '') || ' ' || COALESCE(new.extra, ''),
new.tenant_id, new.id
);
END;
-- ============================================================================
-- Schema version tracking
-- ============================================================================
CREATE TABLE IF NOT EXISTS sibyl_memory_schema_version (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
description TEXT
);
INSERT OR IGNORE INTO sibyl_memory_schema_version (version, description)
VALUES (1, 'sibyl-memory-client v1. SQLite port of sibyl_memory.* Postgres schema. 10 tables (entities, entity_relations, state_documents, journal_events, revenue_events, error_events, reference_documents, archived_entities, flagged_actors, schema_version) + 2 FTS5 virtual tables. Local-first plugin foundation.');
-- ============================================================================
-- Schema v2 — self-learning skill proposals (review queue)
-- ============================================================================
-- The Learner module scans journal_events for repeating patterns and writes
-- proposed skill documents here. The user reviews via `sibyl learn review`
-- and either accepts (which writes to reference_documents under skill/<slug>)
-- or rejects. Idempotent; safe to re-apply against a v1 database.
CREATE TABLE IF NOT EXISTS skill_proposals (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
-- detector output
pattern_kind TEXT NOT NULL, -- 'repeated_action' / 'structural_similarity' / 'temporal_routine' / 'co_occurrence'
proposed_slug TEXT NOT NULL, -- the reference_documents.doc_key it would land under (skill/<slug>)
proposed_title TEXT, -- one-line human-readable title
proposed_body TEXT NOT NULL, -- the actual skill body (markdown text)
-- evidence + provenance
evidence TEXT NOT NULL CHECK (json_valid(evidence)), -- list of source journal_event ids + snippets
confidence REAL NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
summarizer TEXT NOT NULL, -- 'local-deterministic' / 'byok-anthropic' / 'byok-openai' / 'venice-x402' / etc.
-- review state
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'superseded')),
reviewed_at TEXT,
review_note TEXT,
-- when accepted, points at the reference_documents row that was created
accepted_doc_key TEXT
);
CREATE INDEX IF NOT EXISTS skill_proposals_tenant_status
ON skill_proposals (tenant_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS skill_proposals_slug
ON skill_proposals (tenant_id, proposed_slug);
-- ============================================================================
-- Schema v2 — learning run log (so detectors don't re-scan ground they covered)
-- ============================================================================
CREATE TABLE IF NOT EXISTS learning_runs (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
completed_at TEXT,
summarizer TEXT NOT NULL,
events_scanned INTEGER NOT NULL DEFAULT 0,
proposals_made INTEGER NOT NULL DEFAULT 0,
cursor_after_ts TEXT, -- watermark — newest journal ts processed
notes TEXT
);
CREATE INDEX IF NOT EXISTS learning_runs_tenant
ON learning_runs (tenant_id, started_at DESC);
INSERT OR IGNORE INTO sibyl_memory_schema_version (version, description)
VALUES (2, 'sibyl-memory-client v2. Adds skill_proposals (self-learning review queue) and learning_runs (detector watermark log). Idempotent migration; v1 databases auto-upgrade on first open. Free tier uses local-deterministic summarizer; paid tier can opt into BYOK or Venice/x402-routed summarization.');
INSERT OR IGNORE INTO sibyl_memory_schema_version (version, description)
VALUES (3, 'sibyl-memory-client v3. External-content FTS5 across entities + state_documents + reference_documents + contentless FTS5 over journal_events. Fixes the v0.3.0 "search covers warm entities only" bug. Eliminates body duplication (v2 stored body twice — base table + FTS5). v2 to v3 migration handled in storage.py:_migrate_to_v3: drops the standalone FTS5 tables and rebuilds in external-content shape from existing base-table data. No data loss.');
-- ============================================================================
-- Schema v4 — folded-trigram search shadow (v0.5.0 multi-language search)
-- ============================================================================
-- The standalone ``search_shadow`` FTS5 trigram table + its 10 maintenance
-- triggers are NOT declared here: their DDL is GENERATED at runtime by
-- sibyl_memory_client/shadow.py, because two pieces cannot live in a static file
-- — (a) the fold map (the ł/ß/ø/... non-decomposable → ASCII rendering baked into
-- the trigger + backfill SQL, single source of truth in shadow.FOLD_MAP), and
-- (b) the tokenizer clause, which is selected at runtime across the SQLite 3.45
-- boundary ('trigram remove_diacritics 1' on >= 3.45, bare 'trigram' below).
-- The shadow holds a FOLDED copy of the searchable text across all four tiers and
-- is consulted ONLY as a zero-hit fallback (client.py search()), giving substring
-- + non-decomposable-fold matching for scripts the porter-unicode61 primary index
-- cannot reach. v3 → v4 migration + crash-atomic marker + heal: storage.py
-- (_migrate_if_needed, _SHADOW_MARKER). Rollback to exact v3 behaviour: DROP the
-- 10 *_shadow triggers + DROP TABLE search_shadow + PRAGMA user_version = 3.
INSERT OR IGNORE INTO sibyl_memory_schema_version (version, description)
VALUES (4, 'sibyl-memory-client v4. Adds the standalone folded-trigram search_shadow table + maintenance triggers (DDL generated by shadow.py — fold map + runtime tokenizer clause cannot live in a static file). A FOLDED copy of searchable text across all four tiers, maintained by DB-side triggers, consulted only as a zero-hit fallback in MemoryClient.search() to give substring + non-decomposable-diacritic (ł ß ø æ đ ı œ þ ð) matching for the scripts the porter-unicode61 primary index cannot reach. Strictly additive: primary results are byte-identical to v3. v3 → v4 migration + crash-atomic PRAGMA user_version marker + portability/heal handled in storage.py:_migrate_if_needed. Derived state, always rebuildable from the base tables; base data untouched. Rollback: DROP the *_shadow triggers + search_shadow, PRAGMA user_version = 3.');
-- ============================================================================
-- Schema v5 (0.9.0) - pre-sign-in adoption audit (not tenant-scoped)
-- ============================================================================
-- One row per adoption of DEFAULT_TENANT rows into an account tenant
-- (_adopt.py), written in the SAME transaction as the move, plus a 'seen' row
-- the first time a verified account uses this store (so automatic adoption runs
-- for an account's FIRST sign-in only, and never merges into a different
-- account once another one has used this store). Counts and renamed business
-- keys only; never bodies. Local only; nothing reads it over the network.
CREATE TABLE IF NOT EXISTS tenant_adoptions (
id TEXT PRIMARY KEY,
from_tenant TEXT NOT NULL,
to_tenant TEXT NOT NULL,
account_id TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('auto', 'command', 'seen')),
moved_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
counts TEXT NOT NULL CHECK (json_valid(counts)),
renamed TEXT CHECK (renamed IS NULL OR json_valid(renamed))
);
CREATE INDEX IF NOT EXISTS tenant_adoptions_account
ON tenant_adoptions (account_id, to_tenant);
INSERT OR IGNORE INTO sibyl_memory_schema_version (version, description)
VALUES (5, 'sibyl-memory-client v5 (0.9.0). Adds tenant_adoptions: the local audit of moving pre-sign-in DEFAULT_TENANT rows into the signed-in account tenant (_adopt.py), written in the same transaction as the move. Additive; no existing table changes.');
-- ============================================================================
-- Schema v6 (spec tiered-retrieval 2.B) - memory_links: typed edges across tiers
-- ============================================================================
-- Edges between nodes addressed by BUSINESS KEYS (entity: category + name;
-- reference: doc_key; state: document_key), so they survive archive_entity and
-- re-creation, which id foreign keys with ON DELETE CASCADE would not. The
-- category columns are NOT NULL: entity endpoints always carry a non-empty
-- category and reference/state endpoints store '' (SQLite UNIQUE treats NULLs
-- as distinct, so a NULL would defeat the uniqueness of reference/state edges);
-- the CHECKs enforce (tier = 'entity') = (category <> '').
-- These statements are ALSO the Python constant _links.V6_DDL (a test asserts
-- the two are identical). A fresh store gets them here; a pre-v6 store gets
-- them from Storage._ensure_schema in ONE transaction with the one-time copy of
-- entity_relations rows, so the table's existence is the crash-atomic "copied"
-- marker. from_entity_id / to_entity_id stamp entity endpoints with the row id
-- the edge was written against (NULL for reference/state, or unknown); a key now
-- held by another id reads stale (_links.STALE_SIDE_SQL). Stores created before
-- the stamp get these columns by ALTER on open (_links.apply_identity_columns).
-- entity_relations stays in place, unwritten (frozen), until R4. Nothing gates on
-- the version-6 row below (another release may claim the same number).
CREATE TABLE IF NOT EXISTS memory_links (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
from_tier TEXT NOT NULL CHECK (from_tier IN ('entity','reference','state')),
from_category TEXT NOT NULL,
from_key TEXT NOT NULL,
to_tier TEXT NOT NULL CHECK (to_tier IN ('entity','reference','state')),
to_category TEXT NOT NULL,
to_key TEXT NOT NULL,
relation_type TEXT NOT NULL,
metadata TEXT CHECK (metadata IS NULL OR json_valid(metadata)),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
from_entity_id TEXT,
to_entity_id TEXT,
updated_at TEXT,
CHECK ((from_tier = 'entity') = (from_category <> '')),
CHECK ((to_tier = 'entity') = (to_category <> '')),
UNIQUE (tenant_id, from_tier, from_category, from_key, to_tier, to_category, to_key, relation_type)
);
CREATE INDEX IF NOT EXISTS memory_links_from
ON memory_links (tenant_id, from_tier, from_category, from_key, relation_type);
CREATE INDEX IF NOT EXISTS memory_links_to
ON memory_links (tenant_id, to_tier, to_category, to_key, relation_type);
CREATE INDEX IF NOT EXISTS memory_links_created
ON memory_links (tenant_id, created_at, id);
INSERT OR IGNORE INTO sibyl_memory_schema_version (version, description)
VALUES (6, 'sibyl-memory-client v6. Adds memory_links: typed, directed edges between entity / reference / state nodes keyed by business keys (link, unlink, neighbors, links_of, links= on set_entity and set_reference). Pre-v6 entity_relations rows are copied once, in the same transaction that creates the table; entity_relations stays, unwritten, until it is dropped in a later major release. Additive.');
-- ============================================================================
-- Stage-0 exact-name index (spec tiered-retrieval 2.B DDL / 2.C)
-- ============================================================================
-- multi_record_search's stage 0 resolves a query that IS an entity name with a
-- parameterised `lower(name) = ?` equality (no MATCH syntax). This expression
-- index makes that one probe, not a tenant scan. Idempotent and applied on
-- every open like the rest of this file, so existing stores get it on their
-- next open (one index build, no data change). Not part of _links.V6_DDL: it
-- belongs to search, not to the memory_links migration transaction.
CREATE INDEX IF NOT EXISTS entities_tenant_lname
ON entities (tenant_id, lower(name));
|