UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

940 lines (934 loc) • 43.2 kB
import { a as runSqliteImmediateTransactionSync } from "./sqlite-transaction-DsZ3X6nD.mjs"; import { t as assertSqliteSchemaContains } from "./sqlite-schema-contract-BTxZz0OV.mjs"; import { t as migrateSqliteSchemaToStrict } from "./sqlite-strict-CYeSH78z.mjs"; import { t as formatErrorMessage } from "./error-utils-B15vDx2L.mjs"; import { n as decodeMemoryEmbedding, r as encodeMemoryEmbedding } from "./embedding-vector-DA_9H0FL.mjs"; //#region packages/memory-host-sdk/src/host/memory-schema-provenance.ts const MEMORY_INDEX_CHUNK_PROVENANCE_TABLE = "memory_index_chunk_provenance"; const MEMORY_INDEX_CHUNK_PROVENANCE_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS ${MEMORY_INDEX_CHUNK_PROVENANCE_TABLE} ( chunk_id TEXT PRIMARY KEY, origin_class TEXT NOT NULL CHECK (origin_class IN ('owner', 'agent', 'untrusted', 'system')), session_kind TEXT NOT NULL CHECK (session_kind IN ('interactive', 'cron', 'heartbeat', 'subagent', 'unknown')), observed_at INTEGER NOT NULL, supersedes_key TEXT, FOREIGN KEY (chunk_id) REFERENCES memory_index_chunks(id) ON DELETE CASCADE ) STRICT; `; const missingChunkIdsSql = ` SELECT chunk.id FROM memory_index_chunks AS chunk LEFT JOIN ${MEMORY_INDEX_CHUNK_PROVENANCE_TABLE} AS provenance ON provenance.chunk_id = chunk.id WHERE provenance.chunk_id IS NULL `; function ensureMemoryChunkProvenance(db) { const ensure = () => { db.exec("DROP TRIGGER IF EXISTS memory_index_chunk_provenance_after_insert"); db.exec(MEMORY_INDEX_CHUNK_PROVENANCE_SCHEMA_SQL); db.exec(` WITH missing AS MATERIALIZED (${missingChunkIdsSql}), affected AS MATERIALIZED ( SELECT chunk.path, chunk.source FROM missing CROSS JOIN memory_index_chunks AS chunk ON chunk.id = missing.id ) UPDATE memory_index_sources SET hash = '' WHERE EXISTS ( SELECT 1 FROM affected WHERE affected.path = memory_index_sources.path AND affected.source IS memory_index_sources.source ); WITH missing AS MATERIALIZED (${missingChunkIdsSql}) INSERT OR IGNORE INTO ${MEMORY_INDEX_CHUNK_PROVENANCE_TABLE} ( chunk_id, origin_class, session_kind, observed_at ) SELECT chunk.id, 'untrusted', 'unknown', chunk.updated_at FROM missing JOIN memory_index_chunks AS chunk ON chunk.id = missing.id; `); }; if (db.isTransaction) { ensure(); return; } runSqliteImmediateTransactionSync(db, ensure); } //#endregion //#region packages/memory-host-sdk/src/host/memory-schema-recall.ts const MEMORY_INDEX_CHUNKS_TABLE$1 = "memory_index_chunks"; const MEMORY_INDEX_CHUNK_RECALL_METADATA_TABLE = "memory_index_chunk_recall_metadata"; const MEMORY_INDEX_CHUNK_RECALL_METADATA_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS ${MEMORY_INDEX_CHUNK_RECALL_METADATA_TABLE} ( chunk_id TEXT PRIMARY KEY, importance INTEGER CHECK (importance IS NULL OR importance BETWEEN 1 AND 10), triggers TEXT, project_key TEXT, FOREIGN KEY (chunk_id) REFERENCES ${MEMORY_INDEX_CHUNKS_TABLE$1}(id) ON DELETE CASCADE ) STRICT; `; function readMemoryChunkColumns(db) { const rows = db.prepare(`PRAGMA table_info(${MEMORY_INDEX_CHUNKS_TABLE$1})`).all(); return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : [])); } function tableExists$1(db, tableName) { return Boolean(db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ? LIMIT 1").get(tableName)); } function legacyRecallMetadataColumns(db) { const columns = readMemoryChunkColumns(db); return [ "importance", "triggers", "project_key" ].filter((column) => columns.has(column)); } function hasLegacyMemoryRecallMetadataColumns(db) { return legacyRecallMetadataColumns(db).length > 0; } function ensureMemoryRecallMetadataSchema(db) { if (legacyRecallMetadataColumns(db).length === 0 && tableExists$1(db, "memory_index_chunk_recall_metadata")) return; const ensure = () => { db.exec(MEMORY_INDEX_CHUNK_RECALL_METADATA_SCHEMA_SQL); const columns = new Set(legacyRecallMetadataColumns(db)); if (columns.size === 0) return; const importance = columns.has("importance") ? "importance" : "NULL"; const triggers = columns.has("triggers") ? "triggers" : "NULL"; const projectKey = columns.has("project_key") ? "project_key" : "NULL"; db.exec(` INSERT INTO ${MEMORY_INDEX_CHUNK_RECALL_METADATA_TABLE} ( chunk_id, importance, triggers, project_key ) SELECT id, ${importance}, ${triggers}, ${projectKey} FROM ${MEMORY_INDEX_CHUNKS_TABLE$1} WHERE ${importance} IS NOT NULL OR ${triggers} IS NOT NULL OR ${projectKey} IS NOT NULL ON CONFLICT(chunk_id) DO UPDATE SET importance=excluded.importance, triggers=excluded.triggers, project_key=excluded.project_key; `); for (const column of [ "project_key", "triggers", "importance" ]) if (columns.has(column)) db.exec(`ALTER TABLE ${MEMORY_INDEX_CHUNKS_TABLE$1} DROP COLUMN ${column}`); }; if (db.isTransaction) { ensure(); return; } runSqliteImmediateTransactionSync(db, ensure); } //#endregion //#region packages/memory-host-sdk/src/host/memory-schema-fts.ts const MEMORY_INDEX_SOURCES_TABLE = "memory_index_sources"; const MEMORY_INDEX_CHUNKS_TABLE = "memory_index_chunks"; const MEMORY_INDEX_FTS_TABLE = "memory_index_chunks_fts"; const MEMORY_INDEX_PATHS_FTS_TABLE = "memory_index_paths_fts"; /** Check every persisted FTS column declaration and supported table option. */ function ftsTableMatchesSchema(params) { const table = params.db.prepare("SELECT type, sql FROM sqlite_schema WHERE type IN ('table', 'view') AND name = ? COLLATE NOCASE").get(params.tableName); if (table?.type === "view") throw new Error(`cannot modify ${params.tableName} because it is a view`); if (typeof table?.sql !== "string") return "missing"; const definition = /^\s*CREATE\s+VIRTUAL\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"[^"]+"|`[^`]+`|\[[^\]]+\]|[^\s(]+)\s+USING\s+fts5\s*\(([\s\S]*)\)\s*$/iu.exec(table.sql)?.[1]; if (definition === void 0) return "not-fts"; const declarations = definition.split(",").map((declaration) => declaration.trim().replace(/\s+/g, " ").toLowerCase()); const expectedDeclarations = params.expectedColumns.map((column, index) => index === 0 ? column : `${column} unindexed`); if (declarations.length < expectedDeclarations.length || expectedDeclarations.some((column, index) => declarations[index] !== column)) return "mismatched"; const options = declarations.slice(expectedDeclarations.length); const tokenizerOption = options[0] ?? ""; if (!(params.tokenizeClause.includes("trigram") ? options.length === 1 && /^tokenize\s*=\s*(['"])trigram case_sensitive 0\1$/u.test(tokenizerOption) : options.length === 0 || options.length === 1 && /^tokenize\s*=\s*(['"])unicode61\1$/u.test(tokenizerOption))) return "mismatched"; const columns = params.db.prepare("SELECT name FROM pragma_table_info(?) ORDER BY cid").all(params.tableName); return columns.length === params.expectedColumns.length && columns.every((column, index) => column.name === params.expectedColumns[index]) ? "matching" : "mismatched"; } /** Remove only derived FTS state whose persisted schema no longer matches. */ function dropMismatchedFtsTable(params) { const status = ftsTableMatchesSchema(params); if (status === "missing" || status === "matching") return; if (status === "not-fts" && params.tableName !== "memory_index_chunks_fts" && params.tableName !== "memory_index_paths_fts") throw new Error(`Memory FTS table "${params.tableName}" collides with a non-FTS table`); if (params.tableName === "memory_index_paths_fts") dropMemoryPathFtsTriggers(params.db); if (params.tableName === "memory_index_chunks_fts") dropMemoryChunkFtsTriggers(params.db); params.db.exec(`DROP TABLE ${params.tableName}`); } /** Optional canonical triggers owned by the derived path FTS index. */ const MEMORY_PATH_FTS_TRIGGER_DEFINITIONS = [ { name: "memory_index_paths_fts_after_insert", sql: ` CREATE TRIGGER IF NOT EXISTS main.memory_index_paths_fts_after_insert AFTER INSERT ON ${MEMORY_INDEX_SOURCES_TABLE} BEGIN INSERT INTO ${MEMORY_INDEX_PATHS_FTS_TABLE} (rowid, path, source) VALUES (NEW.id, NEW.path, NEW.source); END; ` }, { name: "memory_index_paths_fts_after_update", sql: ` CREATE TRIGGER IF NOT EXISTS main.memory_index_paths_fts_after_update AFTER UPDATE OF id, path, source ON ${MEMORY_INDEX_SOURCES_TABLE} BEGIN DELETE FROM ${MEMORY_INDEX_PATHS_FTS_TABLE} WHERE rowid = OLD.id; INSERT INTO ${MEMORY_INDEX_PATHS_FTS_TABLE} (rowid, path, source) VALUES (NEW.id, NEW.path, NEW.source); END; ` }, { name: "memory_index_paths_fts_after_delete", sql: ` CREATE TRIGGER IF NOT EXISTS main.memory_index_paths_fts_after_delete AFTER DELETE ON ${MEMORY_INDEX_SOURCES_TABLE} BEGIN DELETE FROM ${MEMORY_INDEX_PATHS_FTS_TABLE} WHERE rowid = OLD.id; END; ` } ]; /** Canonical chunks own the optional body index; every mutation addresses its rowid. */ const MEMORY_CHUNK_FTS_TRIGGER_DEFINITIONS = [ { name: "memory_index_chunks_fts_after_insert", sql: ` CREATE TRIGGER IF NOT EXISTS main.memory_index_chunks_fts_after_insert AFTER INSERT ON ${MEMORY_INDEX_CHUNKS_TABLE} BEGIN INSERT INTO ${MEMORY_INDEX_FTS_TABLE} (rowid, text, id, path, source, model, start_line, end_line) VALUES (NEW.chunk_rowid, NEW.text, NEW.id, NEW.path, NEW.source, NEW.model, NEW.start_line, NEW.end_line); END; ` }, { name: "memory_index_chunks_fts_after_update", sql: ` CREATE TRIGGER IF NOT EXISTS main.memory_index_chunks_fts_after_update AFTER UPDATE OF chunk_rowid, text, id, path, source, model, start_line, end_line ON ${MEMORY_INDEX_CHUNKS_TABLE} BEGIN DELETE FROM ${MEMORY_INDEX_FTS_TABLE} WHERE rowid = OLD.chunk_rowid; INSERT INTO ${MEMORY_INDEX_FTS_TABLE} (rowid, text, id, path, source, model, start_line, end_line) VALUES (NEW.chunk_rowid, NEW.text, NEW.id, NEW.path, NEW.source, NEW.model, NEW.start_line, NEW.end_line); END; ` }, { name: "memory_index_chunks_fts_after_delete", sql: ` CREATE TRIGGER IF NOT EXISTS main.memory_index_chunks_fts_after_delete AFTER DELETE ON ${MEMORY_INDEX_CHUNKS_TABLE} BEGIN DELETE FROM ${MEMORY_INDEX_FTS_TABLE} WHERE rowid = OLD.chunk_rowid; END; ` } ]; function dropMemoryChunkFtsTriggers(db) { for (const trigger of MEMORY_CHUNK_FTS_TRIGGER_DEFINITIONS) db.exec(`DROP TRIGGER IF EXISTS main.${trigger.name}`); } function ensureMemoryChunkFtsTriggers(db) { for (const trigger of MEMORY_CHUNK_FTS_TRIGGER_DEFINITIONS) db.exec(trigger.sql); } function rebuildMemoryChunkFts(db, ftsTable) { db.exec(` DELETE FROM ${ftsTable}; INSERT INTO ${ftsTable} ( rowid, text, id, path, source, model, start_line, end_line ) SELECT chunk_rowid, text, id, path, source, model, start_line, end_line FROM ${MEMORY_INDEX_CHUNKS_TABLE}; `); } /** Reconcile and backfill the derived body index in one atomic savepoint. */ function ensureMemoryChunkFtsSchema(params) { if (params.ftsTable.toLowerCase() === "memory_index_paths_fts".toLowerCase()) throw new Error(`Memory body FTS table "${params.ftsTable}" collides with the path FTS index`); params.db.exec("SAVEPOINT ensure_memory_index_chunks_fts"); try { dropMismatchedFtsTable({ db: params.db, tableName: params.ftsTable, expectedColumns: [ "text", "id", "path", "source", "model", "start_line", "end_line" ], tokenizeClause: params.tokenizeClause }); params.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${params.ftsTable} USING fts5(\n text,\n id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n model UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n${params.tokenizeClause});`); const rowCounts = params.db.prepare(`SELECT (SELECT COUNT(*) FROM ${MEMORY_INDEX_CHUNKS_TABLE}) AS canonical_count, (SELECT COUNT(*) FROM ${params.ftsTable}) AS derived_count`).get(); const canonical = params.ftsTable === MEMORY_INDEX_FTS_TABLE; if (!(!canonical || MEMORY_CHUNK_FTS_TRIGGER_DEFINITIONS.every((trigger) => params.db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'trigger' AND name = ?").get(trigger.name))) || rowCounts.canonical_count !== rowCounts.derived_count) rebuildMemoryChunkFts(params.db, params.ftsTable); if (canonical) ensureMemoryChunkFtsTriggers(params.db); params.db.exec("RELEASE ensure_memory_index_chunks_fts"); } catch (err) { params.db.exec("ROLLBACK TO ensure_memory_index_chunks_fts"); params.db.exec("RELEASE ensure_memory_index_chunks_fts"); throw err; } } function dropDisabledMemoryFts(db, ftsTable, enabled) { if (enabled) return; dropMemoryPathFtsTriggers(db); db.exec(`DROP TABLE IF EXISTS ${MEMORY_INDEX_PATHS_FTS_TABLE}`); if (ftsTable === "memory_index_chunks_fts") { dropMemoryChunkFtsTriggers(db); db.exec(`DROP TABLE IF EXISTS ${ftsTable}`); } } /** Drop the canonical source-to-path-FTS maintenance triggers. */ function dropMemoryPathFtsTriggers(db) { for (const trigger of MEMORY_PATH_FTS_TRIGGER_DEFINITIONS) db.exec(`DROP TRIGGER IF EXISTS main.${trigger.name}`); } /** Install the canonical source-to-path-FTS maintenance triggers. */ function ensureMemoryPathFtsTriggers(db) { for (const trigger of MEMORY_PATH_FTS_TRIGGER_DEFINITIONS) db.exec(trigger.sql); } function ensureMemoryPathFtsSchema(params) { params.db.exec("SAVEPOINT ensure_memory_index_paths_fts"); try { dropMismatchedFtsTable({ db: params.db, tableName: MEMORY_INDEX_PATHS_FTS_TABLE, expectedColumns: ["path", "source"], tokenizeClause: params.tokenizeClause }); params.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS ${MEMORY_INDEX_PATHS_FTS_TABLE} USING fts5( path, source UNINDEXED ${params.tokenizeClause} ); -- The initial copy and trigger installation share this savepoint. Once -- populated, the triggers own completeness; per-row FTS probes are too costly. INSERT INTO ${MEMORY_INDEX_PATHS_FTS_TABLE} (rowid, path, source) SELECT id, path, source FROM ${MEMORY_INDEX_SOURCES_TABLE} WHERE NOT EXISTS (SELECT 1 FROM ${MEMORY_INDEX_PATHS_FTS_TABLE} LIMIT 1); `); ensureMemoryPathFtsTriggers(params.db); params.db.exec("RELEASE ensure_memory_index_paths_fts"); } catch (err) { params.db.exec("ROLLBACK TO ensure_memory_index_paths_fts"); params.db.exec("RELEASE ensure_memory_index_paths_fts"); throw err; } } //#endregion //#region packages/memory-host-sdk/src/host/memory-schema-base.ts const MEMORY_INDEX_META_TABLE = "memory_index_meta"; const MEMORY_EMBEDDING_CACHE_TABLE = "memory_embedding_cache"; const MEMORY_INDEX_STATE_TABLE = "memory_index_state"; const MEMORY_INDEX_VECTOR_TABLE = "memory_index_chunks_vec"; const MEMORY_INDEX_CHUNKS_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS ${MEMORY_INDEX_CHUNKS_TABLE} ( chunk_rowid INTEGER PRIMARY KEY, id TEXT NOT NULL UNIQUE, 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 BLOB NOT NULL, updated_at INTEGER NOT NULL ) STRICT; `; function buildMemoryEmbeddingCacheSchema(table) { return ` CREATE TABLE IF NOT EXISTS ${table} ( provider TEXT NOT NULL, model TEXT NOT NULL, provider_key TEXT NOT NULL, hash TEXT NOT NULL, embedding BLOB NOT NULL, dims INTEGER, updated_at INTEGER NOT NULL, PRIMARY KEY (provider, model, provider_key, hash) ) STRICT; `; } const MEMORY_INDEX_DERIVED_TABLES = [ MEMORY_INDEX_VECTOR_TABLE, MEMORY_INDEX_FTS_TABLE, MEMORY_INDEX_PATHS_FTS_TABLE, MEMORY_INDEX_CHUNK_RECALL_METADATA_TABLE, MEMORY_INDEX_CHUNK_PROVENANCE_TABLE, MEMORY_INDEX_CHUNKS_TABLE, MEMORY_INDEX_SOURCES_TABLE, MEMORY_INDEX_META_TABLE, MEMORY_EMBEDDING_CACHE_TABLE, MEMORY_INDEX_STATE_TABLE ]; function buildMemoryIndexStrictSchema(params) { const embeddingCacheSql = params.includeEmbeddingCache ? buildMemoryEmbeddingCacheSchema(params.embeddingCacheTable) : ""; return ` CREATE TABLE IF NOT EXISTS ${MEMORY_INDEX_META_TABLE} ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; CREATE TABLE IF NOT EXISTS ${MEMORY_INDEX_SOURCES_TABLE} ( id INTEGER PRIMARY KEY, path TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'memory', hash TEXT NOT NULL, mtime REAL NOT NULL, size INTEGER NOT NULL, UNIQUE (path, source) ) STRICT; ${MEMORY_INDEX_CHUNKS_SCHEMA_SQL} ${MEMORY_INDEX_CHUNK_RECALL_METADATA_SCHEMA_SQL} ${MEMORY_INDEX_CHUNK_PROVENANCE_SCHEMA_SQL} CREATE TABLE IF NOT EXISTS ${MEMORY_INDEX_STATE_TABLE} ( id INTEGER PRIMARY KEY CHECK (id = 1), revision INTEGER NOT NULL ) STRICT; ${embeddingCacheSql} `; } //#endregion //#region packages/memory-host-sdk/src/host/memory-schema-storage-migration.ts const LEGACY_CHUNK_SCHEMA = ` 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 ) STRICT; `; function legacyCacheSchema(table) { return `CREATE TABLE IF NOT EXISTS ${table} ( 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) ) STRICT;`; } const CHUNK_INDEXES = [ { name: "idx_memory_index_chunks_path_source", columns: "path, source" }, { name: "idx_memory_index_chunks_path", columns: "path" }, { name: "idx_memory_index_chunks_source", columns: "source" } ]; const CHUNK_REVISION_TRIGGERS = [ "insert", "update", "delete" ].map((event) => ({ name: `memory_index_chunks_revision_after_${event}`, sql: `CREATE TRIGGER IF NOT EXISTS memory_index_chunks_revision_after_${event} AFTER ${event.toUpperCase()} ON memory_index_chunks BEGIN UPDATE memory_index_state SET revision = revision + 1 WHERE id = 1; END;` })); const INLINE_RECALL_COLUMNS = [ ["importance", "importance INTEGER CHECK (importance IS NULL OR importance BETWEEN 1 AND 10)"], ["triggers", "triggers TEXT"], ["project_key", "project_key TEXT"] ]; function storageShape(db, table, chunks) { const tableColumns = columns(db, table); if (tableColumns.size === 0) return "absent"; const shape = tableColumns.get("embedding") === "TEXT" ? "legacy" : tableColumns.get("embedding") === "BLOB" ? "binary" : void 0; if (!shape || chunks && tableColumns.has("chunk_rowid") !== (shape === "binary")) throw new Error(`Unsupported partial memory storage schema: ${table}`); let schema = chunks ? shape === "legacy" ? LEGACY_CHUNK_SCHEMA : MEMORY_INDEX_CHUNKS_SCHEMA_SQL : shape === "legacy" ? legacyCacheSchema(table) : buildMemoryEmbeddingCacheSchema(table); if (chunks) { const inline = INLINE_RECALL_COLUMNS.filter(([name]) => tableColumns.has(name)); if (inline.length > 0) schema = schema.replace("updated_at INTEGER NOT NULL", `updated_at INTEGER NOT NULL,\n${inline.map(([, declaration]) => declaration).join(",\n")}`); } if (shape === "legacy" && db.prepare("SELECT strict FROM pragma_table_list WHERE schema = 'main' AND name = ?").get(table)?.strict === 0) schema = schema.replace(") STRICT;", ");"); const indexes = chunks ? CHUNK_INDEXES : [{ name: table === "memory_embedding_cache" ? "idx_memory_embedding_cache_updated_at" : "idx_embedding_cache_updated_at", columns: "updated_at" }]; schema += indexes.map((index) => `CREATE INDEX ${index.name} ON ${table}(${index.columns});`).join("\n"); assertSqliteSchemaContains(db, `memory storage ${table}`, schema, { allowCompatibleAdditiveColumns: shape === "binary", allowedMissingIndexes: indexes.map((index) => index.name), optionalCanonicalTriggerGroups: chunks ? [{ tableName: table, triggers: CHUNK_REVISION_TRIGGERS }, ...shape === "binary" ? [{ tableName: table, triggers: MEMORY_CHUNK_FTS_TRIGGER_DEFINITIONS }] : []] : [] }); if (shape === "legacy") assertKnownRebuildDependents(db, table, indexes.map((index) => index.name), chunks ? CHUNK_REVISION_TRIGGERS.map((trigger) => trigger.name) : []); return shape; } function assertKnownRebuildDependents(db, table, indexes, triggers) { const known = /* @__PURE__ */ new Set([...indexes, ...triggers]); const dependent = db.prepare(` SELECT type, name, tbl_name FROM main.sqlite_schema WHERE (type IN ('trigger', 'index') AND tbl_name = ? AND sql IS NOT NULL) OR (type IN ('view', 'trigger') AND instr(lower(sql), ?) > 0) `).all(table, table).find((row) => row.tbl_name !== table || !known.has(String(row.name))); if (dependent) throw new Error(`Memory storage migration cannot rebuild unknown ${String(dependent.type)} ${String(dependent.name)} on ${table}`); for (const row of db.prepare("SELECT name FROM main.sqlite_schema WHERE type = 'table'").all()) { const name = String(row.name); const references = db.prepare(`PRAGMA main.foreign_key_list("${name.replaceAll("\"", "\"\"")}")`).all().filter((key) => key.table === table); const knownChild = table === "memory_index_chunks" && ["memory_index_chunk_provenance", "memory_index_chunk_recall_metadata"].includes(name); if (references.some((key) => !knownChild || key.from !== "chunk_id" || key.to !== "id" || key.on_delete !== "CASCADE" || key.on_update !== "NO ACTION")) throw new Error(`Memory storage migration cannot rebuild ${table} referenced by an unknown foreign key in ${name}`); } } function storageShapes(db, cacheTable) { return { chunks: storageShape(db, "memory_index_chunks", true), cache: storageShape(db, cacheTable, false) }; } function assertBinaryEmbeddings(db, table) { const rows = db.prepare(`SELECT openclaw_memory_embedding_blob_valid(embedding) AS valid FROM ${table}`); for (const row of rows.iterate()) if (row.valid === 0) throw new Error(`Memory storage migration found invalid binary embeddings in ${table}`); } function copyMemoryStorageRows(db, table, sql) { const rows = db.prepare(`SELECT rowid AS storage_rowid FROM ${table}`); rows.setReadBigInts(true); const copy = db.prepare(sql); copy.setReadBigInts(true); for (const row of rows.iterate()) { if (typeof row.storage_rowid !== "bigint") throw new Error("Invalid memory storage identity during migration"); copy.run(row.storage_rowid); } } function markInvalidMemoryEmbeddings(db, sql) { const rows = db.prepare(sql); rows.setReadBigInts(true); const dirtySource = db.prepare(` UPDATE main.memory_index_sources SET hash = '' WHERE (path, source) IN ( SELECT path, source FROM main.memory_index_chunks WHERE rowid = ? )`); dirtySource.setReadBigInts(true); let invalid = false; for (const row of rows.iterate()) if (row.valid === 0) { if (typeof row.storage_rowid !== "bigint") throw new Error("Invalid memory storage identity during migration"); dirtySource.run(row.storage_rowid); invalid = true; } if (invalid) db.exec(` INSERT INTO main.memory_index_meta (key, value) VALUES ('memory_vector_rebuild_v1', '1') ON CONFLICT(key) DO UPDATE SET value = excluded.value; `); } function legacyEmbedding(raw) { if (typeof raw !== "string") return; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) && parsed.every((value) => typeof value === "number" && Number.isFinite(value)) ? parsed : void 0; } catch { return; } } function copyLegacyMemoryEmbeddingCache(db, table, replacement, renewAuthority) { const maxEmbeddingBytes = 1048576; const rows = db.prepare(` SELECT rowid AS storage_rowid, CASE WHEN length(CAST(embedding AS BLOB)) <= ${maxEmbeddingBytes} THEN embedding ELSE NULL END AS embedding FROM ${table} WHERE rowid >= ? ORDER BY rowid LIMIT 128 `); rows.setReadBigInts(true); const copy = db.prepare(` INSERT INTO ${replacement} (rowid, provider, model, provider_key, hash, embedding, dims, updated_at) SELECT rowid, provider, model, provider_key, hash, ?, dims, updated_at FROM ${table} WHERE rowid = ? `); copy.setReadBigInts(true); let nextRowid = -9223372036854775808n; let skipped = 0; const firstSkippedRowids = []; while (nextRowid <= 9223372036854775807n) { let count = 0; for (const row of rows.iterate(nextRowid)) { if (typeof row.storage_rowid !== "bigint") throw new Error("Invalid memory storage identity during migration"); renewAuthority(); nextRowid = row.storage_rowid + 1n; count += 1; const embedding = legacyEmbedding(row.embedding ?? null); if (embedding === void 0) { skipped += 1; if (firstSkippedRowids.length < 8) firstSkippedRowids.push(String(row.storage_rowid)); continue; } copy.run(encodeMemoryEmbedding(embedding), row.storage_rowid); } if (count < 128) break; } return skipped > 0 ? `Skipped ${skipped} ${table} rows with invalid embeddings or embeddings larger than ${maxEmbeddingBytes} bytes (first rowids: ${firstSkippedRowids.join(", ")}). These cache entries will be rebuilt on demand.` : void 0; } /** Only migrations/imports interpret the retired JSON representation. */ function registerMemoryEmbeddingMigrationFunctions(db, renewAuthority) { let renewedAt = Number.NEGATIVE_INFINITY; const renew = () => { if (!renewAuthority) return; const now = performance.now(); if (now - renewedAt >= 1e3) { renewAuthority(); renewedAt = now; } }; db.function("openclaw_memory_embedding_from_json", { deterministic: true }, (raw) => { renew(); return encodeMemoryEmbedding(legacyEmbedding(raw) ?? []); }); db.function("openclaw_memory_embedding_json_valid", { deterministic: true }, (raw) => { renew(); return Number(legacyEmbedding(raw) !== void 0); }); db.function("openclaw_memory_embedding_blob_valid", { deterministic: true }, (raw) => { renew(); return Number(raw instanceof Uint8Array && raw.byteLength % 8 === 0 && decodeMemoryEmbedding(raw).length * 8 === raw.byteLength); }); return renew; } function existingStorageObjects(db, table) { return db.prepare(`SELECT sql FROM main.sqlite_schema WHERE tbl_name = ? AND type IN ('index', 'trigger') AND sql IS NOT NULL ORDER BY type, name`).all(table).map((row) => String(row.sql)); } /** Record regeneration debt after a legacy import has verified its canonical copy. */ function markInvalidImportedMemoryEmbeddings(db, schema) { if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(schema)) throw new Error("Invalid legacy memory schema identifier"); markInvalidMemoryEmbeddings(db, ` SELECT chunk.rowid AS storage_rowid, openclaw_memory_embedding_json_valid(legacy.embedding) AS valid FROM ${schema}.chunks AS legacy JOIN main.memory_index_chunks AS chunk ON chunk.id = legacy.id WHERE length(chunk.embedding) = 0 `); } function columns(db, table) { return new Map(db.prepare("SELECT name, type FROM pragma_table_info(?)").all(table).map((row) => [String(row.name), String(row.type).toUpperCase()])); } /** * Convert only memory storage, without provider calls. The agent migration owner * supplies its admitted transaction with foreign keys disabled before BEGIN. * Standalone/shadow index owners receive the same atomic conversion here. */ function migrateMemoryIndexStorage(db, options = {}) { const cacheTable = options.embeddingCacheTable ?? "memory_embedding_cache"; if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(cacheTable)) throw new Error("Invalid memory embedding cache table identifier"); const shapes = storageShapes(db, cacheTable); const migrateChunks = shapes.chunks === "legacy"; const migrateCache = shapes.cache === "legacy"; if (!migrateChunks && !migrateCache) return; const foreignKeys = Number(db.prepare("PRAGMA foreign_keys").get()?.foreign_keys) !== 0; if (migrateChunks && foreignKeys && db.isTransaction) throw new Error("Memory storage migration requires foreign keys disabled before BEGIN"); if (migrateChunks && foreignKeys) db.exec("PRAGMA foreign_keys = OFF"); let cacheWarning; try { runSqliteImmediateTransactionSync(db, () => { const current = storageShapes(db, cacheTable); if (current.chunks !== shapes.chunks || current.cache !== shapes.cache) throw new Error("Memory storage schema changed before migration admission"); const renewAuthority = registerMemoryEmbeddingMigrationFunctions(db, options.renewAuthority); const chunkObjects = migrateChunks ? existingStorageObjects(db, "memory_index_chunks") : []; const cacheObjects = migrateCache ? existingStorageObjects(db, cacheTable) : []; if (current.chunks === "binary") assertBinaryEmbeddings(db, "memory_index_chunks"); if (current.cache === "binary") assertBinaryEmbeddings(db, cacheTable); if (migrateChunks) { ensureMemoryRecallMetadataSchema(db); markInvalidMemoryEmbeddings(db, ` SELECT rowid AS storage_rowid, openclaw_memory_embedding_json_valid(embedding) AS valid FROM memory_index_chunks `); dropMemoryChunkFtsTriggers(db); db.exec(MEMORY_INDEX_CHUNKS_SCHEMA_SQL.replace("CREATE TABLE IF NOT EXISTS", "CREATE TABLE").replace("memory_index_chunks", "memory_index_chunks_storage_migration")); copyMemoryStorageRows(db, "memory_index_chunks", ` INSERT INTO memory_index_chunks_storage_migration ( chunk_rowid, id, path, source, start_line, end_line, hash, model, text, embedding, updated_at ) SELECT rowid, id, path, source, start_line, end_line, hash, model, text, openclaw_memory_embedding_from_json(embedding), updated_at FROM memory_index_chunks WHERE rowid = ? `); db.exec(` DROP TABLE memory_index_chunks; ALTER TABLE memory_index_chunks_storage_migration RENAME TO memory_index_chunks; `); for (const sql of chunkObjects) db.exec(sql); if (db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get("memory_index_chunks_fts")) { rebuildMemoryChunkFts(db, MEMORY_INDEX_FTS_TABLE); ensureMemoryChunkFtsTriggers(db); } } if (migrateCache) { const replacement = `${cacheTable}_storage_migration`; db.exec(buildMemoryEmbeddingCacheSchema(replacement).replace("CREATE TABLE IF NOT EXISTS", "CREATE TABLE")); cacheWarning = copyLegacyMemoryEmbeddingCache(db, cacheTable, replacement, renewAuthority); db.exec(` DROP TABLE ${cacheTable}; ALTER TABLE ${replacement} RENAME TO ${cacheTable}; `); for (const sql of cacheObjects) db.exec(sql); } if (migrateChunks) { for (const table of ["memory_index_chunk_provenance", "memory_index_chunk_recall_metadata"]) if (columns(db, table).size > 0 && db.prepare(`PRAGMA foreign_key_check(${table})`).all().length > 0) throw new Error("Memory storage migration failed foreign key validation"); } }); } finally { if (options.renewAuthority) registerMemoryEmbeddingMigrationFunctions(db); if (migrateChunks && foreignKeys) db.exec("PRAGMA foreign_keys = ON"); } if (cacheWarning) { if (options.onWarning) options.onWarning(cacheWarning); else process.emitWarning(cacheWarning); } } //#endregion //#region packages/memory-host-sdk/src/host/memory-schema.ts const LEGACY_MEMORY_INDEX_SOURCE_COLUMNS = [ "path", "source", "hash", "mtime", "size" ]; const MEMORY_INDEX_SOURCE_COLUMNS = ["id", ...LEGACY_MEMORY_INDEX_SOURCE_COLUMNS]; const MEMORY_INDEX_SOURCE_COLUMN_TYPES = /* @__PURE__ */ new Map([ ["id", "INTEGER"], ["path", "TEXT"], ["source", "TEXT"], ["hash", "TEXT"], ["mtime", "REAL"], ["size", "INTEGER"] ]); function tableColumnInfo(db, tableName) { return db.prepare(`PRAGMA main.table_xinfo(${tableName})`).all().flatMap((row) => typeof row.name === "string" && typeof row.type === "string" ? [{ name: row.name, type: row.type.toUpperCase(), notnull: Number(row.notnull ?? 0), pk: Number(row.pk ?? 0), defaultValue: typeof row.dflt_value === "string" ? row.dflt_value : null, hidden: Number(row.hidden ?? 0) }] : []); } function tableHasExactColumns(db, tableName, expected) { const columns = new Set(tableColumnInfo(db, tableName).map((row) => row.name)); return columns.size === expected.length && expected.every((column) => columns.has(column)); } function tablePrimaryKeyColumns(db, tableName) { return tableColumnInfo(db, tableName).filter((row) => row.pk > 0).toSorted((left, right) => left.pk - right.pk).map((row) => row.name); } function tableHasPrimaryKey(db, tableName, expectedColumns) { const columns = tablePrimaryKeyColumns(db, tableName); return columns.length === expectedColumns.length && columns.every((column, index) => column === expectedColumns[index]); } function tableHasUniqueIndex(db, tableName, expectedColumns) { const indexes = db.prepare(`SELECT name, partial FROM pragma_index_list(?) WHERE "unique" = 1`).all(tableName); if (indexes.length !== 1) return false; return indexes.some((index) => { if (typeof index.name !== "string" || Number(index.partial ?? 0) !== 0) return false; const columns = db.prepare(`SELECT cid, name, coll, "desc" AS sort_desc, key FROM pragma_index_xinfo(?) ORDER BY seqno`).all(index.name).filter((row) => Number(row.key ?? 0) === 1); return columns.length === expectedColumns.length && columns.every((column, columnIndex) => Number(column.cid ?? -1) >= 0 && column.name === expectedColumns[columnIndex] && column.coll === "BINARY" && Number(column.sort_desc ?? 0) === 0); }); } function tableHasNoDeclaredCollations(db, tableName) { const row = db.prepare(`SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ?`).get(tableName); return typeof row?.sql === "string" && !/\bCOLLATE\b/iu.test(row.sql); } function tableHasSourceColumnContract(db, nullableColumn) { return tableColumnInfo(db, MEMORY_INDEX_SOURCES_TABLE).every((column) => (column.type === MEMORY_INDEX_SOURCE_COLUMN_TYPES.get(column.name) || column.name === "mtime" && column.type === "INTEGER") && column.defaultValue === (column.name === "source" ? "'memory'" : null) && column.hidden === 0 && (column.name === nullableColumn || column.notnull === 1)); } function tableHasIntegerRowIdPrimaryKey(db) { if (tableColumnInfo(db, "memory_index_sources").find((column) => column.name === "id")?.type !== "INTEGER" || !tableHasPrimaryKey(db, "memory_index_sources", ["id"])) return false; return db.prepare(`SELECT 1 AS found FROM pragma_index_list(?) WHERE origin = 'pk' LIMIT 1`).get(MEMORY_INDEX_SOURCES_TABLE)?.found !== 1; } function tableExists(db, tableName) { return db.prepare(`SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ?`).get(tableName)?.found === 1; } /** Upgrade canonical memory sources to stable integer identities. */ function migrateMemoryIndexSourcesIdentity(db) { if (!tableExists(db, "memory_index_sources")) return; if (tableHasExactColumns(db, "memory_index_sources", MEMORY_INDEX_SOURCE_COLUMNS)) { if (tableHasSourceColumnContract(db, "id") && tableHasIntegerRowIdPrimaryKey(db) && tableHasNoDeclaredCollations(db, "memory_index_sources") && tableHasUniqueIndex(db, "memory_index_sources", ["path", "source"])) return; throw new Error("canonical memory source identity schema is invalid"); } if (!tableHasExactColumns(db, "memory_index_sources", LEGACY_MEMORY_INDEX_SOURCE_COLUMNS)) throw new Error("canonical memory source identity schema is invalid"); const hasPathPrimaryKey = tableHasPrimaryKey(db, MEMORY_INDEX_SOURCES_TABLE, ["path"]); const hasPathSourcePrimaryKey = tableHasPrimaryKey(db, MEMORY_INDEX_SOURCES_TABLE, ["path", "source"]); if (!hasPathPrimaryKey && !hasPathSourcePrimaryKey) throw new Error("canonical memory source identity schema is invalid"); if (!tableHasSourceColumnContract(db, hasPathPrimaryKey ? "path" : void 0)) throw new Error("canonical memory source identity schema is invalid"); const rebuildsPathFts = tableExists(db, MEMORY_INDEX_PATHS_FTS_TABLE); db.exec("SAVEPOINT migrate_memory_index_sources_identity"); try { dropMemoryPathFtsTriggers(db); db.exec(` DROP TRIGGER IF EXISTS memory_index_sources_revision_after_insert; DROP TRIGGER IF EXISTS memory_index_sources_revision_after_update; DROP TRIGGER IF EXISTS memory_index_sources_revision_after_delete; ALTER TABLE ${MEMORY_INDEX_SOURCES_TABLE} RENAME TO memory_index_sources_identity_migration; CREATE TABLE ${MEMORY_INDEX_SOURCES_TABLE} ( id INTEGER PRIMARY KEY, path TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'memory', hash TEXT NOT NULL, mtime REAL NOT NULL, size INTEGER NOT NULL, UNIQUE (path, source) ) STRICT; INSERT INTO ${MEMORY_INDEX_SOURCES_TABLE} (id, path, source, hash, mtime, size) SELECT rowid, path, source, hash, mtime, size FROM memory_index_sources_identity_migration; DROP TABLE memory_index_sources_identity_migration; `); if (rebuildsPathFts) { db.exec(` DELETE FROM ${MEMORY_INDEX_PATHS_FTS_TABLE}; INSERT INTO ${MEMORY_INDEX_PATHS_FTS_TABLE} (rowid, path, source) SELECT id, path, source FROM ${MEMORY_INDEX_SOURCES_TABLE}; `); ensureMemoryPathFtsTriggers(db); } db.exec("RELEASE migrate_memory_index_sources_identity"); } catch (err) { db.exec("ROLLBACK TO migrate_memory_index_sources_identity"); db.exec("RELEASE migrate_memory_index_sources_identity"); throw err; } } /** Ensure canonical memory index tables and the optional FTS table exist. */ function ensureMemoryIndexSchema(params) { if (tableHasExactColumns(params.db, "meta", ["key", "value"]) && tableHasExactColumns(params.db, "files", [ "path", "source", "hash", "mtime", "size" ]) && tableHasExactColumns(params.db, "chunks", [ "id", "path", "source", "start_line", "end_line", "hash", "model", "text", "embedding", "updated_at" ])) throw new Error("Retired memory index format detected. Preserve a complete copy of your state and configuration, then use OpenClaw 2026.9.7 to migrate a compatible copy of this index before retrying the upgrade."); const embeddingCacheTable = params.embeddingCacheTable ?? "memory_embedding_cache"; const ftsTable = params.ftsTable ?? "memory_index_chunks_fts"; params.db.exec(buildMemoryIndexStrictSchema({ embeddingCacheTable, includeEmbeddingCache: params.cacheEnabled })); migrateMemoryIndexStorage(params.db, { embeddingCacheTable }); ensureMemoryRecallMetadataSchema(params.db); params.db.exec(` INSERT OR IGNORE INTO ${MEMORY_INDEX_STATE_TABLE} (id, revision) VALUES (1, 0); `); migrateMemoryIndexSourcesIdentity(params.db); params.db.exec(` CREATE TRIGGER IF NOT EXISTS memory_index_sources_revision_after_insert AFTER INSERT ON ${MEMORY_INDEX_SOURCES_TABLE} BEGIN UPDATE ${MEMORY_INDEX_STATE_TABLE} 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_TABLE} BEGIN UPDATE ${MEMORY_INDEX_STATE_TABLE} 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_TABLE} BEGIN UPDATE ${MEMORY_INDEX_STATE_TABLE} 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_TABLE} BEGIN UPDATE ${MEMORY_INDEX_STATE_TABLE} 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_TABLE} BEGIN UPDATE ${MEMORY_INDEX_STATE_TABLE} 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_TABLE} BEGIN UPDATE ${MEMORY_INDEX_STATE_TABLE} SET revision = revision + 1 WHERE id = 1; END; CREATE INDEX IF NOT EXISTS idx_memory_index_sources_source ON ${MEMORY_INDEX_SOURCES_TABLE}(source); CREATE INDEX IF NOT EXISTS idx_memory_index_chunks_path_source ON ${MEMORY_INDEX_CHUNKS_TABLE}(path, source); CREATE INDEX IF NOT EXISTS idx_memory_index_chunks_path ON ${MEMORY_INDEX_CHUNKS_TABLE}(path); CREATE INDEX IF NOT EXISTS idx_memory_index_chunks_source ON ${MEMORY_INDEX_CHUNKS_TABLE}(source); `); ensureMemoryChunkProvenance(params.db); dropDisabledMemoryFts(params.db, ftsTable, params.ftsEnabled); if (params.cacheEnabled) { const updatedAtIndex = embeddingCacheTable === "memory_embedding_cache" ? "idx_memory_embedding_cache_updated_at" : "idx_embedding_cache_updated_at"; params.db.exec(` CREATE INDEX IF NOT EXISTS ${updatedAtIndex} ON ${embeddingCacheTable}(updated_at); `); } migrateSqliteSchemaToStrict(params.db, buildMemoryIndexStrictSchema({ embeddingCacheTable, includeEmbeddingCache: params.cacheEnabled || tableExists(params.db, embeddingCacheTable) }), { databaseLabel: "memory index" }); let ftsAvailable = false; let ftsError; if (params.ftsEnabled) try { const tokenizeClause = (params.ftsTokenizer ?? "unicode61") === "trigram" ? `, tokenize='trigram case_sensitive 0'` : ""; ensureMemoryChunkFtsSchema({ db: params.db, ftsTable, tokenizeClause }); if (ftsTable === "memory_index_chunks_fts") ensureMemoryPathFtsSchema({ db: params.db, tokenizeClause }); ftsAvailable = true; } catch (err) { if (ftsTable === "memory_index_chunks_fts") { dropMemoryChunkFtsTriggers(params.db); dropMemoryPathFtsTriggers(params.db); } const message = formatErrorMessage(err); ftsAvailable = false; ftsError = message; } return { ftsAvailable, ...ftsError ? { ftsError } : {} }; } //#endregion export { ensureMemoryRecallMetadataSchema as C, ensureMemoryChunkProvenance as E, MEMORY_INDEX_CHUNK_RECALL_METADATA_TABLE as S, MEMORY_INDEX_CHUNK_PROVENANCE_TABLE as T, dropMemoryChunkFtsTriggers as _, registerMemoryEmbeddingMigrationFunctions as a, ensureMemoryPathFtsTriggers as b, MEMORY_INDEX_META_TABLE as c, MEMORY_CHUNK_FTS_TRIGGER_DEFINITIONS as d, MEMORY_INDEX_CHUNKS_TABLE as f, MEMORY_PATH_FTS_TRIGGER_DEFINITIONS as g, MEMORY_INDEX_SOURCES_TABLE as h, migrateMemoryIndexStorage as i, MEMORY_INDEX_STATE_TABLE as l, MEMORY_INDEX_PATHS_FTS_TABLE as m, migrateMemoryIndexSourcesIdentity as n, MEMORY_EMBEDDING_CACHE_TABLE as o, MEMORY_INDEX_FTS_TABLE as p, markInvalidImportedMemoryEmbeddings as r, MEMORY_INDEX_DERIVED_TABLES as s, ensureMemoryIndexSchema as t, MEMORY_INDEX_VECTOR_TABLE as u, dropMemoryPathFtsTriggers as v, hasLegacyMemoryRecallMetadataColumns as w, rebuildMemoryChunkFts as x, ensureMemoryChunkFtsTriggers as y };