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.
- package/README.md +67 -68
- package/dist/{chunk-X4ECGLYH.js → chunk-4WCDAEPP.js} +897 -826
- package/dist/chunk-4WCDAEPP.js.map +1 -0
- package/dist/chunk-ITMR6THY.js +859 -0
- package/dist/chunk-ITMR6THY.js.map +1 -0
- package/dist/{chunk-XK4Y53J2.js → chunk-ONQHLUGM.js} +1 -8
- package/dist/{chunk-XK4Y53J2.js.map → chunk-ONQHLUGM.js.map} +1 -1
- package/dist/chunk-XJGDYQ34.js +22 -0
- package/dist/chunk-XJGDYQ34.js.map +1 -0
- package/dist/{chunk-FA324IBN.js → chunk-YU7WBJU5.js} +24 -19
- package/dist/chunk-YU7WBJU5.js.map +1 -0
- package/dist/{engine-LVKGZHBY.js → engine-S47HHGAO.js} +4 -3
- package/dist/index.js +1817 -383
- package/dist/index.js.map +1 -1
- package/dist/{resolver-AVK7XKX7.js → resolver-UHU4MBQX.js} +53 -20
- package/dist/resolver-UHU4MBQX.js.map +1 -0
- package/dist/sdk.js +8 -5
- package/dist/server-5PWQTRGZ.js +517 -0
- package/dist/server-5PWQTRGZ.js.map +1 -0
- package/dist/storage/schema.sql +112 -3
- package/package.json +1 -1
- package/scripts/verify-codegraph-stats.mjs +122 -0
- package/skills/remem-mcp/SKILL.md +44 -3
- package/dist/chunk-FA324IBN.js.map +0 -1
- package/dist/chunk-X4ECGLYH.js.map +0 -1
- package/dist/resolver-AVK7XKX7.js.map +0 -1
- /package/dist/{engine-LVKGZHBY.js.map → engine-S47HHGAO.js.map} +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateId,
|
|
3
|
-
init_engine,
|
|
4
3
|
init_ulid
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XJGDYQ34.js";
|
|
6
5
|
import {
|
|
7
6
|
__esm,
|
|
8
7
|
__export,
|
|
9
8
|
init_esm_shims
|
|
10
|
-
} from "./chunk-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
2342
|
+
//# sourceMappingURL=chunk-4WCDAEPP.js.map
|