remem-mcp 0.7.3 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -221,7 +221,7 @@ function stripQueryProperNouns(query) {
221
221
  if (noProperNouns.length > 0) return noProperNouns.join(" ");
222
222
  return filtered.length > 0 ? filtered.join(" ") : tokens.join(" ");
223
223
  }
224
- var CURRENT_SCHEMA_VERSION = 13;
224
+ var CURRENT_SCHEMA_VERSION = 17;
225
225
  var EVERGREEN_TAGS = new Set(
226
226
  (process.env.REMEM_EVERGREEN_TAGS ?? "evergreen,never-forget").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean)
227
227
  );
@@ -423,6 +423,14 @@ var SQLiteBackend = class {
423
423
  this.migrateV5ToV6();
424
424
  this.migrateV6ToV7();
425
425
  this.migrateV7ToV8();
426
+ this.migrateV8ToV9();
427
+ this.migrateV9ToV10();
428
+ this.migrateV11ToV12();
429
+ this.migrateV12ToV13();
430
+ this.migrateV13ToV14();
431
+ this.migrateV14ToV15();
432
+ this.migrateV15ToV16();
433
+ this.migrateV16ToV17();
426
434
  this.runSchema();
427
435
  this.writeSchemaVersion(CURRENT_SCHEMA_VERSION);
428
436
  })();
@@ -540,6 +548,42 @@ var SQLiteBackend = class {
540
548
  this.writeSchemaVersion(13);
541
549
  })();
542
550
  }
551
+ if (currentVersion < 14) {
552
+ migrationsRan = true;
553
+ this.backupDatabase(dbPath, 13);
554
+ this.db.transaction(() => {
555
+ this.migrateV13ToV14();
556
+ this.runSchema();
557
+ this.writeSchemaVersion(14);
558
+ })();
559
+ }
560
+ if (currentVersion < 15) {
561
+ migrationsRan = true;
562
+ this.backupDatabase(dbPath, 14);
563
+ this.db.transaction(() => {
564
+ this.migrateV14ToV15();
565
+ this.runSchema();
566
+ this.writeSchemaVersion(15);
567
+ })();
568
+ }
569
+ if (currentVersion < 16) {
570
+ migrationsRan = true;
571
+ this.backupDatabase(dbPath, 15);
572
+ this.db.transaction(() => {
573
+ this.migrateV15ToV16();
574
+ this.runSchema();
575
+ this.writeSchemaVersion(16);
576
+ })();
577
+ }
578
+ if (currentVersion < 17) {
579
+ migrationsRan = true;
580
+ this.backupDatabase(dbPath, 16);
581
+ this.db.transaction(() => {
582
+ this.migrateV16ToV17();
583
+ this.runSchema();
584
+ this.writeSchemaVersion(17);
585
+ })();
586
+ }
543
587
  this.rebuildFtsIfNeeded(migrationsRan);
544
588
  }
545
589
  /**
@@ -630,6 +674,7 @@ var SQLiteBackend = class {
630
674
  addColumnIfMissing("scenarios", "team_id", "TEXT");
631
675
  addColumnIfMissing("scenarios", "agent_id", "TEXT");
632
676
  addColumnIfMissing("scenarios", "user_id", "TEXT");
677
+ addColumnIfMissing("scenarios", "session_key", "TEXT");
633
678
  this.db.exec(`
634
679
  CREATE TABLE IF NOT EXISTS messages (
635
680
  id TEXT PRIMARY KEY,
@@ -770,6 +815,11 @@ var SQLiteBackend = class {
770
815
  }
771
816
  /** Migrate schema v8 → v9: add CodeGraph call resolution columns (module_path, confidence, call_type). */
772
817
  migrateV8ToV9() {
818
+ const symbolsExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='symbols'").get();
819
+ if (!symbolsExists) {
820
+ console.error("[remem-mcp] Migrated schema v8 \u2192 v9 (symbols table not yet created, skipping)");
821
+ return;
822
+ }
773
823
  let addedAny = false;
774
824
  try {
775
825
  this.db.exec("ALTER TABLE symbols ADD COLUMN module_path TEXT");
@@ -840,6 +890,11 @@ var SQLiteBackend = class {
840
890
  * v12 → v13: Add weight column to memory_links for Hebbian co-retrieval strengthening.
841
891
  */
842
892
  migrateV12ToV13() {
893
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_links'").get();
894
+ if (!tableExists) {
895
+ console.error("[remem-mcp] Migrated schema v12 \u2192 v13 (memory_links not yet created, skipping)");
896
+ return;
897
+ }
843
898
  const cols = this.db.prepare("PRAGMA table_info(memory_links)").all();
844
899
  const colNames = new Set(cols.map((c) => c.name));
845
900
  if (!colNames.has("weight")) {
@@ -847,6 +902,118 @@ var SQLiteBackend = class {
847
902
  }
848
903
  console.error("[remem-mcp] Migrated schema v12 \u2192 v13 (Hebbian link weights)");
849
904
  }
905
+ /**
906
+ * v13 → v14: Add session_key column to scenarios so L2 scenario injection in
907
+ * hooks can be scoped to the current project (hash(cwd)). Legacy rows keep
908
+ * NULL session_key and are excluded from hook injection (no cross-project leak).
909
+ */
910
+ migrateV13ToV14() {
911
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='scenarios'").get();
912
+ if (!tableExists) {
913
+ console.error("[remem-mcp] Migrated schema v13 \u2192 v14 (scenarios table not yet created, skipping)");
914
+ return;
915
+ }
916
+ const cols = this.db.prepare("PRAGMA table_info(scenarios)").all();
917
+ const colNames = new Set(cols.map((c) => c.name));
918
+ if (!colNames.has("session_key")) {
919
+ this.db.exec("ALTER TABLE scenarios ADD COLUMN session_key TEXT");
920
+ }
921
+ this.db.exec(
922
+ "CREATE INDEX IF NOT EXISTS idx_scenarios_session ON scenarios (session_key, created_at DESC)"
923
+ );
924
+ console.error("[remem-mcp] Migrated schema v13 \u2192 v14 (scenario session_key scoping)");
925
+ }
926
+ /**
927
+ * v14 → v15: Add v9 skill columns to the existing skills table.
928
+ *
929
+ * The skills table was created by migrateV2ToV3 with only 8 columns
930
+ * (id, team_id, agent_id, name, description, content, version, created_at,
931
+ * updated_at). The v9 columns (trigger_conditions, steps, validation_rules,
932
+ * source_capture_ids, archived) are defined in schema.sql's CREATE TABLE IF
933
+ * NOT EXISTS, but that statement is a no-op on the existing table. Without
934
+ * this migration, every putSkill / getSkillContext / skill injection fails
935
+ * with SqliteError: no such column: steps.
936
+ */
937
+ migrateV14ToV15() {
938
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='skills'").get();
939
+ if (!tableExists) {
940
+ console.error("[remem-mcp] Migrated schema v14 \u2192 v15 (skills table not yet created, skipping)");
941
+ return;
942
+ }
943
+ const cols = this.db.prepare("PRAGMA table_info(skills)").all();
944
+ const colNames = new Set(cols.map((c) => c.name));
945
+ const addColumnIfMissing = (column, definition) => {
946
+ if (!colNames.has(column)) {
947
+ this.db.exec(`ALTER TABLE skills ADD COLUMN ${column} ${definition}`);
948
+ }
949
+ };
950
+ addColumnIfMissing("trigger_conditions", "TEXT");
951
+ addColumnIfMissing("steps", "TEXT");
952
+ addColumnIfMissing("validation_rules", "TEXT");
953
+ addColumnIfMissing("source_capture_ids", "TEXT");
954
+ addColumnIfMissing("archived", "INTEGER NOT NULL DEFAULT 0");
955
+ console.error("[remem-mcp] Migrated schema v14 \u2192 v15 (skills table v9 columns)");
956
+ }
957
+ /**
958
+ * v15 → v16: Add session_key column to persona table for per-project personas.
959
+ *
960
+ * Previously persona was keyed by (team_id, agent_id, user_id) only — all
961
+ * projects shared one persona. This caused cross-project tag pollution
962
+ * (e.g., bugbounty tags appearing in AZR persona). Adding session_key
963
+ * scoping lets each project maintain its own persona.
964
+ *
965
+ * Since SQLite can't ALTER a PRIMARY KEY, we rebuild the table: create a
966
+ * temp table with the new schema, copy data (legacy rows get NULL session_key),
967
+ * drop old, rename new.
968
+ */
969
+ migrateV15ToV16() {
970
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='persona'").get();
971
+ if (!tableExists) {
972
+ console.error("[remem-mcp] Migrated schema v15 \u2192 v16 (persona table not yet created, skipping)");
973
+ return;
974
+ }
975
+ const cols = this.db.prepare("PRAGMA table_info(persona)").all();
976
+ const colNames = new Set(cols.map((c) => c.name));
977
+ if (!colNames.has("session_key")) {
978
+ this.db.exec(`
979
+ CREATE TABLE persona_v16 (
980
+ team_id TEXT NOT NULL,
981
+ agent_id TEXT NOT NULL,
982
+ user_id TEXT NOT NULL,
983
+ content TEXT NOT NULL,
984
+ updated_at INTEGER NOT NULL,
985
+ session_key TEXT,
986
+ PRIMARY KEY (team_id, agent_id, user_id, session_key)
987
+ );
988
+ INSERT INTO persona_v16 (team_id, agent_id, user_id, content, updated_at, session_key)
989
+ SELECT team_id, agent_id, user_id, content, updated_at, NULL FROM persona;
990
+ DROP TABLE persona;
991
+ ALTER TABLE persona_v16 RENAME TO persona;
992
+ CREATE INDEX IF NOT EXISTS idx_persona_session ON persona (session_key, team_id, user_id);
993
+ `);
994
+ }
995
+ console.error("[remem-mcp] Migrated schema v15 \u2192 v16 (persona session_key scoping)");
996
+ }
997
+ /**
998
+ * v16 → v17: Add temporal validity (valid_from, valid_until) and provenance (source_ref)
999
+ * columns to captures. SodaMem-inspired: supersede now closes the old version with valid_until.
1000
+ * source_ref links a capture to its originating tool call or conversation turn.
1001
+ */
1002
+ migrateV16ToV17() {
1003
+ const cols = this.db.prepare("PRAGMA table_info(captures)").all();
1004
+ const colNames = new Set(cols.map((c) => c.name));
1005
+ if (!colNames.has("valid_from")) {
1006
+ this.db.exec("ALTER TABLE captures ADD COLUMN valid_from INTEGER");
1007
+ }
1008
+ if (!colNames.has("valid_until")) {
1009
+ this.db.exec("ALTER TABLE captures ADD COLUMN valid_until INTEGER");
1010
+ }
1011
+ if (!colNames.has("source_ref")) {
1012
+ this.db.exec("ALTER TABLE captures ADD COLUMN source_ref TEXT");
1013
+ }
1014
+ this.db.exec("UPDATE captures SET valid_from = created_at WHERE valid_from IS NULL");
1015
+ console.error("[remem-mcp] Migrated schema v16 \u2192 v17 (temporal validity + provenance)");
1016
+ }
850
1017
  async put(entry) {
851
1018
  this.putInternal(entry);
852
1019
  }
@@ -859,8 +1026,8 @@ var SQLiteBackend = class {
859
1026
  ).get(contentHash, entry.sessionKey, entry.agentId);
860
1027
  if (dup) return false;
861
1028
  this.db.prepare(
862
- `INSERT INTO captures (id, session_key, agent_id, type, content, content_hash, tags, created_at, metadata, team_id, user_id, task_id, trust_state, tier)
863
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1029
+ `INSERT INTO captures (id, session_key, agent_id, type, content, content_hash, tags, created_at, metadata, team_id, user_id, task_id, trust_state, tier, valid_from, source_ref)
1030
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
864
1031
  ).run(
865
1032
  entry.id,
866
1033
  entry.sessionKey,
@@ -875,7 +1042,11 @@ var SQLiteBackend = class {
875
1042
  entry.userId ?? null,
876
1043
  entry.taskId ?? null,
877
1044
  entry.trustState ?? "candidate",
878
- tier
1045
+ tier,
1046
+ entry.createdAt,
1047
+ // valid_from = creation time (SodaMem: when the fact became true)
1048
+ entry.metadata?.source ?? null
1049
+ // source_ref from metadata
879
1050
  );
880
1051
  if (entry.messages && entry.messages.length > 0) {
881
1052
  const msgStmt = this.db.prepare(
@@ -896,6 +1067,7 @@ var SQLiteBackend = class {
896
1067
  this.putEntitiesInternal(entry.id, entities);
897
1068
  }
898
1069
  this.autoLinkCapture(entry.id, entry.tags, entities, entry.sessionKey);
1070
+ this.extractCausalLinks(entry.id, entry.content, entry.tags, entry.sessionKey);
899
1071
  } catch {
900
1072
  }
901
1073
  this.recordAuditInternal("put", entry.id, { type: entry.type, tags: entry.tags }, entry.agentId);
@@ -1178,8 +1350,9 @@ var SQLiteBackend = class {
1178
1350
  rawFallbackSearch(query, limit, sessionKey, filters) {
1179
1351
  const ftsQuery = this.escapeFtsQuery(query);
1180
1352
  if (!ftsQuery) return [];
1181
- const conditions = ["c.deleted_at IS NULL", "c.superseded_by IS NULL"];
1182
- const params = [];
1353
+ const now = Date.now();
1354
+ const conditions = ["c.deleted_at IS NULL", "c.superseded_by IS NULL", "(c.valid_until IS NULL OR c.valid_until > ?)"];
1355
+ const params = [now];
1183
1356
  if (sessionKey) {
1184
1357
  conditions.push("c.session_key = ?");
1185
1358
  params.push(sessionKey);
@@ -1222,13 +1395,15 @@ var SQLiteBackend = class {
1222
1395
  const ftsQuery = this.escapeFtsQuery(query);
1223
1396
  if (!ftsQuery) return [];
1224
1397
  let sql = "";
1225
- const params = [ftsQuery];
1398
+ const now = Date.now();
1399
+ const params = [ftsQuery, now];
1226
1400
  try {
1227
1401
  sql = `
1228
1402
  SELECT fts.id as id, bm25(captures_fts) as score
1229
1403
  FROM captures_fts fts
1230
1404
  JOIN captures c ON c.id = fts.id
1231
1405
  WHERE captures_fts MATCH ? AND c.deleted_at IS NULL AND c.trust_state != 'rejected' AND c.superseded_by IS NULL
1406
+ AND (c.valid_until IS NULL OR c.valid_until > ?)
1232
1407
  `;
1233
1408
  if (sessionKey) {
1234
1409
  sql += " AND c.session_key = ?";
@@ -1284,13 +1459,15 @@ var SQLiteBackend = class {
1284
1459
  /** Run a vector search via sqlite-vec. */
1285
1460
  vectorSearch(embedding, limit, sessionKey, filters) {
1286
1461
  const buffer = new Float32Array(embedding);
1462
+ const now = Date.now();
1287
1463
  let sql = `
1288
1464
  SELECT vec.id as id, vec.distance as score
1289
1465
  FROM captures_vec vec
1290
1466
  JOIN captures c ON c.id = vec.id
1291
1467
  WHERE vec.embedding MATCH ? AND vec.k = ? AND c.deleted_at IS NULL AND c.trust_state != 'rejected' AND c.superseded_by IS NULL
1468
+ AND (c.valid_until IS NULL OR c.valid_until > ?)
1292
1469
  `;
1293
- const params = [Buffer.from(buffer.buffer), limit];
1470
+ const params = [Buffer.from(buffer.buffer), limit, now];
1294
1471
  if (sessionKey) {
1295
1472
  sql += " AND c.session_key = ?";
1296
1473
  params.push(sessionKey);
@@ -1596,9 +1773,10 @@ var SQLiteBackend = class {
1596
1773
  }));
1597
1774
  }
1598
1775
  async supersede(loserId, winnerId) {
1776
+ const now = Date.now();
1599
1777
  const updated = this.db.prepare(
1600
- "UPDATE captures SET trust_state = 'stale', superseded_by = ? WHERE id = ? AND deleted_at IS NULL AND trust_state != 'rejected'"
1601
- ).run(winnerId, loserId).changes;
1778
+ "UPDATE captures SET trust_state = 'stale', superseded_by = ?, valid_until = ? WHERE id = ? AND deleted_at IS NULL AND trust_state != 'rejected'"
1779
+ ).run(winnerId, now, loserId).changes;
1602
1780
  if (updated > 0) {
1603
1781
  this.db.prepare("DELETE FROM captures_vec WHERE id = ?").run(loserId);
1604
1782
  }
@@ -1625,25 +1803,33 @@ var SQLiteBackend = class {
1625
1803
  );
1626
1804
  }
1627
1805
  async listAtoms(opts) {
1628
- let sql = "SELECT * FROM atoms WHERE 1=1";
1806
+ let sql = `SELECT a.* FROM atoms a`;
1629
1807
  const params = [];
1808
+ if (opts.sessionKey) {
1809
+ sql += ` JOIN captures c ON a.capture_id = c.id`;
1810
+ }
1811
+ sql += ` WHERE 1=1`;
1812
+ if (opts.sessionKey) {
1813
+ sql += ` AND c.session_key = ?`;
1814
+ params.push(opts.sessionKey);
1815
+ }
1630
1816
  if (opts.teamId) {
1631
- sql += " AND team_id = ?";
1817
+ sql += " AND a.team_id = ?";
1632
1818
  params.push(opts.teamId);
1633
1819
  }
1634
1820
  if (opts.agentId) {
1635
- sql += " AND agent_id = ?";
1821
+ sql += " AND a.agent_id = ?";
1636
1822
  params.push(opts.agentId);
1637
1823
  }
1638
1824
  if (opts.userId) {
1639
- sql += " AND user_id = ?";
1825
+ sql += " AND a.user_id = ?";
1640
1826
  params.push(opts.userId);
1641
1827
  }
1642
1828
  if (opts.captureId) {
1643
- sql += " AND capture_id = ?";
1829
+ sql += " AND a.capture_id = ?";
1644
1830
  params.push(opts.captureId);
1645
1831
  }
1646
- sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
1832
+ sql += " ORDER BY a.created_at DESC LIMIT ? OFFSET ?";
1647
1833
  params.push(opts.limit ?? 20, opts.offset ?? 0);
1648
1834
  const rows = this.db.prepare(sql).all(...params);
1649
1835
  return rows.map((r) => ({
@@ -1690,7 +1876,7 @@ var SQLiteBackend = class {
1690
1876
  // ─── L2 scenarios ───────────────────────────────────────────
1691
1877
  async putScenario(scenario) {
1692
1878
  this.db.prepare(
1693
- "INSERT INTO scenarios (id, atom_ids, summary, persona_tags, created_at, team_id, agent_id, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
1879
+ "INSERT INTO scenarios (id, atom_ids, summary, persona_tags, created_at, team_id, agent_id, user_id, session_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
1694
1880
  ).run(
1695
1881
  scenario.id,
1696
1882
  JSON.stringify(scenario.atomIds),
@@ -1699,7 +1885,8 @@ var SQLiteBackend = class {
1699
1885
  scenario.createdAt,
1700
1886
  scenario.teamId ?? null,
1701
1887
  scenario.agentId ?? null,
1702
- scenario.userId ?? null
1888
+ scenario.userId ?? null,
1889
+ scenario.sessionKey ?? null
1703
1890
  );
1704
1891
  }
1705
1892
  async listScenarios(opts) {
@@ -1717,6 +1904,10 @@ var SQLiteBackend = class {
1717
1904
  sql += " AND user_id = ?";
1718
1905
  params.push(opts.userId);
1719
1906
  }
1907
+ if (opts.sessionKey) {
1908
+ sql += " AND session_key = ?";
1909
+ params.push(opts.sessionKey);
1910
+ }
1720
1911
  sql += " ORDER BY created_at DESC LIMIT ? OFFSET ?";
1721
1912
  params.push(opts.limit ?? 20, opts.offset ?? 0);
1722
1913
  const rows = this.db.prepare(sql).all(...params);
@@ -1728,7 +1919,8 @@ var SQLiteBackend = class {
1728
1919
  createdAt: r.created_at,
1729
1920
  teamId: r.team_id ?? void 0,
1730
1921
  agentId: r.agent_id ?? void 0,
1731
- userId: r.user_id ?? void 0
1922
+ userId: r.user_id ?? void 0,
1923
+ sessionKey: r.session_key ?? void 0
1732
1924
  }));
1733
1925
  }
1734
1926
  async getScenario(id) {
@@ -1742,12 +1934,25 @@ var SQLiteBackend = class {
1742
1934
  createdAt: row.created_at,
1743
1935
  teamId: row.team_id ?? void 0,
1744
1936
  agentId: row.agent_id ?? void 0,
1745
- userId: row.user_id ?? void 0
1937
+ userId: row.user_id ?? void 0,
1938
+ sessionKey: row.session_key ?? void 0
1746
1939
  };
1747
1940
  }
1748
1941
  // ─── L3 persona ─────────────────────────────────────────────
1749
- async readPersona(teamId, agentId, userId) {
1750
- const row = this.db.prepare("SELECT * FROM persona WHERE team_id = ? AND agent_id = ? AND user_id = ?").get(teamId, agentId, userId);
1942
+ async readPersona(teamId, agentId, userId, sessionKey) {
1943
+ if (sessionKey) {
1944
+ const scopedRow = this.db.prepare("SELECT * FROM persona WHERE team_id = ? AND agent_id = ? AND user_id = ? AND session_key = ? ORDER BY updated_at DESC LIMIT 1").get(teamId, agentId, userId, sessionKey);
1945
+ if (scopedRow) {
1946
+ return {
1947
+ teamId: scopedRow.team_id,
1948
+ agentId: scopedRow.agent_id,
1949
+ userId: scopedRow.user_id,
1950
+ content: scopedRow.content,
1951
+ updatedAt: scopedRow.updated_at
1952
+ };
1953
+ }
1954
+ }
1955
+ const row = this.db.prepare("SELECT * FROM persona WHERE team_id = ? AND agent_id = ? AND user_id = ? AND (session_key IS NULL OR session_key = '') ORDER BY updated_at DESC LIMIT 1").get(teamId, agentId, userId);
1751
1956
  if (!row) return null;
1752
1957
  return {
1753
1958
  teamId: row.team_id,
@@ -1757,12 +1962,19 @@ var SQLiteBackend = class {
1757
1962
  updatedAt: row.updated_at
1758
1963
  };
1759
1964
  }
1760
- async writePersona(teamId, agentId, userId, content) {
1761
- this.db.prepare(
1762
- `INSERT INTO persona (team_id, agent_id, user_id, content, updated_at)
1763
- VALUES (?, ?, ?, ?, ?)
1764
- ON CONFLICT(team_id, agent_id, user_id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at`
1765
- ).run(teamId, agentId, userId, content, Date.now());
1965
+ async writePersona(teamId, agentId, userId, content, sessionKey) {
1966
+ if (sessionKey) {
1967
+ this.db.prepare(
1968
+ `INSERT INTO persona (team_id, agent_id, user_id, content, updated_at, session_key)
1969
+ VALUES (?, ?, ?, ?, ?, ?)
1970
+ ON CONFLICT(team_id, agent_id, user_id, session_key) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at`
1971
+ ).run(teamId, agentId, userId, content, Date.now(), sessionKey);
1972
+ } else {
1973
+ this.db.transaction(() => {
1974
+ this.db.prepare("DELETE FROM persona WHERE team_id = ? AND agent_id = ? AND user_id = ? AND (session_key IS NULL OR session_key = '')").run(teamId, agentId, userId);
1975
+ this.db.prepare("INSERT INTO persona (team_id, agent_id, user_id, content, updated_at) VALUES (?, ?, ?, ?, ?)").run(teamId, agentId, userId, content, Date.now());
1976
+ })();
1977
+ }
1766
1978
  }
1767
1979
  // ─── Knowledge ──────────────────────────────────────────────
1768
1980
  async putKnowledge(entry) {
@@ -2035,11 +2247,13 @@ var SQLiteBackend = class {
2035
2247
  if (entities.length === 0) return [];
2036
2248
  const normalized = entities.map((e) => e.toLowerCase());
2037
2249
  const placeholders = normalized.map(() => "?").join(",");
2038
- const params = [...normalized];
2250
+ const now = Date.now();
2251
+ const params = [...normalized, now];
2039
2252
  const conditions = [
2040
2253
  "c.deleted_at IS NULL",
2041
2254
  "c.trust_state != 'rejected'",
2042
- "c.superseded_by IS NULL"
2255
+ "c.superseded_by IS NULL",
2256
+ "(c.valid_until IS NULL OR c.valid_until > ?)"
2043
2257
  ];
2044
2258
  if (sessionKey) {
2045
2259
  conditions.push("c.session_key = ?");
@@ -2206,6 +2420,63 @@ var SQLiteBackend = class {
2206
2420
  linkStmt.run(captureId, row.other_id, "session-proximity", now);
2207
2421
  }
2208
2422
  }
2423
+ /**
2424
+ * v17: Causal link auto-extraction (MAGMA-inspired).
2425
+ * Detects causal language in capture content and links to nearby captures
2426
+ * in the same session that share tags or entities.
2427
+ *
2428
+ * Causal patterns: "caused by", "led to", "because of", "resulted in",
2429
+ * "due to", "fixed by", "root cause", "triggered by".
2430
+ *
2431
+ * Direction: if capture A says "X caused Y" and capture B (nearby, same tags)
2432
+ * contains "X", then A → B with link_type "cause-effect".
2433
+ */
2434
+ extractCausalLinks(captureId, content, tags, sessionKey) {
2435
+ const causalPatterns = [
2436
+ /\bcaused\s+by\b/i,
2437
+ /\bled\s+to\b/i,
2438
+ /\bbecause\s+of\b/i,
2439
+ /\bresulted\s+in\b/i,
2440
+ /\bdue\s+to\b/i,
2441
+ /\bfixed\s+by\b/i,
2442
+ /\broot\s+cause\b/i,
2443
+ /\btriggered\s+by\b/i,
2444
+ /\bfix(?:ed|es)?\s+(?:this|that|the)\b/i
2445
+ ];
2446
+ const hasCausalLanguage = causalPatterns.some((p) => p.test(content));
2447
+ if (tags.length === 0) return;
2448
+ const now = Date.now();
2449
+ const tagConditions = tags.map(() => "c.tags LIKE ?").join(" OR ");
2450
+ const nearbyRows = this.db.prepare(
2451
+ `SELECT c.id as other_id, c.content as other_content, c.created_at
2452
+ FROM captures c
2453
+ WHERE c.id != ? AND c.session_key = ? AND c.deleted_at IS NULL
2454
+ AND c.superseded_by IS NULL
2455
+ AND ABS(c.created_at - ?) < 600000
2456
+ AND (${tagConditions})
2457
+ ORDER BY ABS(c.created_at - ?) ASC
2458
+ LIMIT 5`
2459
+ ).all(
2460
+ captureId,
2461
+ sessionKey,
2462
+ now,
2463
+ ...tags.map((t) => `%"${t}"%`),
2464
+ now
2465
+ );
2466
+ const linkStmt = this.db.prepare(
2467
+ `INSERT OR IGNORE INTO memory_links (from_id, to_id, link_type, auto, created_at)
2468
+ VALUES (?, ?, 'cause-effect', 1, ?)`
2469
+ );
2470
+ for (const row of nearbyRows) {
2471
+ const nearbyHasCausal = causalPatterns.some((p) => p.test(row.other_content));
2472
+ if (!hasCausalLanguage && !nearbyHasCausal) continue;
2473
+ if (row.created_at < now) {
2474
+ linkStmt.run(row.other_id, captureId, now);
2475
+ } else {
2476
+ linkStmt.run(captureId, row.other_id, now);
2477
+ }
2478
+ }
2479
+ }
2209
2480
  // ─── v12: Feedback + audit + TTL ──────────────────────────────
2210
2481
  /**
2211
2482
  * Record feedback signal for a capture. Adjusts feedback_salience multiplier.
@@ -2297,7 +2568,10 @@ function rowToEntry(row) {
2297
2568
  taskId: row.task_id ?? void 0,
2298
2569
  trustState: row.trust_state ?? "candidate",
2299
2570
  rejectionReason: row.rejection_reason ?? void 0,
2300
- supersededBy: row.superseded_by ?? void 0
2571
+ supersededBy: row.superseded_by ?? void 0,
2572
+ validFrom: row.valid_from ?? void 0,
2573
+ validUntil: row.valid_until ?? void 0,
2574
+ sourceRef: row.source_ref ?? void 0
2301
2575
  };
2302
2576
  }
2303
2577
  function knowledgeRowToEntry(row) {
@@ -2339,4 +2613,4 @@ export {
2339
2613
  local_exports,
2340
2614
  init_local
2341
2615
  };
2342
- //# sourceMappingURL=chunk-4WCDAEPP.js.map
2616
+ //# sourceMappingURL=chunk-L7YZBBIB.js.map