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