remem-mcp 0.6.8 → 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.
@@ -1,13 +1,12 @@
1
1
  import {
2
2
  generateId,
3
- init_engine,
4
3
  init_ulid
5
- } from "./chunk-FA324IBN.js";
4
+ } from "./chunk-XJGDYQ34.js";
6
5
  import {
7
6
  __esm,
8
7
  __export,
9
8
  init_esm_shims
10
- } from "./chunk-XK4Y53J2.js";
9
+ } from "./chunk-ONQHLUGM.js";
11
10
 
12
11
  // src/embedding/local.ts
13
12
  var local_exports = {};
@@ -50,91 +49,11 @@ var init_local = __esm({
50
49
  }
51
50
  });
52
51
 
53
- // src/sdk.ts
54
- init_esm_shims();
55
- init_local();
56
- import { createHash as createHash3 } from "crypto";
57
- import { homedir } from "os";
58
- import { join as join3 } from "path";
59
-
60
- // src/security/redactor.ts
61
- init_esm_shims();
62
- var SECRET_PATTERNS = [
63
- // OpenAI API key
64
- /sk-[a-zA-Z0-9]{20,}/g,
65
- // Anthropic API key
66
- /sk-ant-[a-zA-Z0-9-]+/g,
67
- // GitHub personal access token
68
- /ghp_[a-zA-Z0-9]{36}/g,
69
- // GitHub OAuth token
70
- /gho_[a-zA-Z0-9]{36}/g,
71
- // GitHub fine-grained token
72
- /github_pat_[a-zA-Z0-9_]{82}/g,
73
- // Slack token
74
- /xox[baprs]-[a-zA-Z0-9-]+/g,
75
- // AWS access key ID
76
- /AKIA[0-9A-Z]{16}/g,
77
- // AWS secret access key (40 chars, base64-ish)
78
- /(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{40}(?![A-Za-z0-9+/])/g,
79
- // PEM private key block
80
- /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
81
- // Google API key
82
- /AIza[0-9A-Za-z\-_]{35}/g,
83
- // Generic bearer token (require 20+ char token to avoid redacting "Bearer with JWT")
84
- /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/g
85
- ];
86
- var MIN_LENGTH = 40;
87
- var ENTROPY_THRESHOLD = 4.5;
88
- function shannonEntropy(str) {
89
- const freq = /* @__PURE__ */ new Map();
90
- for (const ch of str) {
91
- freq.set(ch, (freq.get(ch) ?? 0) + 1);
92
- }
93
- let entropy = 0;
94
- const len = str.length;
95
- for (const count of freq.values()) {
96
- const p = count / len;
97
- entropy -= p * Math.log2(p);
98
- }
99
- return entropy;
100
- }
101
- function findHighEntropyStrings(text) {
102
- const results = [];
103
- const regex = /[a-zA-Z0-9+/=]{40,}/g;
104
- let match;
105
- match = regex.exec(text);
106
- while (match !== null) {
107
- const str = match[0];
108
- if (str.length >= MIN_LENGTH) {
109
- const entropy = shannonEntropy(str);
110
- if (entropy >= ENTROPY_THRESHOLD) {
111
- results.push(str);
112
- }
113
- }
114
- match = regex.exec(text);
115
- }
116
- return results;
117
- }
118
- function redact(text) {
119
- let result = text;
120
- let redacted = false;
121
- for (const pattern of SECRET_PATTERNS) {
122
- const before = result;
123
- result = result.replace(pattern, "[REDACTED]");
124
- if (result !== before) redacted = true;
125
- }
126
- const highEntropy = findHighEntropyStrings(result);
127
- for (const str of highEntropy) {
128
- result = result.replace(str, "[REDACTED]");
129
- redacted = true;
130
- }
131
- return { text: result, redacted };
132
- }
133
-
134
52
  // src/storage/sqlite.ts
135
53
  init_esm_shims();
136
54
  import { createHash } from "crypto";
137
- import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
55
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
56
+ import { homedir } from "os";
138
57
  import { dirname, join } from "path";
139
58
  import { fileURLToPath } from "url";
140
59
  import Database from "better-sqlite3";
@@ -163,6 +82,42 @@ function rrfMerge(bm25Results, vecResults, limit) {
163
82
 
164
83
  // src/storage/sqlite.ts
165
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
166
121
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
167
122
  var FTS_STOPWORDS_SET = /* @__PURE__ */ new Set([
168
123
  "the",
@@ -266,7 +221,7 @@ function stripQueryProperNouns(query) {
266
221
  if (noProperNouns.length > 0) return noProperNouns.join(" ");
267
222
  return filtered.length > 0 ? filtered.join(" ") : tokens.join(" ");
268
223
  }
269
- var CURRENT_SCHEMA_VERSION = 8;
224
+ var CURRENT_SCHEMA_VERSION = 13;
270
225
  var EVERGREEN_TAGS = new Set(
271
226
  (process.env.REMEM_EVERGREEN_TAGS ?? "evergreen,never-forget").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean)
272
227
  );
@@ -274,8 +229,145 @@ function isEvergreen(tags) {
274
229
  if (!tags || tags.length === 0) return false;
275
230
  return tags.some((t) => EVERGREEN_TAGS.has(t.toLowerCase()));
276
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
+ }
277
367
  var SQLiteBackend = class {
278
368
  db;
369
+ /** v11: Single-writer queue — serializes all write operations to prevent "database is locked". */
370
+ writeQueue = new WriteQueue();
279
371
  /** Get the underlying database instance (for CodeGraph/Wiki operations). */
280
372
  getDatabase() {
281
373
  return this.db;
@@ -292,7 +384,7 @@ var SQLiteBackend = class {
292
384
  this.db = new Database(dbPath, { readonly: true });
293
385
  }
294
386
  try {
295
- this.db.pragma("busy_timeout = 5000");
387
+ this.db.pragma("busy_timeout = 10000");
296
388
  } catch {
297
389
  }
298
390
  if (!readonly) {
@@ -413,6 +505,41 @@ var SQLiteBackend = class {
413
505
  this.writeSchemaVersion(9);
414
506
  })();
415
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
+ }
416
543
  this.rebuildFtsIfNeeded(migrationsRan);
417
544
  }
418
545
  /**
@@ -647,33 +774,93 @@ var SQLiteBackend = class {
647
774
  try {
648
775
  this.db.exec("ALTER TABLE symbols ADD COLUMN module_path TEXT");
649
776
  addedAny = true;
650
- } catch {
777
+ } catch (err) {
778
+ if (!String(err).includes("duplicate column"))
779
+ console.error(`[remem-mcp] migrateV8ToV9: ${err}`);
651
780
  }
652
781
  try {
653
782
  this.db.exec("ALTER TABLE calls ADD COLUMN confidence REAL");
654
783
  addedAny = true;
655
- } catch {
784
+ } catch (err) {
785
+ if (!String(err).includes("duplicate column"))
786
+ console.error(`[remem-mcp] migrateV8ToV9: ${err}`);
656
787
  }
657
788
  try {
658
789
  this.db.exec("ALTER TABLE calls ADD COLUMN call_type TEXT NOT NULL DEFAULT 'direct'");
659
790
  addedAny = true;
660
- } catch {
791
+ } catch (err) {
792
+ if (!String(err).includes("duplicate column"))
793
+ console.error(`[remem-mcp] migrateV8ToV9: ${err}`);
661
794
  }
662
795
  if (addedAny) {
663
- console.error("[remem-mcp] Added CodeGraph call resolution columns (module_path, confidence, call_type)");
796
+ console.error(
797
+ "[remem-mcp] Added CodeGraph call resolution columns (module_path, confidence, call_type)"
798
+ );
664
799
  }
665
800
  console.error("[remem-mcp] Migrated schema v8 \u2192 v9 (CodeGraph call resolution)");
666
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
+ }
667
850
  async put(entry) {
851
+ this.putInternal(entry);
852
+ }
853
+ putInternal(entry) {
668
854
  const contentHash = entry.contentHash ?? createHash("sha256").update(entry.content).digest("hex");
855
+ const tier = autoAssignTier(entry.type, entry.tags);
669
856
  const putTx = this.db.transaction(() => {
670
857
  const dup = this.db.prepare(
671
858
  "SELECT 1 FROM captures WHERE content_hash = ? AND session_key = ? AND agent_id = ? AND deleted_at IS NULL LIMIT 1"
672
859
  ).get(contentHash, entry.sessionKey, entry.agentId);
673
860
  if (dup) return false;
674
861
  this.db.prepare(
675
- `INSERT INTO captures (id, session_key, agent_id, type, content, content_hash, tags, created_at, metadata, team_id, user_id, task_id, trust_state)
676
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
677
864
  ).run(
678
865
  entry.id,
679
866
  entry.sessionKey,
@@ -687,7 +874,8 @@ var SQLiteBackend = class {
687
874
  entry.teamId ?? null,
688
875
  entry.userId ?? null,
689
876
  entry.taskId ?? null,
690
- entry.trustState ?? "candidate"
877
+ entry.trustState ?? "candidate",
878
+ tier
691
879
  );
692
880
  if (entry.messages && entry.messages.length > 0) {
693
881
  const msgStmt = this.db.prepare(
@@ -700,7 +888,18 @@ var SQLiteBackend = class {
700
888
  }
701
889
  return true;
702
890
  });
703
- 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
+ }
704
903
  }
705
904
  async putVector(id, embedding) {
706
905
  const buffer = new Float32Array(embedding);
@@ -832,7 +1031,7 @@ var SQLiteBackend = class {
832
1031
  }
833
1032
  async listAll(limit = 50, offset = 0) {
834
1033
  const rows = this.db.prepare(
835
- "SELECT * FROM captures WHERE deleted_at IS NULL AND trust_state != 'rejected' AND superseded_by IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?"
1034
+ "SELECT * FROM captures WHERE deleted_at IS NULL AND trust_state != 'rejected' AND superseded_by IS NULL AND id IS NOT NULL ORDER BY created_at DESC LIMIT ? OFFSET ?"
836
1035
  ).all(limit, offset);
837
1036
  return rows.map(rowToEntry);
838
1037
  }
@@ -840,7 +1039,8 @@ var SQLiteBackend = class {
840
1039
  this.db.prepare("DELETE FROM atoms WHERE capture_id = ?").run(captureId);
841
1040
  }
842
1041
  async search(query, queryEmbedding, opts) {
843
- const { mode, limit, offset, sessionKey, filters } = opts;
1042
+ const { mode, limit, offset, sessionKey, filters, explain } = opts;
1043
+ const explainMap = /* @__PURE__ */ new Map();
844
1044
  const temporalIntent = /\b(currently|current|now|latest|new|present|today|active)\b/i.test(
845
1045
  query
846
1046
  );
@@ -856,8 +1056,48 @@ var SQLiteBackend = class {
856
1056
  if (mode === "hybrid" || mode === "keyword") {
857
1057
  bm25Results = this.bm25Search(query, bm25CandidateLimit, sessionKey, filters);
858
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
+ }
859
1086
  if (mode === "keyword") {
860
- 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);
861
1101
  }
862
1102
  if (mode === "vector") {
863
1103
  const similarityResults = vecResults.map((r) => ({ id: r.id, score: 1 / (1 + r.score) }));
@@ -866,6 +1106,14 @@ var SQLiteBackend = class {
866
1106
  if (isScale) {
867
1107
  const rrfPoolSize = Math.min(Math.max((limit + offset) * 20, 100), 500);
868
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
+ }
869
1117
  const fusedIds = new Set(fused2.map((f) => f.id));
870
1118
  const extraVec = vecResults.filter((v) => !fusedIds.has(v.id)).map((v) => ({ id: v.id, score: 0 }));
871
1119
  const paged2 = [...fused2, ...extraVec];
@@ -873,8 +1121,101 @@ var SQLiteBackend = class {
873
1121
  return scored.slice(0, limit);
874
1122
  }
875
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
+ }
876
1152
  const paged = fused.slice(offset, offset + limit);
877
- 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
+ }
878
1219
  }
879
1220
  /** Run a BM25 search via FTS5. */
880
1221
  bm25Search(query, limit, sessionKey, filters) {
@@ -999,7 +1340,7 @@ var SQLiteBackend = class {
999
1340
  return this.fetchEntriesById(paged, temporalIntent);
1000
1341
  }
1001
1342
  /** Fetch capture entries by ID, preserving the order of the input list. Applies memory decay and trust-state ranking. */
1002
- async fetchEntriesById(results, temporalIntent = false, scaleBoost = false) {
1343
+ async fetchEntriesById(results, temporalIntent = false, scaleBoost = false, explainMap) {
1003
1344
  if (results.length === 0) return [];
1004
1345
  const ids = results.map((r) => r.id);
1005
1346
  const placeholders = ids.map(() => "?").join(",");
@@ -1040,7 +1381,17 @@ var SQLiteBackend = class {
1040
1381
  const lastAccessed = row.last_accessed_at;
1041
1382
  const accessBoost = accessCount > 0 && lastAccessed ? 0.3 + 1.2 * (1 / (1 + (now - lastAccessed) / (7 * 24 * 60 * 60 * 1e3))) : 1;
1042
1383
  const accessScored = biasedScore >= 0 ? biasedScore * accessBoost : biasedScore / accessBoost;
1043
- 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 };
1044
1395
  }).filter((r) => r !== null).filter((r) => !temporalIntent || r.entry.trustState !== "stale").sort((a, b) => {
1045
1396
  if (temporalIntent) {
1046
1397
  const timeDiff = b.entry.createdAt - a.entry.createdAt;
@@ -1454,9 +1805,9 @@ var SQLiteBackend = class {
1454
1805
  // ─── Skills ─────────────────────────────────────────────────
1455
1806
  async putSkill(entry) {
1456
1807
  this.db.prepare(
1457
- `INSERT INTO skills (id, team_id, agent_id, name, description, content, version, created_at, updated_at)
1458
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1459
- ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, content = excluded.content, version = excluded.version, updated_at = excluded.updated_at`
1808
+ `INSERT INTO skills (id, team_id, agent_id, name, description, content, version, created_at, updated_at, trigger_conditions, steps, validation_rules, source_capture_ids, archived)
1809
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1810
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, content = excluded.content, version = excluded.version, updated_at = excluded.updated_at, trigger_conditions = excluded.trigger_conditions, steps = excluded.steps, validation_rules = excluded.validation_rules, source_capture_ids = excluded.source_capture_ids, archived = excluded.archived`
1460
1811
  ).run(
1461
1812
  entry.id,
1462
1813
  entry.teamId,
@@ -1466,7 +1817,12 @@ var SQLiteBackend = class {
1466
1817
  entry.content ?? null,
1467
1818
  entry.version,
1468
1819
  entry.createdAt,
1469
- entry.updatedAt
1820
+ entry.updatedAt,
1821
+ entry.triggerConditions ? JSON.stringify(entry.triggerConditions) : null,
1822
+ entry.steps ? JSON.stringify(entry.steps) : null,
1823
+ entry.validationRules ? JSON.stringify(entry.validationRules) : null,
1824
+ entry.sourceCaptureIds ? JSON.stringify(entry.sourceCaptureIds) : null,
1825
+ entry.archived ? 1 : 0
1470
1826
  );
1471
1827
  }
1472
1828
  async getSkill(id) {
@@ -1497,6 +1853,434 @@ var SQLiteBackend = class {
1497
1853
  close() {
1498
1854
  this.db.close();
1499
1855
  }
1856
+ // ─── Canvas + Refs (v9: symbolic short-term memory) ───────────────────
1857
+ // These methods implement the StorageBackend interface for F1 (Mermaid canvas).
1858
+ async appendCanvasNode(sessionKey, node, edges, teamId) {
1859
+ const now = Date.now();
1860
+ let canvasRow = this.db.prepare("SELECT id FROM canvases WHERE session_key = ? ORDER BY updated_at DESC LIMIT 1").get(sessionKey);
1861
+ if (!canvasRow) {
1862
+ const canvasId = `01${now.toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 10).toUpperCase()}`;
1863
+ this.db.prepare(
1864
+ "INSERT INTO canvases (id, session_key, mermaid_text, node_count, created_at, updated_at, team_id) VALUES (?, ?, NULL, 0, ?, ?, ?)"
1865
+ ).run(canvasId, sessionKey, now, now, teamId ?? null);
1866
+ canvasRow = { id: canvasId };
1867
+ }
1868
+ const maxSeq = this.db.prepare("SELECT MAX(seq) as max_seq FROM canvas_nodes WHERE canvas_id = ?").get(canvasRow.id);
1869
+ const seq = (maxSeq.max_seq ?? 0) + 1;
1870
+ this.db.prepare(
1871
+ "INSERT INTO canvas_nodes (id, canvas_id, node_id, label, seq, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1872
+ ).run(`01${now.toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 10).toUpperCase()}`, canvasRow.id, node.id, node.label, seq, now);
1873
+ for (const edge of edges) {
1874
+ this.db.prepare(
1875
+ "INSERT INTO canvas_edges (id, canvas_id, from_node_id, to_node_id, label, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1876
+ ).run(`01${now.toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 10).toUpperCase()}`, canvasRow.id, edge.from, edge.to, edge.label ?? null, now);
1877
+ }
1878
+ this.db.prepare("UPDATE canvases SET node_count = node_count + 1, updated_at = ? WHERE id = ?").run(now, canvasRow.id);
1879
+ const allNodes = this.db.prepare("SELECT node_id, label FROM canvas_nodes WHERE canvas_id = ? ORDER BY seq ASC").all(canvasRow.id);
1880
+ const allEdges = this.db.prepare("SELECT from_node_id, to_node_id, label FROM canvas_edges WHERE canvas_id = ?").all(canvasRow.id);
1881
+ const mermaidLines = ["graph LR"];
1882
+ for (const n of allNodes) {
1883
+ mermaidLines.push(` ${n.node_id}["${n.label.replace(/"/g, "'")}"]`);
1884
+ }
1885
+ for (const e of allEdges) {
1886
+ if (e.label) {
1887
+ mermaidLines.push(` ${e.from_node_id} -->|${e.label.replace(/"/g, "'")}| ${e.to_node_id}`);
1888
+ } else {
1889
+ mermaidLines.push(` ${e.from_node_id} --> ${e.to_node_id}`);
1890
+ }
1891
+ }
1892
+ this.db.prepare("UPDATE canvases SET mermaid_text = ? WHERE id = ?").run(mermaidLines.join("\n"), canvasRow.id);
1893
+ }
1894
+ async getLatestCanvasNode(sessionKey) {
1895
+ const row = this.db.prepare(
1896
+ `SELECT cn.node_id as id, cn.label, cn.node_id as captureId
1897
+ FROM canvas_nodes cn
1898
+ JOIN canvases c ON cn.canvas_id = c.id
1899
+ WHERE c.session_key = ?
1900
+ ORDER BY cn.seq DESC LIMIT 1`
1901
+ ).get(sessionKey);
1902
+ return row ?? null;
1903
+ }
1904
+ async getCanvasMermaidText(sessionKey) {
1905
+ const row = this.db.prepare("SELECT mermaid_text FROM canvases WHERE session_key = ? ORDER BY updated_at DESC LIMIT 1").get(sessionKey);
1906
+ return row?.mermaid_text ?? null;
1907
+ }
1908
+ async writeRef(sessionKey, nodeId, content) {
1909
+ const refsDir = join(homedir(), ".local", "share", "remem-mcp", "refs", sessionKey);
1910
+ try {
1911
+ mkdirSync(refsDir, { recursive: true });
1912
+ writeFileSync(join(refsDir, `${nodeId}.md`), content, "utf-8");
1913
+ } catch (err) {
1914
+ console.error(`[remem-mcp] writeRef failed: ${err}`);
1915
+ }
1916
+ }
1917
+ async readRef(nodeId) {
1918
+ const refsBase = join(homedir(), ".local", "share", "remem-mcp", "refs");
1919
+ try {
1920
+ const sessions = readdirSync(refsBase);
1921
+ for (const session of sessions) {
1922
+ const refPath = join(refsBase, session, `${nodeId}.md`);
1923
+ if (existsSync(refPath)) {
1924
+ return readFileSync(refPath, "utf-8");
1925
+ }
1926
+ }
1927
+ } catch {
1928
+ }
1929
+ return null;
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
+ }
1500
2284
  };
1501
2285
  function rowToEntry(row) {
1502
2286
  return {
@@ -1539,733 +2323,20 @@ function skillRowToEntry(row) {
1539
2323
  content: row.content ?? void 0,
1540
2324
  version: row.version,
1541
2325
  createdAt: row.created_at,
1542
- updatedAt: row.updated_at
2326
+ updatedAt: row.updated_at,
2327
+ triggerConditions: row.trigger_conditions ? JSON.parse(row.trigger_conditions) : void 0,
2328
+ steps: row.steps ? JSON.parse(row.steps) : void 0,
2329
+ validationRules: row.validation_rules ? JSON.parse(row.validation_rules) : void 0,
2330
+ sourceCaptureIds: row.source_capture_ids ? JSON.parse(row.source_capture_ids) : void 0,
2331
+ archived: row.archived === 1
1543
2332
  };
1544
2333
  }
1545
2334
 
1546
- // src/sdk.ts
1547
- init_ulid();
1548
- init_engine();
1549
-
1550
- // src/wiki/engine.ts
1551
- init_esm_shims();
1552
- init_ulid();
1553
- import { createHash as createHash2 } from "crypto";
1554
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
1555
- import { basename, extname, join as join2, relative, sep } from "path";
1556
- function parseFrontmatter(content) {
1557
- if (!content.startsWith("---")) {
1558
- return { frontmatter: null, body: content };
1559
- }
1560
- const end = content.indexOf("\n---", 3);
1561
- if (end === -1) {
1562
- return { frontmatter: null, body: content };
1563
- }
1564
- const frontmatter = content.slice(3, end).trim();
1565
- const body = content.slice(end + 4).replace(/^\n/, "");
1566
- return { frontmatter, body };
1567
- }
1568
- function extractTags(frontmatter) {
1569
- if (!frontmatter) return null;
1570
- const match = frontmatter.match(/^tags:\s*(.+)$/m);
1571
- if (!match) return null;
1572
- return match[1].replace(/[[\]]/g, "").trim();
1573
- }
1574
- function extractTitle(frontmatter, body, fileName) {
1575
- if (frontmatter) {
1576
- const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
1577
- if (titleMatch) return titleMatch[1].replace(/["']/g, "").trim();
1578
- }
1579
- const h1Match = body.match(/^#\s+(.+)$/m);
1580
- if (h1Match) return h1Match[1].trim();
1581
- return basename(fileName, extname(fileName));
1582
- }
1583
- function extractWikilinks(content) {
1584
- const links = [];
1585
- const lines = content.split("\n");
1586
- const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
1587
- for (let i = 0; i < lines.length; i++) {
1588
- let match;
1589
- regex.lastIndex = 0;
1590
- match = regex.exec(lines[i]);
1591
- while (match !== null) {
1592
- links.push({
1593
- target: match[1].trim(),
1594
- text: (match[2] ?? match[1]).trim(),
1595
- line: i + 1
1596
- });
1597
- match = regex.exec(lines[i]);
1598
- }
1599
- }
1600
- return links;
1601
- }
1602
- function extractMarkdownLinks(content) {
1603
- const links = [];
1604
- const lines = content.split("\n");
1605
- const regex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
1606
- for (let i = 0; i < lines.length; i++) {
1607
- let match;
1608
- regex.lastIndex = 0;
1609
- match = regex.exec(lines[i]);
1610
- while (match !== null) {
1611
- const target = match[2].trim();
1612
- if (!target.startsWith("http") && !target.startsWith("mailto:")) {
1613
- links.push({
1614
- target: target.replace(/\.md$/i, ""),
1615
- text: match[1].trim(),
1616
- line: i + 1
1617
- });
1618
- }
1619
- match = regex.exec(lines[i]);
1620
- }
1621
- }
1622
- return links;
1623
- }
1624
- function parseMarkdownFile(filePath, repoPath) {
1625
- const ext = extname(filePath).toLowerCase();
1626
- if (ext !== ".md" && ext !== ".markdown") return null;
1627
- let source;
1628
- try {
1629
- source = readFileSync2(filePath, "utf-8");
1630
- } catch {
1631
- return null;
1632
- }
1633
- const relPath = relative(repoPath, filePath).split(sep).join("/");
1634
- const { frontmatter, body } = parseFrontmatter(source);
1635
- const title = extractTitle(frontmatter, body, filePath);
1636
- const tags = extractTags(frontmatter);
1637
- const contentHash = createHash2("sha256").update(source).digest("hex");
1638
- const page = {
1639
- id: generateId(),
1640
- title,
1641
- content: body,
1642
- sourceFile: relPath,
1643
- section: null,
1644
- tags,
1645
- frontmatter,
1646
- contentHash
1647
- };
1648
- const wikilinks = extractWikilinks(body).map((l) => ({ ...l, type: "wikilink" }));
1649
- const mdlinks = extractMarkdownLinks(body).map((l) => ({ ...l, type: "markdown" }));
1650
- const links = [...wikilinks, ...mdlinks];
1651
- return { page, links };
1652
- }
1653
- function ingestFile(db, filePath, repoPath, teamId) {
1654
- const ext = extname(filePath).toLowerCase();
1655
- if (ext !== ".md" && ext !== ".markdown") {
1656
- return { file: filePath, pages: 0, links: 0, skipped: true, reason: "not markdown" };
1657
- }
1658
- const parsed = parseMarkdownFile(filePath, repoPath);
1659
- if (!parsed) {
1660
- return { file: filePath, pages: 0, links: 0, skipped: true, reason: "parse failed" };
1661
- }
1662
- const relPath = relative(repoPath, filePath).split(sep).join("/");
1663
- const now = Date.now();
1664
- const existing = db.prepare("SELECT id FROM wiki_pages WHERE source_file = ? AND team_id IS ?").get(relPath, teamId);
1665
- let pageId = parsed.page.id;
1666
- if (existing) {
1667
- pageId = existing.id;
1668
- db.prepare(
1669
- "UPDATE wiki_pages SET title = ?, content = ?, tags = ?, frontmatter = ?, content_hash = ?, updated_at = ? WHERE id = ?"
1670
- ).run(
1671
- parsed.page.title,
1672
- parsed.page.content,
1673
- parsed.page.tags,
1674
- parsed.page.frontmatter,
1675
- parsed.page.contentHash,
1676
- now,
1677
- pageId
1678
- );
1679
- db.prepare("DELETE FROM wiki_links WHERE from_page_id = ?").run(pageId);
1680
- } else {
1681
- db.prepare(
1682
- `INSERT INTO wiki_pages (id, title, content, source_file, section, tags, frontmatter, content_hash, team_id, created_at, updated_at)
1683
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1684
- ).run(
1685
- parsed.page.id,
1686
- parsed.page.title,
1687
- parsed.page.content,
1688
- parsed.page.sourceFile,
1689
- parsed.page.section,
1690
- parsed.page.tags,
1691
- parsed.page.frontmatter,
1692
- parsed.page.contentHash,
1693
- teamId,
1694
- now,
1695
- now
1696
- );
1697
- }
1698
- const linkStmt = db.prepare(
1699
- "INSERT INTO wiki_links (from_page_id, to_page_id, to_title, link_text, link_type, line) VALUES (?, ?, ?, ?, ?, ?)"
1700
- );
1701
- for (const link of parsed.links) {
1702
- const target = db.prepare("SELECT id FROM wiki_pages WHERE title = ? AND team_id IS ? LIMIT 1").get(link.target, teamId);
1703
- linkStmt.run(pageId, target?.id ?? null, link.target, link.text, link.type, link.line);
1704
- }
1705
- return {
1706
- file: relPath,
1707
- pages: 1,
1708
- links: parsed.links.length,
1709
- skipped: false
1710
- };
1711
- }
1712
- function ingestDirectory(db, dirPath, repoPath, teamId, maxFiles = 200) {
1713
- const results = [];
1714
- const files = [];
1715
- const walk = (dir) => {
1716
- if (files.length >= maxFiles) return;
1717
- let entries;
1718
- try {
1719
- entries = readdirSync(dir);
1720
- } catch {
1721
- return;
1722
- }
1723
- for (const entry of entries) {
1724
- if (files.length >= maxFiles) return;
1725
- const fullPath = join2(dir, entry);
1726
- let stat;
1727
- try {
1728
- stat = statSync(fullPath);
1729
- } catch {
1730
- continue;
1731
- }
1732
- if (stat.isDirectory()) {
1733
- if (entry.startsWith(".") || entry === "node_modules" || entry === "vendor") continue;
1734
- walk(fullPath);
1735
- } else if (stat.isFile()) {
1736
- const ext = extname(fullPath).toLowerCase();
1737
- if (ext === ".md" || ext === ".markdown") {
1738
- files.push(fullPath);
1739
- }
1740
- }
1741
- }
1742
- };
1743
- walk(dirPath);
1744
- for (const file of files) {
1745
- const result = ingestFile(db, file, repoPath, teamId);
1746
- results.push(result);
1747
- }
1748
- const unresolved = db.prepare("SELECT id, to_title FROM wiki_links WHERE to_page_id IS NULL").all();
1749
- if (unresolved.length > 0) {
1750
- const updateStmt = db.prepare("UPDATE wiki_links SET to_page_id = ? WHERE id = ?");
1751
- for (const u of unresolved) {
1752
- const target = db.prepare("SELECT id FROM wiki_pages WHERE title = ? AND team_id IS ? LIMIT 1").get(u.to_title, teamId);
1753
- if (target) {
1754
- updateStmt.run(target.id, u.id);
1755
- }
1756
- }
1757
- }
1758
- return results;
1759
- }
1760
- function searchWiki(db, query, opts = {}) {
1761
- const limit = opts.limit ?? 10;
1762
- let sql = `
1763
- SELECT w.id, w.title, w.source_file, snippet(wiki_fts, 1, '<b>', '</b>', '...', 20) as snippet
1764
- FROM wiki_fts f
1765
- JOIN wiki_pages w ON w.id = f.id
1766
- WHERE wiki_fts MATCH ?
1767
- `;
1768
- const params = [query];
1769
- if (opts.teamId !== void 0) {
1770
- sql += " AND w.team_id IS ?";
1771
- params.push(opts.teamId);
1772
- }
1773
- sql += " LIMIT ?";
1774
- params.push(limit);
1775
- const rows = db.prepare(sql).all(...params);
1776
- return rows.map((r) => ({
1777
- id: r.id,
1778
- title: r.title,
1779
- sourceFile: r.source_file,
1780
- snippet: r.snippet
1781
- }));
1782
- }
1783
- function getWikiPage(db, pageId) {
1784
- const row = db.prepare("SELECT * FROM wiki_pages WHERE id = ?").get(pageId);
1785
- if (!row) return null;
1786
- const page = {
1787
- id: row.id,
1788
- title: row.title,
1789
- content: row.content,
1790
- sourceFile: row.source_file,
1791
- section: row.section,
1792
- tags: row.tags,
1793
- frontmatter: row.frontmatter,
1794
- contentHash: row.content_hash
1795
- };
1796
- const linkRows = db.prepare("SELECT * FROM wiki_links WHERE from_page_id = ? ORDER BY line").all(pageId);
1797
- const links = linkRows.map((r) => ({
1798
- fromPageId: r.from_page_id,
1799
- toPageId: r.to_page_id,
1800
- toTitle: r.to_title,
1801
- linkText: r.link_text,
1802
- linkType: r.link_type,
1803
- line: r.line
1804
- }));
1805
- const backlinkRows = db.prepare("SELECT * FROM wiki_links WHERE to_page_id = ? ORDER BY line").all(pageId);
1806
- const backlinks = backlinkRows.map((r) => ({
1807
- fromPageId: r.from_page_id,
1808
- toPageId: r.to_page_id,
1809
- toTitle: r.to_title,
1810
- linkText: r.link_text,
1811
- linkType: r.link_type,
1812
- line: r.line
1813
- }));
1814
- return { page, links, backlinks };
1815
- }
1816
- function findOutdatedPages(db, repoPath, opts = {}) {
1817
- const pages = db.prepare("SELECT id, title, source_file, content_hash FROM wiki_pages WHERE team_id IS ?").all(opts.teamId ?? null);
1818
- const outdated = [];
1819
- for (const page of pages) {
1820
- const fullPath = join2(repoPath, page.source_file);
1821
- if (!existsSync2(fullPath)) {
1822
- outdated.push({
1823
- id: page.id,
1824
- title: page.title,
1825
- sourceFile: page.source_file,
1826
- reason: "file deleted"
1827
- });
1828
- continue;
1829
- }
1830
- try {
1831
- const content = readFileSync2(fullPath, "utf-8");
1832
- const hash = createHash2("sha256").update(content).digest("hex");
1833
- if (hash !== page.content_hash) {
1834
- outdated.push({
1835
- id: page.id,
1836
- title: page.title,
1837
- sourceFile: page.source_file,
1838
- reason: "content changed"
1839
- });
1840
- }
1841
- } catch {
1842
- outdated.push({
1843
- id: page.id,
1844
- title: page.title,
1845
- sourceFile: page.source_file,
1846
- reason: "read error"
1847
- });
1848
- }
1849
- }
1850
- return outdated;
1851
- }
1852
-
1853
- // src/sdk.ts
1854
- function classifyGlobalContent(content, type) {
1855
- if (type !== "learning" && type !== "decision" && type !== "task") return false;
1856
- const projectSignals = [
1857
- /\b\/[a-z]/i,
1858
- /\b\w+\/\w+\.\w{1,5}\b/,
1859
- /line\s*\d+/i,
1860
- /\b\d{4,}\b/,
1861
- /\bcommit\s+[0-9a-f]{7,}/i,
1862
- /\bsrc\//,
1863
- /\bdist\//,
1864
- /\btests?\//
1865
- ];
1866
- for (const sig of projectSignals) {
1867
- if (sig.test(content)) return false;
1868
- }
1869
- const lower = content.toLowerCase();
1870
- const globalSignals = [
1871
- /\balways\s+/i,
1872
- /\bnever\s+/i,
1873
- /\brule\b/i,
1874
- /\bconvention\b/i,
1875
- /\bpattern\b/i,
1876
- /\bprefer\b/i,
1877
- /\bavoid\b/i,
1878
- /\bshould\b/i,
1879
- /\bbest\s+practice\b/i
1880
- ];
1881
- let score = 0;
1882
- for (const sig of globalSignals) {
1883
- if (sig.test(lower)) score++;
1884
- }
1885
- if (content.length < 300 && score >= 1) return true;
1886
- if (content.length < 150 && type === "learning") return true;
1887
- return false;
1888
- }
1889
- var Memory = class {
1890
- storage;
1891
- embedder;
1892
- sessionKey;
1893
- globalKey;
1894
- redactSecrets;
1895
- constructor(opts) {
1896
- const dbPath = opts?.dbPath ?? join3(homedir(), ".local", "share", "remem-mcp", "memory.db");
1897
- this.storage = new SQLiteBackend(dbPath);
1898
- this.embedder = new LocalEmbedder();
1899
- this.sessionKey = opts?.sessionKey ?? process.env.REMEM_SESSION_KEY ?? createHash3("sha256").update(process.cwd()).digest("hex").slice(0, 16);
1900
- this.globalKey = opts?.globalSessionKey ?? process.env.REMEM_GLOBAL_SESSION_KEY ?? null;
1901
- this.redactSecrets = opts?.redactSecrets ?? true;
1902
- }
1903
- /** Capture a memory entry. Returns the ID, or null if duplicate. */
1904
- async capture(content, type, tags = [], opts) {
1905
- const { text: redactedContent } = this.redactSecrets ? redact(content) : { text: content };
1906
- let sessionKey = opts?.sessionKey ?? this.sessionKey;
1907
- if (opts?.autoGlobal && !opts?.sessionKey && this.globalKey && this.globalKey !== sessionKey) {
1908
- if (classifyGlobalContent(content, type)) {
1909
- sessionKey = this.globalKey;
1910
- }
1911
- }
1912
- const contentHash = createHash3("sha256").update(redactedContent).digest("hex");
1913
- const existing = await this.storage.findByContentHash(contentHash, sessionKey);
1914
- if (existing.length > 0) return null;
1915
- const id = generateId();
1916
- const entry = {
1917
- id,
1918
- sessionKey,
1919
- agentId: "sdk",
1920
- type,
1921
- content: redactedContent,
1922
- tags,
1923
- createdAt: Date.now()
1924
- };
1925
- await this.storage.put(entry);
1926
- try {
1927
- const embedding = await this.embedder.embed(redactedContent);
1928
- await this.storage.putVector(id, embedding);
1929
- } catch {
1930
- }
1931
- return id;
1932
- }
1933
- /** Recall relevant memory. Searches project + global (if configured). */
1934
- async recall(query, opts) {
1935
- const limit = Math.min(opts?.limit ?? 10, 50);
1936
- const mode = opts?.mode ?? "hybrid";
1937
- const sessionKey = opts?.sessionKey ?? this.sessionKey;
1938
- let queryEmbedding = null;
1939
- if (mode === "hybrid" || mode === "vector") {
1940
- const stripped = stripQueryProperNouns(query);
1941
- queryEmbedding = await this.embedder.embed(stripped || query);
1942
- }
1943
- if (this.globalKey && !opts?.sessionKey && this.globalKey !== sessionKey) {
1944
- const reservedGlobal = Math.min(3, limit);
1945
- const projectLimit = limit - reservedGlobal;
1946
- const projectResults = await this.storage.search(query, queryEmbedding, {
1947
- sessionKey,
1948
- limit: projectLimit,
1949
- offset: 0,
1950
- mode
1951
- });
1952
- let globalResults = [];
1953
- try {
1954
- globalResults = await this.storage.search(query, queryEmbedding, {
1955
- sessionKey: this.globalKey,
1956
- limit: reservedGlobal,
1957
- offset: 0,
1958
- mode
1959
- });
1960
- } catch {
1961
- }
1962
- const seen = new Set(projectResults.map((r) => r.entry.id));
1963
- return [...projectResults, ...globalResults.filter((r) => !seen.has(r.entry.id))].slice(
1964
- 0,
1965
- limit
1966
- );
1967
- }
1968
- return this.storage.search(query, queryEmbedding, {
1969
- sessionKey,
1970
- limit,
1971
- offset: 0,
1972
- mode
1973
- });
1974
- }
1975
- /** Search with filters. Searches project + global (if configured). */
1976
- async search(query, opts) {
1977
- const limit = Math.min(opts?.limit ?? 20, 100);
1978
- const mode = opts?.mode ?? "hybrid";
1979
- const sessionKey = opts?.sessionKey ?? this.sessionKey;
1980
- let queryEmbedding = null;
1981
- if (mode === "hybrid" || mode === "vector") {
1982
- const stripped = stripQueryProperNouns(query);
1983
- queryEmbedding = await this.embedder.embed(stripped || query);
1984
- }
1985
- if (this.globalKey && !opts?.sessionKey && this.globalKey !== sessionKey) {
1986
- const reservedGlobal = Math.min(3, limit);
1987
- const projectLimit = limit - reservedGlobal;
1988
- const projectResults = await this.storage.search(query, queryEmbedding, {
1989
- sessionKey,
1990
- limit: projectLimit,
1991
- offset: 0,
1992
- mode,
1993
- filters: opts?.filters
1994
- });
1995
- let globalResults = [];
1996
- try {
1997
- globalResults = await this.storage.search(query, queryEmbedding, {
1998
- sessionKey: this.globalKey,
1999
- limit: reservedGlobal,
2000
- offset: 0,
2001
- mode,
2002
- filters: opts?.filters
2003
- });
2004
- } catch {
2005
- }
2006
- const seen = new Set(projectResults.map((r) => r.entry.id));
2007
- return [...projectResults, ...globalResults.filter((r) => !seen.has(r.entry.id))].slice(
2008
- 0,
2009
- limit
2010
- );
2011
- }
2012
- return this.storage.search(query, queryEmbedding, {
2013
- sessionKey,
2014
- limit,
2015
- offset: 0,
2016
- mode,
2017
- filters: opts?.filters
2018
- });
2019
- }
2020
- /** Compute the embedding vector for a piece of text using the configured embedder. */
2021
- async embed(text) {
2022
- return this.embedder.embed(text);
2023
- }
2024
- /** Delete a capture by ID. */
2025
- async forget(id) {
2026
- return this.storage.delete(id);
2027
- }
2028
- /** Update an existing capture's content, tags, type, or trust state. */
2029
- async update(id, opts) {
2030
- const existing = await this.storage.get(id);
2031
- if (!existing) return false;
2032
- const newContent = opts.content ?? existing.content;
2033
- const newTags = opts.tags ?? existing.tags;
2034
- const newType = opts.type ?? existing.type;
2035
- const newTrust = opts.verified ? "verified" : existing.trustState ?? "candidate";
2036
- const contentHash = createHash3("sha256").update(newContent).digest("hex");
2037
- const db = this.storage.getDatabase();
2038
- db.prepare(
2039
- "UPDATE captures SET content = ?, tags = ?, type = ?, trust_state = ?, content_hash = ? WHERE id = ?"
2040
- ).run(newContent, JSON.stringify(newTags), newType, newTrust, contentHash, id);
2041
- const rowid = db.prepare("SELECT rowid FROM captures WHERE id = ?").get(id)?.rowid;
2042
- if (rowid) {
2043
- db.prepare(
2044
- "INSERT INTO captures_fts(captures_fts, rowid, content, tags, type) VALUES('delete', ?, '', '', '')"
2045
- ).run(rowid);
2046
- db.prepare(
2047
- "INSERT INTO captures_fts (rowid, id, content, tags, type) VALUES (?, ?, ?, ?, ?)"
2048
- ).run(rowid, id, newContent, JSON.stringify(newTags), newType);
2049
- }
2050
- if (opts.content && opts.content !== existing.content) {
2051
- try {
2052
- const embedding = await this.embedder.embed(newContent);
2053
- await this.storage.putVector(id, embedding);
2054
- } catch {
2055
- }
2056
- }
2057
- return true;
2058
- }
2059
- /** Find and optionally merge duplicate captures by content similarity (Jaccard). */
2060
- async consolidate(opts) {
2061
- const threshold = opts.threshold ?? 0.75;
2062
- const confirm = opts.confirm ?? false;
2063
- const sessionKey = opts.sessionKey ?? this.sessionKey;
2064
- const db = this.storage.getDatabase();
2065
- let sql = "SELECT id, content, type, tags, created_at FROM captures WHERE deleted_at IS NULL";
2066
- const params = [];
2067
- if (sessionKey !== "all") {
2068
- sql += " AND session_key = ?";
2069
- params.push(sessionKey);
2070
- }
2071
- sql += " ORDER BY created_at DESC";
2072
- const rows = db.prepare(sql).all(...params);
2073
- if (rows.length < 2) return { groups: [], merged: 0 };
2074
- const groups = [];
2075
- const seen = /* @__PURE__ */ new Set();
2076
- for (let i = 0; i < rows.length; i++) {
2077
- if (seen.has(rows[i].id)) continue;
2078
- const words1 = new Set(rows[i].content.toLowerCase().split(/\s+/));
2079
- const group = [rows[i].id];
2080
- for (let j = i + 1; j < rows.length; j++) {
2081
- if (seen.has(rows[j].id)) continue;
2082
- const words2 = new Set(rows[j].content.toLowerCase().split(/\s+/));
2083
- const intersection = [...words1].filter((w) => words2.has(w)).length;
2084
- const union = (/* @__PURE__ */ new Set([...words1, ...words2])).size;
2085
- const sim = union > 0 ? intersection / union : 0;
2086
- if (sim >= threshold) {
2087
- group.push(rows[j].id);
2088
- seen.add(rows[j].id);
2089
- }
2090
- }
2091
- if (group.length > 1) {
2092
- seen.add(rows[i].id);
2093
- groups.push({
2094
- ids: group,
2095
- similarity: threshold,
2096
- preview: rows[i].content.slice(0, 80).replace(/\n/g, " ")
2097
- });
2098
- }
2099
- }
2100
- if (!confirm || groups.length === 0) {
2101
- return { groups, merged: 0 };
2102
- }
2103
- let merged = 0;
2104
- const softDelete = db.prepare("UPDATE captures SET deleted_at = ? WHERE id = ?");
2105
- const deleteVec = db.prepare("DELETE FROM captures_vec WHERE id = ?");
2106
- const deleteAtoms = db.prepare("DELETE FROM atoms WHERE capture_id = ?");
2107
- const mergeTx = db.transaction(() => {
2108
- for (const g of groups) {
2109
- const groupRows = g.ids.map((id) => rows.find((r) => r.id === id)).filter(Boolean).sort((a, b) => a.created_at - b.created_at);
2110
- const dups = groupRows.slice(1);
2111
- for (const dup of dups) {
2112
- softDelete.run(Date.now(), dup.id);
2113
- deleteVec.run(dup.id);
2114
- deleteAtoms.run(dup.id);
2115
- merged++;
2116
- }
2117
- }
2118
- });
2119
- mergeTx();
2120
- return { groups, merged };
2121
- }
2122
- /** Create a handoff packet for the next agent session. */
2123
- async handoff(opts) {
2124
- const lines = [];
2125
- lines.push(`# Handoff: ${opts.task}`);
2126
- lines.push(`Status: ${opts.status}`);
2127
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
2128
- lines.push("");
2129
- lines.push("## Progress");
2130
- lines.push(opts.progress);
2131
- lines.push("");
2132
- if (opts.decisions && opts.decisions.length > 0) {
2133
- lines.push("## Decisions");
2134
- for (const d of opts.decisions) lines.push(`- ${d}`);
2135
- lines.push("");
2136
- }
2137
- if (opts.files && opts.files.length > 0) {
2138
- lines.push("## Files");
2139
- for (const f of opts.files) lines.push(`- ${f}`);
2140
- lines.push("");
2141
- }
2142
- if (opts.nextSteps && opts.nextSteps.length > 0) {
2143
- lines.push("## Next steps");
2144
- opts.nextSteps.forEach((s, i) => {
2145
- lines.push(`${i + 1}. ${s}`);
2146
- });
2147
- lines.push("");
2148
- }
2149
- const content = lines.join("\n");
2150
- const dedupPayload = JSON.stringify({
2151
- task: opts.task,
2152
- status: opts.status,
2153
- progress: opts.progress,
2154
- decisions: opts.decisions ?? [],
2155
- files: opts.files ?? [],
2156
- nextSteps: opts.nextSteps ?? []
2157
- });
2158
- const contentHash = createHash3("sha256").update(dedupPayload).digest("hex");
2159
- const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
2160
- if (existing.length > 0) return null;
2161
- const id = generateId();
2162
- const entry = {
2163
- id,
2164
- sessionKey: this.sessionKey,
2165
- agentId: "sdk",
2166
- type: "task",
2167
- content,
2168
- tags: ["handoff", `status:${opts.status}`],
2169
- createdAt: Date.now(),
2170
- metadata: {
2171
- handoff: true,
2172
- task: opts.task,
2173
- status: opts.status,
2174
- progress: opts.progress,
2175
- decisions: opts.decisions ?? [],
2176
- files: opts.files ?? [],
2177
- nextSteps: opts.nextSteps ?? []
2178
- },
2179
- contentHash
2180
- };
2181
- await this.storage.put(entry);
2182
- try {
2183
- const embedding = await this.embedder.embed(content);
2184
- await this.storage.putVector(id, embedding);
2185
- } catch {
2186
- }
2187
- return id;
2188
- }
2189
- /** Record an Architecture Decision Record (ADR). */
2190
- async adr(opts) {
2191
- const lines = [];
2192
- lines.push(`# ADR: ${opts.title}`);
2193
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
2194
- lines.push("");
2195
- lines.push("## Context");
2196
- lines.push(opts.context);
2197
- lines.push("");
2198
- lines.push("## Decision");
2199
- lines.push(opts.decision);
2200
- lines.push("");
2201
- const alternatives = Array.isArray(opts.alternatives) ? opts.alternatives : typeof opts.alternatives === "string" && opts.alternatives ? [opts.alternatives] : [];
2202
- if (alternatives.length > 0) {
2203
- lines.push("## Alternatives considered");
2204
- for (const alt of alternatives) lines.push(`- ${alt}`);
2205
- lines.push("");
2206
- }
2207
- if (opts.consequences) {
2208
- lines.push("## Consequences");
2209
- lines.push(opts.consequences);
2210
- lines.push("");
2211
- }
2212
- const content = lines.join("\n");
2213
- const dedupPayload = JSON.stringify({
2214
- title: opts.title,
2215
- context: opts.context,
2216
- decision: opts.decision,
2217
- alternatives,
2218
- consequences: opts.consequences ?? ""
2219
- });
2220
- const contentHash = createHash3("sha256").update(dedupPayload).digest("hex");
2221
- const existing = await this.storage.findByContentHash(contentHash, this.sessionKey);
2222
- if (existing.length > 0) return null;
2223
- const id = generateId();
2224
- const entry = {
2225
- id,
2226
- sessionKey: this.sessionKey,
2227
- agentId: "sdk",
2228
- type: "decision",
2229
- content,
2230
- tags: ["adr", ...opts.tags ?? []],
2231
- createdAt: Date.now(),
2232
- metadata: {
2233
- adr: true,
2234
- title: opts.title,
2235
- context: opts.context,
2236
- decision: opts.decision,
2237
- alternatives,
2238
- consequences: opts.consequences ?? ""
2239
- },
2240
- contentHash
2241
- };
2242
- await this.storage.put(entry);
2243
- try {
2244
- const embedding = await this.embedder.embed(content);
2245
- await this.storage.putVector(id, embedding);
2246
- } catch {
2247
- }
2248
- return id;
2249
- }
2250
- /** Close the database connection. */
2251
- close() {
2252
- this.storage.close();
2253
- }
2254
- };
2255
-
2256
2335
  export {
2257
2336
  stripQueryProperNouns,
2258
2337
  SQLiteBackend,
2259
2338
  LocalEmbedder,
2260
2339
  local_exports,
2261
- init_local,
2262
- redact,
2263
- ingestFile,
2264
- ingestDirectory,
2265
- searchWiki,
2266
- getWikiPage,
2267
- findOutdatedPages,
2268
- classifyGlobalContent,
2269
- Memory
2340
+ init_local
2270
2341
  };
2271
- //# sourceMappingURL=chunk-X4ECGLYH.js.map
2342
+ //# sourceMappingURL=chunk-4WCDAEPP.js.map