memory-braid 0.4.1 → 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/package.json +1 -1
- package/src/index.ts +353 -4
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,19 +1243,38 @@ 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
|
|
|
921
|
-
const prependContext = formatRelevantMemories(injected, cfg.debug.maxSnippetChars);
|
|
1267
|
+
const prependContext = formatRelevantMemories(selected.injected, cfg.debug.maxSnippetChars);
|
|
922
1268
|
const scope = resolveScopeFromHookContext(ctx);
|
|
923
1269
|
log.debug("memory_braid.search.inject", {
|
|
924
1270
|
runId,
|
|
925
1271
|
agentId: scope.agentId,
|
|
926
1272
|
sessionKey: scope.sessionKey,
|
|
927
1273
|
workspaceHash: scope.workspaceHash,
|
|
928
|
-
count: injected.length,
|
|
1274
|
+
count: selected.injected.length,
|
|
1275
|
+
queryTokens: selected.queryTokens,
|
|
1276
|
+
filteredOut: selected.filteredOut,
|
|
1277
|
+
genericRejected: selected.genericRejected,
|
|
929
1278
|
injectedTextPreview: prependContext,
|
|
930
1279
|
});
|
|
931
1280
|
|