memory-braid 0.4.0 → 0.4.2
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 +1 -0
- package/package.json +1 -1
- package/src/index.ts +355 -4
package/README.md
CHANGED
|
@@ -553,6 +553,7 @@ Key events:
|
|
|
553
553
|
- `memory_braid.mem0.request|response|error`
|
|
554
554
|
|
|
555
555
|
`debug.includePayloads=true` includes payload fields; otherwise sensitive text fields are omitted.
|
|
556
|
+
`memory_braid.search.inject` now logs `injectedTextPreview` when payloads are enabled.
|
|
556
557
|
|
|
557
558
|
Traceability tips:
|
|
558
559
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -105,6 +105,315 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
105
105
|
return value as Record<string, unknown>;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const OVERLAP_STOPWORDS = new Set([
|
|
109
|
+
"a",
|
|
110
|
+
"an",
|
|
111
|
+
"and",
|
|
112
|
+
"are",
|
|
113
|
+
"as",
|
|
114
|
+
"at",
|
|
115
|
+
"be",
|
|
116
|
+
"by",
|
|
117
|
+
"for",
|
|
118
|
+
"from",
|
|
119
|
+
"how",
|
|
120
|
+
"i",
|
|
121
|
+
"in",
|
|
122
|
+
"is",
|
|
123
|
+
"it",
|
|
124
|
+
"my",
|
|
125
|
+
"of",
|
|
126
|
+
"on",
|
|
127
|
+
"or",
|
|
128
|
+
"our",
|
|
129
|
+
"that",
|
|
130
|
+
"the",
|
|
131
|
+
"this",
|
|
132
|
+
"to",
|
|
133
|
+
"we",
|
|
134
|
+
"with",
|
|
135
|
+
"you",
|
|
136
|
+
"your",
|
|
137
|
+
"de",
|
|
138
|
+
"del",
|
|
139
|
+
"el",
|
|
140
|
+
"en",
|
|
141
|
+
"es",
|
|
142
|
+
"la",
|
|
143
|
+
"las",
|
|
144
|
+
"los",
|
|
145
|
+
"mi",
|
|
146
|
+
"mis",
|
|
147
|
+
"para",
|
|
148
|
+
"por",
|
|
149
|
+
"que",
|
|
150
|
+
"se",
|
|
151
|
+
"su",
|
|
152
|
+
"sus",
|
|
153
|
+
"un",
|
|
154
|
+
"una",
|
|
155
|
+
"y",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
function normalizeToken(value: string): string {
|
|
159
|
+
return value
|
|
160
|
+
.toLowerCase()
|
|
161
|
+
.normalize("NFKD")
|
|
162
|
+
.replace(/\p{M}+/gu, "");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function tokenizeForOverlap(text: string): Set<string> {
|
|
166
|
+
const tokens = text.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
167
|
+
const out = new Set<string>();
|
|
168
|
+
for (const token of tokens) {
|
|
169
|
+
const normalized = normalizeToken(token);
|
|
170
|
+
if (normalized.length < 3 || OVERLAP_STOPWORDS.has(normalized)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
out.add(normalized);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function lexicalOverlap(queryTokens: Set<string>, text: string): { shared: number; ratio: number } {
|
|
179
|
+
if (queryTokens.size === 0) {
|
|
180
|
+
return { shared: 0, ratio: 0 };
|
|
181
|
+
}
|
|
182
|
+
const textTokens = tokenizeForOverlap(text);
|
|
183
|
+
let shared = 0;
|
|
184
|
+
for (const token of queryTokens) {
|
|
185
|
+
if (textTokens.has(token)) {
|
|
186
|
+
shared += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
shared,
|
|
191
|
+
ratio: shared / queryTokens.size,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeCategory(raw: unknown): "preference" | "decision" | "fact" | "task" | "other" | undefined {
|
|
196
|
+
if (typeof raw !== "string") {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
const normalized = raw.trim().toLowerCase();
|
|
200
|
+
if (
|
|
201
|
+
normalized === "preference" ||
|
|
202
|
+
normalized === "decision" ||
|
|
203
|
+
normalized === "fact" ||
|
|
204
|
+
normalized === "task" ||
|
|
205
|
+
normalized === "other"
|
|
206
|
+
) {
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeSessionKey(raw: unknown): string | undefined {
|
|
213
|
+
if (typeof raw !== "string") {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const trimmed = raw.trim();
|
|
217
|
+
return trimmed || undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isGenericUserSummary(text: string): boolean {
|
|
221
|
+
const normalized = text.trim().toLowerCase();
|
|
222
|
+
return (
|
|
223
|
+
/^(the user|user|usuario)\b/.test(normalized) ||
|
|
224
|
+
/\b(user|usuario)\s+(asked|wants|needs|prefers|likes|said)\b/.test(normalized)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function applyMem0QualityAdjustments(params: {
|
|
229
|
+
results: MemoryBraidResult[];
|
|
230
|
+
query: string;
|
|
231
|
+
scope: ScopeKey;
|
|
232
|
+
nowMs: number;
|
|
233
|
+
}): {
|
|
234
|
+
results: MemoryBraidResult[];
|
|
235
|
+
adjusted: number;
|
|
236
|
+
overlapBoosted: number;
|
|
237
|
+
overlapPenalized: number;
|
|
238
|
+
categoryPenalized: number;
|
|
239
|
+
sessionBoosted: number;
|
|
240
|
+
sessionPenalized: number;
|
|
241
|
+
genericPenalized: number;
|
|
242
|
+
} {
|
|
243
|
+
if (params.results.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
results: params.results,
|
|
246
|
+
adjusted: 0,
|
|
247
|
+
overlapBoosted: 0,
|
|
248
|
+
overlapPenalized: 0,
|
|
249
|
+
categoryPenalized: 0,
|
|
250
|
+
sessionBoosted: 0,
|
|
251
|
+
sessionPenalized: 0,
|
|
252
|
+
genericPenalized: 0,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const queryTokens = tokenizeForOverlap(params.query);
|
|
257
|
+
let adjusted = 0;
|
|
258
|
+
let overlapBoosted = 0;
|
|
259
|
+
let overlapPenalized = 0;
|
|
260
|
+
let categoryPenalized = 0;
|
|
261
|
+
let sessionBoosted = 0;
|
|
262
|
+
let sessionPenalized = 0;
|
|
263
|
+
let genericPenalized = 0;
|
|
264
|
+
|
|
265
|
+
const next = params.results.map((result, index) => {
|
|
266
|
+
let multiplier = 1;
|
|
267
|
+
const metadata = asRecord(result.metadata);
|
|
268
|
+
const overlap = lexicalOverlap(queryTokens, result.snippet);
|
|
269
|
+
const category = normalizeCategory(metadata.category);
|
|
270
|
+
const isGeneric = isGenericUserSummary(result.snippet);
|
|
271
|
+
const ts = resolveTimestampMs(result);
|
|
272
|
+
const ageDays = ts ? Math.max(0, (params.nowMs - ts) / (24 * 60 * 60 * 1000)) : undefined;
|
|
273
|
+
|
|
274
|
+
if ((category === "task" || category === "other") && typeof ageDays === "number") {
|
|
275
|
+
if (ageDays >= 30) {
|
|
276
|
+
multiplier *= 0.5;
|
|
277
|
+
categoryPenalized += 1;
|
|
278
|
+
} else if (ageDays >= 7) {
|
|
279
|
+
multiplier *= category === "task" ? 0.65 : 0.72;
|
|
280
|
+
categoryPenalized += 1;
|
|
281
|
+
}
|
|
282
|
+
} else if (typeof ageDays === "number" && ageDays >= 180) {
|
|
283
|
+
multiplier *= 0.9;
|
|
284
|
+
categoryPenalized += 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (queryTokens.size > 0) {
|
|
288
|
+
if (overlap.shared >= 2 || overlap.ratio >= 0.45) {
|
|
289
|
+
multiplier *= 1.25;
|
|
290
|
+
overlapBoosted += 1;
|
|
291
|
+
} else if (overlap.shared === 1 || overlap.ratio >= 0.2) {
|
|
292
|
+
multiplier *= 1.1;
|
|
293
|
+
overlapBoosted += 1;
|
|
294
|
+
} else {
|
|
295
|
+
multiplier *= 0.62;
|
|
296
|
+
overlapPenalized += 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const metadataSession =
|
|
301
|
+
normalizeSessionKey(metadata.sessionKey) ??
|
|
302
|
+
normalizeSessionKey(metadata.runId) ??
|
|
303
|
+
normalizeSessionKey(metadata.run_id);
|
|
304
|
+
if (params.scope.sessionKey && metadataSession) {
|
|
305
|
+
if (metadataSession === params.scope.sessionKey) {
|
|
306
|
+
multiplier *= 1.1;
|
|
307
|
+
sessionBoosted += 1;
|
|
308
|
+
} else {
|
|
309
|
+
multiplier *= 0.82;
|
|
310
|
+
sessionPenalized += 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (isGeneric && overlap.ratio < 0.2 && overlap.shared < 2) {
|
|
315
|
+
multiplier *= 0.6;
|
|
316
|
+
genericPenalized += 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const normalizedMultiplier = Math.min(2.5, Math.max(0.1, multiplier));
|
|
320
|
+
const nextScore = result.score * normalizedMultiplier;
|
|
321
|
+
if (nextScore !== result.score) {
|
|
322
|
+
adjusted += 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
index,
|
|
327
|
+
result: {
|
|
328
|
+
...result,
|
|
329
|
+
score: nextScore,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
next.sort((left, right) => {
|
|
335
|
+
const scoreDelta = right.result.score - left.result.score;
|
|
336
|
+
if (scoreDelta !== 0) {
|
|
337
|
+
return scoreDelta;
|
|
338
|
+
}
|
|
339
|
+
return left.index - right.index;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
results: next.map((entry) => entry.result),
|
|
344
|
+
adjusted,
|
|
345
|
+
overlapBoosted,
|
|
346
|
+
overlapPenalized,
|
|
347
|
+
categoryPenalized,
|
|
348
|
+
sessionBoosted,
|
|
349
|
+
sessionPenalized,
|
|
350
|
+
genericPenalized,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function selectMemoriesForInjection(params: {
|
|
355
|
+
query: string;
|
|
356
|
+
results: MemoryBraidResult[];
|
|
357
|
+
limit: number;
|
|
358
|
+
}): {
|
|
359
|
+
injected: MemoryBraidResult[];
|
|
360
|
+
queryTokens: number;
|
|
361
|
+
filteredOut: number;
|
|
362
|
+
genericRejected: number;
|
|
363
|
+
} {
|
|
364
|
+
const limit = Math.max(0, Math.floor(params.limit));
|
|
365
|
+
if (limit === 0 || params.results.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
injected: [],
|
|
368
|
+
queryTokens: 0,
|
|
369
|
+
filteredOut: 0,
|
|
370
|
+
genericRejected: 0,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const queryTokens = tokenizeForOverlap(params.query);
|
|
375
|
+
if (queryTokens.size === 0) {
|
|
376
|
+
return {
|
|
377
|
+
injected: params.results.slice(0, limit),
|
|
378
|
+
queryTokens: 0,
|
|
379
|
+
filteredOut: Math.max(0, params.results.length - Math.min(limit, params.results.length)),
|
|
380
|
+
genericRejected: 0,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const injected: MemoryBraidResult[] = [];
|
|
385
|
+
let filteredOut = 0;
|
|
386
|
+
let genericRejected = 0;
|
|
387
|
+
|
|
388
|
+
for (const result of params.results) {
|
|
389
|
+
if (injected.length >= limit) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
const overlap = lexicalOverlap(queryTokens, result.snippet);
|
|
393
|
+
const generic = isGenericUserSummary(result.snippet);
|
|
394
|
+
const strongThreshold = result.source === "local" ? 0.26 : 0.34;
|
|
395
|
+
const weakThreshold = result.source === "local" ? 0.12 : 0.18;
|
|
396
|
+
const strongMatch = overlap.shared >= 2 || overlap.ratio >= strongThreshold;
|
|
397
|
+
const weakMatch = overlap.shared >= 1 && overlap.ratio >= weakThreshold;
|
|
398
|
+
const keep = generic ? overlap.shared >= 2 || overlap.ratio >= 0.5 : strongMatch || weakMatch;
|
|
399
|
+
if (keep) {
|
|
400
|
+
injected.push(result);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
filteredOut += 1;
|
|
404
|
+
if (generic) {
|
|
405
|
+
genericRejected += 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
injected,
|
|
411
|
+
queryTokens: queryTokens.size,
|
|
412
|
+
filteredOut,
|
|
413
|
+
genericRejected,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
108
417
|
function resolveCoreTemporalDecay(params: {
|
|
109
418
|
config?: unknown;
|
|
110
419
|
agentId?: string;
|
|
@@ -528,6 +837,27 @@ async function runHybridRecall(params: {
|
|
|
528
837
|
});
|
|
529
838
|
}
|
|
530
839
|
}
|
|
840
|
+
const qualityAdjusted = applyMem0QualityAdjustments({
|
|
841
|
+
results: mem0ForMerge,
|
|
842
|
+
query: params.query,
|
|
843
|
+
scope,
|
|
844
|
+
nowMs: Date.now(),
|
|
845
|
+
});
|
|
846
|
+
mem0ForMerge = qualityAdjusted.results;
|
|
847
|
+
params.log.debug("memory_braid.search.mem0_quality", {
|
|
848
|
+
runId: params.runId,
|
|
849
|
+
agentId: scope.agentId,
|
|
850
|
+
sessionKey: scope.sessionKey,
|
|
851
|
+
workspaceHash: scope.workspaceHash,
|
|
852
|
+
inputCount: mem0Search.length,
|
|
853
|
+
adjusted: qualityAdjusted.adjusted,
|
|
854
|
+
overlapBoosted: qualityAdjusted.overlapBoosted,
|
|
855
|
+
overlapPenalized: qualityAdjusted.overlapPenalized,
|
|
856
|
+
categoryPenalized: qualityAdjusted.categoryPenalized,
|
|
857
|
+
sessionBoosted: qualityAdjusted.sessionBoosted,
|
|
858
|
+
sessionPenalized: qualityAdjusted.sessionPenalized,
|
|
859
|
+
genericPenalized: qualityAdjusted.genericPenalized,
|
|
860
|
+
});
|
|
531
861
|
params.log.debug("memory_braid.search.mem0", {
|
|
532
862
|
runId: params.runId,
|
|
533
863
|
agentId: scope.agentId,
|
|
@@ -913,22 +1243,43 @@ const memoryBraidPlugin = {
|
|
|
913
1243
|
runId,
|
|
914
1244
|
});
|
|
915
1245
|
|
|
916
|
-
const
|
|
917
|
-
|
|
1246
|
+
const selected = selectMemoriesForInjection({
|
|
1247
|
+
query: event.prompt,
|
|
1248
|
+
results: recall.merged,
|
|
1249
|
+
limit: cfg.recall.injectTopK,
|
|
1250
|
+
});
|
|
1251
|
+
if (selected.injected.length === 0) {
|
|
1252
|
+
const scope = resolveScopeFromHookContext(ctx);
|
|
1253
|
+
log.debug("memory_braid.search.inject", {
|
|
1254
|
+
runId,
|
|
1255
|
+
agentId: scope.agentId,
|
|
1256
|
+
sessionKey: scope.sessionKey,
|
|
1257
|
+
workspaceHash: scope.workspaceHash,
|
|
1258
|
+
count: 0,
|
|
1259
|
+
queryTokens: selected.queryTokens,
|
|
1260
|
+
filteredOut: selected.filteredOut,
|
|
1261
|
+
genericRejected: selected.genericRejected,
|
|
1262
|
+
reason: "no_relevant_memories",
|
|
1263
|
+
});
|
|
918
1264
|
return;
|
|
919
1265
|
}
|
|
920
1266
|
|
|
1267
|
+
const prependContext = formatRelevantMemories(selected.injected, cfg.debug.maxSnippetChars);
|
|
921
1268
|
const scope = resolveScopeFromHookContext(ctx);
|
|
922
1269
|
log.debug("memory_braid.search.inject", {
|
|
923
1270
|
runId,
|
|
924
1271
|
agentId: scope.agentId,
|
|
925
1272
|
sessionKey: scope.sessionKey,
|
|
926
1273
|
workspaceHash: scope.workspaceHash,
|
|
927
|
-
count: injected.length,
|
|
1274
|
+
count: selected.injected.length,
|
|
1275
|
+
queryTokens: selected.queryTokens,
|
|
1276
|
+
filteredOut: selected.filteredOut,
|
|
1277
|
+
genericRejected: selected.genericRejected,
|
|
1278
|
+
injectedTextPreview: prependContext,
|
|
928
1279
|
});
|
|
929
1280
|
|
|
930
1281
|
return {
|
|
931
|
-
prependContext
|
|
1282
|
+
prependContext,
|
|
932
1283
|
};
|
|
933
1284
|
});
|
|
934
1285
|
|