memory-braid 0.6.0 → 0.7.0
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 +37 -3
- package/openclaw.plugin.json +47 -0
- package/package.json +2 -1
- package/src/capture.ts +64 -2
- package/src/config.ts +122 -0
- package/src/consolidation.ts +403 -0
- package/src/extract.ts +8 -3
- package/src/index.ts +797 -11
- package/src/memory-model.ts +336 -0
- package/src/memory-selection.ts +257 -0
- package/src/state.ts +40 -0
- package/src/temporal.ts +292 -0
- package/src/types.ts +40 -0
package/src/index.ts
CHANGED
|
@@ -11,11 +11,25 @@ import {
|
|
|
11
11
|
normalizeHookMessages,
|
|
12
12
|
} from "./capture.js";
|
|
13
13
|
import { parseConfig, pluginConfigSchema } from "./config.js";
|
|
14
|
+
import {
|
|
15
|
+
buildConsolidationDrafts,
|
|
16
|
+
findSupersededSemanticMemories,
|
|
17
|
+
} from "./consolidation.js";
|
|
14
18
|
import { stagedDedupe } from "./dedupe.js";
|
|
15
19
|
import { EntityExtractionManager } from "./entities.js";
|
|
16
20
|
import { extractCandidates } from "./extract.js";
|
|
17
21
|
import { MemoryBraidLogger } from "./logger.js";
|
|
18
22
|
import { resolveLocalTools, runLocalGet, runLocalSearch } from "./local-memory.js";
|
|
23
|
+
import {
|
|
24
|
+
buildTaxonomy,
|
|
25
|
+
formatTaxonomySummary,
|
|
26
|
+
inferMemoryLayer as inferNormalizedMemoryLayer,
|
|
27
|
+
normalizeTaxonomy,
|
|
28
|
+
} from "./memory-model.js";
|
|
29
|
+
import {
|
|
30
|
+
scoreObservedMemory,
|
|
31
|
+
scoreProceduralMemory,
|
|
32
|
+
} from "./memory-selection.js";
|
|
19
33
|
import { Mem0Adapter } from "./mem0-client.js";
|
|
20
34
|
import { mergeWithRrf } from "./merge.js";
|
|
21
35
|
import {
|
|
@@ -36,20 +50,32 @@ import {
|
|
|
36
50
|
createStatePaths,
|
|
37
51
|
ensureStateDir,
|
|
38
52
|
readCaptureDedupeState,
|
|
53
|
+
readConsolidationState,
|
|
39
54
|
readLifecycleState,
|
|
40
55
|
readRemediationState,
|
|
41
56
|
readStatsState,
|
|
42
57
|
type StatePaths,
|
|
43
58
|
withStateLock,
|
|
44
59
|
writeCaptureDedupeState,
|
|
60
|
+
writeConsolidationState,
|
|
45
61
|
writeLifecycleState,
|
|
46
62
|
writeRemediationState,
|
|
47
63
|
writeStatsState,
|
|
48
64
|
} from "./state.js";
|
|
65
|
+
import {
|
|
66
|
+
buildTimeRange,
|
|
67
|
+
formatTimeRange,
|
|
68
|
+
inferQuerySpecificity,
|
|
69
|
+
isResultInTimeRange,
|
|
70
|
+
resolveResultTimeMs,
|
|
71
|
+
type TimeRange,
|
|
72
|
+
} from "./temporal.js";
|
|
49
73
|
import type {
|
|
50
74
|
CaptureIntent,
|
|
75
|
+
ConsolidationReason,
|
|
51
76
|
LifecycleEntry,
|
|
52
77
|
MemoryKind,
|
|
78
|
+
MemoryLayer,
|
|
53
79
|
MemoryOwner,
|
|
54
80
|
MemoryBraidResult,
|
|
55
81
|
PendingInboundTurn,
|
|
@@ -71,7 +97,7 @@ function jsonToolResult(payload: unknown) {
|
|
|
71
97
|
return {
|
|
72
98
|
content: [
|
|
73
99
|
{
|
|
74
|
-
type: "text",
|
|
100
|
+
type: "text" as const,
|
|
75
101
|
text: JSON.stringify(payload, null, 2),
|
|
76
102
|
},
|
|
77
103
|
],
|
|
@@ -163,6 +189,13 @@ function resolveCommandScope(config?: unknown): {
|
|
|
163
189
|
};
|
|
164
190
|
}
|
|
165
191
|
|
|
192
|
+
function resolveCommandPersistentScope(config?: unknown): ScopeKey {
|
|
193
|
+
return {
|
|
194
|
+
workspaceHash: workspaceHashFromDir(resolveWorkspaceDirFromConfig(config)),
|
|
195
|
+
agentId: "main",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
166
199
|
function resolveLatestUserTurnSignature(messages?: unknown[]): string | undefined {
|
|
167
200
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
168
201
|
return undefined;
|
|
@@ -183,6 +216,26 @@ function resolveLatestUserTurnSignature(messages?: unknown[]): string | undefine
|
|
|
183
216
|
return undefined;
|
|
184
217
|
}
|
|
185
218
|
|
|
219
|
+
function resolveLatestUserTurnText(messages?: unknown[]): string | undefined {
|
|
220
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const normalized = normalizeHookMessages(messages);
|
|
225
|
+
for (let i = normalized.length - 1; i >= 0; i -= 1) {
|
|
226
|
+
const message = normalized[i];
|
|
227
|
+
if (!message || message.role !== "user") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const text = normalizeWhitespace(message.text);
|
|
231
|
+
if (!text) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
return text;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
186
239
|
function resolvePromptTurnSignature(prompt: string): string | undefined {
|
|
187
240
|
const normalized = normalizeForHash(prompt);
|
|
188
241
|
if (!normalized) {
|
|
@@ -452,6 +505,10 @@ function inferRecallTarget(result: MemoryBraidResult): RecallTarget {
|
|
|
452
505
|
return normalizeRecallTarget(metadata.recallTarget) ?? "both";
|
|
453
506
|
}
|
|
454
507
|
|
|
508
|
+
function inferMemoryLayer(result: MemoryBraidResult): MemoryLayer {
|
|
509
|
+
return inferNormalizedMemoryLayer(result);
|
|
510
|
+
}
|
|
511
|
+
|
|
455
512
|
function normalizeSessionKey(raw: unknown): string | undefined {
|
|
456
513
|
if (typeof raw !== "string") {
|
|
457
514
|
return undefined;
|
|
@@ -460,6 +517,16 @@ function normalizeSessionKey(raw: unknown): string | undefined {
|
|
|
460
517
|
return trimmed || undefined;
|
|
461
518
|
}
|
|
462
519
|
|
|
520
|
+
function summarizeResultTaxonomy(result: MemoryBraidResult): string {
|
|
521
|
+
const metadata = asRecord(result.metadata);
|
|
522
|
+
const taxonomy = buildTaxonomy({
|
|
523
|
+
text: result.snippet,
|
|
524
|
+
entities: metadata.entities,
|
|
525
|
+
existingTaxonomy: metadata.taxonomy,
|
|
526
|
+
});
|
|
527
|
+
return formatTaxonomySummary(taxonomy) || "n/a";
|
|
528
|
+
}
|
|
529
|
+
|
|
463
530
|
function isGenericUserSummary(text: string): boolean {
|
|
464
531
|
const normalized = text.trim().toLowerCase();
|
|
465
532
|
return (
|
|
@@ -484,6 +551,7 @@ function applyMem0QualityAdjustments(params: {
|
|
|
484
551
|
query: string;
|
|
485
552
|
scope: ScopeKey;
|
|
486
553
|
nowMs: number;
|
|
554
|
+
timeRange?: TimeRange;
|
|
487
555
|
}): {
|
|
488
556
|
results: MemoryBraidResult[];
|
|
489
557
|
adjusted: number;
|
|
@@ -508,6 +576,7 @@ function applyMem0QualityAdjustments(params: {
|
|
|
508
576
|
}
|
|
509
577
|
|
|
510
578
|
const queryTokens = tokenizeForOverlap(params.query);
|
|
579
|
+
const specificity = inferQuerySpecificity(params.query);
|
|
511
580
|
let adjusted = 0;
|
|
512
581
|
let overlapBoosted = 0;
|
|
513
582
|
let overlapPenalized = 0;
|
|
@@ -522,6 +591,7 @@ function applyMem0QualityAdjustments(params: {
|
|
|
522
591
|
const overlap = lexicalOverlap(queryTokens, result.snippet);
|
|
523
592
|
const category = normalizeCategory(metadata.category);
|
|
524
593
|
const isGeneric = isGenericUserSummary(result.snippet);
|
|
594
|
+
const layer = inferMemoryLayer(result);
|
|
525
595
|
const ts = resolveTimestampMs(result);
|
|
526
596
|
const ageDays = ts ? Math.max(0, (params.nowMs - ts) / (24 * 60 * 60 * 1000)) : undefined;
|
|
527
597
|
|
|
@@ -570,6 +640,26 @@ function applyMem0QualityAdjustments(params: {
|
|
|
570
640
|
genericPenalized += 1;
|
|
571
641
|
}
|
|
572
642
|
|
|
643
|
+
if (metadata.supersededBy || metadata.supersededAt) {
|
|
644
|
+
multiplier *= 0.2;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (params.timeRange) {
|
|
648
|
+
if (isResultInTimeRange(result, params.timeRange)) {
|
|
649
|
+
multiplier *= layer === "episodic" ? 1.6 : 1.05;
|
|
650
|
+
} else {
|
|
651
|
+
multiplier *= layer === "episodic" ? 0.15 : 0.55;
|
|
652
|
+
}
|
|
653
|
+
} else if (specificity === "broad") {
|
|
654
|
+
if (layer === "semantic") {
|
|
655
|
+
multiplier *= 1.18;
|
|
656
|
+
} else if (layer === "episodic" && (metadata.consolidatedAt || metadata.compendiumKey)) {
|
|
657
|
+
multiplier *= 0.82;
|
|
658
|
+
}
|
|
659
|
+
} else if (specificity === "specific" && layer === "episodic") {
|
|
660
|
+
multiplier *= 1.1;
|
|
661
|
+
}
|
|
662
|
+
|
|
573
663
|
const normalizedMultiplier = Math.min(2.5, Math.max(0.1, multiplier));
|
|
574
664
|
const nextScore = result.score * normalizedMultiplier;
|
|
575
665
|
if (nextScore !== result.score) {
|
|
@@ -734,6 +824,10 @@ function resolveDateFromPath(pathValue?: string): number | undefined {
|
|
|
734
824
|
}
|
|
735
825
|
|
|
736
826
|
function resolveTimestampMs(result: MemoryBraidResult): number | undefined {
|
|
827
|
+
const normalized = resolveResultTimeMs(result);
|
|
828
|
+
if (normalized) {
|
|
829
|
+
return normalized;
|
|
830
|
+
}
|
|
737
831
|
const metadata = asRecord(result.metadata);
|
|
738
832
|
const fields = [
|
|
739
833
|
metadata.indexedAt,
|
|
@@ -1086,13 +1180,21 @@ async function runMem0Recall(params: {
|
|
|
1086
1180
|
statePaths?: StatePaths | null;
|
|
1087
1181
|
runId: string;
|
|
1088
1182
|
}): Promise<MemoryBraidResult[]> {
|
|
1183
|
+
const builtTimeRange = buildTimeRange({
|
|
1184
|
+
query: params.query,
|
|
1185
|
+
enabled: params.cfg.consolidation.timeQueryParsing,
|
|
1186
|
+
});
|
|
1187
|
+
const effectiveQuery = builtTimeRange.queryWithoutTime || params.query;
|
|
1188
|
+
const fetchLimit = builtTimeRange.range
|
|
1189
|
+
? Math.max(params.maxResults, Math.min(200, params.maxResults * 4))
|
|
1190
|
+
: params.maxResults;
|
|
1089
1191
|
const remediationState = params.statePaths
|
|
1090
1192
|
? await readRemediationState(params.statePaths)
|
|
1091
1193
|
: undefined;
|
|
1092
1194
|
|
|
1093
1195
|
const persistentRaw = await params.mem0.searchMemories({
|
|
1094
|
-
query:
|
|
1095
|
-
maxResults:
|
|
1196
|
+
query: effectiveQuery,
|
|
1197
|
+
maxResults: fetchLimit,
|
|
1096
1198
|
scope: params.persistentScope,
|
|
1097
1199
|
runId: params.runId,
|
|
1098
1200
|
});
|
|
@@ -1109,8 +1211,8 @@ async function runMem0Recall(params: {
|
|
|
1109
1211
|
params.legacyScope.sessionKey !== params.persistentScope.sessionKey
|
|
1110
1212
|
) {
|
|
1111
1213
|
const legacyRaw = await params.mem0.searchMemories({
|
|
1112
|
-
query:
|
|
1113
|
-
maxResults:
|
|
1214
|
+
query: effectiveQuery,
|
|
1215
|
+
maxResults: fetchLimit,
|
|
1114
1216
|
scope: params.legacyScope,
|
|
1115
1217
|
runId: params.runId,
|
|
1116
1218
|
});
|
|
@@ -1123,6 +1225,14 @@ async function runMem0Recall(params: {
|
|
|
1123
1225
|
}
|
|
1124
1226
|
|
|
1125
1227
|
let combined = [...persistentFiltered.results, ...legacyFiltered];
|
|
1228
|
+
if (builtTimeRange.range) {
|
|
1229
|
+
combined = combined.filter((result) => {
|
|
1230
|
+
if (inferMemoryLayer(result) === "semantic") {
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
return isResultInTimeRange(result, builtTimeRange.range);
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1126
1236
|
if (params.cfg.timeDecay.enabled) {
|
|
1127
1237
|
const coreDecay = resolveCoreTemporalDecay({
|
|
1128
1238
|
config: params.coreConfig,
|
|
@@ -1139,9 +1249,10 @@ async function runMem0Recall(params: {
|
|
|
1139
1249
|
|
|
1140
1250
|
combined = applyMem0QualityAdjustments({
|
|
1141
1251
|
results: combined,
|
|
1142
|
-
query:
|
|
1252
|
+
query: effectiveQuery,
|
|
1143
1253
|
scope: params.runtimeScope,
|
|
1144
1254
|
nowMs: Date.now(),
|
|
1255
|
+
timeRange: builtTimeRange.range,
|
|
1145
1256
|
}).results;
|
|
1146
1257
|
|
|
1147
1258
|
const deduped = await stagedDedupe(sortMemoriesStable(combined), {
|
|
@@ -1166,6 +1277,7 @@ async function runMem0Recall(params: {
|
|
|
1166
1277
|
legacyCount: legacyFiltered.length,
|
|
1167
1278
|
quarantinedFiltered:
|
|
1168
1279
|
persistentFiltered.quarantinedFiltered + legacyQuarantinedFiltered,
|
|
1280
|
+
timeRange: builtTimeRange.range ? formatTimeRange(builtTimeRange.range) : "n/a",
|
|
1169
1281
|
dedupedCount: deduped.length,
|
|
1170
1282
|
});
|
|
1171
1283
|
|
|
@@ -1345,6 +1457,41 @@ function parseIntegerFlag(tokens: string[], flag: string, fallback: number): num
|
|
|
1345
1457
|
return Math.max(1, Math.round(raw));
|
|
1346
1458
|
}
|
|
1347
1459
|
|
|
1460
|
+
function parseStringFlag(tokens: string[], flag: string): string | undefined {
|
|
1461
|
+
const index = tokens.findIndex((token) => token === flag);
|
|
1462
|
+
if (index < 0 || index === tokens.length - 1) {
|
|
1463
|
+
return undefined;
|
|
1464
|
+
}
|
|
1465
|
+
const raw = tokens[index + 1]?.trim();
|
|
1466
|
+
return raw || undefined;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function hasFlag(tokens: string[], flag: string): boolean {
|
|
1470
|
+
return tokens.includes(flag);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function stripFlags(tokens: string[]): string[] {
|
|
1474
|
+
const out: string[] = [];
|
|
1475
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1476
|
+
const token = tokens[index];
|
|
1477
|
+
if (
|
|
1478
|
+
token === "--limit" ||
|
|
1479
|
+
token === "--layer" ||
|
|
1480
|
+
token === "--kind" ||
|
|
1481
|
+
token === "--from" ||
|
|
1482
|
+
token === "--to"
|
|
1483
|
+
) {
|
|
1484
|
+
index += 1;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (token === "--include-quarantined") {
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
out.push(token);
|
|
1491
|
+
}
|
|
1492
|
+
return out;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1348
1495
|
function resolveRecordScope(
|
|
1349
1496
|
memory: MemoryBraidResult,
|
|
1350
1497
|
fallbackScope: { workspaceHash: string; agentId?: string; sessionKey?: string },
|
|
@@ -1537,6 +1684,7 @@ const memoryBraidPlugin = {
|
|
|
1537
1684
|
const assistantLearningWritesByRunScope = new Map<string, number[]>();
|
|
1538
1685
|
|
|
1539
1686
|
let lifecycleTimer: NodeJS.Timeout | null = null;
|
|
1687
|
+
let consolidationTimer: NodeJS.Timeout | null = null;
|
|
1540
1688
|
let statePaths: StatePaths | null = null;
|
|
1541
1689
|
|
|
1542
1690
|
async function ensureRuntimeStatePaths(): Promise<StatePaths | null> {
|
|
@@ -1596,6 +1744,7 @@ const memoryBraidPlugin = {
|
|
|
1596
1744
|
}): Promise<{ accepted: boolean; reason: string; normalizedText: string; memoryId?: string }> {
|
|
1597
1745
|
const validated = validateAtomicMemoryText(params.text);
|
|
1598
1746
|
if (!validated.ok) {
|
|
1747
|
+
const failedReason = validated.reason;
|
|
1599
1748
|
if (params.runtimeStatePaths) {
|
|
1600
1749
|
await withStateLock(params.runtimeStatePaths.stateLockFile, async () => {
|
|
1601
1750
|
const stats = await readStatsState(params.runtimeStatePaths!);
|
|
@@ -1605,7 +1754,7 @@ const memoryBraidPlugin = {
|
|
|
1605
1754
|
}
|
|
1606
1755
|
return {
|
|
1607
1756
|
accepted: false,
|
|
1608
|
-
reason:
|
|
1757
|
+
reason: failedReason,
|
|
1609
1758
|
normalizedText: normalizeWhitespace(params.text),
|
|
1610
1759
|
};
|
|
1611
1760
|
}
|
|
@@ -1659,8 +1808,56 @@ const memoryBraidPlugin = {
|
|
|
1659
1808
|
};
|
|
1660
1809
|
}
|
|
1661
1810
|
|
|
1811
|
+
const selection = scoreProceduralMemory({
|
|
1812
|
+
text: validated.normalized,
|
|
1813
|
+
confidence: params.confidence,
|
|
1814
|
+
captureIntent: params.captureIntent,
|
|
1815
|
+
cfg,
|
|
1816
|
+
});
|
|
1817
|
+
if (selection.decision !== "procedural") {
|
|
1818
|
+
log.debug("memory_braid.capture.selection", {
|
|
1819
|
+
runId: params.runId,
|
|
1820
|
+
target: "procedural",
|
|
1821
|
+
decision: selection.decision,
|
|
1822
|
+
kind: params.kind,
|
|
1823
|
+
captureIntent: params.captureIntent,
|
|
1824
|
+
score: selection.score,
|
|
1825
|
+
reasons: selection.reasons,
|
|
1826
|
+
workspaceHash: params.runtimeScope.workspaceHash,
|
|
1827
|
+
agentId: params.runtimeScope.agentId,
|
|
1828
|
+
sessionKey: params.runtimeScope.sessionKey,
|
|
1829
|
+
contentHashPrefix: exactHash.slice(0, 12),
|
|
1830
|
+
});
|
|
1831
|
+
if (params.runtimeStatePaths) {
|
|
1832
|
+
await withStateLock(params.runtimeStatePaths.stateLockFile, async () => {
|
|
1833
|
+
const stats = await readStatsState(params.runtimeStatePaths!);
|
|
1834
|
+
stats.capture.agentLearningRejectedSelection += 1;
|
|
1835
|
+
await writeStatsState(params.runtimeStatePaths!, stats);
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
accepted: false,
|
|
1840
|
+
reason: "selection_rejected",
|
|
1841
|
+
normalizedText: validated.normalized,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
log.debug("memory_braid.capture.selection", {
|
|
1845
|
+
runId: params.runId,
|
|
1846
|
+
target: "procedural",
|
|
1847
|
+
decision: selection.decision,
|
|
1848
|
+
kind: params.kind,
|
|
1849
|
+
captureIntent: params.captureIntent,
|
|
1850
|
+
score: selection.score,
|
|
1851
|
+
reasons: selection.reasons,
|
|
1852
|
+
workspaceHash: params.runtimeScope.workspaceHash,
|
|
1853
|
+
agentId: params.runtimeScope.agentId,
|
|
1854
|
+
sessionKey: params.runtimeScope.sessionKey,
|
|
1855
|
+
contentHashPrefix: exactHash.slice(0, 12),
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1662
1858
|
const metadata: Record<string, unknown> = {
|
|
1663
1859
|
sourceType: "agent_learning",
|
|
1860
|
+
memoryLayer: "procedural",
|
|
1664
1861
|
memoryOwner: "agent",
|
|
1665
1862
|
memoryKind: params.kind,
|
|
1666
1863
|
captureIntent: params.captureIntent,
|
|
@@ -1670,7 +1867,13 @@ const memoryBraidPlugin = {
|
|
|
1670
1867
|
agentId: params.runtimeScope.agentId,
|
|
1671
1868
|
sessionKey: params.runtimeScope.sessionKey,
|
|
1672
1869
|
indexedAt: new Date().toISOString(),
|
|
1870
|
+
firstSeenAt: new Date().toISOString(),
|
|
1871
|
+
lastSeenAt: new Date().toISOString(),
|
|
1872
|
+
eventAt: new Date().toISOString(),
|
|
1673
1873
|
contentHash: exactHash,
|
|
1874
|
+
selectionDecision: selection.decision,
|
|
1875
|
+
rememberabilityScore: selection.score,
|
|
1876
|
+
rememberabilityReasons: selection.reasons,
|
|
1674
1877
|
};
|
|
1675
1878
|
if (typeof params.confidence === "number") {
|
|
1676
1879
|
metadata.confidence = Math.max(0, Math.min(1, params.confidence));
|
|
@@ -1705,6 +1908,288 @@ const memoryBraidPlugin = {
|
|
|
1705
1908
|
};
|
|
1706
1909
|
}
|
|
1707
1910
|
|
|
1911
|
+
async function runConsolidationOnce(params: {
|
|
1912
|
+
scope: ScopeKey;
|
|
1913
|
+
reason: ConsolidationReason;
|
|
1914
|
+
runtimeStatePaths?: StatePaths | null;
|
|
1915
|
+
runId: string;
|
|
1916
|
+
}): Promise<{
|
|
1917
|
+
candidates: number;
|
|
1918
|
+
clusters: number;
|
|
1919
|
+
created: number;
|
|
1920
|
+
updated: number;
|
|
1921
|
+
episodicMarked: number;
|
|
1922
|
+
contradictions: number;
|
|
1923
|
+
superseded: number;
|
|
1924
|
+
}> {
|
|
1925
|
+
if (!cfg.consolidation.enabled) {
|
|
1926
|
+
return {
|
|
1927
|
+
candidates: 0,
|
|
1928
|
+
clusters: 0,
|
|
1929
|
+
created: 0,
|
|
1930
|
+
updated: 0,
|
|
1931
|
+
episodicMarked: 0,
|
|
1932
|
+
contradictions: 0,
|
|
1933
|
+
superseded: 0,
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
const runtimeStatePaths = params.runtimeStatePaths ?? (await ensureRuntimeStatePaths());
|
|
1937
|
+
if (!runtimeStatePaths) {
|
|
1938
|
+
return {
|
|
1939
|
+
candidates: 0,
|
|
1940
|
+
clusters: 0,
|
|
1941
|
+
created: 0,
|
|
1942
|
+
updated: 0,
|
|
1943
|
+
episodicMarked: 0,
|
|
1944
|
+
contradictions: 0,
|
|
1945
|
+
superseded: 0,
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
const [allMemories, remediationState, lifecycle, consolidation] = await Promise.all([
|
|
1950
|
+
mem0.getAllMemories({
|
|
1951
|
+
scope: params.scope,
|
|
1952
|
+
limit: 500,
|
|
1953
|
+
runId: params.runId,
|
|
1954
|
+
}),
|
|
1955
|
+
readRemediationState(runtimeStatePaths),
|
|
1956
|
+
readLifecycleState(runtimeStatePaths),
|
|
1957
|
+
readConsolidationState(runtimeStatePaths),
|
|
1958
|
+
]);
|
|
1959
|
+
|
|
1960
|
+
const activeMemories = allMemories.filter((memory) => !isQuarantinedMemory(memory, remediationState).quarantined);
|
|
1961
|
+
const episodic = activeMemories.filter((memory) => inferMemoryLayer(memory) === "episodic");
|
|
1962
|
+
const semantic = activeMemories.filter((memory) => inferMemoryLayer(memory) === "semantic");
|
|
1963
|
+
|
|
1964
|
+
const draftPlan = await buildConsolidationDrafts({
|
|
1965
|
+
episodic,
|
|
1966
|
+
existingSemantic: semantic,
|
|
1967
|
+
lifecycleEntries: lifecycle.entries,
|
|
1968
|
+
cfg,
|
|
1969
|
+
minSupportCount: cfg.consolidation.minSupportCount,
|
|
1970
|
+
minRecallCount: cfg.consolidation.minRecallCount,
|
|
1971
|
+
semanticMaxSourceIds: cfg.consolidation.semanticMaxSourceIds,
|
|
1972
|
+
state: consolidation,
|
|
1973
|
+
semanticSimilarity: async (leftText, rightText) =>
|
|
1974
|
+
mem0.semanticSimilarity({
|
|
1975
|
+
leftText,
|
|
1976
|
+
rightText,
|
|
1977
|
+
scope: params.scope,
|
|
1978
|
+
runId: params.runId,
|
|
1979
|
+
}),
|
|
1980
|
+
});
|
|
1981
|
+
log.debug("memory_braid.consolidation.plan", {
|
|
1982
|
+
runId: params.runId,
|
|
1983
|
+
reason: params.reason,
|
|
1984
|
+
workspaceHash: params.scope.workspaceHash,
|
|
1985
|
+
agentId: params.scope.agentId,
|
|
1986
|
+
candidates: draftPlan.candidates,
|
|
1987
|
+
clusters: draftPlan.clustersFormed,
|
|
1988
|
+
promotedDrafts: draftPlan.drafts.length,
|
|
1989
|
+
drafts: draftPlan.drafts.map((draft) => ({
|
|
1990
|
+
compendiumKeyPrefix: draft.compendiumKey.slice(0, 12),
|
|
1991
|
+
existingMemoryId: draft.existingMemoryId ?? null,
|
|
1992
|
+
kind: draft.kind,
|
|
1993
|
+
anchor: draft.anchor ?? null,
|
|
1994
|
+
sourceCount: draft.sourceMemories.length,
|
|
1995
|
+
supportCount:
|
|
1996
|
+
typeof asRecord(draft.metadata).supportCount === "number"
|
|
1997
|
+
? asRecord(draft.metadata).supportCount
|
|
1998
|
+
: null,
|
|
1999
|
+
promotionScore:
|
|
2000
|
+
typeof asRecord(draft.metadata).promotionScore === "number"
|
|
2001
|
+
? asRecord(draft.metadata).promotionScore
|
|
2002
|
+
: null,
|
|
2003
|
+
promotionReasons: asRecord(draft.metadata).promotionReasons,
|
|
2004
|
+
})),
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
let created = 0;
|
|
2008
|
+
let updated = 0;
|
|
2009
|
+
let episodicMarked = 0;
|
|
2010
|
+
const semanticByKey = { ...consolidation.semanticByCompendiumKey };
|
|
2011
|
+
const semanticMemoryIds = new Map<string, string>();
|
|
2012
|
+
const contradictionCandidates = [...semantic];
|
|
2013
|
+
|
|
2014
|
+
for (const draft of draftPlan.drafts) {
|
|
2015
|
+
let semanticMemoryId = draft.existingMemoryId;
|
|
2016
|
+
if (draft.existingMemoryId) {
|
|
2017
|
+
const updatedRemote = await mem0.updateMemoryMetadata({
|
|
2018
|
+
memoryId: draft.existingMemoryId,
|
|
2019
|
+
scope: params.scope,
|
|
2020
|
+
text: draft.text,
|
|
2021
|
+
metadata: draft.metadata,
|
|
2022
|
+
runId: params.runId,
|
|
2023
|
+
});
|
|
2024
|
+
if (!updatedRemote) {
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
updated += 1;
|
|
2028
|
+
} else {
|
|
2029
|
+
const createdRemote = await mem0.addMemory({
|
|
2030
|
+
text: draft.text,
|
|
2031
|
+
scope: params.scope,
|
|
2032
|
+
metadata: draft.metadata,
|
|
2033
|
+
runId: params.runId,
|
|
2034
|
+
});
|
|
2035
|
+
if (!createdRemote.id) {
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
semanticMemoryId = createdRemote.id;
|
|
2039
|
+
created += 1;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
if (!semanticMemoryId) {
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
semanticMemoryIds.set(draft.compendiumKey, semanticMemoryId);
|
|
2046
|
+
semanticByKey[draft.compendiumKey] = {
|
|
2047
|
+
memoryId: semanticMemoryId,
|
|
2048
|
+
updatedAt: Date.now(),
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
contradictionCandidates.push({
|
|
2052
|
+
id: semanticMemoryId,
|
|
2053
|
+
source: "mem0",
|
|
2054
|
+
snippet: draft.text,
|
|
2055
|
+
score: 0,
|
|
2056
|
+
metadata: draft.metadata,
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
for (const sourceMemory of draft.sourceMemories) {
|
|
2060
|
+
if (!sourceMemory.id) {
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
const updatedSource = await mem0.updateMemoryMetadata({
|
|
2064
|
+
memoryId: sourceMemory.id,
|
|
2065
|
+
scope: params.scope,
|
|
2066
|
+
text: sourceMemory.snippet,
|
|
2067
|
+
metadata: {
|
|
2068
|
+
...asRecord(sourceMemory.metadata),
|
|
2069
|
+
memoryLayer: "episodic",
|
|
2070
|
+
consolidatedAt: new Date().toISOString(),
|
|
2071
|
+
compendiumKey: draft.compendiumKey,
|
|
2072
|
+
semanticMemoryId,
|
|
2073
|
+
},
|
|
2074
|
+
runId: params.runId,
|
|
2075
|
+
});
|
|
2076
|
+
if (updatedSource) {
|
|
2077
|
+
episodicMarked += 1;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const superseded = await findSupersededSemanticMemories({
|
|
2083
|
+
semanticMemories: contradictionCandidates,
|
|
2084
|
+
semanticSimilarity: async (leftText, rightText) =>
|
|
2085
|
+
mem0.semanticSimilarity({
|
|
2086
|
+
leftText,
|
|
2087
|
+
rightText,
|
|
2088
|
+
scope: params.scope,
|
|
2089
|
+
runId: params.runId,
|
|
2090
|
+
}),
|
|
2091
|
+
});
|
|
2092
|
+
let supersededMarked = 0;
|
|
2093
|
+
if (superseded.length > 0) {
|
|
2094
|
+
log.debug("memory_braid.consolidation.supersede", {
|
|
2095
|
+
runId: params.runId,
|
|
2096
|
+
reason: params.reason,
|
|
2097
|
+
workspaceHash: params.scope.workspaceHash,
|
|
2098
|
+
agentId: params.scope.agentId,
|
|
2099
|
+
count: superseded.length,
|
|
2100
|
+
updates: superseded.map((target) => ({
|
|
2101
|
+
memoryId: target.memoryId,
|
|
2102
|
+
supersededBy:
|
|
2103
|
+
typeof asRecord(target.metadata).supersededBy === "string"
|
|
2104
|
+
? asRecord(target.metadata).supersededBy
|
|
2105
|
+
: null,
|
|
2106
|
+
})),
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
for (const target of superseded) {
|
|
2110
|
+
const updatedRemote = await mem0.updateMemoryMetadata({
|
|
2111
|
+
memoryId: target.memoryId,
|
|
2112
|
+
scope: params.scope,
|
|
2113
|
+
text: target.text,
|
|
2114
|
+
metadata: target.metadata,
|
|
2115
|
+
runId: params.runId,
|
|
2116
|
+
});
|
|
2117
|
+
if (updatedRemote) {
|
|
2118
|
+
supersededMarked += 1;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const nowIso = new Date().toISOString();
|
|
2123
|
+
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
2124
|
+
const stats = await readStatsState(runtimeStatePaths);
|
|
2125
|
+
const next = await readConsolidationState(runtimeStatePaths);
|
|
2126
|
+
next.lastConsolidationAt = nowIso;
|
|
2127
|
+
next.lastConsolidationReason = params.reason;
|
|
2128
|
+
next.newEpisodicSinceLastRun = 0;
|
|
2129
|
+
next.semanticByCompendiumKey = semanticByKey;
|
|
2130
|
+
stats.capture.consolidationRuns += 1;
|
|
2131
|
+
stats.capture.consolidationCandidates += draftPlan.candidates;
|
|
2132
|
+
stats.capture.clustersFormed += draftPlan.clustersFormed;
|
|
2133
|
+
stats.capture.semanticCreated += created;
|
|
2134
|
+
stats.capture.semanticUpdated += updated;
|
|
2135
|
+
stats.capture.episodicMarkedConsolidated += episodicMarked;
|
|
2136
|
+
stats.capture.contradictionsDetected += superseded.length;
|
|
2137
|
+
stats.capture.supersededMarked += supersededMarked;
|
|
2138
|
+
stats.capture.lastConsolidationAt = nowIso;
|
|
2139
|
+
await writeConsolidationState(runtimeStatePaths, next);
|
|
2140
|
+
await writeStatsState(runtimeStatePaths, stats);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
log.debug("memory_braid.consolidation.run", {
|
|
2144
|
+
runId: params.runId,
|
|
2145
|
+
reason: params.reason,
|
|
2146
|
+
workspaceHash: params.scope.workspaceHash,
|
|
2147
|
+
agentId: params.scope.agentId,
|
|
2148
|
+
candidates: draftPlan.candidates,
|
|
2149
|
+
clusters: draftPlan.clustersFormed,
|
|
2150
|
+
created,
|
|
2151
|
+
updated,
|
|
2152
|
+
episodicMarked,
|
|
2153
|
+
contradictions: superseded.length,
|
|
2154
|
+
supersededMarked,
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
return {
|
|
2158
|
+
candidates: draftPlan.candidates,
|
|
2159
|
+
clusters: draftPlan.clustersFormed,
|
|
2160
|
+
created,
|
|
2161
|
+
updated,
|
|
2162
|
+
episodicMarked,
|
|
2163
|
+
contradictions: superseded.length,
|
|
2164
|
+
superseded: supersededMarked,
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
async function maybeRunOpportunisticConsolidation(params: {
|
|
2169
|
+
scope: ScopeKey;
|
|
2170
|
+
runtimeStatePaths: StatePaths;
|
|
2171
|
+
runId: string;
|
|
2172
|
+
}): Promise<void> {
|
|
2173
|
+
if (!cfg.consolidation.enabled) {
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const state = await readConsolidationState(params.runtimeStatePaths);
|
|
2177
|
+
const lastRunAt = state.lastConsolidationAt ? Date.parse(state.lastConsolidationAt) : 0;
|
|
2178
|
+
const minutesSinceLastRun =
|
|
2179
|
+
lastRunAt > 0 ? (Date.now() - lastRunAt) / (60 * 1000) : Number.POSITIVE_INFINITY;
|
|
2180
|
+
const enoughNew = state.newEpisodicSinceLastRun >= cfg.consolidation.opportunisticNewMemoryThreshold;
|
|
2181
|
+
const enoughTime = minutesSinceLastRun >= cfg.consolidation.opportunisticMinMinutesSinceLastRun;
|
|
2182
|
+
if (!enoughNew && !enoughTime) {
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
await runConsolidationOnce({
|
|
2186
|
+
scope: params.scope,
|
|
2187
|
+
reason: "opportunistic",
|
|
2188
|
+
runtimeStatePaths: params.runtimeStatePaths,
|
|
2189
|
+
runId: params.runId,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
|
|
1708
2193
|
api.registerTool(
|
|
1709
2194
|
(ctx) => {
|
|
1710
2195
|
const local = resolveLocalTools(api, ctx);
|
|
@@ -1895,7 +2380,8 @@ const memoryBraidPlugin = {
|
|
|
1895
2380
|
|
|
1896
2381
|
api.registerCommand({
|
|
1897
2382
|
name: "memorybraid",
|
|
1898
|
-
description:
|
|
2383
|
+
description:
|
|
2384
|
+
"Memory Braid status, stats, search, consolidation, remediation, lifecycle cleanup, and entity extraction warmup.",
|
|
1899
2385
|
acceptsArgs: true,
|
|
1900
2386
|
handler: async (ctx) => {
|
|
1901
2387
|
const args = ctx.args?.trim() ?? "";
|
|
@@ -1907,6 +2393,15 @@ const memoryBraidPlugin = {
|
|
|
1907
2393
|
config: ctx.config,
|
|
1908
2394
|
});
|
|
1909
2395
|
const paths = await ensureRuntimeStatePaths();
|
|
2396
|
+
const consolidation = paths
|
|
2397
|
+
? await readConsolidationState(paths)
|
|
2398
|
+
: {
|
|
2399
|
+
version: 1 as const,
|
|
2400
|
+
semanticByCompendiumKey: {},
|
|
2401
|
+
newEpisodicSinceLastRun: 0,
|
|
2402
|
+
lastConsolidationAt: undefined,
|
|
2403
|
+
lastConsolidationReason: undefined,
|
|
2404
|
+
};
|
|
1910
2405
|
const lifecycle =
|
|
1911
2406
|
cfg.lifecycle.enabled && paths
|
|
1912
2407
|
? await readLifecycleState(paths)
|
|
@@ -1915,6 +2410,11 @@ const memoryBraidPlugin = {
|
|
|
1915
2410
|
text: [
|
|
1916
2411
|
`capture.mode: ${cfg.capture.mode}`,
|
|
1917
2412
|
`capture.includeAssistant: ${cfg.capture.includeAssistant}`,
|
|
2413
|
+
`capture.selection.minPreferenceDecisionScore: ${cfg.capture.selection.minPreferenceDecisionScore}`,
|
|
2414
|
+
`capture.selection.minFactScore: ${cfg.capture.selection.minFactScore}`,
|
|
2415
|
+
`capture.selection.minTaskScore: ${cfg.capture.selection.minTaskScore}`,
|
|
2416
|
+
`capture.selection.minOtherScore: ${cfg.capture.selection.minOtherScore}`,
|
|
2417
|
+
`capture.selection.minProceduralScore: ${cfg.capture.selection.minProceduralScore}`,
|
|
1918
2418
|
`capture.assistant.autoCapture: ${cfg.capture.assistant.autoCapture}`,
|
|
1919
2419
|
`capture.assistant.explicitTool: ${cfg.capture.assistant.explicitTool}`,
|
|
1920
2420
|
`recall.user.injectTopK: ${cfg.recall.user.injectTopK}`,
|
|
@@ -1927,6 +2427,14 @@ const memoryBraidPlugin = {
|
|
|
1927
2427
|
`lifecycle.captureTtlDays: ${cfg.lifecycle.captureTtlDays}`,
|
|
1928
2428
|
`lifecycle.cleanupIntervalMinutes: ${cfg.lifecycle.cleanupIntervalMinutes}`,
|
|
1929
2429
|
`lifecycle.reinforceOnRecall: ${cfg.lifecycle.reinforceOnRecall}`,
|
|
2430
|
+
`consolidation.enabled: ${cfg.consolidation.enabled}`,
|
|
2431
|
+
`consolidation.startupRun: ${cfg.consolidation.startupRun}`,
|
|
2432
|
+
`consolidation.intervalMinutes: ${cfg.consolidation.intervalMinutes}`,
|
|
2433
|
+
`consolidation.minSelectionScore: ${cfg.consolidation.minSelectionScore}`,
|
|
2434
|
+
`consolidation.newEpisodicSinceLastRun: ${consolidation.newEpisodicSinceLastRun}`,
|
|
2435
|
+
`consolidation.semanticTracked: ${Object.keys(consolidation.semanticByCompendiumKey).length}`,
|
|
2436
|
+
`consolidation.lastConsolidationAt: ${consolidation.lastConsolidationAt ?? "n/a"}`,
|
|
2437
|
+
`consolidation.lastConsolidationReason: ${consolidation.lastConsolidationReason ?? "n/a"}`,
|
|
1930
2438
|
`lifecycle.tracked: ${Object.keys(lifecycle.entries).length}`,
|
|
1931
2439
|
`lifecycle.lastCleanupAt: ${lifecycle.lastCleanupAt ?? "n/a"}`,
|
|
1932
2440
|
`lifecycle.lastCleanupReason: ${lifecycle.lastCleanupReason ?? "n/a"}`,
|
|
@@ -1946,6 +2454,7 @@ const memoryBraidPlugin = {
|
|
|
1946
2454
|
|
|
1947
2455
|
const stats = await readStatsState(paths);
|
|
1948
2456
|
const lifecycle = await readLifecycleState(paths);
|
|
2457
|
+
const consolidation = await readConsolidationState(paths);
|
|
1949
2458
|
const capture = stats.capture;
|
|
1950
2459
|
const mem0SuccessRate =
|
|
1951
2460
|
capture.mem0AddAttempts > 0
|
|
@@ -1990,8 +2499,19 @@ const memoryBraidPlugin = {
|
|
|
1990
2499
|
`- agentLearningAutoRejected: ${capture.agentLearningAutoRejected}`,
|
|
1991
2500
|
`- agentLearningInjected: ${capture.agentLearningInjected}`,
|
|
1992
2501
|
`- agentLearningRecallHits: ${capture.agentLearningRecallHits}`,
|
|
2502
|
+
`- selectionSkipped: ${capture.selectionSkipped}`,
|
|
2503
|
+
`- agentLearningRejectedSelection: ${capture.agentLearningRejectedSelection}`,
|
|
2504
|
+
`- consolidationRuns: ${capture.consolidationRuns}`,
|
|
2505
|
+
`- consolidationCandidates: ${capture.consolidationCandidates}`,
|
|
2506
|
+
`- clustersFormed: ${capture.clustersFormed}`,
|
|
2507
|
+
`- semanticCreated: ${capture.semanticCreated}`,
|
|
2508
|
+
`- semanticUpdated: ${capture.semanticUpdated}`,
|
|
2509
|
+
`- episodicMarkedConsolidated: ${capture.episodicMarkedConsolidated}`,
|
|
2510
|
+
`- contradictionsDetected: ${capture.contradictionsDetected}`,
|
|
2511
|
+
`- supersededMarked: ${capture.supersededMarked}`,
|
|
1993
2512
|
`- lastRunAt: ${capture.lastRunAt ?? "n/a"}`,
|
|
1994
2513
|
`- lastRemediationAt: ${capture.lastRemediationAt ?? "n/a"}`,
|
|
2514
|
+
`- lastConsolidationAt: ${capture.lastConsolidationAt ?? "n/a"}`,
|
|
1995
2515
|
"",
|
|
1996
2516
|
"Lifecycle:",
|
|
1997
2517
|
`- enabled: ${cfg.lifecycle.enabled}`,
|
|
@@ -2005,10 +2525,144 @@ const memoryBraidPlugin = {
|
|
|
2005
2525
|
`- lastCleanupExpired: ${lifecycle.lastCleanupExpired ?? "n/a"}`,
|
|
2006
2526
|
`- lastCleanupDeleted: ${lifecycle.lastCleanupDeleted ?? "n/a"}`,
|
|
2007
2527
|
`- lastCleanupFailed: ${lifecycle.lastCleanupFailed ?? "n/a"}`,
|
|
2528
|
+
"",
|
|
2529
|
+
"Consolidation:",
|
|
2530
|
+
`- enabled: ${cfg.consolidation.enabled}`,
|
|
2531
|
+
`- startupRun: ${cfg.consolidation.startupRun}`,
|
|
2532
|
+
`- intervalMinutes: ${cfg.consolidation.intervalMinutes}`,
|
|
2533
|
+
`- minSelectionScore: ${cfg.consolidation.minSelectionScore}`,
|
|
2534
|
+
`- newEpisodicSinceLastRun: ${consolidation.newEpisodicSinceLastRun}`,
|
|
2535
|
+
`- semanticTracked: ${Object.keys(consolidation.semanticByCompendiumKey).length}`,
|
|
2536
|
+
`- lastConsolidationAt: ${consolidation.lastConsolidationAt ?? "n/a"}`,
|
|
2537
|
+
`- lastConsolidationReason: ${consolidation.lastConsolidationReason ?? "n/a"}`,
|
|
2008
2538
|
].join("\n"),
|
|
2009
2539
|
};
|
|
2010
2540
|
}
|
|
2011
2541
|
|
|
2542
|
+
if (action === "search") {
|
|
2543
|
+
const paths = await ensureRuntimeStatePaths();
|
|
2544
|
+
if (!paths) {
|
|
2545
|
+
return {
|
|
2546
|
+
text: "Search unavailable: state directory is not ready.",
|
|
2547
|
+
isError: true,
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
const queryTokens = stripFlags(tokens.slice(1));
|
|
2552
|
+
const queryText = normalizeWhitespace(queryTokens.join(" "));
|
|
2553
|
+
if (!queryText) {
|
|
2554
|
+
return {
|
|
2555
|
+
text:
|
|
2556
|
+
"Usage: /memorybraid search <query> [--limit N] [--layer episodic|semantic|procedural|all] [--kind fact|preference|decision|task|heuristic|lesson|strategy|other] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--include-quarantined]",
|
|
2557
|
+
isError: true,
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
const layer = parseStringFlag(tokens, "--layer") ?? "all";
|
|
2562
|
+
const allowedLayers = new Set(["episodic", "semantic", "procedural", "all"]);
|
|
2563
|
+
if (!allowedLayers.has(layer)) {
|
|
2564
|
+
return {
|
|
2565
|
+
text: "Invalid --layer. Use episodic, semantic, procedural, or all.",
|
|
2566
|
+
isError: true,
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
const kind = parseStringFlag(tokens, "--kind");
|
|
2571
|
+
if (
|
|
2572
|
+
kind &&
|
|
2573
|
+
![
|
|
2574
|
+
"fact",
|
|
2575
|
+
"preference",
|
|
2576
|
+
"decision",
|
|
2577
|
+
"task",
|
|
2578
|
+
"heuristic",
|
|
2579
|
+
"lesson",
|
|
2580
|
+
"strategy",
|
|
2581
|
+
"other",
|
|
2582
|
+
].includes(kind)
|
|
2583
|
+
) {
|
|
2584
|
+
return {
|
|
2585
|
+
text: "Invalid --kind.",
|
|
2586
|
+
isError: true,
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
const limit = Math.min(50, Math.max(1, parseIntegerFlag(tokens, "--limit", 10)));
|
|
2591
|
+
const includeQuarantined = hasFlag(tokens, "--include-quarantined");
|
|
2592
|
+
const timeRangeResult = buildTimeRange({
|
|
2593
|
+
query: queryText,
|
|
2594
|
+
from: parseStringFlag(tokens, "--from"),
|
|
2595
|
+
to: parseStringFlag(tokens, "--to"),
|
|
2596
|
+
enabled: cfg.consolidation.timeQueryParsing,
|
|
2597
|
+
});
|
|
2598
|
+
const effectiveQuery = timeRangeResult.queryWithoutTime || queryText;
|
|
2599
|
+
const commandScope = resolveCommandPersistentScope(ctx.config);
|
|
2600
|
+
const fetched = await mem0.searchMemories({
|
|
2601
|
+
query: effectiveQuery,
|
|
2602
|
+
maxResults: Math.min(200, Math.max(25, limit * 5)),
|
|
2603
|
+
scope: commandScope,
|
|
2604
|
+
runId: log.newRunId(),
|
|
2605
|
+
});
|
|
2606
|
+
const remediationState = await readRemediationState(paths);
|
|
2607
|
+
const lifecycle = await readLifecycleState(paths);
|
|
2608
|
+
let hiddenQuarantined = 0;
|
|
2609
|
+
const filtered = fetched.filter((result) => {
|
|
2610
|
+
const quarantine = isQuarantinedMemory(result, remediationState);
|
|
2611
|
+
if (quarantine.quarantined && !includeQuarantined) {
|
|
2612
|
+
hiddenQuarantined += 1;
|
|
2613
|
+
return false;
|
|
2614
|
+
}
|
|
2615
|
+
if (layer !== "all" && inferMemoryLayer(result) !== layer) {
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
if (kind && inferMemoryKind(result) !== kind) {
|
|
2619
|
+
return false;
|
|
2620
|
+
}
|
|
2621
|
+
if (timeRangeResult.range && !isResultInTimeRange(result, timeRangeResult.range)) {
|
|
2622
|
+
return false;
|
|
2623
|
+
}
|
|
2624
|
+
return true;
|
|
2625
|
+
});
|
|
2626
|
+
const rows = filtered.slice(0, limit);
|
|
2627
|
+
const lines = [
|
|
2628
|
+
"Memory Braid search",
|
|
2629
|
+
`- query: ${queryText}`,
|
|
2630
|
+
`- effectiveQuery: ${effectiveQuery}`,
|
|
2631
|
+
`- scope.workspaceHash: ${commandScope.workspaceHash}`,
|
|
2632
|
+
`- scope.agentId: ${commandScope.agentId}`,
|
|
2633
|
+
`- layer: ${layer}`,
|
|
2634
|
+
`- kind: ${kind ?? "all"}`,
|
|
2635
|
+
`- timeRange: ${formatTimeRange(timeRangeResult.range)}`,
|
|
2636
|
+
`- fetched: ${fetched.length}`,
|
|
2637
|
+
`- shown: ${rows.length}`,
|
|
2638
|
+
`- hiddenQuarantined: ${hiddenQuarantined}`,
|
|
2639
|
+
];
|
|
2640
|
+
if (rows.length === 0) {
|
|
2641
|
+
lines.push("", "No matching memories.");
|
|
2642
|
+
return { text: lines.join("\n") };
|
|
2643
|
+
}
|
|
2644
|
+
for (const [index, result] of rows.entries()) {
|
|
2645
|
+
const metadata = asRecord(result.metadata);
|
|
2646
|
+
const quarantine = isQuarantinedMemory(result, remediationState);
|
|
2647
|
+
const lifecycleEntry =
|
|
2648
|
+
result.id && lifecycle.entries[result.id] ? lifecycle.entries[result.id] : undefined;
|
|
2649
|
+
lines.push(
|
|
2650
|
+
"",
|
|
2651
|
+
`${index + 1}. [${result.score.toFixed(3)}] ${result.id ?? "unknown"} ${result.snippet}`,
|
|
2652
|
+
` sourceType=${metadata.sourceType ?? "n/a"} layer=${inferMemoryLayer(result)} owner=${inferMemoryOwner(result)} kind=${inferMemoryKind(result)} stability=${metadata.stability ?? "n/a"}`,
|
|
2653
|
+
` selection=decision:${metadata.selectionDecision ?? "n/a"} score:${typeof metadata.rememberabilityScore === "number" ? metadata.rememberabilityScore.toFixed(2) : "n/a"} reasons:${Array.isArray(metadata.rememberabilityReasons) ? metadata.rememberabilityReasons.join(", ") : "n/a"}`,
|
|
2654
|
+
` timestamps=eventAt:${metadata.eventAt ?? "n/a"} firstSeenAt:${metadata.firstSeenAt ?? "n/a"} indexedAt:${metadata.indexedAt ?? "n/a"} updatedAt:${metadata.updatedAt ?? "n/a"}`,
|
|
2655
|
+
` taxonomy=${summarizeResultTaxonomy(result)}`,
|
|
2656
|
+
` provenance=captureOrigin:${metadata.captureOrigin ?? "n/a"} capturePath:${metadata.capturePath ?? "n/a"} pluginCaptureVersion:${metadata.pluginCaptureVersion ?? "n/a"}`,
|
|
2657
|
+
` quarantine=${quarantine.quarantined ? `yes (${quarantine.reason ?? "n/a"})` : "no"}`,
|
|
2658
|
+
` lifecycle=recallCount:${lifecycleEntry?.recallCount ?? "n/a"} lastRecalledAt:${lifecycleEntry?.lastRecalledAt ? new Date(lifecycleEntry.lastRecalledAt).toISOString() : "n/a"}`,
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
text: lines.join("\n"),
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2012
2666
|
if (action === "audit" || action === "remediate") {
|
|
2013
2667
|
const subAction = action === "audit" ? "audit" : (tokens[1] ?? "audit").toLowerCase();
|
|
2014
2668
|
if (
|
|
@@ -2081,6 +2735,34 @@ const memoryBraidPlugin = {
|
|
|
2081
2735
|
};
|
|
2082
2736
|
}
|
|
2083
2737
|
|
|
2738
|
+
if (action === "consolidate") {
|
|
2739
|
+
const paths = await ensureRuntimeStatePaths();
|
|
2740
|
+
if (!paths) {
|
|
2741
|
+
return {
|
|
2742
|
+
text: "Consolidation unavailable: state directory is not ready.",
|
|
2743
|
+
isError: true,
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
const summary = await runConsolidationOnce({
|
|
2747
|
+
scope: resolveCommandPersistentScope(ctx.config),
|
|
2748
|
+
reason: "command",
|
|
2749
|
+
runtimeStatePaths: paths,
|
|
2750
|
+
runId: log.newRunId(),
|
|
2751
|
+
});
|
|
2752
|
+
return {
|
|
2753
|
+
text: [
|
|
2754
|
+
"Consolidation complete.",
|
|
2755
|
+
`- candidates: ${summary.candidates}`,
|
|
2756
|
+
`- clusters: ${summary.clusters}`,
|
|
2757
|
+
`- semanticCreated: ${summary.created}`,
|
|
2758
|
+
`- semanticUpdated: ${summary.updated}`,
|
|
2759
|
+
`- episodicMarked: ${summary.episodicMarked}`,
|
|
2760
|
+
`- contradictions: ${summary.contradictions}`,
|
|
2761
|
+
`- supersededMarked: ${summary.superseded}`,
|
|
2762
|
+
].join("\n"),
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2084
2766
|
if (action === "warmup") {
|
|
2085
2767
|
const runId = log.newRunId();
|
|
2086
2768
|
const forceReload = tokens.some((token) => token === "--force");
|
|
@@ -2114,7 +2796,7 @@ const memoryBraidPlugin = {
|
|
|
2114
2796
|
|
|
2115
2797
|
return {
|
|
2116
2798
|
text:
|
|
2117
|
-
"Usage: /memorybraid [status|stats|audit|remediate <audit|quarantine|delete|purge-all-captured> [--apply] [--limit N] [--sample N]|cleanup|warmup [--force]]",
|
|
2799
|
+
"Usage: /memorybraid [status|stats|search <query> [--limit N] [--layer episodic|semantic|procedural|all] [--kind fact|preference|decision|task|heuristic|lesson|strategy|other] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--include-quarantined]|consolidate|audit|remediate <audit|quarantine|delete|purge-all-captured> [--apply] [--limit N] [--sample N]|cleanup|warmup [--force]]",
|
|
2118
2800
|
};
|
|
2119
2801
|
},
|
|
2120
2802
|
});
|
|
@@ -2248,7 +2930,8 @@ const memoryBraidPlugin = {
|
|
|
2248
2930
|
return baseResult;
|
|
2249
2931
|
}
|
|
2250
2932
|
|
|
2251
|
-
const
|
|
2933
|
+
const latestUserTurnText = resolveLatestUserTurnText(event.messages);
|
|
2934
|
+
const recallQuery = sanitizeRecallQuery(latestUserTurnText ?? event.prompt);
|
|
2252
2935
|
if (!recallQuery) {
|
|
2253
2936
|
return baseResult;
|
|
2254
2937
|
}
|
|
@@ -2554,6 +3237,7 @@ const memoryBraidPlugin = {
|
|
|
2554
3237
|
let mem0AddWithId = 0;
|
|
2555
3238
|
let mem0AddWithoutId = 0;
|
|
2556
3239
|
let remoteQuarantineFiltered = 0;
|
|
3240
|
+
let selectionSkipped = 0;
|
|
2557
3241
|
const remediationState = await readRemediationState(runtimeStatePaths);
|
|
2558
3242
|
const successfulAdds: Array<{
|
|
2559
3243
|
memoryId: string;
|
|
@@ -2623,8 +3307,10 @@ const memoryBraidPlugin = {
|
|
|
2623
3307
|
continue;
|
|
2624
3308
|
}
|
|
2625
3309
|
|
|
3310
|
+
const indexedAt = new Date().toISOString();
|
|
2626
3311
|
const metadata: Record<string, unknown> = {
|
|
2627
3312
|
sourceType: "capture",
|
|
3313
|
+
memoryLayer: "episodic",
|
|
2628
3314
|
memoryOwner: "user",
|
|
2629
3315
|
memoryKind: mapCategoryToMemoryKind(candidate.category),
|
|
2630
3316
|
captureIntent: "observed",
|
|
@@ -2642,7 +3328,11 @@ const memoryBraidPlugin = {
|
|
|
2642
3328
|
capturePath: captureInput.capturePath,
|
|
2643
3329
|
pluginCaptureVersion: PLUGIN_CAPTURE_VERSION,
|
|
2644
3330
|
contentHash: hash,
|
|
2645
|
-
indexedAt
|
|
3331
|
+
indexedAt,
|
|
3332
|
+
eventAt: indexedAt,
|
|
3333
|
+
firstSeenAt: indexedAt,
|
|
3334
|
+
lastSeenAt: indexedAt,
|
|
3335
|
+
supportCount: 1,
|
|
2646
3336
|
};
|
|
2647
3337
|
|
|
2648
3338
|
if (cfg.entityExtraction.enabled) {
|
|
@@ -2657,6 +3347,50 @@ const memoryBraidPlugin = {
|
|
|
2657
3347
|
metadata.entities = entities;
|
|
2658
3348
|
}
|
|
2659
3349
|
}
|
|
3350
|
+
metadata.taxonomy = buildTaxonomy({
|
|
3351
|
+
text: candidate.text,
|
|
3352
|
+
entities: metadata.entities,
|
|
3353
|
+
existingTaxonomy: metadata.taxonomy,
|
|
3354
|
+
});
|
|
3355
|
+
metadata.taxonomySummary = formatTaxonomySummary(normalizeTaxonomy(metadata.taxonomy));
|
|
3356
|
+
const selection = scoreObservedMemory({
|
|
3357
|
+
text: candidate.text,
|
|
3358
|
+
kind: mapCategoryToMemoryKind(candidate.category),
|
|
3359
|
+
extractionScore: candidate.score,
|
|
3360
|
+
taxonomy: normalizeTaxonomy(metadata.taxonomy),
|
|
3361
|
+
source: candidate.source,
|
|
3362
|
+
cfg,
|
|
3363
|
+
});
|
|
3364
|
+
metadata.selectionDecision = selection.decision;
|
|
3365
|
+
metadata.rememberabilityScore = selection.score;
|
|
3366
|
+
metadata.rememberabilityReasons = selection.reasons;
|
|
3367
|
+
log.debug("memory_braid.capture.selection", {
|
|
3368
|
+
runId,
|
|
3369
|
+
target: "episodic",
|
|
3370
|
+
decision: selection.decision,
|
|
3371
|
+
kind: mapCategoryToMemoryKind(candidate.category),
|
|
3372
|
+
source: candidate.source,
|
|
3373
|
+
score: selection.score,
|
|
3374
|
+
reasons: selection.reasons,
|
|
3375
|
+
workspaceHash: scope.workspaceHash,
|
|
3376
|
+
agentId: scope.agentId,
|
|
3377
|
+
sessionKey: scope.sessionKey,
|
|
3378
|
+
contentHashPrefix: hash.slice(0, 12),
|
|
3379
|
+
});
|
|
3380
|
+
if (selection.decision !== "episodic") {
|
|
3381
|
+
selectionSkipped += 1;
|
|
3382
|
+
log.debug("memory_braid.capture.skip", {
|
|
3383
|
+
runId,
|
|
3384
|
+
reason: "selection_rejected",
|
|
3385
|
+
workspaceHash: scope.workspaceHash,
|
|
3386
|
+
agentId: scope.agentId,
|
|
3387
|
+
sessionKey: scope.sessionKey,
|
|
3388
|
+
category: candidate.category,
|
|
3389
|
+
score: selection.score,
|
|
3390
|
+
reasons: selection.reasons,
|
|
3391
|
+
});
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
2660
3394
|
|
|
2661
3395
|
const quarantine = isQuarantinedMemory(
|
|
2662
3396
|
{
|
|
@@ -2702,6 +3436,7 @@ const memoryBraidPlugin = {
|
|
|
2702
3436
|
|
|
2703
3437
|
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
2704
3438
|
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
3439
|
+
const consolidation = await readConsolidationState(runtimeStatePaths);
|
|
2705
3440
|
const stats = await readStatsState(runtimeStatePaths);
|
|
2706
3441
|
const lifecycle = cfg.lifecycle.enabled
|
|
2707
3442
|
? await readLifecycleState(runtimeStatePaths)
|
|
@@ -2752,11 +3487,14 @@ const memoryBraidPlugin = {
|
|
|
2752
3487
|
stats.capture.provenanceSkipped += provenanceSkipped;
|
|
2753
3488
|
stats.capture.transcriptShapeSkipped += transcriptShapeSkipped;
|
|
2754
3489
|
stats.capture.quarantinedFiltered += remoteQuarantineFiltered;
|
|
3490
|
+
stats.capture.selectionSkipped += selectionSkipped;
|
|
2755
3491
|
stats.capture.agentLearningAutoCaptured += agentLearningAutoCaptured;
|
|
2756
3492
|
stats.capture.agentLearningAutoRejected += agentLearningAutoRejected;
|
|
2757
3493
|
stats.capture.lastRunAt = new Date(now).toISOString();
|
|
3494
|
+
consolidation.newEpisodicSinceLastRun += persisted;
|
|
2758
3495
|
|
|
2759
3496
|
await writeCaptureDedupeState(runtimeStatePaths, dedupe);
|
|
3497
|
+
await writeConsolidationState(runtimeStatePaths, consolidation);
|
|
2760
3498
|
if (lifecycle) {
|
|
2761
3499
|
await writeLifecycleState(runtimeStatePaths, lifecycle);
|
|
2762
3500
|
}
|
|
@@ -2786,6 +3524,12 @@ const memoryBraidPlugin = {
|
|
|
2786
3524
|
agentLearningAutoRejected,
|
|
2787
3525
|
}, true);
|
|
2788
3526
|
});
|
|
3527
|
+
|
|
3528
|
+
await maybeRunOpportunisticConsolidation({
|
|
3529
|
+
scope: persistentScope,
|
|
3530
|
+
runtimeStatePaths,
|
|
3531
|
+
runId,
|
|
3532
|
+
});
|
|
2789
3533
|
});
|
|
2790
3534
|
|
|
2791
3535
|
api.registerService({
|
|
@@ -2827,6 +3571,12 @@ const memoryBraidPlugin = {
|
|
|
2827
3571
|
lifecycleCaptureTtlDays: cfg.lifecycle.captureTtlDays,
|
|
2828
3572
|
lifecycleCleanupIntervalMinutes: cfg.lifecycle.cleanupIntervalMinutes,
|
|
2829
3573
|
lifecycleReinforceOnRecall: cfg.lifecycle.reinforceOnRecall,
|
|
3574
|
+
consolidationEnabled: cfg.consolidation.enabled,
|
|
3575
|
+
consolidationStartupRun: cfg.consolidation.startupRun,
|
|
3576
|
+
consolidationIntervalMinutes: cfg.consolidation.intervalMinutes,
|
|
3577
|
+
consolidationMinSupportCount: cfg.consolidation.minSupportCount,
|
|
3578
|
+
consolidationMinRecallCount: cfg.consolidation.minRecallCount,
|
|
3579
|
+
consolidationTimeQueryParsing: cfg.consolidation.timeQueryParsing,
|
|
2830
3580
|
entityExtractionEnabled: cfg.entityExtraction.enabled,
|
|
2831
3581
|
entityProvider: cfg.entityExtraction.provider,
|
|
2832
3582
|
entityModel: cfg.entityExtraction.model,
|
|
@@ -2885,12 +3635,48 @@ const memoryBraidPlugin = {
|
|
|
2885
3635
|
});
|
|
2886
3636
|
}, intervalMs);
|
|
2887
3637
|
}
|
|
3638
|
+
|
|
3639
|
+
if (cfg.consolidation.enabled && cfg.consolidation.startupRun) {
|
|
3640
|
+
void runConsolidationOnce({
|
|
3641
|
+
scope: resolveCommandPersistentScope(api.config),
|
|
3642
|
+
reason: "startup",
|
|
3643
|
+
runtimeStatePaths: statePaths,
|
|
3644
|
+
runId,
|
|
3645
|
+
}).catch((err) => {
|
|
3646
|
+
log.warn("memory_braid.consolidation.run", {
|
|
3647
|
+
runId,
|
|
3648
|
+
reason: "startup",
|
|
3649
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3650
|
+
});
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
if (cfg.consolidation.enabled) {
|
|
3655
|
+
const intervalMs = cfg.consolidation.intervalMinutes * 60 * 1000;
|
|
3656
|
+
consolidationTimer = setInterval(() => {
|
|
3657
|
+
void runConsolidationOnce({
|
|
3658
|
+
scope: resolveCommandPersistentScope(api.config),
|
|
3659
|
+
reason: "interval",
|
|
3660
|
+
runtimeStatePaths: statePaths!,
|
|
3661
|
+
runId: log.newRunId(),
|
|
3662
|
+
}).catch((err) => {
|
|
3663
|
+
log.warn("memory_braid.consolidation.run", {
|
|
3664
|
+
reason: "interval",
|
|
3665
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3666
|
+
});
|
|
3667
|
+
});
|
|
3668
|
+
}, intervalMs);
|
|
3669
|
+
}
|
|
2888
3670
|
},
|
|
2889
3671
|
stop: async () => {
|
|
2890
3672
|
if (lifecycleTimer) {
|
|
2891
3673
|
clearInterval(lifecycleTimer);
|
|
2892
3674
|
lifecycleTimer = null;
|
|
2893
3675
|
}
|
|
3676
|
+
if (consolidationTimer) {
|
|
3677
|
+
clearInterval(consolidationTimer);
|
|
3678
|
+
consolidationTimer = null;
|
|
3679
|
+
}
|
|
2894
3680
|
},
|
|
2895
3681
|
});
|
|
2896
3682
|
},
|