memory-braid 0.2.0 → 0.3.3

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 CHANGED
@@ -7,12 +7,182 @@ Memory Braid is an OpenClaw `kind: "memory"` plugin that augments local memory s
7
7
  - Hybrid recall: local memory + Mem0, merged with weighted RRF.
8
8
  - Install-time bootstrap import: indexes existing `MEMORY.md`, `memory.md`, `memory/**/*.md`, and recent sessions.
9
9
  - Periodic reconcile: keeps remote Mem0 chunks updated and deletes stale remote chunks.
10
- - Capture pipeline: heuristic extraction with optional ML enrichment mode.
10
+ - Capture pipeline modes: `local`, `hybrid`, `ml`.
11
+ - Optional entity extraction: multilingual NER with canonical `entity://...` URIs in memory metadata.
11
12
  - Structured debug logs for troubleshooting and tuning.
12
13
 
13
14
  ## Install
14
15
 
15
- Add this plugin to your OpenClaw plugin load path, then enable it as the active memory plugin.
16
+ ### Install from npm (recommended)
17
+
18
+ On the target machine:
19
+
20
+ 1. Install from npm:
21
+
22
+ ```bash
23
+ openclaw plugins install memory-braid@0.3.3
24
+ ```
25
+
26
+ 2. Rebuild native dependencies inside the installed extension:
27
+
28
+ ```bash
29
+ cd ~/.openclaw/extensions/memory-braid
30
+ npm rebuild sqlite3 sharp
31
+ ```
32
+
33
+ Why this step exists:
34
+ - OpenClaw plugin installs run `npm install --omit=dev --ignore-scripts` for safety.
35
+ - This behavior is currently not user-overridable from `openclaw plugins install`.
36
+ - `memory-braid` needs native artifacts for `sqlite3` (required by Mem0 OSS) and `sharp` (used by `@xenova/transformers`).
37
+
38
+ 3. Enable and set as active memory slot:
39
+
40
+ ```bash
41
+ openclaw plugins enable memory-braid
42
+ openclaw config set plugins.slots.memory memory-braid
43
+ ```
44
+
45
+ 4. Restart gateway:
46
+
47
+ ```bash
48
+ openclaw gateway restart
49
+ ```
50
+
51
+ 5. Confirm plugin is loaded:
52
+
53
+ ```bash
54
+ openclaw plugins info memory-braid
55
+ ```
56
+
57
+ Expected:
58
+ - `Status: loaded`
59
+ - `Tools: memory_search, memory_get`
60
+ - `Services: memory-braid-service`
61
+
62
+ ### Install from local path (development)
63
+
64
+ ```bash
65
+ openclaw plugins install --link /absolute/path/to/memory-braid
66
+ openclaw plugins enable memory-braid
67
+ openclaw config set plugins.slots.memory memory-braid
68
+ openclaw gateway restart
69
+ ```
70
+
71
+ If you install from npm and see native module errors like:
72
+
73
+ - `Could not locate the bindings file` (sqlite3)
74
+ - `Cannot find module ... sharp-*.node`
75
+
76
+ run:
77
+
78
+ ```bash
79
+ cd ~/.openclaw/extensions/memory-braid
80
+ npm rebuild sqlite3 sharp
81
+ openclaw gateway restart
82
+ ```
83
+
84
+ ## Quick start: hybrid capture + multilingual NER
85
+
86
+ Add this under `plugins.entries["memory-braid"].config` in your OpenClaw config:
87
+
88
+ ```json
89
+ {
90
+ "mem0": {
91
+ "mode": "oss",
92
+ "ossConfig": {
93
+ "version": "v1.1",
94
+ "embedder": {
95
+ "provider": "openai",
96
+ "config": {
97
+ "apiKey": "${OPENAI_API_KEY}",
98
+ "model": "text-embedding-3-small"
99
+ }
100
+ },
101
+ "vectorStore": {
102
+ "provider": "memory",
103
+ "config": {
104
+ "collectionName": "memories",
105
+ "dimension": 1536
106
+ }
107
+ },
108
+ "llm": {
109
+ "provider": "openai",
110
+ "config": {
111
+ "apiKey": "${OPENAI_API_KEY}",
112
+ "model": "gpt-4o-mini"
113
+ }
114
+ },
115
+ "enableGraph": false
116
+ }
117
+ },
118
+ "capture": {
119
+ "enabled": true,
120
+ "mode": "hybrid",
121
+ "maxItemsPerRun": 6,
122
+ "ml": {
123
+ "provider": "openai",
124
+ "model": "gpt-4o-mini",
125
+ "timeoutMs": 2500
126
+ }
127
+ },
128
+ "entityExtraction": {
129
+ "enabled": true,
130
+ "provider": "multilingual_ner",
131
+ "model": "Xenova/bert-base-multilingual-cased-ner-hrl",
132
+ "minScore": 0.65,
133
+ "maxEntitiesPerMemory": 8,
134
+ "startup": {
135
+ "downloadOnStartup": true,
136
+ "warmupText": "John works at Acme in Berlin."
137
+ }
138
+ },
139
+ "debug": {
140
+ "enabled": true
141
+ }
142
+ }
143
+ ```
144
+
145
+ Then restart:
146
+
147
+ ```bash
148
+ openclaw gateway restart
149
+ ```
150
+
151
+ ## Verification checklist
152
+
153
+ 1. Check runtime status:
154
+
155
+ ```bash
156
+ openclaw plugins info memory-braid
157
+ openclaw gateway status
158
+ ```
159
+
160
+ 2. Trigger/inspect NER warmup:
161
+
162
+ ```bash
163
+ openclaw agent --agent main --message "/memorybraid warmup" --json
164
+ ```
165
+
166
+ 3. Send a message that should be captured:
167
+
168
+ ```bash
169
+ openclaw agent --agent main --message "Remember that Ana works at OpenClaw and likes ramen." --json
170
+ ```
171
+
172
+ 4. Inspect logs for capture + NER:
173
+
174
+ ```bash
175
+ rg -n "memory_braid\\.startup|memory_braid\\.capture|memory_braid\\.entity|memory_braid\\.mem0" ~/.openclaw/logs/gateway.log | tail -n 80
176
+ ```
177
+
178
+ Expected events:
179
+ - `memory_braid.startup`
180
+ - `memory_braid.entity.model_load`
181
+ - `memory_braid.entity.warmup`
182
+ - `memory_braid.capture.extract`
183
+ - `memory_braid.capture.ml` (for `capture.mode=hybrid|ml`)
184
+ - `memory_braid.entity.extract`
185
+ - `memory_braid.capture.persist`
16
186
 
17
187
  ## Self-hosting quick guide
18
188
 
@@ -241,14 +411,23 @@ Use this preset when:
241
411
  },
242
412
  "capture": {
243
413
  "enabled": true,
244
- "extraction": {
245
- "mode": "heuristic"
246
- },
414
+ "mode": "hybrid",
415
+ "maxItemsPerRun": 6,
247
416
  "ml": {
248
417
  "provider": "openai",
249
418
  "model": "gpt-4o-mini",
250
- "timeoutMs": 2500,
251
- "maxItemsPerRun": 6
419
+ "timeoutMs": 2500
420
+ }
421
+ },
422
+ "entityExtraction": {
423
+ "enabled": true,
424
+ "provider": "multilingual_ner",
425
+ "model": "Xenova/bert-base-multilingual-cased-ner-hrl",
426
+ "minScore": 0.65,
427
+ "maxEntitiesPerMemory": 8,
428
+ "startup": {
429
+ "downloadOnStartup": true,
430
+ "warmupText": "John works at Acme in Berlin."
252
431
  }
253
432
  },
254
433
  "dedupe": {
@@ -266,6 +445,48 @@ Use this preset when:
266
445
  }
267
446
  ```
268
447
 
448
+ ## Capture defaults
449
+
450
+ Capture defaults are:
451
+
452
+ - `capture.enabled`: `true`
453
+ - `capture.mode`: `"local"`
454
+ - `capture.maxItemsPerRun`: `6`
455
+ - `capture.ml.provider`: unset
456
+ - `capture.ml.model`: unset
457
+ - `capture.ml.timeoutMs`: `2500`
458
+
459
+ Important behavior:
460
+
461
+ - `capture.mode = "local"`: heuristic-only extraction.
462
+ - `capture.mode = "hybrid"`: heuristic extraction + ML enrichment when ML config is set.
463
+ - `capture.mode = "ml"`: ML-first extraction; falls back to heuristic if ML config/call is unavailable.
464
+ - ML calls run only when both `capture.ml.provider` and `capture.ml.model` are set.
465
+
466
+ ## Entity extraction defaults
467
+
468
+ Entity extraction defaults are:
469
+
470
+ - `entityExtraction.enabled`: `false`
471
+ - `entityExtraction.provider`: `"multilingual_ner"`
472
+ - `entityExtraction.model`: `"Xenova/bert-base-multilingual-cased-ner-hrl"`
473
+ - `entityExtraction.minScore`: `0.65`
474
+ - `entityExtraction.maxEntitiesPerMemory`: `8`
475
+ - `entityExtraction.startup.downloadOnStartup`: `true`
476
+ - `entityExtraction.startup.warmupText`: `"John works at Acme in Berlin."`
477
+
478
+ When enabled:
479
+
480
+ - Model cache/download path is `<OPENCLAW_STATE_DIR>/memory-braid/models/entity-extraction` (typically `~/.openclaw/memory-braid/models/entity-extraction`).
481
+ - Captured memories get `metadata.entities` and `metadata.entityUris` (canonical IDs like `entity://person/john-doe`).
482
+ - Startup can pre-download/warm the model (`downloadOnStartup: true`).
483
+
484
+ Warmup command:
485
+
486
+ - `/memorybraid status`
487
+ - `/memorybraid warmup`
488
+ - `/memorybraid warmup --force`
489
+
269
490
  ## Debugging
270
491
 
271
492
  Set:
@@ -285,14 +506,35 @@ Set:
285
506
  Key events:
286
507
 
287
508
  - `memory_braid.startup`
509
+ - `memory_braid.config`
288
510
  - `memory_braid.bootstrap.begin|complete|error`
289
511
  - `memory_braid.reconcile.begin|progress|complete|error`
290
- - `memory_braid.search.local|mem0|merge|inject`
512
+ - `memory_braid.search.local|mem0|merge|inject|skip`
291
513
  - `memory_braid.capture.extract|ml|persist|skip`
514
+ - `memory_braid.entity.model_load|warmup|extract`
292
515
  - `memory_braid.mem0.request|response|error`
293
516
 
294
517
  `debug.includePayloads=true` includes payload fields; otherwise sensitive text fields are omitted.
295
518
 
519
+ Traceability tips:
520
+
521
+ - Use `runId` to follow one execution end-to-end across capture/search/entity/mem0 events.
522
+ - `memory_braid.capture.persist` includes high-signal counters:
523
+ - `dedupeSkipped`
524
+ - `mem0AddAttempts`
525
+ - `mem0AddWithId`
526
+ - `mem0AddWithoutId`
527
+ - `entityAnnotatedCandidates`
528
+ - `totalEntitiesAttached`
529
+ - `memory_braid.capture.ml` includes `fallbackUsed` and fallback reasons when ML is unavailable.
530
+ - `memory_braid.entity.extract` includes `entityTypes` and `sampleEntityUris`.
531
+
532
+ Example:
533
+
534
+ ```bash
535
+ rg -n "memory_braid\\.|runId\":\"<RUN_ID>\"" ~/.openclaw/logs/gateway.log | tail -n 120
536
+ ```
537
+
296
538
  ## Tests
297
539
 
298
540
  ```bash
@@ -47,25 +47,48 @@
47
47
  "additionalProperties": false,
48
48
  "properties": {
49
49
  "enabled": { "type": "boolean", "default": true },
50
- "extraction": {
50
+ "mode": {
51
+ "type": "string",
52
+ "enum": ["local", "hybrid", "ml"],
53
+ "default": "local"
54
+ },
55
+ "maxItemsPerRun": { "type": "integer", "minimum": 1, "maximum": 50, "default": 6 },
56
+ "ml": {
51
57
  "type": "object",
52
58
  "additionalProperties": false,
53
59
  "properties": {
54
- "mode": {
55
- "type": "string",
56
- "enum": ["heuristic", "heuristic_plus_ml"],
57
- "default": "heuristic"
58
- }
60
+ "provider": { "type": "string", "enum": ["openai", "anthropic", "gemini"] },
61
+ "model": { "type": "string" },
62
+ "timeoutMs": { "type": "integer", "minimum": 250, "maximum": 30000, "default": 2500 }
59
63
  }
64
+ }
65
+ }
66
+ },
67
+ "entityExtraction": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "enabled": { "type": "boolean", "default": false },
72
+ "provider": {
73
+ "type": "string",
74
+ "enum": ["multilingual_ner"],
75
+ "default": "multilingual_ner"
60
76
  },
61
- "ml": {
77
+ "model": {
78
+ "type": "string",
79
+ "default": "Xenova/bert-base-multilingual-cased-ner-hrl"
80
+ },
81
+ "minScore": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.65 },
82
+ "maxEntitiesPerMemory": { "type": "integer", "minimum": 1, "maximum": 50, "default": 8 },
83
+ "startup": {
62
84
  "type": "object",
63
85
  "additionalProperties": false,
64
86
  "properties": {
65
- "provider": { "type": "string", "enum": ["openai", "anthropic", "gemini"] },
66
- "model": { "type": "string" },
67
- "timeoutMs": { "type": "integer", "minimum": 250, "maximum": 30000, "default": 2500 },
68
- "maxItemsPerRun": { "type": "integer", "minimum": 1, "maximum": 50, "default": 6 }
87
+ "downloadOnStartup": { "type": "boolean", "default": true },
88
+ "warmupText": {
89
+ "type": "string",
90
+ "default": "John works at Acme in Berlin."
91
+ }
69
92
  }
70
93
  }
71
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-braid",
3
- "version": "0.2.0",
3
+ "version": "0.3.3",
4
4
  "description": "OpenClaw memory plugin that augments local memory with Mem0, bootstrap import, reconcile, and capture.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -31,6 +31,7 @@
31
31
  "openclaw": ">=2026.2.18"
32
32
  },
33
33
  "dependencies": {
34
+ "@xenova/transformers": "^2.17.2",
34
35
  "mem0ai": "^2.2.3"
35
36
  },
36
37
  "devDependencies": {
package/src/config.ts CHANGED
@@ -20,14 +20,23 @@ export type MemoryBraidConfig = {
20
20
  };
21
21
  capture: {
22
22
  enabled: boolean;
23
- extraction: {
24
- mode: "heuristic" | "heuristic_plus_ml";
25
- };
23
+ mode: "local" | "hybrid" | "ml";
24
+ maxItemsPerRun: number;
26
25
  ml: {
27
26
  provider?: "openai" | "anthropic" | "gemini";
28
27
  model?: string;
29
28
  timeoutMs: number;
30
- maxItemsPerRun: number;
29
+ };
30
+ };
31
+ entityExtraction: {
32
+ enabled: boolean;
33
+ provider: "multilingual_ner";
34
+ model: string;
35
+ minScore: number;
36
+ maxEntitiesPerMemory: number;
37
+ startup: {
38
+ downloadOnStartup: boolean;
39
+ warmupText: string;
31
40
  };
32
41
  };
33
42
  bootstrap: {
@@ -84,14 +93,23 @@ const DEFAULTS: MemoryBraidConfig = {
84
93
  },
85
94
  capture: {
86
95
  enabled: true,
87
- extraction: {
88
- mode: "heuristic",
89
- },
96
+ mode: "local",
97
+ maxItemsPerRun: 6,
90
98
  ml: {
91
99
  provider: undefined,
92
100
  model: undefined,
93
101
  timeoutMs: 2500,
94
- maxItemsPerRun: 6,
102
+ },
103
+ },
104
+ entityExtraction: {
105
+ enabled: false,
106
+ provider: "multilingual_ner",
107
+ model: "Xenova/bert-base-multilingual-cased-ner-hrl",
108
+ minScore: 0.65,
109
+ maxEntitiesPerMemory: 8,
110
+ startup: {
111
+ downloadOnStartup: true,
112
+ warmupText: "John works at Acme in Berlin.",
95
113
  },
96
114
  },
97
115
  bootstrap: {
@@ -160,7 +178,8 @@ export function parseConfig(raw: unknown): MemoryBraidConfig {
160
178
  const recall = asRecord(root.recall);
161
179
  const merge = asRecord(recall.merge);
162
180
  const capture = asRecord(root.capture);
163
- const extraction = asRecord(capture.extraction);
181
+ const entityExtraction = asRecord(root.entityExtraction);
182
+ const entityStartup = asRecord(entityExtraction.startup);
164
183
  const ml = asRecord(capture.ml);
165
184
  const bootstrap = asRecord(root.bootstrap);
166
185
  const reconcile = asRecord(root.reconcile);
@@ -170,8 +189,11 @@ export function parseConfig(raw: unknown): MemoryBraidConfig {
170
189
  const debug = asRecord(root.debug);
171
190
 
172
191
  const mode = mem0.mode === "oss" ? "oss" : "cloud";
173
- const extractionMode =
174
- extraction.mode === "heuristic_plus_ml" ? "heuristic_plus_ml" : "heuristic";
192
+ const rawCaptureMode = asString(capture.mode)?.toLowerCase();
193
+ const captureMode =
194
+ rawCaptureMode === "local" || rawCaptureMode === "hybrid" || rawCaptureMode === "ml"
195
+ ? rawCaptureMode
196
+ : DEFAULTS.capture.mode;
175
197
 
176
198
  return {
177
199
  enabled: asBoolean(root.enabled, DEFAULTS.enabled),
@@ -195,9 +217,8 @@ export function parseConfig(raw: unknown): MemoryBraidConfig {
195
217
  },
196
218
  capture: {
197
219
  enabled: asBoolean(capture.enabled, DEFAULTS.capture.enabled),
198
- extraction: {
199
- mode: extractionMode,
200
- },
220
+ mode: captureMode,
221
+ maxItemsPerRun: asInt(capture.maxItemsPerRun, DEFAULTS.capture.maxItemsPerRun, 1, 50),
201
222
  ml: {
202
223
  provider:
203
224
  ml.provider === "openai" || ml.provider === "anthropic" || ml.provider === "gemini"
@@ -205,7 +226,29 @@ export function parseConfig(raw: unknown): MemoryBraidConfig {
205
226
  : DEFAULTS.capture.ml.provider,
206
227
  model: asString(ml.model),
207
228
  timeoutMs: asInt(ml.timeoutMs, DEFAULTS.capture.ml.timeoutMs, 250, 30_000),
208
- maxItemsPerRun: asInt(ml.maxItemsPerRun, DEFAULTS.capture.ml.maxItemsPerRun, 1, 50),
229
+ },
230
+ },
231
+ entityExtraction: {
232
+ enabled: asBoolean(entityExtraction.enabled, DEFAULTS.entityExtraction.enabled),
233
+ provider:
234
+ entityExtraction.provider === "multilingual_ner"
235
+ ? "multilingual_ner"
236
+ : DEFAULTS.entityExtraction.provider,
237
+ model: asString(entityExtraction.model) ?? DEFAULTS.entityExtraction.model,
238
+ minScore: asNumber(entityExtraction.minScore, DEFAULTS.entityExtraction.minScore, 0, 1),
239
+ maxEntitiesPerMemory: asInt(
240
+ entityExtraction.maxEntitiesPerMemory,
241
+ DEFAULTS.entityExtraction.maxEntitiesPerMemory,
242
+ 1,
243
+ 50,
244
+ ),
245
+ startup: {
246
+ downloadOnStartup: asBoolean(
247
+ entityStartup.downloadOnStartup,
248
+ DEFAULTS.entityExtraction.startup.downloadOnStartup,
249
+ ),
250
+ warmupText:
251
+ asString(entityStartup.warmupText) ?? DEFAULTS.entityExtraction.startup.warmupText,
209
252
  },
210
253
  },
211
254
  bootstrap: {