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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-braid",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "OpenClaw memory plugin that augments local memory with Mem0 capture and recall.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
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 injected = recall.merged.slice(0, cfg.recall.injectTopK);
917
- if (injected.length === 0) {
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: formatRelevantMemories(injected, cfg.debug.maxSnippetChars),
1282
+ prependContext,
932
1283
  };
933
1284
  });
934
1285