memory-braid 0.3.7 → 0.4.1
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 +57 -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 +681 -163
- 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,
|
|
@@ -401,6 +918,7 @@ const memoryBraidPlugin = {
|
|
|
401
918
|
return;
|
|
402
919
|
}
|
|
403
920
|
|
|
921
|
+
const prependContext = formatRelevantMemories(injected, cfg.debug.maxSnippetChars);
|
|
404
922
|
const scope = resolveScopeFromHookContext(ctx);
|
|
405
923
|
log.debug("memory_braid.search.inject", {
|
|
406
924
|
runId,
|
|
@@ -408,10 +926,11 @@ const memoryBraidPlugin = {
|
|
|
408
926
|
sessionKey: scope.sessionKey,
|
|
409
927
|
workspaceHash: scope.workspaceHash,
|
|
410
928
|
count: injected.length,
|
|
929
|
+
injectedTextPreview: prependContext,
|
|
411
930
|
});
|
|
412
931
|
|
|
413
932
|
return {
|
|
414
|
-
prependContext
|
|
933
|
+
prependContext,
|
|
415
934
|
};
|
|
416
935
|
});
|
|
417
936
|
|
|
@@ -427,8 +946,18 @@ const memoryBraidPlugin = {
|
|
|
427
946
|
log,
|
|
428
947
|
runId,
|
|
429
948
|
});
|
|
949
|
+
const runtimeStatePaths = await ensureRuntimeStatePaths();
|
|
430
950
|
|
|
431
951
|
if (candidates.length === 0) {
|
|
952
|
+
if (runtimeStatePaths) {
|
|
953
|
+
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
954
|
+
const stats = await readStatsState(runtimeStatePaths);
|
|
955
|
+
stats.capture.runs += 1;
|
|
956
|
+
stats.capture.runsNoCandidates += 1;
|
|
957
|
+
stats.capture.lastRunAt = new Date().toISOString();
|
|
958
|
+
await writeStatsState(runtimeStatePaths, stats);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
432
961
|
log.debug("memory_braid.capture.skip", {
|
|
433
962
|
runId,
|
|
434
963
|
reason: "no_candidates",
|
|
@@ -439,35 +968,7 @@ const memoryBraidPlugin = {
|
|
|
439
968
|
return;
|
|
440
969
|
}
|
|
441
970
|
|
|
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) {
|
|
971
|
+
if (!runtimeStatePaths) {
|
|
471
972
|
log.warn("memory_braid.capture.skip", {
|
|
472
973
|
runId,
|
|
473
974
|
reason: "state_not_ready",
|
|
@@ -478,96 +979,135 @@ const memoryBraidPlugin = {
|
|
|
478
979
|
return;
|
|
479
980
|
}
|
|
480
981
|
|
|
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;
|
|
982
|
+
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
983
|
+
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
984
|
+
const stats = await readStatsState(runtimeStatePaths);
|
|
985
|
+
const lifecycle = cfg.lifecycle.enabled
|
|
986
|
+
? await readLifecycleState(runtimeStatePaths)
|
|
987
|
+
: null;
|
|
988
|
+
const now = Date.now();
|
|
989
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
990
|
+
for (const [key, ts] of Object.entries(dedupe.seen)) {
|
|
991
|
+
if (now - ts > thirtyDays) {
|
|
992
|
+
delete dedupe.seen[key];
|
|
993
|
+
}
|
|
502
994
|
}
|
|
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
995
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
996
|
+
let persisted = 0;
|
|
997
|
+
let dedupeSkipped = 0;
|
|
998
|
+
let entityAnnotatedCandidates = 0;
|
|
999
|
+
let totalEntitiesAttached = 0;
|
|
1000
|
+
let mem0AddAttempts = 0;
|
|
1001
|
+
let mem0AddWithId = 0;
|
|
1002
|
+
let mem0AddWithoutId = 0;
|
|
1003
|
+
for (const candidate of candidates) {
|
|
1004
|
+
const hash = sha256(normalizeForHash(candidate.text));
|
|
1005
|
+
if (dedupe.seen[hash]) {
|
|
1006
|
+
dedupeSkipped += 1;
|
|
1007
|
+
continue;
|
|
527
1008
|
}
|
|
528
|
-
}
|
|
529
1009
|
|
|
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",
|
|
1010
|
+
const metadata: Record<string, unknown> = {
|
|
1011
|
+
sourceType: "capture",
|
|
544
1012
|
workspaceHash: scope.workspaceHash,
|
|
545
1013
|
agentId: scope.agentId,
|
|
546
1014
|
sessionKey: scope.sessionKey,
|
|
547
|
-
contentHashPrefix: hash.slice(0, 12),
|
|
548
1015
|
category: candidate.category,
|
|
1016
|
+
captureScore: candidate.score,
|
|
1017
|
+
extractionSource: candidate.source,
|
|
1018
|
+
contentHash: hash,
|
|
1019
|
+
indexedAt: new Date(now).toISOString(),
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
if (cfg.entityExtraction.enabled) {
|
|
1023
|
+
const entities = await entityExtraction.extract({
|
|
1024
|
+
text: candidate.text,
|
|
1025
|
+
runId,
|
|
1026
|
+
});
|
|
1027
|
+
if (entities.length > 0) {
|
|
1028
|
+
entityAnnotatedCandidates += 1;
|
|
1029
|
+
totalEntitiesAttached += entities.length;
|
|
1030
|
+
metadata.entityUris = entities.map((entity) => entity.canonicalUri);
|
|
1031
|
+
metadata.entities = entities;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
mem0AddAttempts += 1;
|
|
1036
|
+
const addResult = await mem0.addMemory({
|
|
1037
|
+
text: candidate.text,
|
|
1038
|
+
scope,
|
|
1039
|
+
metadata,
|
|
1040
|
+
runId,
|
|
549
1041
|
});
|
|
1042
|
+
if (addResult.id) {
|
|
1043
|
+
dedupe.seen[hash] = now;
|
|
1044
|
+
mem0AddWithId += 1;
|
|
1045
|
+
persisted += 1;
|
|
1046
|
+
if (lifecycle) {
|
|
1047
|
+
const memoryId = addResult.id;
|
|
1048
|
+
const existing = lifecycle.entries[memoryId];
|
|
1049
|
+
lifecycle.entries[memoryId] = {
|
|
1050
|
+
memoryId,
|
|
1051
|
+
contentHash: hash,
|
|
1052
|
+
workspaceHash: scope.workspaceHash,
|
|
1053
|
+
agentId: scope.agentId,
|
|
1054
|
+
sessionKey: scope.sessionKey,
|
|
1055
|
+
category: candidate.category,
|
|
1056
|
+
createdAt: existing?.createdAt ?? now,
|
|
1057
|
+
lastCapturedAt: now,
|
|
1058
|
+
lastRecalledAt: existing?.lastRecalledAt,
|
|
1059
|
+
recallCount: existing?.recallCount ?? 0,
|
|
1060
|
+
updatedAt: now,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
mem0AddWithoutId += 1;
|
|
1065
|
+
log.warn("memory_braid.capture.persist", {
|
|
1066
|
+
runId,
|
|
1067
|
+
reason: "mem0_add_missing_id",
|
|
1068
|
+
workspaceHash: scope.workspaceHash,
|
|
1069
|
+
agentId: scope.agentId,
|
|
1070
|
+
sessionKey: scope.sessionKey,
|
|
1071
|
+
contentHashPrefix: hash.slice(0, 12),
|
|
1072
|
+
category: candidate.category,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
550
1075
|
}
|
|
551
|
-
persisted += 1;
|
|
552
|
-
}
|
|
553
1076
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1077
|
+
stats.capture.runs += 1;
|
|
1078
|
+
stats.capture.runsWithCandidates += 1;
|
|
1079
|
+
stats.capture.candidates += candidates.length;
|
|
1080
|
+
stats.capture.dedupeSkipped += dedupeSkipped;
|
|
1081
|
+
stats.capture.persisted += persisted;
|
|
1082
|
+
stats.capture.mem0AddAttempts += mem0AddAttempts;
|
|
1083
|
+
stats.capture.mem0AddWithId += mem0AddWithId;
|
|
1084
|
+
stats.capture.mem0AddWithoutId += mem0AddWithoutId;
|
|
1085
|
+
stats.capture.entityAnnotatedCandidates += entityAnnotatedCandidates;
|
|
1086
|
+
stats.capture.totalEntitiesAttached += totalEntitiesAttached;
|
|
1087
|
+
stats.capture.lastRunAt = new Date(now).toISOString();
|
|
1088
|
+
|
|
1089
|
+
await writeCaptureDedupeState(runtimeStatePaths, dedupe);
|
|
1090
|
+
if (lifecycle) {
|
|
1091
|
+
await writeLifecycleState(runtimeStatePaths, lifecycle);
|
|
1092
|
+
}
|
|
1093
|
+
await writeStatsState(runtimeStatePaths, stats);
|
|
1094
|
+
log.debug("memory_braid.capture.persist", {
|
|
1095
|
+
runId,
|
|
1096
|
+
mode: cfg.capture.mode,
|
|
1097
|
+
workspaceHash: scope.workspaceHash,
|
|
1098
|
+
agentId: scope.agentId,
|
|
1099
|
+
sessionKey: scope.sessionKey,
|
|
1100
|
+
candidates: candidates.length,
|
|
1101
|
+
dedupeSkipped,
|
|
1102
|
+
persisted,
|
|
1103
|
+
mem0AddAttempts,
|
|
1104
|
+
mem0AddWithId,
|
|
1105
|
+
mem0AddWithoutId,
|
|
1106
|
+
entityExtractionEnabled: cfg.entityExtraction.enabled,
|
|
1107
|
+
entityAnnotatedCandidates,
|
|
1108
|
+
totalEntitiesAttached,
|
|
1109
|
+
}, true);
|
|
1110
|
+
});
|
|
571
1111
|
});
|
|
572
1112
|
|
|
573
1113
|
api.registerService({
|
|
@@ -577,31 +1117,26 @@ const memoryBraidPlugin = {
|
|
|
577
1117
|
entityExtraction.setStateDir(ctx.stateDir);
|
|
578
1118
|
statePaths = createStatePaths(ctx.stateDir);
|
|
579
1119
|
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
1120
|
|
|
591
1121
|
const runId = log.newRunId();
|
|
592
1122
|
log.info("memory_braid.startup", {
|
|
593
1123
|
runId,
|
|
594
1124
|
stateDir: ctx.stateDir,
|
|
595
|
-
targets: targets.length,
|
|
596
1125
|
});
|
|
597
1126
|
log.info("memory_braid.config", {
|
|
598
1127
|
runId,
|
|
599
1128
|
mem0Mode: cfg.mem0.mode,
|
|
600
1129
|
captureEnabled: cfg.capture.enabled,
|
|
601
1130
|
captureMode: cfg.capture.mode,
|
|
1131
|
+
captureIncludeAssistant: cfg.capture.includeAssistant,
|
|
602
1132
|
captureMaxItemsPerRun: cfg.capture.maxItemsPerRun,
|
|
603
1133
|
captureMlProvider: cfg.capture.ml.provider ?? "unset",
|
|
604
1134
|
captureMlModel: cfg.capture.ml.model ?? "unset",
|
|
1135
|
+
timeDecayEnabled: cfg.timeDecay.enabled,
|
|
1136
|
+
lifecycleEnabled: cfg.lifecycle.enabled,
|
|
1137
|
+
lifecycleCaptureTtlDays: cfg.lifecycle.captureTtlDays,
|
|
1138
|
+
lifecycleCleanupIntervalMinutes: cfg.lifecycle.cleanupIntervalMinutes,
|
|
1139
|
+
lifecycleReinforceOnRecall: cfg.lifecycle.reinforceOnRecall,
|
|
605
1140
|
entityExtractionEnabled: cfg.entityExtraction.enabled,
|
|
606
1141
|
entityProvider: cfg.entityExtraction.provider,
|
|
607
1142
|
entityModel: cfg.entityExtraction.model,
|
|
@@ -613,32 +1148,15 @@ const memoryBraidPlugin = {
|
|
|
613
1148
|
debugSamplingRate: cfg.debug.logSamplingRate,
|
|
614
1149
|
});
|
|
615
1150
|
|
|
616
|
-
|
|
617
|
-
void runBootstrapIfNeeded({
|
|
1151
|
+
void runLifecycleCleanupOnce({
|
|
618
1152
|
cfg,
|
|
619
1153
|
mem0,
|
|
620
|
-
statePaths,
|
|
621
1154
|
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
1155
|
statePaths,
|
|
636
|
-
log,
|
|
637
|
-
targets,
|
|
638
1156
|
reason: "startup",
|
|
639
1157
|
runId,
|
|
640
1158
|
}).catch((err) => {
|
|
641
|
-
log.warn("memory_braid.
|
|
1159
|
+
log.warn("memory_braid.lifecycle.cleanup", {
|
|
642
1160
|
runId,
|
|
643
1161
|
reason: "startup",
|
|
644
1162
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -660,18 +1178,18 @@ const memoryBraidPlugin = {
|
|
|
660
1178
|
});
|
|
661
1179
|
}
|
|
662
1180
|
|
|
663
|
-
if (cfg.
|
|
664
|
-
const intervalMs = cfg.
|
|
665
|
-
|
|
666
|
-
void
|
|
1181
|
+
if (cfg.lifecycle.enabled) {
|
|
1182
|
+
const intervalMs = cfg.lifecycle.cleanupIntervalMinutes * 60 * 1000;
|
|
1183
|
+
lifecycleTimer = setInterval(() => {
|
|
1184
|
+
void runLifecycleCleanupOnce({
|
|
667
1185
|
cfg,
|
|
668
1186
|
mem0,
|
|
669
|
-
statePaths: statePaths!,
|
|
670
1187
|
log,
|
|
671
|
-
|
|
1188
|
+
statePaths: statePaths!,
|
|
672
1189
|
reason: "interval",
|
|
673
1190
|
}).catch((err) => {
|
|
674
|
-
log.warn("memory_braid.
|
|
1191
|
+
log.warn("memory_braid.lifecycle.cleanup", {
|
|
1192
|
+
reason: "interval",
|
|
675
1193
|
error: err instanceof Error ? err.message : String(err),
|
|
676
1194
|
});
|
|
677
1195
|
});
|
|
@@ -679,9 +1197,9 @@ const memoryBraidPlugin = {
|
|
|
679
1197
|
}
|
|
680
1198
|
},
|
|
681
1199
|
stop: async () => {
|
|
682
|
-
if (
|
|
683
|
-
clearInterval(
|
|
684
|
-
|
|
1200
|
+
if (lifecycleTimer) {
|
|
1201
|
+
clearInterval(lifecycleTimer);
|
|
1202
|
+
lifecycleTimer = null;
|
|
685
1203
|
}
|
|
686
1204
|
},
|
|
687
1205
|
});
|