memory-braid 0.3.6 → 0.4.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 +56 -27
- package/openclaw.plugin.json +24 -24
- package/package.json +2 -2
- package/src/chunking.ts +0 -267
- package/src/config.ts +45 -57
- package/src/dedupe.ts +4 -4
- package/src/extract.ts +10 -2
- package/src/index.ts +678 -134
- package/src/mem0-client.ts +91 -5
- package/src/state.ts +55 -28
- package/src/types.ts +34 -52
- package/src/bootstrap.ts +0 -82
- package/src/reconcile.ts +0 -278
package/src/index.ts
CHANGED
|
@@ -11,17 +11,20 @@ import { MemoryBraidLogger } from "./logger.js";
|
|
|
11
11
|
import { resolveLocalTools, runLocalGet, runLocalSearch } from "./local-memory.js";
|
|
12
12
|
import { Mem0Adapter } from "./mem0-client.js";
|
|
13
13
|
import { mergeWithRrf } from "./merge.js";
|
|
14
|
-
import { resolveTargets, runReconcileOnce } from "./reconcile.js";
|
|
15
14
|
import {
|
|
16
15
|
createStatePaths,
|
|
17
16
|
ensureStateDir,
|
|
18
17
|
readCaptureDedupeState,
|
|
18
|
+
readLifecycleState,
|
|
19
|
+
readStatsState,
|
|
19
20
|
type StatePaths,
|
|
21
|
+
withStateLock,
|
|
20
22
|
writeCaptureDedupeState,
|
|
23
|
+
writeLifecycleState,
|
|
24
|
+
writeStatsState,
|
|
21
25
|
} from "./state.js";
|
|
22
|
-
import type { MemoryBraidResult, ScopeKey
|
|
26
|
+
import type { LifecycleEntry, MemoryBraidResult, ScopeKey } from "./types.js";
|
|
23
27
|
import { normalizeForHash, sha256 } from "./chunking.js";
|
|
24
|
-
import { runBootstrapIfNeeded } from "./bootstrap.js";
|
|
25
28
|
|
|
26
29
|
function jsonToolResult(payload: unknown) {
|
|
27
30
|
return {
|
|
@@ -95,12 +98,341 @@ function formatEntityExtractionStatus(params: {
|
|
|
95
98
|
].join("\n");
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
return value as Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveCoreTemporalDecay(params: {
|
|
109
|
+
config?: unknown;
|
|
110
|
+
agentId?: string;
|
|
111
|
+
}): { enabled: boolean; halfLifeDays: number } {
|
|
112
|
+
const root = asRecord(params.config);
|
|
113
|
+
const agents = asRecord(root.agents);
|
|
114
|
+
const defaults = asRecord(agents.defaults);
|
|
115
|
+
const defaultMemorySearch = asRecord(defaults.memorySearch);
|
|
116
|
+
const defaultTemporalDecay = asRecord(asRecord(asRecord(defaultMemorySearch.query).hybrid).temporalDecay);
|
|
117
|
+
|
|
118
|
+
const requestedAgent = (params.agentId ?? "").trim().toLowerCase();
|
|
119
|
+
let agentTemporalDecay: Record<string, unknown> = {};
|
|
120
|
+
if (requestedAgent) {
|
|
121
|
+
const agentList = Array.isArray(agents.list) ? agents.list : [];
|
|
122
|
+
for (const entry of agentList) {
|
|
123
|
+
const row = asRecord(entry);
|
|
124
|
+
const rowAgentId = typeof row.id === "string" ? row.id.trim().toLowerCase() : "";
|
|
125
|
+
if (!rowAgentId || rowAgentId !== requestedAgent) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const memorySearch = asRecord(row.memorySearch);
|
|
129
|
+
agentTemporalDecay = asRecord(asRecord(asRecord(memorySearch.query).hybrid).temporalDecay);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const enabledRaw =
|
|
135
|
+
typeof agentTemporalDecay.enabled === "boolean"
|
|
136
|
+
? agentTemporalDecay.enabled
|
|
137
|
+
: typeof defaultTemporalDecay.enabled === "boolean"
|
|
138
|
+
? defaultTemporalDecay.enabled
|
|
139
|
+
: false;
|
|
140
|
+
const halfLifeRaw =
|
|
141
|
+
typeof agentTemporalDecay.halfLifeDays === "number"
|
|
142
|
+
? agentTemporalDecay.halfLifeDays
|
|
143
|
+
: typeof defaultTemporalDecay.halfLifeDays === "number"
|
|
144
|
+
? defaultTemporalDecay.halfLifeDays
|
|
145
|
+
: 30;
|
|
146
|
+
const halfLifeDays = Math.max(1, Math.min(3650, Math.round(halfLifeRaw)));
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
enabled: enabledRaw,
|
|
150
|
+
halfLifeDays,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveDateFromPath(pathValue?: string): number | undefined {
|
|
155
|
+
if (!pathValue) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const match = /(?:^|[/\\])memory[/\\](\d{4})-(\d{2})-(\d{2})\.md$/i.exec(pathValue);
|
|
159
|
+
if (!match) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const [, yearRaw, monthRaw, dayRaw] = match;
|
|
163
|
+
const year = Number(yearRaw);
|
|
164
|
+
const month = Number(monthRaw);
|
|
165
|
+
const day = Number(dayRaw);
|
|
166
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const parsed = new Date(year, month - 1, day).getTime();
|
|
170
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveTimestampMs(result: MemoryBraidResult): number | undefined {
|
|
174
|
+
const metadata = asRecord(result.metadata);
|
|
175
|
+
const fields = [
|
|
176
|
+
metadata.indexedAt,
|
|
177
|
+
metadata.updatedAt,
|
|
178
|
+
metadata.createdAt,
|
|
179
|
+
metadata.timestamp,
|
|
180
|
+
metadata.lastSeenAt,
|
|
181
|
+
];
|
|
182
|
+
for (const value of fields) {
|
|
183
|
+
if (typeof value === "string") {
|
|
184
|
+
const parsed = Date.parse(value);
|
|
185
|
+
if (Number.isFinite(parsed)) {
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
190
|
+
return value > 1e12 ? value : value * 1000;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return resolveDateFromPath(result.path);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function applyTemporalDecayToMem0(params: {
|
|
197
|
+
results: MemoryBraidResult[];
|
|
198
|
+
halfLifeDays: number;
|
|
199
|
+
nowMs: number;
|
|
200
|
+
}): { results: MemoryBraidResult[]; decayed: number; missingTimestamp: number } {
|
|
201
|
+
if (params.results.length === 0) {
|
|
202
|
+
return {
|
|
203
|
+
results: params.results,
|
|
204
|
+
decayed: 0,
|
|
205
|
+
missingTimestamp: 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lambda = Math.LN2 / Math.max(1, params.halfLifeDays);
|
|
210
|
+
let decayed = 0;
|
|
211
|
+
let missingTimestamp = 0;
|
|
212
|
+
const out = params.results.map((result, index) => {
|
|
213
|
+
const ts = resolveTimestampMs(result);
|
|
214
|
+
if (!ts) {
|
|
215
|
+
missingTimestamp += 1;
|
|
216
|
+
return { result, index };
|
|
217
|
+
}
|
|
218
|
+
const ageDays = Math.max(0, (params.nowMs - ts) / (24 * 60 * 60 * 1000));
|
|
219
|
+
const decay = Math.exp(-lambda * ageDays);
|
|
220
|
+
decayed += 1;
|
|
221
|
+
return {
|
|
222
|
+
index,
|
|
223
|
+
result: {
|
|
224
|
+
...result,
|
|
225
|
+
score: result.score * decay,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
out.sort((left, right) => {
|
|
231
|
+
const scoreDelta = right.result.score - left.result.score;
|
|
232
|
+
if (scoreDelta !== 0) {
|
|
233
|
+
return scoreDelta;
|
|
234
|
+
}
|
|
235
|
+
return left.index - right.index;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
results: out.map((entry) => entry.result),
|
|
240
|
+
decayed,
|
|
241
|
+
missingTimestamp,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveLifecycleReferenceTs(entry: LifecycleEntry, reinforceOnRecall: boolean): number {
|
|
246
|
+
const capturedTs = Number.isFinite(entry.lastCapturedAt)
|
|
247
|
+
? entry.lastCapturedAt
|
|
248
|
+
: Number.isFinite(entry.createdAt)
|
|
249
|
+
? entry.createdAt
|
|
250
|
+
: 0;
|
|
251
|
+
if (!reinforceOnRecall) {
|
|
252
|
+
return capturedTs;
|
|
253
|
+
}
|
|
254
|
+
const recalledTs = Number.isFinite(entry.lastRecalledAt) ? entry.lastRecalledAt : 0;
|
|
255
|
+
return Math.max(capturedTs, recalledTs);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function reinforceLifecycleEntries(params: {
|
|
259
|
+
cfg: ReturnType<typeof parseConfig>;
|
|
260
|
+
log: MemoryBraidLogger;
|
|
261
|
+
statePaths: StatePaths;
|
|
262
|
+
runId: string;
|
|
263
|
+
scope: ScopeKey;
|
|
264
|
+
results: MemoryBraidResult[];
|
|
265
|
+
}): Promise<void> {
|
|
266
|
+
if (!params.cfg.lifecycle.enabled || !params.cfg.lifecycle.reinforceOnRecall) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const memoryIds = Array.from(
|
|
271
|
+
new Set(
|
|
272
|
+
params.results
|
|
273
|
+
.filter((result) => result.source === "mem0" && typeof result.id === "string" && result.id)
|
|
274
|
+
.map((result) => result.id as string),
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
if (memoryIds.length === 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const updatedIds = await withStateLock(params.statePaths.stateLockFile, async () => {
|
|
283
|
+
const lifecycle = await readLifecycleState(params.statePaths);
|
|
284
|
+
const touched: string[] = [];
|
|
285
|
+
|
|
286
|
+
for (const memoryId of memoryIds) {
|
|
287
|
+
const entry = lifecycle.entries[memoryId];
|
|
288
|
+
if (!entry) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
lifecycle.entries[memoryId] = {
|
|
292
|
+
...entry,
|
|
293
|
+
recallCount: Math.max(0, entry.recallCount ?? 0) + 1,
|
|
294
|
+
lastRecalledAt: now,
|
|
295
|
+
updatedAt: now,
|
|
296
|
+
};
|
|
297
|
+
touched.push(memoryId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (touched.length > 0) {
|
|
301
|
+
await writeLifecycleState(params.statePaths, lifecycle);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return touched;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (updatedIds.length === 0) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
params.log.debug("memory_braid.lifecycle.reinforce", {
|
|
312
|
+
runId: params.runId,
|
|
313
|
+
workspaceHash: params.scope.workspaceHash,
|
|
314
|
+
agentId: params.scope.agentId,
|
|
315
|
+
sessionKey: params.scope.sessionKey,
|
|
316
|
+
matchedResults: memoryIds.length,
|
|
317
|
+
reinforced: updatedIds.length,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function runLifecycleCleanupOnce(params: {
|
|
322
|
+
cfg: ReturnType<typeof parseConfig>;
|
|
323
|
+
mem0: Mem0Adapter;
|
|
324
|
+
log: MemoryBraidLogger;
|
|
325
|
+
statePaths: StatePaths;
|
|
326
|
+
reason: "startup" | "interval" | "command";
|
|
327
|
+
runId?: string;
|
|
328
|
+
}): Promise<{ scanned: number; expired: number; deleted: number; failed: number }> {
|
|
329
|
+
if (!params.cfg.lifecycle.enabled) {
|
|
330
|
+
return {
|
|
331
|
+
scanned: 0,
|
|
332
|
+
expired: 0,
|
|
333
|
+
deleted: 0,
|
|
334
|
+
failed: 0,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
const ttlMs = params.cfg.lifecycle.captureTtlDays * 24 * 60 * 60 * 1000;
|
|
340
|
+
const expiredCandidates = await withStateLock(params.statePaths.stateLockFile, async () => {
|
|
341
|
+
const lifecycle = await readLifecycleState(params.statePaths);
|
|
342
|
+
const expired: Array<{ memoryId: string; scope: ScopeKey }> = [];
|
|
343
|
+
const malformedIds: string[] = [];
|
|
344
|
+
|
|
345
|
+
for (const [memoryId, entry] of Object.entries(lifecycle.entries)) {
|
|
346
|
+
if (!memoryId || !entry.workspaceHash || !entry.agentId) {
|
|
347
|
+
malformedIds.push(memoryId);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const referenceTs = resolveLifecycleReferenceTs(entry, params.cfg.lifecycle.reinforceOnRecall);
|
|
351
|
+
if (!Number.isFinite(referenceTs) || referenceTs <= 0) {
|
|
352
|
+
malformedIds.push(memoryId);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (now - referenceTs < ttlMs) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
expired.push({
|
|
359
|
+
memoryId,
|
|
360
|
+
scope: {
|
|
361
|
+
workspaceHash: entry.workspaceHash,
|
|
362
|
+
agentId: entry.agentId,
|
|
363
|
+
sessionKey: entry.sessionKey,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const memoryId of malformedIds) {
|
|
369
|
+
delete lifecycle.entries[memoryId];
|
|
370
|
+
}
|
|
371
|
+
if (malformedIds.length > 0) {
|
|
372
|
+
await writeLifecycleState(params.statePaths, lifecycle);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
scanned: Object.keys(lifecycle.entries).length + malformedIds.length,
|
|
377
|
+
expired,
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
let deleted = 0;
|
|
382
|
+
let failed = 0;
|
|
383
|
+
const deletedIds = new Set<string>();
|
|
384
|
+
for (const candidate of expiredCandidates.expired) {
|
|
385
|
+
const ok = await params.mem0.deleteMemory({
|
|
386
|
+
memoryId: candidate.memoryId,
|
|
387
|
+
scope: candidate.scope,
|
|
388
|
+
runId: params.runId,
|
|
389
|
+
});
|
|
390
|
+
if (ok) {
|
|
391
|
+
deleted += 1;
|
|
392
|
+
deletedIds.add(candidate.memoryId);
|
|
393
|
+
} else {
|
|
394
|
+
failed += 1;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await withStateLock(params.statePaths.stateLockFile, async () => {
|
|
399
|
+
const lifecycle = await readLifecycleState(params.statePaths);
|
|
400
|
+
for (const memoryId of deletedIds) {
|
|
401
|
+
delete lifecycle.entries[memoryId];
|
|
402
|
+
}
|
|
403
|
+
lifecycle.lastCleanupAt = new Date(now).toISOString();
|
|
404
|
+
lifecycle.lastCleanupReason = params.reason;
|
|
405
|
+
lifecycle.lastCleanupScanned = expiredCandidates.scanned;
|
|
406
|
+
lifecycle.lastCleanupExpired = expiredCandidates.expired.length;
|
|
407
|
+
lifecycle.lastCleanupDeleted = deleted;
|
|
408
|
+
lifecycle.lastCleanupFailed = failed;
|
|
409
|
+
await writeLifecycleState(params.statePaths, lifecycle);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
params.log.debug("memory_braid.lifecycle.cleanup", {
|
|
413
|
+
runId: params.runId,
|
|
414
|
+
reason: params.reason,
|
|
415
|
+
scanned: expiredCandidates.scanned,
|
|
416
|
+
expired: expiredCandidates.expired.length,
|
|
417
|
+
deleted,
|
|
418
|
+
failed,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
scanned: expiredCandidates.scanned,
|
|
423
|
+
expired: expiredCandidates.expired.length,
|
|
424
|
+
deleted,
|
|
425
|
+
failed,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
98
429
|
async function runHybridRecall(params: {
|
|
99
430
|
api: OpenClawPluginApi;
|
|
100
431
|
cfg: ReturnType<typeof parseConfig>;
|
|
101
432
|
mem0: Mem0Adapter;
|
|
102
433
|
log: MemoryBraidLogger;
|
|
103
434
|
ctx: OpenClawPluginToolContext;
|
|
435
|
+
statePaths?: StatePaths | null;
|
|
104
436
|
query: string;
|
|
105
437
|
toolCallId?: string;
|
|
106
438
|
args?: Record<string, unknown>;
|
|
@@ -151,24 +483,63 @@ async function runHybridRecall(params: {
|
|
|
151
483
|
|
|
152
484
|
const scope = resolveScopeFromToolContext(params.ctx);
|
|
153
485
|
const mem0Started = Date.now();
|
|
154
|
-
const
|
|
486
|
+
const mem0Raw = await params.mem0.searchMemories({
|
|
155
487
|
query: params.query,
|
|
156
488
|
maxResults,
|
|
157
489
|
scope,
|
|
158
490
|
runId: params.runId,
|
|
159
491
|
});
|
|
492
|
+
const mem0Search = mem0Raw.filter((result) => {
|
|
493
|
+
const sourceType = asRecord(result.metadata).sourceType;
|
|
494
|
+
return sourceType !== "markdown" && sourceType !== "session";
|
|
495
|
+
});
|
|
496
|
+
let mem0ForMerge = mem0Search;
|
|
497
|
+
if (params.cfg.timeDecay.enabled) {
|
|
498
|
+
const coreDecay = resolveCoreTemporalDecay({
|
|
499
|
+
config: params.ctx.config,
|
|
500
|
+
agentId: params.ctx.agentId,
|
|
501
|
+
});
|
|
502
|
+
if (coreDecay.enabled) {
|
|
503
|
+
const decayed = applyTemporalDecayToMem0({
|
|
504
|
+
results: mem0Search,
|
|
505
|
+
halfLifeDays: coreDecay.halfLifeDays,
|
|
506
|
+
nowMs: Date.now(),
|
|
507
|
+
});
|
|
508
|
+
mem0ForMerge = decayed.results;
|
|
509
|
+
params.log.debug("memory_braid.search.mem0_decay", {
|
|
510
|
+
runId: params.runId,
|
|
511
|
+
agentId: scope.agentId,
|
|
512
|
+
sessionKey: scope.sessionKey,
|
|
513
|
+
workspaceHash: scope.workspaceHash,
|
|
514
|
+
enabled: true,
|
|
515
|
+
halfLifeDays: coreDecay.halfLifeDays,
|
|
516
|
+
inputCount: mem0Search.length,
|
|
517
|
+
decayed: decayed.decayed,
|
|
518
|
+
missingTimestamp: decayed.missingTimestamp,
|
|
519
|
+
});
|
|
520
|
+
} else {
|
|
521
|
+
params.log.debug("memory_braid.search.mem0_decay", {
|
|
522
|
+
runId: params.runId,
|
|
523
|
+
agentId: scope.agentId,
|
|
524
|
+
sessionKey: scope.sessionKey,
|
|
525
|
+
workspaceHash: scope.workspaceHash,
|
|
526
|
+
enabled: false,
|
|
527
|
+
reason: "memory_core_temporal_decay_disabled",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
160
531
|
params.log.debug("memory_braid.search.mem0", {
|
|
161
532
|
runId: params.runId,
|
|
162
533
|
agentId: scope.agentId,
|
|
163
534
|
sessionKey: scope.sessionKey,
|
|
164
535
|
workspaceHash: scope.workspaceHash,
|
|
165
|
-
count:
|
|
536
|
+
count: mem0ForMerge.length,
|
|
166
537
|
durMs: Date.now() - mem0Started,
|
|
167
538
|
});
|
|
168
539
|
|
|
169
540
|
const merged = mergeWithRrf({
|
|
170
541
|
local: localSearch.results,
|
|
171
|
-
mem0:
|
|
542
|
+
mem0: mem0ForMerge,
|
|
172
543
|
options: {
|
|
173
544
|
rrfK: params.cfg.recall.merge.rrfK,
|
|
174
545
|
localWeight: params.cfg.recall.merge.localWeight,
|
|
@@ -193,22 +564,34 @@ async function runHybridRecall(params: {
|
|
|
193
564
|
runId: params.runId,
|
|
194
565
|
workspaceHash: scope.workspaceHash,
|
|
195
566
|
localCount: localSearch.results.length,
|
|
196
|
-
mem0Count:
|
|
567
|
+
mem0Count: mem0ForMerge.length,
|
|
197
568
|
mergedCount: merged.length,
|
|
198
569
|
dedupedCount: deduped.length,
|
|
199
570
|
});
|
|
200
571
|
|
|
572
|
+
const topMerged = deduped.slice(0, maxResults);
|
|
573
|
+
if (params.statePaths) {
|
|
574
|
+
await reinforceLifecycleEntries({
|
|
575
|
+
cfg: params.cfg,
|
|
576
|
+
log: params.log,
|
|
577
|
+
statePaths: params.statePaths,
|
|
578
|
+
runId: params.runId,
|
|
579
|
+
scope,
|
|
580
|
+
results: topMerged,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
201
584
|
return {
|
|
202
585
|
local: localSearch.results,
|
|
203
|
-
mem0:
|
|
204
|
-
merged:
|
|
586
|
+
mem0: mem0ForMerge,
|
|
587
|
+
merged: topMerged,
|
|
205
588
|
};
|
|
206
589
|
}
|
|
207
590
|
|
|
208
591
|
const memoryBraidPlugin = {
|
|
209
592
|
id: "memory-braid",
|
|
210
593
|
name: "Memory Braid",
|
|
211
|
-
description: "Hybrid memory plugin with local + Mem0 recall
|
|
594
|
+
description: "Hybrid memory plugin with local + Mem0 recall and capture.",
|
|
212
595
|
kind: "memory" as const,
|
|
213
596
|
configSchema: pluginConfigSchema,
|
|
214
597
|
|
|
@@ -221,9 +604,29 @@ const memoryBraidPlugin = {
|
|
|
221
604
|
stateDir: initialStateDir,
|
|
222
605
|
});
|
|
223
606
|
|
|
224
|
-
let
|
|
607
|
+
let lifecycleTimer: NodeJS.Timeout | null = null;
|
|
225
608
|
let statePaths: StatePaths | null = null;
|
|
226
|
-
|
|
609
|
+
|
|
610
|
+
async function ensureRuntimeStatePaths(): Promise<StatePaths | null> {
|
|
611
|
+
if (statePaths) {
|
|
612
|
+
return statePaths;
|
|
613
|
+
}
|
|
614
|
+
const resolvedStateDir = api.runtime.state.resolveStateDir();
|
|
615
|
+
if (!resolvedStateDir) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const next = createStatePaths(resolvedStateDir);
|
|
620
|
+
try {
|
|
621
|
+
await ensureStateDir(next);
|
|
622
|
+
statePaths = next;
|
|
623
|
+
mem0.setStateDir(resolvedStateDir);
|
|
624
|
+
entityExtraction.setStateDir(resolvedStateDir);
|
|
625
|
+
return statePaths;
|
|
626
|
+
} catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
227
630
|
|
|
228
631
|
api.registerTool(
|
|
229
632
|
(ctx) => {
|
|
@@ -259,12 +662,14 @@ const memoryBraidPlugin = {
|
|
|
259
662
|
});
|
|
260
663
|
}
|
|
261
664
|
|
|
665
|
+
const runtimeStatePaths = await ensureRuntimeStatePaths();
|
|
262
666
|
const recall = await runHybridRecall({
|
|
263
667
|
api,
|
|
264
668
|
cfg,
|
|
265
669
|
mem0,
|
|
266
670
|
log,
|
|
267
671
|
ctx,
|
|
672
|
+
statePaths: runtimeStatePaths,
|
|
268
673
|
query,
|
|
269
674
|
toolCallId,
|
|
270
675
|
args,
|
|
@@ -320,7 +725,7 @@ const memoryBraidPlugin = {
|
|
|
320
725
|
|
|
321
726
|
api.registerCommand({
|
|
322
727
|
name: "memorybraid",
|
|
323
|
-
description: "Memory Braid status and entity extraction warmup.",
|
|
728
|
+
description: "Memory Braid status, stats, lifecycle cleanup, and entity extraction warmup.",
|
|
324
729
|
acceptsArgs: true,
|
|
325
730
|
handler: async (ctx) => {
|
|
326
731
|
const args = ctx.args?.trim() ?? "";
|
|
@@ -328,14 +733,124 @@ const memoryBraidPlugin = {
|
|
|
328
733
|
const action = (tokens[0] ?? "status").toLowerCase();
|
|
329
734
|
|
|
330
735
|
if (action === "status") {
|
|
736
|
+
const coreDecay = resolveCoreTemporalDecay({
|
|
737
|
+
config: ctx.config,
|
|
738
|
+
});
|
|
739
|
+
const paths = await ensureRuntimeStatePaths();
|
|
740
|
+
const lifecycle =
|
|
741
|
+
cfg.lifecycle.enabled && paths
|
|
742
|
+
? await readLifecycleState(paths)
|
|
743
|
+
: { entries: {}, lastCleanupAt: undefined, lastCleanupReason: undefined };
|
|
331
744
|
return {
|
|
332
745
|
text: [
|
|
333
746
|
`capture.mode: ${cfg.capture.mode}`,
|
|
747
|
+
`capture.includeAssistant: ${cfg.capture.includeAssistant}`,
|
|
748
|
+
`timeDecay.enabled: ${cfg.timeDecay.enabled}`,
|
|
749
|
+
`memoryCore.temporalDecay.enabled: ${coreDecay.enabled}`,
|
|
750
|
+
`memoryCore.temporalDecay.halfLifeDays: ${coreDecay.halfLifeDays}`,
|
|
751
|
+
`lifecycle.enabled: ${cfg.lifecycle.enabled}`,
|
|
752
|
+
`lifecycle.captureTtlDays: ${cfg.lifecycle.captureTtlDays}`,
|
|
753
|
+
`lifecycle.cleanupIntervalMinutes: ${cfg.lifecycle.cleanupIntervalMinutes}`,
|
|
754
|
+
`lifecycle.reinforceOnRecall: ${cfg.lifecycle.reinforceOnRecall}`,
|
|
755
|
+
`lifecycle.tracked: ${Object.keys(lifecycle.entries).length}`,
|
|
756
|
+
`lifecycle.lastCleanupAt: ${lifecycle.lastCleanupAt ?? "n/a"}`,
|
|
757
|
+
`lifecycle.lastCleanupReason: ${lifecycle.lastCleanupReason ?? "n/a"}`,
|
|
334
758
|
formatEntityExtractionStatus(entityExtraction.getStatus()),
|
|
335
759
|
].join("\n\n"),
|
|
336
760
|
};
|
|
337
761
|
}
|
|
338
762
|
|
|
763
|
+
if (action === "stats") {
|
|
764
|
+
const paths = await ensureRuntimeStatePaths();
|
|
765
|
+
if (!paths) {
|
|
766
|
+
return {
|
|
767
|
+
text: "Stats unavailable: state directory is not ready.",
|
|
768
|
+
isError: true,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const stats = await readStatsState(paths);
|
|
773
|
+
const lifecycle = await readLifecycleState(paths);
|
|
774
|
+
const capture = stats.capture;
|
|
775
|
+
const mem0SuccessRate =
|
|
776
|
+
capture.mem0AddAttempts > 0
|
|
777
|
+
? `${((capture.mem0AddWithId / capture.mem0AddAttempts) * 100).toFixed(1)}%`
|
|
778
|
+
: "n/a";
|
|
779
|
+
const mem0NoIdRate =
|
|
780
|
+
capture.mem0AddAttempts > 0
|
|
781
|
+
? `${((capture.mem0AddWithoutId / capture.mem0AddAttempts) * 100).toFixed(1)}%`
|
|
782
|
+
: "n/a";
|
|
783
|
+
const dedupeSkipRate =
|
|
784
|
+
capture.candidates > 0
|
|
785
|
+
? `${((capture.dedupeSkipped / capture.candidates) * 100).toFixed(1)}%`
|
|
786
|
+
: "n/a";
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
text: [
|
|
790
|
+
"Memory Braid stats",
|
|
791
|
+
"",
|
|
792
|
+
"Capture:",
|
|
793
|
+
`- runs: ${capture.runs}`,
|
|
794
|
+
`- runsWithCandidates: ${capture.runsWithCandidates}`,
|
|
795
|
+
`- runsNoCandidates: ${capture.runsNoCandidates}`,
|
|
796
|
+
`- candidates: ${capture.candidates}`,
|
|
797
|
+
`- dedupeSkipped: ${capture.dedupeSkipped} (${dedupeSkipRate})`,
|
|
798
|
+
`- persisted: ${capture.persisted}`,
|
|
799
|
+
`- mem0AddAttempts: ${capture.mem0AddAttempts}`,
|
|
800
|
+
`- mem0AddWithId: ${capture.mem0AddWithId} (${mem0SuccessRate})`,
|
|
801
|
+
`- mem0AddWithoutId: ${capture.mem0AddWithoutId} (${mem0NoIdRate})`,
|
|
802
|
+
`- lastRunAt: ${capture.lastRunAt ?? "n/a"}`,
|
|
803
|
+
"",
|
|
804
|
+
"Lifecycle:",
|
|
805
|
+
`- enabled: ${cfg.lifecycle.enabled}`,
|
|
806
|
+
`- tracked: ${Object.keys(lifecycle.entries).length}`,
|
|
807
|
+
`- captureTtlDays: ${cfg.lifecycle.captureTtlDays}`,
|
|
808
|
+
`- cleanupIntervalMinutes: ${cfg.lifecycle.cleanupIntervalMinutes}`,
|
|
809
|
+
`- reinforceOnRecall: ${cfg.lifecycle.reinforceOnRecall}`,
|
|
810
|
+
`- lastCleanupAt: ${lifecycle.lastCleanupAt ?? "n/a"}`,
|
|
811
|
+
`- lastCleanupReason: ${lifecycle.lastCleanupReason ?? "n/a"}`,
|
|
812
|
+
`- lastCleanupScanned: ${lifecycle.lastCleanupScanned ?? "n/a"}`,
|
|
813
|
+
`- lastCleanupExpired: ${lifecycle.lastCleanupExpired ?? "n/a"}`,
|
|
814
|
+
`- lastCleanupDeleted: ${lifecycle.lastCleanupDeleted ?? "n/a"}`,
|
|
815
|
+
`- lastCleanupFailed: ${lifecycle.lastCleanupFailed ?? "n/a"}`,
|
|
816
|
+
].join("\n"),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (action === "cleanup") {
|
|
821
|
+
if (!cfg.lifecycle.enabled) {
|
|
822
|
+
return {
|
|
823
|
+
text: "Lifecycle cleanup skipped: lifecycle.enabled is false.",
|
|
824
|
+
isError: true,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const paths = await ensureRuntimeStatePaths();
|
|
828
|
+
if (!paths) {
|
|
829
|
+
return {
|
|
830
|
+
text: "Cleanup unavailable: state directory is not ready.",
|
|
831
|
+
isError: true,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
const runId = log.newRunId();
|
|
835
|
+
const summary = await runLifecycleCleanupOnce({
|
|
836
|
+
cfg,
|
|
837
|
+
mem0,
|
|
838
|
+
log,
|
|
839
|
+
statePaths: paths,
|
|
840
|
+
reason: "command",
|
|
841
|
+
runId,
|
|
842
|
+
});
|
|
843
|
+
return {
|
|
844
|
+
text: [
|
|
845
|
+
"Lifecycle cleanup complete.",
|
|
846
|
+
`- scanned: ${summary.scanned}`,
|
|
847
|
+
`- expired: ${summary.expired}`,
|
|
848
|
+
`- deleted: ${summary.deleted}`,
|
|
849
|
+
`- failed: ${summary.failed}`,
|
|
850
|
+
].join("\n"),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
339
854
|
if (action === "warmup") {
|
|
340
855
|
const runId = log.newRunId();
|
|
341
856
|
const forceReload = tokens.some((token) => token === "--force");
|
|
@@ -368,7 +883,7 @@ const memoryBraidPlugin = {
|
|
|
368
883
|
}
|
|
369
884
|
|
|
370
885
|
return {
|
|
371
|
-
text: "Usage: /memorybraid [status|warmup [--force]]",
|
|
886
|
+
text: "Usage: /memorybraid [status|stats|cleanup|warmup [--force]]",
|
|
372
887
|
};
|
|
373
888
|
},
|
|
374
889
|
});
|
|
@@ -381,6 +896,7 @@ const memoryBraidPlugin = {
|
|
|
381
896
|
agentId: ctx.agentId,
|
|
382
897
|
sessionKey: ctx.sessionKey,
|
|
383
898
|
};
|
|
899
|
+
const runtimeStatePaths = await ensureRuntimeStatePaths();
|
|
384
900
|
|
|
385
901
|
const recall = await runHybridRecall({
|
|
386
902
|
api,
|
|
@@ -388,6 +904,7 @@ const memoryBraidPlugin = {
|
|
|
388
904
|
mem0,
|
|
389
905
|
log,
|
|
390
906
|
ctx: toolCtx,
|
|
907
|
+
statePaths: runtimeStatePaths,
|
|
391
908
|
query: event.prompt,
|
|
392
909
|
args: {
|
|
393
910
|
query: event.prompt,
|
|
@@ -427,8 +944,18 @@ const memoryBraidPlugin = {
|
|
|
427
944
|
log,
|
|
428
945
|
runId,
|
|
429
946
|
});
|
|
947
|
+
const runtimeStatePaths = await ensureRuntimeStatePaths();
|
|
430
948
|
|
|
431
949
|
if (candidates.length === 0) {
|
|
950
|
+
if (runtimeStatePaths) {
|
|
951
|
+
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
952
|
+
const stats = await readStatsState(runtimeStatePaths);
|
|
953
|
+
stats.capture.runs += 1;
|
|
954
|
+
stats.capture.runsNoCandidates += 1;
|
|
955
|
+
stats.capture.lastRunAt = new Date().toISOString();
|
|
956
|
+
await writeStatsState(runtimeStatePaths, stats);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
432
959
|
log.debug("memory_braid.capture.skip", {
|
|
433
960
|
runId,
|
|
434
961
|
reason: "no_candidates",
|
|
@@ -439,7 +966,7 @@ const memoryBraidPlugin = {
|
|
|
439
966
|
return;
|
|
440
967
|
}
|
|
441
968
|
|
|
442
|
-
if (!
|
|
969
|
+
if (!runtimeStatePaths) {
|
|
443
970
|
log.warn("memory_braid.capture.skip", {
|
|
444
971
|
runId,
|
|
445
972
|
reason: "state_not_ready",
|
|
@@ -450,96 +977,135 @@ const memoryBraidPlugin = {
|
|
|
450
977
|
return;
|
|
451
978
|
}
|
|
452
979
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
let totalEntitiesAttached = 0;
|
|
466
|
-
let mem0AddAttempts = 0;
|
|
467
|
-
let mem0AddWithId = 0;
|
|
468
|
-
let mem0AddWithoutId = 0;
|
|
469
|
-
for (const candidate of candidates) {
|
|
470
|
-
const hash = sha256(normalizeForHash(candidate.text));
|
|
471
|
-
if (dedupe.seen[hash]) {
|
|
472
|
-
dedupeSkipped += 1;
|
|
473
|
-
continue;
|
|
980
|
+
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
981
|
+
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
982
|
+
const stats = await readStatsState(runtimeStatePaths);
|
|
983
|
+
const lifecycle = cfg.lifecycle.enabled
|
|
984
|
+
? await readLifecycleState(runtimeStatePaths)
|
|
985
|
+
: null;
|
|
986
|
+
const now = Date.now();
|
|
987
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
988
|
+
for (const [key, ts] of Object.entries(dedupe.seen)) {
|
|
989
|
+
if (now - ts > thirtyDays) {
|
|
990
|
+
delete dedupe.seen[key];
|
|
991
|
+
}
|
|
474
992
|
}
|
|
475
|
-
dedupe.seen[hash] = now;
|
|
476
|
-
|
|
477
|
-
const metadata: Record<string, unknown> = {
|
|
478
|
-
sourceType: "capture",
|
|
479
|
-
workspaceHash: scope.workspaceHash,
|
|
480
|
-
agentId: scope.agentId,
|
|
481
|
-
sessionKey: scope.sessionKey,
|
|
482
|
-
category: candidate.category,
|
|
483
|
-
captureScore: candidate.score,
|
|
484
|
-
extractionSource: candidate.source,
|
|
485
|
-
contentHash: hash,
|
|
486
|
-
indexedAt: new Date().toISOString(),
|
|
487
|
-
};
|
|
488
993
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
994
|
+
let persisted = 0;
|
|
995
|
+
let dedupeSkipped = 0;
|
|
996
|
+
let entityAnnotatedCandidates = 0;
|
|
997
|
+
let totalEntitiesAttached = 0;
|
|
998
|
+
let mem0AddAttempts = 0;
|
|
999
|
+
let mem0AddWithId = 0;
|
|
1000
|
+
let mem0AddWithoutId = 0;
|
|
1001
|
+
for (const candidate of candidates) {
|
|
1002
|
+
const hash = sha256(normalizeForHash(candidate.text));
|
|
1003
|
+
if (dedupe.seen[hash]) {
|
|
1004
|
+
dedupeSkipped += 1;
|
|
1005
|
+
continue;
|
|
499
1006
|
}
|
|
500
|
-
}
|
|
501
1007
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
text: candidate.text,
|
|
505
|
-
scope,
|
|
506
|
-
metadata,
|
|
507
|
-
runId,
|
|
508
|
-
});
|
|
509
|
-
if (addResult.id) {
|
|
510
|
-
mem0AddWithId += 1;
|
|
511
|
-
} else {
|
|
512
|
-
mem0AddWithoutId += 1;
|
|
513
|
-
log.warn("memory_braid.capture.persist", {
|
|
514
|
-
runId,
|
|
515
|
-
reason: "mem0_add_missing_id",
|
|
1008
|
+
const metadata: Record<string, unknown> = {
|
|
1009
|
+
sourceType: "capture",
|
|
516
1010
|
workspaceHash: scope.workspaceHash,
|
|
517
1011
|
agentId: scope.agentId,
|
|
518
1012
|
sessionKey: scope.sessionKey,
|
|
519
|
-
contentHashPrefix: hash.slice(0, 12),
|
|
520
1013
|
category: candidate.category,
|
|
1014
|
+
captureScore: candidate.score,
|
|
1015
|
+
extractionSource: candidate.source,
|
|
1016
|
+
contentHash: hash,
|
|
1017
|
+
indexedAt: new Date(now).toISOString(),
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
if (cfg.entityExtraction.enabled) {
|
|
1021
|
+
const entities = await entityExtraction.extract({
|
|
1022
|
+
text: candidate.text,
|
|
1023
|
+
runId,
|
|
1024
|
+
});
|
|
1025
|
+
if (entities.length > 0) {
|
|
1026
|
+
entityAnnotatedCandidates += 1;
|
|
1027
|
+
totalEntitiesAttached += entities.length;
|
|
1028
|
+
metadata.entityUris = entities.map((entity) => entity.canonicalUri);
|
|
1029
|
+
metadata.entities = entities;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
mem0AddAttempts += 1;
|
|
1034
|
+
const addResult = await mem0.addMemory({
|
|
1035
|
+
text: candidate.text,
|
|
1036
|
+
scope,
|
|
1037
|
+
metadata,
|
|
1038
|
+
runId,
|
|
521
1039
|
});
|
|
1040
|
+
if (addResult.id) {
|
|
1041
|
+
dedupe.seen[hash] = now;
|
|
1042
|
+
mem0AddWithId += 1;
|
|
1043
|
+
persisted += 1;
|
|
1044
|
+
if (lifecycle) {
|
|
1045
|
+
const memoryId = addResult.id;
|
|
1046
|
+
const existing = lifecycle.entries[memoryId];
|
|
1047
|
+
lifecycle.entries[memoryId] = {
|
|
1048
|
+
memoryId,
|
|
1049
|
+
contentHash: hash,
|
|
1050
|
+
workspaceHash: scope.workspaceHash,
|
|
1051
|
+
agentId: scope.agentId,
|
|
1052
|
+
sessionKey: scope.sessionKey,
|
|
1053
|
+
category: candidate.category,
|
|
1054
|
+
createdAt: existing?.createdAt ?? now,
|
|
1055
|
+
lastCapturedAt: now,
|
|
1056
|
+
lastRecalledAt: existing?.lastRecalledAt,
|
|
1057
|
+
recallCount: existing?.recallCount ?? 0,
|
|
1058
|
+
updatedAt: now,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
mem0AddWithoutId += 1;
|
|
1063
|
+
log.warn("memory_braid.capture.persist", {
|
|
1064
|
+
runId,
|
|
1065
|
+
reason: "mem0_add_missing_id",
|
|
1066
|
+
workspaceHash: scope.workspaceHash,
|
|
1067
|
+
agentId: scope.agentId,
|
|
1068
|
+
sessionKey: scope.sessionKey,
|
|
1069
|
+
contentHashPrefix: hash.slice(0, 12),
|
|
1070
|
+
category: candidate.category,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
522
1073
|
}
|
|
523
|
-
persisted += 1;
|
|
524
|
-
}
|
|
525
1074
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1075
|
+
stats.capture.runs += 1;
|
|
1076
|
+
stats.capture.runsWithCandidates += 1;
|
|
1077
|
+
stats.capture.candidates += candidates.length;
|
|
1078
|
+
stats.capture.dedupeSkipped += dedupeSkipped;
|
|
1079
|
+
stats.capture.persisted += persisted;
|
|
1080
|
+
stats.capture.mem0AddAttempts += mem0AddAttempts;
|
|
1081
|
+
stats.capture.mem0AddWithId += mem0AddWithId;
|
|
1082
|
+
stats.capture.mem0AddWithoutId += mem0AddWithoutId;
|
|
1083
|
+
stats.capture.entityAnnotatedCandidates += entityAnnotatedCandidates;
|
|
1084
|
+
stats.capture.totalEntitiesAttached += totalEntitiesAttached;
|
|
1085
|
+
stats.capture.lastRunAt = new Date(now).toISOString();
|
|
1086
|
+
|
|
1087
|
+
await writeCaptureDedupeState(runtimeStatePaths, dedupe);
|
|
1088
|
+
if (lifecycle) {
|
|
1089
|
+
await writeLifecycleState(runtimeStatePaths, lifecycle);
|
|
1090
|
+
}
|
|
1091
|
+
await writeStatsState(runtimeStatePaths, stats);
|
|
1092
|
+
log.debug("memory_braid.capture.persist", {
|
|
1093
|
+
runId,
|
|
1094
|
+
mode: cfg.capture.mode,
|
|
1095
|
+
workspaceHash: scope.workspaceHash,
|
|
1096
|
+
agentId: scope.agentId,
|
|
1097
|
+
sessionKey: scope.sessionKey,
|
|
1098
|
+
candidates: candidates.length,
|
|
1099
|
+
dedupeSkipped,
|
|
1100
|
+
persisted,
|
|
1101
|
+
mem0AddAttempts,
|
|
1102
|
+
mem0AddWithId,
|
|
1103
|
+
mem0AddWithoutId,
|
|
1104
|
+
entityExtractionEnabled: cfg.entityExtraction.enabled,
|
|
1105
|
+
entityAnnotatedCandidates,
|
|
1106
|
+
totalEntitiesAttached,
|
|
1107
|
+
}, true);
|
|
1108
|
+
});
|
|
543
1109
|
});
|
|
544
1110
|
|
|
545
1111
|
api.registerService({
|
|
@@ -549,31 +1115,26 @@ const memoryBraidPlugin = {
|
|
|
549
1115
|
entityExtraction.setStateDir(ctx.stateDir);
|
|
550
1116
|
statePaths = createStatePaths(ctx.stateDir);
|
|
551
1117
|
await ensureStateDir(statePaths);
|
|
552
|
-
targets = await resolveTargets({
|
|
553
|
-
config: api.config as unknown as {
|
|
554
|
-
agents?: {
|
|
555
|
-
defaults?: { workspace?: string };
|
|
556
|
-
list?: Array<{ id?: string; workspace?: string; default?: boolean }>;
|
|
557
|
-
};
|
|
558
|
-
},
|
|
559
|
-
stateDir: ctx.stateDir,
|
|
560
|
-
fallbackWorkspaceDir: ctx.workspaceDir,
|
|
561
|
-
});
|
|
562
1118
|
|
|
563
1119
|
const runId = log.newRunId();
|
|
564
1120
|
log.info("memory_braid.startup", {
|
|
565
1121
|
runId,
|
|
566
1122
|
stateDir: ctx.stateDir,
|
|
567
|
-
targets: targets.length,
|
|
568
1123
|
});
|
|
569
1124
|
log.info("memory_braid.config", {
|
|
570
1125
|
runId,
|
|
571
1126
|
mem0Mode: cfg.mem0.mode,
|
|
572
1127
|
captureEnabled: cfg.capture.enabled,
|
|
573
1128
|
captureMode: cfg.capture.mode,
|
|
1129
|
+
captureIncludeAssistant: cfg.capture.includeAssistant,
|
|
574
1130
|
captureMaxItemsPerRun: cfg.capture.maxItemsPerRun,
|
|
575
1131
|
captureMlProvider: cfg.capture.ml.provider ?? "unset",
|
|
576
1132
|
captureMlModel: cfg.capture.ml.model ?? "unset",
|
|
1133
|
+
timeDecayEnabled: cfg.timeDecay.enabled,
|
|
1134
|
+
lifecycleEnabled: cfg.lifecycle.enabled,
|
|
1135
|
+
lifecycleCaptureTtlDays: cfg.lifecycle.captureTtlDays,
|
|
1136
|
+
lifecycleCleanupIntervalMinutes: cfg.lifecycle.cleanupIntervalMinutes,
|
|
1137
|
+
lifecycleReinforceOnRecall: cfg.lifecycle.reinforceOnRecall,
|
|
577
1138
|
entityExtractionEnabled: cfg.entityExtraction.enabled,
|
|
578
1139
|
entityProvider: cfg.entityExtraction.provider,
|
|
579
1140
|
entityModel: cfg.entityExtraction.model,
|
|
@@ -585,32 +1146,15 @@ const memoryBraidPlugin = {
|
|
|
585
1146
|
debugSamplingRate: cfg.debug.logSamplingRate,
|
|
586
1147
|
});
|
|
587
1148
|
|
|
588
|
-
|
|
589
|
-
void runBootstrapIfNeeded({
|
|
1149
|
+
void runLifecycleCleanupOnce({
|
|
590
1150
|
cfg,
|
|
591
1151
|
mem0,
|
|
592
|
-
statePaths,
|
|
593
1152
|
log,
|
|
594
|
-
targets,
|
|
595
|
-
runId,
|
|
596
|
-
}).catch((err) => {
|
|
597
|
-
log.warn("memory_braid.bootstrap.error", {
|
|
598
|
-
runId,
|
|
599
|
-
error: err instanceof Error ? err.message : String(err),
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// One startup reconcile pass (non-blocking).
|
|
604
|
-
void runReconcileOnce({
|
|
605
|
-
cfg,
|
|
606
|
-
mem0,
|
|
607
1153
|
statePaths,
|
|
608
|
-
log,
|
|
609
|
-
targets,
|
|
610
1154
|
reason: "startup",
|
|
611
1155
|
runId,
|
|
612
1156
|
}).catch((err) => {
|
|
613
|
-
log.warn("memory_braid.
|
|
1157
|
+
log.warn("memory_braid.lifecycle.cleanup", {
|
|
614
1158
|
runId,
|
|
615
1159
|
reason: "startup",
|
|
616
1160
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -632,18 +1176,18 @@ const memoryBraidPlugin = {
|
|
|
632
1176
|
});
|
|
633
1177
|
}
|
|
634
1178
|
|
|
635
|
-
if (cfg.
|
|
636
|
-
const intervalMs = cfg.
|
|
637
|
-
|
|
638
|
-
void
|
|
1179
|
+
if (cfg.lifecycle.enabled) {
|
|
1180
|
+
const intervalMs = cfg.lifecycle.cleanupIntervalMinutes * 60 * 1000;
|
|
1181
|
+
lifecycleTimer = setInterval(() => {
|
|
1182
|
+
void runLifecycleCleanupOnce({
|
|
639
1183
|
cfg,
|
|
640
1184
|
mem0,
|
|
641
|
-
statePaths: statePaths!,
|
|
642
1185
|
log,
|
|
643
|
-
|
|
1186
|
+
statePaths: statePaths!,
|
|
644
1187
|
reason: "interval",
|
|
645
1188
|
}).catch((err) => {
|
|
646
|
-
log.warn("memory_braid.
|
|
1189
|
+
log.warn("memory_braid.lifecycle.cleanup", {
|
|
1190
|
+
reason: "interval",
|
|
647
1191
|
error: err instanceof Error ? err.message : String(err),
|
|
648
1192
|
});
|
|
649
1193
|
});
|
|
@@ -651,9 +1195,9 @@ const memoryBraidPlugin = {
|
|
|
651
1195
|
}
|
|
652
1196
|
},
|
|
653
1197
|
stop: async () => {
|
|
654
|
-
if (
|
|
655
|
-
clearInterval(
|
|
656
|
-
|
|
1198
|
+
if (lifecycleTimer) {
|
|
1199
|
+
clearInterval(lifecycleTimer);
|
|
1200
|
+
lifecycleTimer = null;
|
|
657
1201
|
}
|
|
658
1202
|
},
|
|
659
1203
|
});
|