memory-braid 0.3.7 → 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 -162
- package/src/mem0-client.ts +62 -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,35 +966,7 @@ const memoryBraidPlugin = {
|
|
|
439
966
|
return;
|
|
440
967
|
}
|
|
441
968
|
|
|
442
|
-
if (!
|
|
443
|
-
const resolvedStateDir = api.runtime.state.resolveStateDir();
|
|
444
|
-
if (resolvedStateDir) {
|
|
445
|
-
const lazyStatePaths = createStatePaths(resolvedStateDir);
|
|
446
|
-
try {
|
|
447
|
-
await ensureStateDir(lazyStatePaths);
|
|
448
|
-
statePaths = lazyStatePaths;
|
|
449
|
-
mem0.setStateDir(resolvedStateDir);
|
|
450
|
-
entityExtraction.setStateDir(resolvedStateDir);
|
|
451
|
-
log.info("memory_braid.state.ready", {
|
|
452
|
-
runId,
|
|
453
|
-
reason: "lazy_capture",
|
|
454
|
-
stateDir: resolvedStateDir,
|
|
455
|
-
});
|
|
456
|
-
} catch (err) {
|
|
457
|
-
log.warn("memory_braid.capture.skip", {
|
|
458
|
-
runId,
|
|
459
|
-
reason: "state_init_failed",
|
|
460
|
-
workspaceHash: scope.workspaceHash,
|
|
461
|
-
agentId: scope.agentId,
|
|
462
|
-
sessionKey: scope.sessionKey,
|
|
463
|
-
error: err instanceof Error ? err.message : String(err),
|
|
464
|
-
});
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (!statePaths) {
|
|
969
|
+
if (!runtimeStatePaths) {
|
|
471
970
|
log.warn("memory_braid.capture.skip", {
|
|
472
971
|
runId,
|
|
473
972
|
reason: "state_not_ready",
|
|
@@ -478,96 +977,135 @@ const memoryBraidPlugin = {
|
|
|
478
977
|
return;
|
|
479
978
|
}
|
|
480
979
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
let totalEntitiesAttached = 0;
|
|
494
|
-
let mem0AddAttempts = 0;
|
|
495
|
-
let mem0AddWithId = 0;
|
|
496
|
-
let mem0AddWithoutId = 0;
|
|
497
|
-
for (const candidate of candidates) {
|
|
498
|
-
const hash = sha256(normalizeForHash(candidate.text));
|
|
499
|
-
if (dedupe.seen[hash]) {
|
|
500
|
-
dedupeSkipped += 1;
|
|
501
|
-
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
|
+
}
|
|
502
992
|
}
|
|
503
|
-
dedupe.seen[hash] = now;
|
|
504
|
-
|
|
505
|
-
const metadata: Record<string, unknown> = {
|
|
506
|
-
sourceType: "capture",
|
|
507
|
-
workspaceHash: scope.workspaceHash,
|
|
508
|
-
agentId: scope.agentId,
|
|
509
|
-
sessionKey: scope.sessionKey,
|
|
510
|
-
category: candidate.category,
|
|
511
|
-
captureScore: candidate.score,
|
|
512
|
-
extractionSource: candidate.source,
|
|
513
|
-
contentHash: hash,
|
|
514
|
-
indexedAt: new Date().toISOString(),
|
|
515
|
-
};
|
|
516
993
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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;
|
|
527
1006
|
}
|
|
528
|
-
}
|
|
529
1007
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
text: candidate.text,
|
|
533
|
-
scope,
|
|
534
|
-
metadata,
|
|
535
|
-
runId,
|
|
536
|
-
});
|
|
537
|
-
if (addResult.id) {
|
|
538
|
-
mem0AddWithId += 1;
|
|
539
|
-
} else {
|
|
540
|
-
mem0AddWithoutId += 1;
|
|
541
|
-
log.warn("memory_braid.capture.persist", {
|
|
542
|
-
runId,
|
|
543
|
-
reason: "mem0_add_missing_id",
|
|
1008
|
+
const metadata: Record<string, unknown> = {
|
|
1009
|
+
sourceType: "capture",
|
|
544
1010
|
workspaceHash: scope.workspaceHash,
|
|
545
1011
|
agentId: scope.agentId,
|
|
546
1012
|
sessionKey: scope.sessionKey,
|
|
547
|
-
contentHashPrefix: hash.slice(0, 12),
|
|
548
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,
|
|
549
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
|
+
}
|
|
550
1073
|
}
|
|
551
|
-
persisted += 1;
|
|
552
|
-
}
|
|
553
1074
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
});
|
|
571
1109
|
});
|
|
572
1110
|
|
|
573
1111
|
api.registerService({
|
|
@@ -577,31 +1115,26 @@ const memoryBraidPlugin = {
|
|
|
577
1115
|
entityExtraction.setStateDir(ctx.stateDir);
|
|
578
1116
|
statePaths = createStatePaths(ctx.stateDir);
|
|
579
1117
|
await ensureStateDir(statePaths);
|
|
580
|
-
targets = await resolveTargets({
|
|
581
|
-
config: api.config as unknown as {
|
|
582
|
-
agents?: {
|
|
583
|
-
defaults?: { workspace?: string };
|
|
584
|
-
list?: Array<{ id?: string; workspace?: string; default?: boolean }>;
|
|
585
|
-
};
|
|
586
|
-
},
|
|
587
|
-
stateDir: ctx.stateDir,
|
|
588
|
-
fallbackWorkspaceDir: ctx.workspaceDir,
|
|
589
|
-
});
|
|
590
1118
|
|
|
591
1119
|
const runId = log.newRunId();
|
|
592
1120
|
log.info("memory_braid.startup", {
|
|
593
1121
|
runId,
|
|
594
1122
|
stateDir: ctx.stateDir,
|
|
595
|
-
targets: targets.length,
|
|
596
1123
|
});
|
|
597
1124
|
log.info("memory_braid.config", {
|
|
598
1125
|
runId,
|
|
599
1126
|
mem0Mode: cfg.mem0.mode,
|
|
600
1127
|
captureEnabled: cfg.capture.enabled,
|
|
601
1128
|
captureMode: cfg.capture.mode,
|
|
1129
|
+
captureIncludeAssistant: cfg.capture.includeAssistant,
|
|
602
1130
|
captureMaxItemsPerRun: cfg.capture.maxItemsPerRun,
|
|
603
1131
|
captureMlProvider: cfg.capture.ml.provider ?? "unset",
|
|
604
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,
|
|
605
1138
|
entityExtractionEnabled: cfg.entityExtraction.enabled,
|
|
606
1139
|
entityProvider: cfg.entityExtraction.provider,
|
|
607
1140
|
entityModel: cfg.entityExtraction.model,
|
|
@@ -613,32 +1146,15 @@ const memoryBraidPlugin = {
|
|
|
613
1146
|
debugSamplingRate: cfg.debug.logSamplingRate,
|
|
614
1147
|
});
|
|
615
1148
|
|
|
616
|
-
|
|
617
|
-
void runBootstrapIfNeeded({
|
|
1149
|
+
void runLifecycleCleanupOnce({
|
|
618
1150
|
cfg,
|
|
619
1151
|
mem0,
|
|
620
|
-
statePaths,
|
|
621
1152
|
log,
|
|
622
|
-
targets,
|
|
623
|
-
runId,
|
|
624
|
-
}).catch((err) => {
|
|
625
|
-
log.warn("memory_braid.bootstrap.error", {
|
|
626
|
-
runId,
|
|
627
|
-
error: err instanceof Error ? err.message : String(err),
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
// One startup reconcile pass (non-blocking).
|
|
632
|
-
void runReconcileOnce({
|
|
633
|
-
cfg,
|
|
634
|
-
mem0,
|
|
635
1153
|
statePaths,
|
|
636
|
-
log,
|
|
637
|
-
targets,
|
|
638
1154
|
reason: "startup",
|
|
639
1155
|
runId,
|
|
640
1156
|
}).catch((err) => {
|
|
641
|
-
log.warn("memory_braid.
|
|
1157
|
+
log.warn("memory_braid.lifecycle.cleanup", {
|
|
642
1158
|
runId,
|
|
643
1159
|
reason: "startup",
|
|
644
1160
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -660,18 +1176,18 @@ const memoryBraidPlugin = {
|
|
|
660
1176
|
});
|
|
661
1177
|
}
|
|
662
1178
|
|
|
663
|
-
if (cfg.
|
|
664
|
-
const intervalMs = cfg.
|
|
665
|
-
|
|
666
|
-
void
|
|
1179
|
+
if (cfg.lifecycle.enabled) {
|
|
1180
|
+
const intervalMs = cfg.lifecycle.cleanupIntervalMinutes * 60 * 1000;
|
|
1181
|
+
lifecycleTimer = setInterval(() => {
|
|
1182
|
+
void runLifecycleCleanupOnce({
|
|
667
1183
|
cfg,
|
|
668
1184
|
mem0,
|
|
669
|
-
statePaths: statePaths!,
|
|
670
1185
|
log,
|
|
671
|
-
|
|
1186
|
+
statePaths: statePaths!,
|
|
672
1187
|
reason: "interval",
|
|
673
1188
|
}).catch((err) => {
|
|
674
|
-
log.warn("memory_braid.
|
|
1189
|
+
log.warn("memory_braid.lifecycle.cleanup", {
|
|
1190
|
+
reason: "interval",
|
|
675
1191
|
error: err instanceof Error ? err.message : String(err),
|
|
676
1192
|
});
|
|
677
1193
|
});
|
|
@@ -679,9 +1195,9 @@ const memoryBraidPlugin = {
|
|
|
679
1195
|
}
|
|
680
1196
|
},
|
|
681
1197
|
stop: async () => {
|
|
682
|
-
if (
|
|
683
|
-
clearInterval(
|
|
684
|
-
|
|
1198
|
+
if (lifecycleTimer) {
|
|
1199
|
+
clearInterval(lifecycleTimer);
|
|
1200
|
+
lifecycleTimer = null;
|
|
685
1201
|
}
|
|
686
1202
|
},
|
|
687
1203
|
});
|