remem-mcp 0.7.0 → 0.7.3

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.
@@ -82,6 +82,42 @@ function rrfMerge(bm25Results, vecResults, limit) {
82
82
 
83
83
  // src/storage/sqlite.ts
84
84
  init_ulid();
85
+
86
+ // src/utils/write-queue.ts
87
+ init_esm_shims();
88
+ var WriteQueue = class {
89
+ queue = Promise.resolve();
90
+ pendingCount = 0;
91
+ /**
92
+ * Enqueue a write task. All tasks are serialized — the next task
93
+ * only starts after the previous one completes (success or failure).
94
+ */
95
+ async enqueue(task) {
96
+ this.pendingCount++;
97
+ const result = this.queue.then(async () => {
98
+ try {
99
+ return await task();
100
+ } finally {
101
+ this.pendingCount--;
102
+ }
103
+ });
104
+ this.queue = result.catch(() => {
105
+ });
106
+ return result;
107
+ }
108
+ /** Number of tasks currently waiting/running. */
109
+ get pending() {
110
+ return this.pendingCount;
111
+ }
112
+ /** Wait for all pending tasks to complete. */
113
+ async drain() {
114
+ while (this.pendingCount > 0) {
115
+ await this.queue;
116
+ }
117
+ }
118
+ };
119
+
120
+ // src/storage/sqlite.ts
85
121
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
86
122
  var FTS_STOPWORDS_SET = /* @__PURE__ */ new Set([
87
123
  "the",
@@ -185,7 +221,7 @@ function stripQueryProperNouns(query) {
185
221
  if (noProperNouns.length > 0) return noProperNouns.join(" ");
186
222
  return filtered.length > 0 ? filtered.join(" ") : tokens.join(" ");
187
223
  }
188
- var CURRENT_SCHEMA_VERSION = 9;
224
+ var CURRENT_SCHEMA_VERSION = 13;
189
225
  var EVERGREEN_TAGS = new Set(
190
226
  (process.env.REMEM_EVERGREEN_TAGS ?? "evergreen,never-forget").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean)
191
227
  );
@@ -193,8 +229,145 @@ function isEvergreen(tags) {
193
229
  if (!tags || tags.length === 0) return false;
194
230
  return tags.some((t) => EVERGREEN_TAGS.has(t.toLowerCase()));
195
231
  }
232
+ function autoAssignTier(type, tags) {
233
+ if (tags) {
234
+ const lower = tags.map((t) => t.toLowerCase());
235
+ if (lower.includes("rule") || lower.includes("canonical")) return "rule";
236
+ if (lower.includes("decision")) return "decision";
237
+ if (lower.includes("test") || lower.includes("test-fixture")) return "test";
238
+ }
239
+ if (type === "decision") return "decision";
240
+ if (type === "pattern") return "rule";
241
+ return "episodic";
242
+ }
243
+ var AUTHORITY_POSITIVE_TAGS = /* @__PURE__ */ new Set([
244
+ "canonical",
245
+ "active",
246
+ "source-of-truth",
247
+ "pinned"
248
+ ]);
249
+ var AUTHORITY_NEGATIVE_TAGS = /* @__PURE__ */ new Set([
250
+ "superseded",
251
+ "historical",
252
+ "test-fixture",
253
+ "do-not-answer-from"
254
+ ]);
255
+ function authorityMultiplier(tier, tags) {
256
+ let multiplier = 1;
257
+ if (tier === "rule") multiplier += 0.2;
258
+ else if (tier === "decision") multiplier += 0.15;
259
+ else if (tier === "test") multiplier -= 0.1;
260
+ if (tags) {
261
+ for (const tag of tags) {
262
+ const lower = tag.toLowerCase();
263
+ if (AUTHORITY_POSITIVE_TAGS.has(lower)) multiplier += 0.1;
264
+ if (AUTHORITY_NEGATIVE_TAGS.has(lower)) multiplier -= 0.15;
265
+ }
266
+ }
267
+ return Math.max(0.5, Math.min(1.5, multiplier));
268
+ }
269
+ var ENTITY_STOPWORDS = /* @__PURE__ */ new Set([
270
+ "the",
271
+ "this",
272
+ "that",
273
+ "with",
274
+ "from",
275
+ "have",
276
+ "been",
277
+ "will",
278
+ "would",
279
+ "could",
280
+ "should",
281
+ "there",
282
+ "their",
283
+ "about",
284
+ "which",
285
+ "when",
286
+ "what",
287
+ "they",
288
+ "them",
289
+ "then",
290
+ "than",
291
+ "into",
292
+ "after",
293
+ "before",
294
+ "between",
295
+ "during",
296
+ "through",
297
+ "above",
298
+ "below",
299
+ "under",
300
+ "over",
301
+ "again",
302
+ "because",
303
+ "while",
304
+ "where",
305
+ "these",
306
+ "those",
307
+ "being",
308
+ "having",
309
+ "using",
310
+ "based",
311
+ "other",
312
+ "some",
313
+ "such",
314
+ "more",
315
+ "most",
316
+ "only",
317
+ "very",
318
+ "also",
319
+ "just",
320
+ "like",
321
+ "even",
322
+ "still",
323
+ "back",
324
+ "make",
325
+ "made",
326
+ "used",
327
+ "uses",
328
+ "using",
329
+ "want",
330
+ "need",
331
+ "know",
332
+ "think",
333
+ "thing",
334
+ "things",
335
+ "stuff",
336
+ "code",
337
+ "file",
338
+ "files",
339
+ "line",
340
+ "lines",
341
+ "here",
342
+ "there",
343
+ "where",
344
+ "when",
345
+ "what",
346
+ "which",
347
+ "whose"
348
+ ]);
349
+ function extractEntities(text) {
350
+ const capsMatches = text.match(/\b[A-Z][a-zA-Z]{2,}\b/g) ?? [];
351
+ const techMatches = text.match(/\b[a-z]+[-_][a-z]+\b/gi) ?? [];
352
+ const acroMatches = text.match(/\b[A-Z]{2,}\b/g) ?? [];
353
+ const words = text.match(/\b[a-z]{5,}\b/gi) ?? [];
354
+ const candidates = [...capsMatches, ...techMatches, ...acroMatches, ...words];
355
+ const seen = /* @__PURE__ */ new Set();
356
+ const entities = [];
357
+ for (const candidate of candidates) {
358
+ const lower = candidate.toLowerCase();
359
+ if (ENTITY_STOPWORDS.has(lower)) continue;
360
+ if (seen.has(lower)) continue;
361
+ seen.add(lower);
362
+ entities.push(lower);
363
+ if (entities.length >= 10) break;
364
+ }
365
+ return entities;
366
+ }
196
367
  var SQLiteBackend = class {
197
368
  db;
369
+ /** v11: Single-writer queue — serializes all write operations to prevent "database is locked". */
370
+ writeQueue = new WriteQueue();
198
371
  /** Get the underlying database instance (for CodeGraph/Wiki operations). */
199
372
  getDatabase() {
200
373
  return this.db;
@@ -332,6 +505,41 @@ var SQLiteBackend = class {
332
505
  this.writeSchemaVersion(9);
333
506
  })();
334
507
  }
508
+ if (currentVersion < 10) {
509
+ migrationsRan = true;
510
+ this.backupDatabase(dbPath, 9);
511
+ this.db.transaction(() => {
512
+ this.runSchema();
513
+ this.migrateV9ToV10();
514
+ this.writeSchemaVersion(10);
515
+ })();
516
+ }
517
+ if (currentVersion < 11) {
518
+ migrationsRan = true;
519
+ this.backupDatabase(dbPath, 10);
520
+ this.db.transaction(() => {
521
+ this.runSchema();
522
+ this.writeSchemaVersion(11);
523
+ })();
524
+ }
525
+ if (currentVersion < 12) {
526
+ migrationsRan = true;
527
+ this.backupDatabase(dbPath, 11);
528
+ this.db.transaction(() => {
529
+ this.runSchema();
530
+ this.migrateV11ToV12();
531
+ this.writeSchemaVersion(12);
532
+ })();
533
+ }
534
+ if (currentVersion < 13) {
535
+ migrationsRan = true;
536
+ this.backupDatabase(dbPath, 12);
537
+ this.db.transaction(() => {
538
+ this.runSchema();
539
+ this.migrateV12ToV13();
540
+ this.writeSchemaVersion(13);
541
+ })();
542
+ }
335
543
  this.rebuildFtsIfNeeded(migrationsRan);
336
544
  }
337
545
  /**
@@ -591,16 +799,68 @@ var SQLiteBackend = class {
591
799
  }
592
800
  console.error("[remem-mcp] Migrated schema v8 \u2192 v9 (CodeGraph call resolution)");
593
801
  }
802
+ /** Migrate schema v9 → v10: add tier + salience columns to captures, create entities table, backfill tiers. */
803
+ migrateV9ToV10() {
804
+ let addedAny = false;
805
+ try {
806
+ this.db.exec("ALTER TABLE captures ADD COLUMN tier TEXT NOT NULL DEFAULT 'episodic'");
807
+ addedAny = true;
808
+ } catch {
809
+ }
810
+ try {
811
+ this.db.exec("ALTER TABLE captures ADD COLUMN salience REAL NOT NULL DEFAULT 1.0");
812
+ addedAny = true;
813
+ } catch {
814
+ }
815
+ if (addedAny) {
816
+ console.error("[remem-mcp] Added tier + salience columns to captures");
817
+ }
818
+ this.db.exec("UPDATE captures SET tier = 'decision' WHERE type = 'decision' AND tier = 'episodic'");
819
+ this.db.exec("UPDATE captures SET tier = 'rule' WHERE type = 'pattern' AND tier = 'episodic'");
820
+ this.db.exec("UPDATE captures SET tier = 'test' WHERE type = 'error' AND tags LIKE '%test%' AND tier = 'episodic'");
821
+ console.error("[remem-mcp] Migrated schema v9 \u2192 v10 (authority tiers + salience + entities table)");
822
+ }
823
+ /**
824
+ * v11 → v12: Add expires_at + feedback_salience columns to captures.
825
+ * Create capture_feedback + audit_log tables (via runSchema).
826
+ * No data migration needed — new columns have defaults.
827
+ */
828
+ migrateV11ToV12() {
829
+ const cols = this.db.prepare("PRAGMA table_info(captures)").all();
830
+ const colNames = new Set(cols.map((c) => c.name));
831
+ if (!colNames.has("expires_at")) {
832
+ this.db.exec("ALTER TABLE captures ADD COLUMN expires_at INTEGER");
833
+ }
834
+ if (!colNames.has("feedback_salience")) {
835
+ this.db.exec("ALTER TABLE captures ADD COLUMN feedback_salience REAL NOT NULL DEFAULT 1.0");
836
+ }
837
+ console.error("[remem-mcp] Migrated schema v11 \u2192 v12 (feedback + mutation log + TTL)");
838
+ }
839
+ /**
840
+ * v12 → v13: Add weight column to memory_links for Hebbian co-retrieval strengthening.
841
+ */
842
+ migrateV12ToV13() {
843
+ const cols = this.db.prepare("PRAGMA table_info(memory_links)").all();
844
+ const colNames = new Set(cols.map((c) => c.name));
845
+ if (!colNames.has("weight")) {
846
+ this.db.exec("ALTER TABLE memory_links ADD COLUMN weight REAL NOT NULL DEFAULT 1.0");
847
+ }
848
+ console.error("[remem-mcp] Migrated schema v12 \u2192 v13 (Hebbian link weights)");
849
+ }
594
850
  async put(entry) {
851
+ this.putInternal(entry);
852
+ }
853
+ putInternal(entry) {
595
854
  const contentHash = entry.contentHash ?? createHash("sha256").update(entry.content).digest("hex");
855
+ const tier = autoAssignTier(entry.type, entry.tags);
596
856
  const putTx = this.db.transaction(() => {
597
857
  const dup = this.db.prepare(
598
858
  "SELECT 1 FROM captures WHERE content_hash = ? AND session_key = ? AND agent_id = ? AND deleted_at IS NULL LIMIT 1"
599
859
  ).get(contentHash, entry.sessionKey, entry.agentId);
600
860
  if (dup) return false;
601
861
  this.db.prepare(
602
- `INSERT INTO captures (id, session_key, agent_id, type, content, content_hash, tags, created_at, metadata, team_id, user_id, task_id, trust_state)
603
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
604
864
  ).run(
605
865
  entry.id,
606
866
  entry.sessionKey,
@@ -614,7 +874,8 @@ var SQLiteBackend = class {
614
874
  entry.teamId ?? null,
615
875
  entry.userId ?? null,
616
876
  entry.taskId ?? null,
617
- entry.trustState ?? "candidate"
877
+ entry.trustState ?? "candidate",
878
+ tier
618
879
  );
619
880
  if (entry.messages && entry.messages.length > 0) {
620
881
  const msgStmt = this.db.prepare(
@@ -627,7 +888,18 @@ var SQLiteBackend = class {
627
888
  }
628
889
  return true;
629
890
  });
630
- putTx();
891
+ const inserted = putTx();
892
+ if (inserted) {
893
+ try {
894
+ const entities = extractEntities(entry.content);
895
+ if (entities.length > 0) {
896
+ this.putEntitiesInternal(entry.id, entities);
897
+ }
898
+ this.autoLinkCapture(entry.id, entry.tags, entities, entry.sessionKey);
899
+ } catch {
900
+ }
901
+ this.recordAuditInternal("put", entry.id, { type: entry.type, tags: entry.tags }, entry.agentId);
902
+ }
631
903
  }
632
904
  async putVector(id, embedding) {
633
905
  const buffer = new Float32Array(embedding);
@@ -767,7 +1039,8 @@ var SQLiteBackend = class {
767
1039
  this.db.prepare("DELETE FROM atoms WHERE capture_id = ?").run(captureId);
768
1040
  }
769
1041
  async search(query, queryEmbedding, opts) {
770
- const { mode, limit, offset, sessionKey, filters } = opts;
1042
+ const { mode, limit, offset, sessionKey, filters, explain } = opts;
1043
+ const explainMap = /* @__PURE__ */ new Map();
771
1044
  const temporalIntent = /\b(currently|current|now|latest|new|present|today|active)\b/i.test(
772
1045
  query
773
1046
  );
@@ -783,8 +1056,48 @@ var SQLiteBackend = class {
783
1056
  if (mode === "hybrid" || mode === "keyword") {
784
1057
  bm25Results = this.bm25Search(query, bm25CandidateLimit, sessionKey, filters);
785
1058
  }
1059
+ let entityResults = [];
1060
+ let queryEntities = [];
1061
+ if (mode === "hybrid" || mode === "keyword") {
1062
+ try {
1063
+ queryEntities = extractEntities(query);
1064
+ if (queryEntities.length > 0) {
1065
+ entityResults = this.searchByEntities(
1066
+ queryEntities,
1067
+ limit * 2,
1068
+ sessionKey,
1069
+ filters ? { teamId: filters.teamId, userId: filters.userId, taskId: filters.taskId, type: filters.type } : void 0
1070
+ );
1071
+ }
1072
+ } catch {
1073
+ }
1074
+ }
1075
+ if (explain) {
1076
+ bm25Results.forEach((r, i) => {
1077
+ explainMap.set(r.id, { ...explainMap.get(r.id) ?? {}, bm25_rank: i + 1, bm25_score: r.score });
1078
+ });
1079
+ vecResults.forEach((r, i) => {
1080
+ explainMap.set(r.id, { ...explainMap.get(r.id) ?? {}, vector_rank: i + 1, vector_score: r.score });
1081
+ });
1082
+ entityResults.forEach((r, i) => {
1083
+ explainMap.set(r.id, { ...explainMap.get(r.id) ?? {}, entity_rank: i + 1, entity_matches: queryEntities });
1084
+ });
1085
+ }
786
1086
  if (mode === "keyword") {
787
- return this.fetchEntries(bm25Results, limit, offset, temporalIntent);
1087
+ const allResults = [...bm25Results, ...entityResults];
1088
+ if (entityResults.length > 0) {
1089
+ const scores = /* @__PURE__ */ new Map();
1090
+ const sortedBm25 = [...bm25Results].sort((a, b) => a.score - b.score);
1091
+ sortedBm25.forEach((r, i) => {
1092
+ scores.set(r.id, (scores.get(r.id) ?? 0) + 1 / (40 + i + 1));
1093
+ });
1094
+ entityResults.forEach((r, i) => {
1095
+ scores.set(r.id, (scores.get(r.id) ?? 0) + r.score);
1096
+ });
1097
+ const merged = [...scores.entries()].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score).slice(0, limit + offset);
1098
+ return this.fetchEntriesById(merged.slice(offset, offset + limit), temporalIntent, false, explain ? explainMap : void 0);
1099
+ }
1100
+ return this.fetchEntriesById((allResults.length > 0 ? allResults : bm25Results).slice(offset, offset + limit), temporalIntent, false, explain ? explainMap : void 0);
788
1101
  }
789
1102
  if (mode === "vector") {
790
1103
  const similarityResults = vecResults.map((r) => ({ id: r.id, score: 1 / (1 + r.score) }));
@@ -793,6 +1106,14 @@ var SQLiteBackend = class {
793
1106
  if (isScale) {
794
1107
  const rrfPoolSize = Math.min(Math.max((limit + offset) * 20, 100), 500);
795
1108
  const fused2 = rrfMerge(bm25Results, vecResults, rrfPoolSize);
1109
+ if (entityResults.length > 0) {
1110
+ const entityIds = new Set(entityResults.map((e) => e.id));
1111
+ for (const e of entityResults) {
1112
+ if (!fused2.some((f) => f.id === e.id)) {
1113
+ fused2.push(e);
1114
+ }
1115
+ }
1116
+ }
796
1117
  const fusedIds = new Set(fused2.map((f) => f.id));
797
1118
  const extraVec = vecResults.filter((v) => !fusedIds.has(v.id)).map((v) => ({ id: v.id, score: 0 }));
798
1119
  const paged2 = [...fused2, ...extraVec];
@@ -800,8 +1121,101 @@ var SQLiteBackend = class {
800
1121
  return scored.slice(0, limit);
801
1122
  }
802
1123
  const fused = rrfMerge(bm25Results, vecResults, limit + offset);
1124
+ if (entityResults.length > 0) {
1125
+ for (const e of entityResults) {
1126
+ if (!fused.some((f) => f.id === e.id)) {
1127
+ fused.push(e);
1128
+ }
1129
+ }
1130
+ fused.sort((a, b) => b.score - a.score);
1131
+ }
1132
+ const topIds = fused.slice(0, Math.min(limit, 20)).map((f) => f.id);
1133
+ if (topIds.length > 0) {
1134
+ const expanded = this.expandByLinks(topIds, limit * 2);
1135
+ const fusedIds = new Set(fused.map((f) => f.id));
1136
+ for (const exp of expanded) {
1137
+ if (!fusedIds.has(exp.id)) {
1138
+ fused.push(exp);
1139
+ if (explain) {
1140
+ const sourceId = topIds.find(
1141
+ (id) => this.getLinksFrom(id).some((l) => l.to_id === exp.id) || this.getLinksTo(id).some((l) => l.from_id === exp.id)
1142
+ );
1143
+ explainMap.set(exp.id, {
1144
+ ...explainMap.get(exp.id) ?? {},
1145
+ link_provenance: sourceId ? `expanded from ${sourceId}` : "link-neighbor"
1146
+ });
1147
+ }
1148
+ }
1149
+ }
1150
+ fused.sort((a, b) => b.score - a.score);
1151
+ }
803
1152
  const paged = fused.slice(offset, offset + limit);
804
- return this.fetchEntriesById(paged, temporalIntent);
1153
+ const results = await this.fetchEntriesById(paged, temporalIntent, false, explain ? explainMap : void 0);
1154
+ if (results.length === 0 && mode === "hybrid") {
1155
+ const rawResults = this.rawFallbackSearch(query, limit, sessionKey, filters);
1156
+ if (rawResults.length > 0) {
1157
+ if (explain) {
1158
+ rawResults.forEach((r) => {
1159
+ explainMap.set(r.id, { ...explainMap.get(r.id) ?? {}, raw_fallback: true });
1160
+ });
1161
+ }
1162
+ return this.fetchEntriesById(rawResults.slice(0, limit), temporalIntent, false, explain ? explainMap : void 0);
1163
+ }
1164
+ }
1165
+ if (results.length >= 2) {
1166
+ try {
1167
+ this.strengthenLinksOnCoRetrieval(results.map((r) => r.entry.id));
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ return results;
1172
+ }
1173
+ /**
1174
+ * v11: Raw observation fallback search.
1175
+ * Searches FTS5 without trust_state filter — includes stale and rejected.
1176
+ * Still excludes soft-deleted and superseded (those are intentionally removed).
1177
+ */
1178
+ rawFallbackSearch(query, limit, sessionKey, filters) {
1179
+ const ftsQuery = this.escapeFtsQuery(query);
1180
+ if (!ftsQuery) return [];
1181
+ const conditions = ["c.deleted_at IS NULL", "c.superseded_by IS NULL"];
1182
+ const params = [];
1183
+ if (sessionKey) {
1184
+ conditions.push("c.session_key = ?");
1185
+ params.push(sessionKey);
1186
+ }
1187
+ if (filters?.teamId) {
1188
+ conditions.push("c.team_id = ?");
1189
+ params.push(filters.teamId);
1190
+ }
1191
+ if (filters?.userId) {
1192
+ conditions.push("c.user_id = ?");
1193
+ params.push(filters.userId);
1194
+ }
1195
+ if (filters?.taskId) {
1196
+ conditions.push("c.task_id = ?");
1197
+ params.push(filters.taskId);
1198
+ }
1199
+ if (filters?.type) {
1200
+ conditions.push("c.type = ?");
1201
+ params.push(filters.type);
1202
+ }
1203
+ try {
1204
+ const rows = this.db.prepare(
1205
+ `SELECT fts.id as id, bm25(captures_fts) as score
1206
+ FROM captures_fts fts
1207
+ JOIN captures c ON c.id = fts.id
1208
+ WHERE captures_fts MATCH ? AND ${conditions.join(" AND ")}
1209
+ ORDER BY score ASC
1210
+ LIMIT ?`
1211
+ ).all(ftsQuery, ...params, limit);
1212
+ return rows.map((r, i) => ({
1213
+ id: r.id,
1214
+ score: 1 / (40 + i + 1)
1215
+ }));
1216
+ } catch {
1217
+ return [];
1218
+ }
805
1219
  }
806
1220
  /** Run a BM25 search via FTS5. */
807
1221
  bm25Search(query, limit, sessionKey, filters) {
@@ -926,7 +1340,7 @@ var SQLiteBackend = class {
926
1340
  return this.fetchEntriesById(paged, temporalIntent);
927
1341
  }
928
1342
  /** Fetch capture entries by ID, preserving the order of the input list. Applies memory decay and trust-state ranking. */
929
- async fetchEntriesById(results, temporalIntent = false, scaleBoost = false) {
1343
+ async fetchEntriesById(results, temporalIntent = false, scaleBoost = false, explainMap) {
930
1344
  if (results.length === 0) return [];
931
1345
  const ids = results.map((r) => r.id);
932
1346
  const placeholders = ids.map(() => "?").join(",");
@@ -967,7 +1381,17 @@ var SQLiteBackend = class {
967
1381
  const lastAccessed = row.last_accessed_at;
968
1382
  const accessBoost = accessCount > 0 && lastAccessed ? 0.3 + 1.2 * (1 / (1 + (now - lastAccessed) / (7 * 24 * 60 * 60 * 1e3))) : 1;
969
1383
  const accessScored = biasedScore >= 0 ? biasedScore * accessBoost : biasedScore / accessBoost;
970
- return { entry: rowToEntry(row), score: accessScored };
1384
+ const tier = row.tier ?? "episodic";
1385
+ const authBoost = authorityMultiplier(tier, tags);
1386
+ const authScored = accessScored >= 0 ? accessScored * authBoost : accessScored / authBoost;
1387
+ const feedbackSalience = row.feedback_salience ?? 1;
1388
+ const feedbackScored = feedbackSalience !== 1 ? authScored >= 0 ? authScored * feedbackSalience : authScored / feedbackSalience : authScored;
1389
+ const scoreDetails = explainMap ? {
1390
+ ...explainMap.get(row.id) ?? {},
1391
+ authority_multiplier: authBoost,
1392
+ feedback_salience: feedbackSalience
1393
+ } : void 0;
1394
+ return { entry: rowToEntry(row), score: feedbackScored, scoreDetails };
971
1395
  }).filter((r) => r !== null).filter((r) => !temporalIntent || r.entry.trustState !== "stale").sort((a, b) => {
972
1396
  if (temporalIntent) {
973
1397
  const timeDiff = b.entry.createdAt - a.entry.createdAt;
@@ -1504,6 +1928,359 @@ var SQLiteBackend = class {
1504
1928
  }
1505
1929
  return null;
1506
1930
  }
1931
+ // ─── v10: Decay/forget sweep ──────────────────────────────────
1932
+ /**
1933
+ * Run a forget sweep: compute salience for all non-deleted, non-evergreen captures,
1934
+ * then soft-delete (set deleted_at) those below the threshold.
1935
+ *
1936
+ * Salience formula (adapted from ai-memory):
1937
+ * salience = exp(-lambda * age_days) + sigma * log(1 + access_count) * exp(-mu * days_since_access)
1938
+ *
1939
+ * - lambda: age decay rate (default 0.01, ~70 day half-life)
1940
+ * - sigma: access frequency weight (default 0.3)
1941
+ * - mu: access recency decay (default 0.02, ~35 day half-life)
1942
+ *
1943
+ * Evergreen tags and tier=rule/decision captures are exempt (salience stays 1.0).
1944
+ * Verified captures get a floor of 0.3.
1945
+ */
1946
+ async forgetSweep(opts = {}) {
1947
+ const dryRun = opts.dryRun ?? false;
1948
+ const threshold = opts.threshold ?? 0.05;
1949
+ const maxAgeDays = opts.maxAgeDays ?? 365;
1950
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
1951
+ const lambda = 0.01;
1952
+ const sigma = 0.3;
1953
+ const mu = 0.02;
1954
+ const now = Date.now();
1955
+ const rows = this.db.prepare(
1956
+ `SELECT id, type, tags, created_at, access_count, last_accessed_at, trust_state, tier, expires_at, feedback_salience
1957
+ FROM captures
1958
+ WHERE deleted_at IS NULL AND (created_at < ? OR (expires_at IS NOT NULL AND expires_at < ?))`
1959
+ ).all(now - maxAgeMs, now);
1960
+ let swept = 0;
1961
+ let remaining = 0;
1962
+ const toDelete = [];
1963
+ for (const row of rows) {
1964
+ if (row.expires_at !== null && row.expires_at < now) {
1965
+ toDelete.push(row.id);
1966
+ swept++;
1967
+ continue;
1968
+ }
1969
+ const tags = row.tags ? JSON.parse(row.tags) : [];
1970
+ const evergreen = isEvergreen(tags);
1971
+ if (evergreen || row.tier === "rule" || row.tier === "decision") {
1972
+ remaining++;
1973
+ continue;
1974
+ }
1975
+ const ageDays = (now - row.created_at) / (24 * 60 * 60 * 1e3);
1976
+ const daysSinceAccess = row.last_accessed_at ? (now - row.last_accessed_at) / (24 * 60 * 60 * 1e3) : ageDays;
1977
+ const ageTerm = Math.exp(-lambda * ageDays);
1978
+ const accessTerm = sigma * Math.log(1 + (row.access_count ?? 0)) * Math.exp(-mu * daysSinceAccess);
1979
+ let salience = ageTerm + accessTerm;
1980
+ if (row.trust_state === "verified") {
1981
+ salience = Math.max(salience, 0.3);
1982
+ }
1983
+ salience *= row.feedback_salience ?? 1;
1984
+ if (salience < threshold) {
1985
+ toDelete.push(row.id);
1986
+ swept++;
1987
+ } else {
1988
+ remaining++;
1989
+ }
1990
+ }
1991
+ if (!dryRun && toDelete.length > 0) {
1992
+ const deleteStmt = this.db.prepare(
1993
+ "UPDATE captures SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL"
1994
+ );
1995
+ const tx = this.db.transaction(() => {
1996
+ for (const id of toDelete) {
1997
+ deleteStmt.run(now, id);
1998
+ this.recordAuditInternal("forget", id, { reason: "salience_below_threshold_or_ttl_expired" });
1999
+ }
2000
+ });
2001
+ tx();
2002
+ }
2003
+ return { swept, remaining, checked: rows.length };
2004
+ }
2005
+ // ─── v10: Entity-assisted recall ──────────────────────────────
2006
+ /** Store extracted entities for a capture (replaces existing). */
2007
+ putEntities(captureId, entities) {
2008
+ this.putEntitiesInternal(captureId, entities);
2009
+ }
2010
+ /** Internal entity storage — called from within putInternal. */
2011
+ putEntitiesInternal(captureId, entities) {
2012
+ if (entities.length === 0) return;
2013
+ const tx = this.db.transaction(() => {
2014
+ this.db.prepare("DELETE FROM entities WHERE capture_id = ?").run(captureId);
2015
+ const stmt = this.db.prepare(
2016
+ "INSERT INTO entities (capture_id, entity, created_at) VALUES (?, ?, ?)"
2017
+ );
2018
+ const now = Date.now();
2019
+ for (const entity of entities.slice(0, 10)) {
2020
+ stmt.run(captureId, entity.toLowerCase(), now);
2021
+ }
2022
+ });
2023
+ tx();
2024
+ }
2025
+ /** Get entities for a capture. */
2026
+ getEntities(captureId) {
2027
+ const rows = this.db.prepare("SELECT entity FROM entities WHERE capture_id = ? ORDER BY id").all(captureId);
2028
+ return rows.map((r) => r.entity);
2029
+ }
2030
+ /**
2031
+ * Search captures by entity match (lexical).
2032
+ * Returns capture IDs ranked by number of matching entities (RRF-style).
2033
+ */
2034
+ searchByEntities(entities, limit, sessionKey, filters) {
2035
+ if (entities.length === 0) return [];
2036
+ const normalized = entities.map((e) => e.toLowerCase());
2037
+ const placeholders = normalized.map(() => "?").join(",");
2038
+ const params = [...normalized];
2039
+ const conditions = [
2040
+ "c.deleted_at IS NULL",
2041
+ "c.trust_state != 'rejected'",
2042
+ "c.superseded_by IS NULL"
2043
+ ];
2044
+ if (sessionKey) {
2045
+ conditions.push("c.session_key = ?");
2046
+ params.push(sessionKey);
2047
+ }
2048
+ if (filters?.teamId) {
2049
+ conditions.push("c.team_id = ?");
2050
+ params.push(filters.teamId);
2051
+ }
2052
+ if (filters?.userId) {
2053
+ conditions.push("c.user_id = ?");
2054
+ params.push(filters.userId);
2055
+ }
2056
+ if (filters?.taskId) {
2057
+ conditions.push("c.task_id = ?");
2058
+ params.push(filters.taskId);
2059
+ }
2060
+ if (filters?.type) {
2061
+ conditions.push("c.type = ?");
2062
+ params.push(filters.type);
2063
+ }
2064
+ const rows = this.db.prepare(
2065
+ `SELECT e.capture_id as id, COUNT(*) as match_count
2066
+ FROM entities e
2067
+ JOIN captures c ON c.id = e.capture_id
2068
+ WHERE e.entity IN (${placeholders})
2069
+ AND ${conditions.join(" AND ")}
2070
+ GROUP BY e.capture_id
2071
+ ORDER BY match_count DESC
2072
+ LIMIT ?`
2073
+ ).all(...params, limit);
2074
+ return rows.map((r, i) => ({
2075
+ id: r.id,
2076
+ score: 1 / (40 + i + 1)
2077
+ // same k as RRF
2078
+ }));
2079
+ }
2080
+ // ─── v11: Memory-to-memory links ──────────────────────────────
2081
+ /** Link two captures. Auto-links use auto=1. */
2082
+ linkCaptures(fromId, toId, linkType, auto = false) {
2083
+ if (fromId === toId) return;
2084
+ try {
2085
+ this.db.prepare(
2086
+ `INSERT OR IGNORE INTO memory_links (from_id, to_id, link_type, auto, created_at)
2087
+ VALUES (?, ?, ?, ?, ?)`
2088
+ ).run(fromId, toId, linkType, auto ? 1 : 0, Date.now());
2089
+ this.recordAuditInternal("link", fromId, { toId, linkType, auto });
2090
+ } catch {
2091
+ }
2092
+ }
2093
+ /** Get links from a capture (outbound). */
2094
+ getLinksFrom(captureId) {
2095
+ return this.db.prepare("SELECT to_id, link_type, auto FROM memory_links WHERE from_id = ?").all(captureId);
2096
+ }
2097
+ /** Get links to a capture (inbound). */
2098
+ getLinksTo(captureId) {
2099
+ return this.db.prepare("SELECT from_id, link_type, auto FROM memory_links WHERE to_id = ?").all(captureId);
2100
+ }
2101
+ /**
2102
+ * Expand capture IDs via link-neighbor traversal (1-hop).
2103
+ * Returns linked capture IDs with decayed score (0.5x of original).
2104
+ * Only includes non-deleted, non-rejected captures.
2105
+ */
2106
+ expandByLinks(ids, limit) {
2107
+ if (ids.length === 0) return [];
2108
+ const placeholders = ids.map(() => "?").join(",");
2109
+ const rows = this.db.prepare(
2110
+ `SELECT linked_id as id, MAX(weight) as weight FROM (
2111
+ SELECT to_id as linked_id, weight FROM memory_links WHERE from_id IN (${placeholders})
2112
+ UNION
2113
+ SELECT from_id as linked_id, weight FROM memory_links WHERE to_id IN (${placeholders})
2114
+ )
2115
+ WHERE linked_id NOT IN (${placeholders})
2116
+ GROUP BY linked_id
2117
+ ORDER BY weight DESC
2118
+ LIMIT ?`
2119
+ ).all(...ids, ...ids, ...ids, limit);
2120
+ if (rows.length === 0) return [];
2121
+ const validPlaceholders = rows.map(() => "?").join(",");
2122
+ const validRows = this.db.prepare(
2123
+ `SELECT id FROM captures WHERE id IN (${validPlaceholders})
2124
+ AND deleted_at IS NULL AND trust_state != 'rejected' AND superseded_by IS NULL`
2125
+ ).all(...rows.map((r) => r.id));
2126
+ const validIds = new Set(validRows.map((r) => r.id));
2127
+ return rows.filter((r) => validIds.has(r.id)).map((r, i) => ({
2128
+ id: r.id,
2129
+ score: 0.5 * (r.weight ?? 1) / (40 + i + 1)
2130
+ // decayed RRF × weight
2131
+ }));
2132
+ }
2133
+ /**
2134
+ * v13: Hebbian co-retrieval strengthening.
2135
+ * When captures co-occur in search results, strengthen links between them.
2136
+ * Creates links if they don't exist, increments weight if they do.
2137
+ */
2138
+ strengthenLinksOnCoRetrieval(ids) {
2139
+ if (ids.length < 2) return;
2140
+ const now = Date.now();
2141
+ const linkType = "co-retrieval";
2142
+ const insertStmt = this.db.prepare(
2143
+ `INSERT OR IGNORE INTO memory_links (from_id, to_id, link_type, auto, weight, created_at)
2144
+ VALUES (?, ?, ?, 1, 1.0, ?)`
2145
+ );
2146
+ const updateStmt = this.db.prepare(
2147
+ `UPDATE memory_links SET weight = weight + 0.1 WHERE from_id = ? AND to_id = ? AND link_type = ?`
2148
+ );
2149
+ const topIds = ids.slice(0, 10);
2150
+ for (let i = 0; i < topIds.length; i++) {
2151
+ for (let j = i + 1; j < topIds.length; j++) {
2152
+ const [a, b] = topIds[i] < topIds[j] ? [topIds[i], topIds[j]] : [topIds[j], topIds[i]];
2153
+ try {
2154
+ insertStmt.run(a, b, linkType, now);
2155
+ updateStmt.run(a, b, linkType);
2156
+ } catch {
2157
+ }
2158
+ }
2159
+ }
2160
+ }
2161
+ /**
2162
+ * Auto-link a new capture to existing captures based on shared tags and entities.
2163
+ * Called after put() + entity extraction.
2164
+ */
2165
+ autoLinkCapture(captureId, tags, entities, sessionKey) {
2166
+ const now = Date.now();
2167
+ const linkStmt = this.db.prepare(
2168
+ `INSERT OR IGNORE INTO memory_links (from_id, to_id, link_type, auto, created_at)
2169
+ VALUES (?, ?, ?, 1, ?)`
2170
+ );
2171
+ if (tags.length > 0) {
2172
+ const tagPlaceholders = tags.map(() => "?").join(",");
2173
+ const tagRows = this.db.prepare(
2174
+ `SELECT DISTINCT c.id as other_id FROM captures c
2175
+ WHERE c.id != ? AND c.session_key = ? AND c.deleted_at IS NULL
2176
+ AND c.tags IS NOT NULL
2177
+ AND (c.tags LIKE ${tags.map(() => "?").join(" OR c.tags LIKE ")})`
2178
+ ).all(
2179
+ captureId,
2180
+ sessionKey,
2181
+ ...tags.map((t) => `%"${t}"%`)
2182
+ );
2183
+ for (const row of tagRows.slice(0, 5)) {
2184
+ linkStmt.run(captureId, row.other_id, "shared-tag", now);
2185
+ }
2186
+ }
2187
+ if (entities.length > 0) {
2188
+ const entityPlaceholders = entities.map(() => "?").join(",");
2189
+ const entityRows = this.db.prepare(
2190
+ `SELECT DISTINCT e.capture_id as other_id FROM entities e
2191
+ JOIN captures c ON c.id = e.capture_id
2192
+ WHERE e.capture_id != ? AND c.session_key = ? AND c.deleted_at IS NULL
2193
+ AND e.entity IN (${entityPlaceholders})`
2194
+ ).all(captureId, sessionKey, ...entities);
2195
+ for (const row of entityRows.slice(0, 5)) {
2196
+ linkStmt.run(captureId, row.other_id, "shared-entity", now);
2197
+ }
2198
+ }
2199
+ const proximityRows = this.db.prepare(
2200
+ `SELECT id as other_id FROM captures
2201
+ WHERE id != ? AND session_key = ? AND deleted_at IS NULL
2202
+ AND ABS(created_at - ?) < 300000
2203
+ ORDER BY ABS(created_at - ?) ASC LIMIT 3`
2204
+ ).all(captureId, sessionKey, now, now);
2205
+ for (const row of proximityRows) {
2206
+ linkStmt.run(captureId, row.other_id, "session-proximity", now);
2207
+ }
2208
+ }
2209
+ // ─── v12: Feedback + audit + TTL ──────────────────────────────
2210
+ /**
2211
+ * Record feedback signal for a capture. Adjusts feedback_salience multiplier.
2212
+ * - helpful: +0.1 (max 2.0)
2213
+ * - not_helpful: -0.1 (min 0.1)
2214
+ * - stale: floor salience at 0.3
2215
+ * - wrong: floor salience at 0.1
2216
+ */
2217
+ recordFeedback(captureId, signal, reason, agentId) {
2218
+ const now = Date.now();
2219
+ this.db.transaction(() => {
2220
+ this.db.prepare(
2221
+ "INSERT INTO capture_feedback (capture_id, signal, reason, agent_id, created_at) VALUES (?, ?, ?, ?, ?)"
2222
+ ).run(captureId, signal, reason ?? null, agentId ?? null, now);
2223
+ const row = this.db.prepare("SELECT feedback_salience FROM captures WHERE id = ?").get(captureId);
2224
+ if (!row) return;
2225
+ let newSalience = row.feedback_salience;
2226
+ if (signal === "helpful") {
2227
+ newSalience = Math.min(2, newSalience + 0.1);
2228
+ } else if (signal === "not_helpful") {
2229
+ newSalience = Math.max(0.1, newSalience - 0.1);
2230
+ } else if (signal === "stale") {
2231
+ newSalience = Math.min(newSalience, 0.3);
2232
+ } else if (signal === "wrong") {
2233
+ newSalience = Math.min(newSalience, 0.1);
2234
+ }
2235
+ this.db.prepare("UPDATE captures SET feedback_salience = ? WHERE id = ?").run(newSalience, captureId);
2236
+ this.recordAuditInternal("feedback", captureId, { signal, reason, newSalience }, agentId);
2237
+ })();
2238
+ }
2239
+ /** Get feedback signals for a capture. */
2240
+ getFeedback(captureId) {
2241
+ return this.db.prepare(
2242
+ "SELECT signal, reason, created_at FROM capture_feedback WHERE capture_id = ? ORDER BY id DESC"
2243
+ ).all(captureId);
2244
+ }
2245
+ /** Record a mutation log entry. Public API. */
2246
+ recordAudit(action, captureId, details, agentId) {
2247
+ this.recordAuditInternal(action, captureId, details, agentId);
2248
+ }
2249
+ /** Internal mutation log — called from within transactions. */
2250
+ recordAuditInternal(action, captureId, details, agentId) {
2251
+ try {
2252
+ this.db.prepare(
2253
+ "INSERT INTO mutation_log (action, capture_id, details, agent_id, created_at) VALUES (?, ?, ?, ?, ?)"
2254
+ ).run(
2255
+ action,
2256
+ captureId,
2257
+ details ? JSON.stringify(details) : null,
2258
+ agentId ?? null,
2259
+ Date.now()
2260
+ );
2261
+ } catch {
2262
+ }
2263
+ }
2264
+ /** Query mutation log. */
2265
+ queryAudit(opts) {
2266
+ const conditions = [];
2267
+ const params = [];
2268
+ if (opts.action) {
2269
+ conditions.push("action = ?");
2270
+ params.push(opts.action);
2271
+ }
2272
+ if (opts.captureId) {
2273
+ conditions.push("capture_id = ?");
2274
+ params.push(opts.captureId);
2275
+ }
2276
+ if (opts.since) {
2277
+ conditions.push("created_at >= ?");
2278
+ params.push(opts.since);
2279
+ }
2280
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2281
+ const limit = opts.limit ?? 100;
2282
+ return this.db.prepare(`SELECT * FROM mutation_log ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
2283
+ }
1507
2284
  };
1508
2285
  function rowToEntry(row) {
1509
2286
  return {
@@ -1562,4 +2339,4 @@ export {
1562
2339
  local_exports,
1563
2340
  init_local
1564
2341
  };
1565
- //# sourceMappingURL=chunk-OYUNKUZK.js.map
2342
+ //# sourceMappingURL=chunk-4WCDAEPP.js.map