memory-braid 0.4.4 → 0.4.5
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/package.json +1 -1
- package/src/extract.ts +11 -0
- package/src/index.ts +120 -71
- package/src/mem0-client.ts +60 -7
- package/src/state.ts +2 -2
package/package.json
CHANGED
package/src/extract.ts
CHANGED
|
@@ -468,6 +468,17 @@ export async function extractCandidates(params: {
|
|
|
468
468
|
|
|
469
469
|
try {
|
|
470
470
|
if (params.cfg.capture.mode === "hybrid") {
|
|
471
|
+
if (heuristic.length === 0) {
|
|
472
|
+
params.log.debug("memory_braid.capture.ml", {
|
|
473
|
+
runId: params.runId,
|
|
474
|
+
mode: params.cfg.capture.mode,
|
|
475
|
+
provider: params.cfg.capture.ml.provider,
|
|
476
|
+
model: params.cfg.capture.ml.model,
|
|
477
|
+
decision: "skip_ml_enrichment_no_heuristic_candidates",
|
|
478
|
+
});
|
|
479
|
+
return heuristic;
|
|
480
|
+
}
|
|
481
|
+
|
|
471
482
|
const ml = await callMlEnrichment({
|
|
472
483
|
provider: params.cfg.capture.ml.provider,
|
|
473
484
|
model: params.cfg.capture.ml.model,
|
package/src/index.ts
CHANGED
|
@@ -1345,105 +1345,153 @@ const memoryBraidPlugin = {
|
|
|
1345
1345
|
return;
|
|
1346
1346
|
}
|
|
1347
1347
|
|
|
1348
|
-
|
|
1348
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
1349
|
+
const candidateEntries = candidates.map((candidate) => ({
|
|
1350
|
+
candidate,
|
|
1351
|
+
hash: sha256(normalizeForHash(candidate.text)),
|
|
1352
|
+
}));
|
|
1353
|
+
|
|
1354
|
+
const prepared = await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
1349
1355
|
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
1350
|
-
const stats = await readStatsState(runtimeStatePaths);
|
|
1351
|
-
const lifecycle = cfg.lifecycle.enabled
|
|
1352
|
-
? await readLifecycleState(runtimeStatePaths)
|
|
1353
|
-
: null;
|
|
1354
1356
|
const now = Date.now();
|
|
1355
|
-
|
|
1357
|
+
|
|
1358
|
+
let pruned = 0;
|
|
1356
1359
|
for (const [key, ts] of Object.entries(dedupe.seen)) {
|
|
1357
1360
|
if (now - ts > thirtyDays) {
|
|
1358
1361
|
delete dedupe.seen[key];
|
|
1362
|
+
pruned += 1;
|
|
1359
1363
|
}
|
|
1360
1364
|
}
|
|
1361
1365
|
|
|
1362
|
-
let persisted = 0;
|
|
1363
1366
|
let dedupeSkipped = 0;
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
let mem0AddWithoutId = 0;
|
|
1369
|
-
for (const candidate of candidates) {
|
|
1370
|
-
const hash = sha256(normalizeForHash(candidate.text));
|
|
1371
|
-
if (dedupe.seen[hash]) {
|
|
1367
|
+
const pending: typeof candidateEntries = [];
|
|
1368
|
+
const seenInBatch = new Set<string>();
|
|
1369
|
+
for (const entry of candidateEntries) {
|
|
1370
|
+
if (dedupe.seen[entry.hash] || seenInBatch.has(entry.hash)) {
|
|
1372
1371
|
dedupeSkipped += 1;
|
|
1373
1372
|
continue;
|
|
1374
1373
|
}
|
|
1374
|
+
seenInBatch.add(entry.hash);
|
|
1375
|
+
pending.push(entry);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (pruned > 0) {
|
|
1379
|
+
await writeCaptureDedupeState(runtimeStatePaths, dedupe);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
dedupeSkipped,
|
|
1384
|
+
pending,
|
|
1385
|
+
};
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
let entityAnnotatedCandidates = 0;
|
|
1389
|
+
let totalEntitiesAttached = 0;
|
|
1390
|
+
let mem0AddAttempts = 0;
|
|
1391
|
+
let mem0AddWithId = 0;
|
|
1392
|
+
let mem0AddWithoutId = 0;
|
|
1393
|
+
const successfulAdds: Array<{
|
|
1394
|
+
memoryId: string;
|
|
1395
|
+
hash: string;
|
|
1396
|
+
category: (typeof candidates)[number]["category"];
|
|
1397
|
+
}> = [];
|
|
1398
|
+
|
|
1399
|
+
for (const entry of prepared.pending) {
|
|
1400
|
+
const { candidate, hash } = entry;
|
|
1401
|
+
const metadata: Record<string, unknown> = {
|
|
1402
|
+
sourceType: "capture",
|
|
1403
|
+
workspaceHash: scope.workspaceHash,
|
|
1404
|
+
agentId: scope.agentId,
|
|
1405
|
+
sessionKey: scope.sessionKey,
|
|
1406
|
+
category: candidate.category,
|
|
1407
|
+
captureScore: candidate.score,
|
|
1408
|
+
extractionSource: candidate.source,
|
|
1409
|
+
contentHash: hash,
|
|
1410
|
+
indexedAt: new Date().toISOString(),
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
if (cfg.entityExtraction.enabled) {
|
|
1414
|
+
const entities = await entityExtraction.extract({
|
|
1415
|
+
text: candidate.text,
|
|
1416
|
+
runId,
|
|
1417
|
+
});
|
|
1418
|
+
if (entities.length > 0) {
|
|
1419
|
+
entityAnnotatedCandidates += 1;
|
|
1420
|
+
totalEntitiesAttached += entities.length;
|
|
1421
|
+
metadata.entityUris = entities.map((entity) => entity.canonicalUri);
|
|
1422
|
+
metadata.entities = entities;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1375
1425
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1426
|
+
mem0AddAttempts += 1;
|
|
1427
|
+
const addResult = await mem0.addMemory({
|
|
1428
|
+
text: candidate.text,
|
|
1429
|
+
scope,
|
|
1430
|
+
metadata,
|
|
1431
|
+
runId,
|
|
1432
|
+
});
|
|
1433
|
+
if (addResult.id) {
|
|
1434
|
+
mem0AddWithId += 1;
|
|
1435
|
+
successfulAdds.push({
|
|
1436
|
+
memoryId: addResult.id,
|
|
1437
|
+
hash,
|
|
1438
|
+
category: candidate.category,
|
|
1439
|
+
});
|
|
1440
|
+
} else {
|
|
1441
|
+
mem0AddWithoutId += 1;
|
|
1442
|
+
log.warn("memory_braid.capture.persist", {
|
|
1443
|
+
runId,
|
|
1444
|
+
reason: "mem0_add_missing_id",
|
|
1378
1445
|
workspaceHash: scope.workspaceHash,
|
|
1379
1446
|
agentId: scope.agentId,
|
|
1380
1447
|
sessionKey: scope.sessionKey,
|
|
1448
|
+
contentHashPrefix: hash.slice(0, 12),
|
|
1381
1449
|
category: candidate.category,
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
indexedAt: new Date(now).toISOString(),
|
|
1386
|
-
};
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1387
1453
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1454
|
+
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
1455
|
+
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
1456
|
+
const stats = await readStatsState(runtimeStatePaths);
|
|
1457
|
+
const lifecycle = cfg.lifecycle.enabled
|
|
1458
|
+
? await readLifecycleState(runtimeStatePaths)
|
|
1459
|
+
: null;
|
|
1460
|
+
const now = Date.now();
|
|
1461
|
+
|
|
1462
|
+
for (const [key, ts] of Object.entries(dedupe.seen)) {
|
|
1463
|
+
if (now - ts > thirtyDays) {
|
|
1464
|
+
delete dedupe.seen[key];
|
|
1399
1465
|
}
|
|
1466
|
+
}
|
|
1400
1467
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
persisted += 1;
|
|
1412
|
-
if (lifecycle) {
|
|
1413
|
-
const memoryId = addResult.id;
|
|
1414
|
-
const existing = lifecycle.entries[memoryId];
|
|
1415
|
-
lifecycle.entries[memoryId] = {
|
|
1416
|
-
memoryId,
|
|
1417
|
-
contentHash: hash,
|
|
1418
|
-
workspaceHash: scope.workspaceHash,
|
|
1419
|
-
agentId: scope.agentId,
|
|
1420
|
-
sessionKey: scope.sessionKey,
|
|
1421
|
-
category: candidate.category,
|
|
1422
|
-
createdAt: existing?.createdAt ?? now,
|
|
1423
|
-
lastCapturedAt: now,
|
|
1424
|
-
lastRecalledAt: existing?.lastRecalledAt,
|
|
1425
|
-
recallCount: existing?.recallCount ?? 0,
|
|
1426
|
-
updatedAt: now,
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
} else {
|
|
1430
|
-
mem0AddWithoutId += 1;
|
|
1431
|
-
log.warn("memory_braid.capture.persist", {
|
|
1432
|
-
runId,
|
|
1433
|
-
reason: "mem0_add_missing_id",
|
|
1468
|
+
let persisted = 0;
|
|
1469
|
+
for (const entry of successfulAdds) {
|
|
1470
|
+
dedupe.seen[entry.hash] = now;
|
|
1471
|
+
persisted += 1;
|
|
1472
|
+
|
|
1473
|
+
if (lifecycle) {
|
|
1474
|
+
const existing = lifecycle.entries[entry.memoryId];
|
|
1475
|
+
lifecycle.entries[entry.memoryId] = {
|
|
1476
|
+
memoryId: entry.memoryId,
|
|
1477
|
+
contentHash: entry.hash,
|
|
1434
1478
|
workspaceHash: scope.workspaceHash,
|
|
1435
1479
|
agentId: scope.agentId,
|
|
1436
1480
|
sessionKey: scope.sessionKey,
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1481
|
+
category: entry.category,
|
|
1482
|
+
createdAt: existing?.createdAt ?? now,
|
|
1483
|
+
lastCapturedAt: now,
|
|
1484
|
+
lastRecalledAt: existing?.lastRecalledAt,
|
|
1485
|
+
recallCount: existing?.recallCount ?? 0,
|
|
1486
|
+
updatedAt: now,
|
|
1487
|
+
};
|
|
1440
1488
|
}
|
|
1441
1489
|
}
|
|
1442
1490
|
|
|
1443
1491
|
stats.capture.runs += 1;
|
|
1444
1492
|
stats.capture.runsWithCandidates += 1;
|
|
1445
1493
|
stats.capture.candidates += candidates.length;
|
|
1446
|
-
stats.capture.dedupeSkipped += dedupeSkipped;
|
|
1494
|
+
stats.capture.dedupeSkipped += prepared.dedupeSkipped;
|
|
1447
1495
|
stats.capture.persisted += persisted;
|
|
1448
1496
|
stats.capture.mem0AddAttempts += mem0AddAttempts;
|
|
1449
1497
|
stats.capture.mem0AddWithId += mem0AddWithId;
|
|
@@ -1464,7 +1512,8 @@ const memoryBraidPlugin = {
|
|
|
1464
1512
|
agentId: scope.agentId,
|
|
1465
1513
|
sessionKey: scope.sessionKey,
|
|
1466
1514
|
candidates: candidates.length,
|
|
1467
|
-
|
|
1515
|
+
pending: prepared.pending.length,
|
|
1516
|
+
dedupeSkipped: prepared.dedupeSkipped,
|
|
1468
1517
|
persisted,
|
|
1469
1518
|
mem0AddAttempts,
|
|
1470
1519
|
mem0AddWithId,
|
package/src/mem0-client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { normalizeForHash } from "./chunking.js";
|
|
5
|
+
import { normalizeForHash, sha256 } from "./chunking.js";
|
|
6
6
|
import type { MemoryBraidConfig } from "./config.js";
|
|
7
7
|
import { MemoryBraidLogger } from "./logger.js";
|
|
8
8
|
import type { MemoryBraidResult, ScopeKey } from "./types.js";
|
|
@@ -403,6 +403,9 @@ type Mem0AdapterOptions = {
|
|
|
403
403
|
stateDir?: string;
|
|
404
404
|
};
|
|
405
405
|
|
|
406
|
+
const SEMANTIC_SEARCH_CACHE_TTL_MS = 30_000;
|
|
407
|
+
const SEMANTIC_SEARCH_CACHE_MAX_ENTRIES = 256;
|
|
408
|
+
|
|
406
409
|
export class Mem0Adapter {
|
|
407
410
|
private cloudClient: CloudClientLike | null = null;
|
|
408
411
|
private ossClient: OssClientLike | null = null;
|
|
@@ -410,6 +413,13 @@ export class Mem0Adapter {
|
|
|
410
413
|
private readonly log: MemoryBraidLogger;
|
|
411
414
|
private readonly pluginDir?: string;
|
|
412
415
|
private stateDir?: string;
|
|
416
|
+
private readonly semanticSearchCache = new Map<
|
|
417
|
+
string,
|
|
418
|
+
{
|
|
419
|
+
expiresAt: number;
|
|
420
|
+
results: MemoryBraidResult[];
|
|
421
|
+
}
|
|
422
|
+
>();
|
|
413
423
|
|
|
414
424
|
constructor(cfg: MemoryBraidConfig, log: MemoryBraidLogger, options?: Mem0AdapterOptions) {
|
|
415
425
|
this.cfg = cfg;
|
|
@@ -425,6 +435,7 @@ export class Mem0Adapter {
|
|
|
425
435
|
}
|
|
426
436
|
this.stateDir = next;
|
|
427
437
|
this.ossClient = null;
|
|
438
|
+
this.semanticSearchCache.clear();
|
|
428
439
|
}
|
|
429
440
|
|
|
430
441
|
private async ensureCloudClient(): Promise<CloudClientLike | null> {
|
|
@@ -844,12 +855,38 @@ export class Mem0Adapter {
|
|
|
844
855
|
runId?: string;
|
|
845
856
|
}): Promise<number | undefined> {
|
|
846
857
|
const rightHash = normalizeForHash(params.rightText);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
858
|
+
if (!rightHash) {
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const leftHash = normalizeForHash(params.leftText);
|
|
863
|
+
if (!leftHash) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const now = Date.now();
|
|
868
|
+
this.pruneSemanticSearchCache(now);
|
|
869
|
+
const scopeSession = params.scope.sessionKey ?? "";
|
|
870
|
+
const cacheKey = `${params.scope.workspaceHash}|${params.scope.agentId}|${scopeSession}|${sha256(leftHash)}`;
|
|
871
|
+
const cached = this.semanticSearchCache.get(cacheKey);
|
|
872
|
+
const results =
|
|
873
|
+
cached && cached.expiresAt > now
|
|
874
|
+
? cached.results
|
|
875
|
+
: await this.searchMemories({
|
|
876
|
+
query: params.leftText,
|
|
877
|
+
maxResults: 5,
|
|
878
|
+
scope: params.scope,
|
|
879
|
+
runId: params.runId,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
if (!cached || cached.expiresAt <= now) {
|
|
883
|
+
this.semanticSearchCache.set(cacheKey, {
|
|
884
|
+
expiresAt: now + SEMANTIC_SEARCH_CACHE_TTL_MS,
|
|
885
|
+
results,
|
|
886
|
+
});
|
|
887
|
+
this.pruneSemanticSearchCache(now);
|
|
888
|
+
}
|
|
889
|
+
|
|
853
890
|
for (const result of results) {
|
|
854
891
|
if (normalizeForHash(result.snippet) === rightHash) {
|
|
855
892
|
return result.score;
|
|
@@ -857,4 +894,20 @@ export class Mem0Adapter {
|
|
|
857
894
|
}
|
|
858
895
|
return undefined;
|
|
859
896
|
}
|
|
897
|
+
|
|
898
|
+
private pruneSemanticSearchCache(now = Date.now()): void {
|
|
899
|
+
for (const [key, entry] of this.semanticSearchCache.entries()) {
|
|
900
|
+
if (entry.expiresAt <= now) {
|
|
901
|
+
this.semanticSearchCache.delete(key);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
while (this.semanticSearchCache.size > SEMANTIC_SEARCH_CACHE_MAX_ENTRIES) {
|
|
906
|
+
const oldest = this.semanticSearchCache.keys().next().value as string | undefined;
|
|
907
|
+
if (!oldest) {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
this.semanticSearchCache.delete(oldest);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
860
913
|
}
|
package/src/state.ts
CHANGED
|
@@ -77,7 +77,7 @@ export async function readCaptureDedupeState(paths: StatePaths): Promise<Capture
|
|
|
77
77
|
const value = await readJsonFile(paths.captureDedupeFile, DEFAULT_CAPTURE_DEDUPE);
|
|
78
78
|
return {
|
|
79
79
|
version: 1,
|
|
80
|
-
seen: value.seen ?? {},
|
|
80
|
+
seen: { ...(value.seen ?? {}) },
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -92,7 +92,7 @@ export async function readLifecycleState(paths: StatePaths): Promise<LifecycleSt
|
|
|
92
92
|
const value = await readJsonFile(paths.lifecycleFile, DEFAULT_LIFECYCLE);
|
|
93
93
|
return {
|
|
94
94
|
version: 1,
|
|
95
|
-
entries: value.entries ?? {},
|
|
95
|
+
entries: { ...(value.entries ?? {}) },
|
|
96
96
|
lastCleanupAt: value.lastCleanupAt,
|
|
97
97
|
lastCleanupReason: value.lastCleanupReason,
|
|
98
98
|
lastCleanupScanned: value.lastCleanupScanned,
|