memory-braid 0.3.1 → 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
@@ -20,23 +20,35 @@ On the target machine:
20
20
  1. Install from npm:
21
21
 
22
22
  ```bash
23
- openclaw plugins install memory-braid@0.3.0
23
+ openclaw plugins install memory-braid@0.3.3
24
24
  ```
25
25
 
26
- 2. Enable and set as active memory slot:
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:
27
39
 
28
40
  ```bash
29
41
  openclaw plugins enable memory-braid
30
42
  openclaw config set plugins.slots.memory memory-braid
31
43
  ```
32
44
 
33
- 3. Restart gateway:
45
+ 4. Restart gateway:
34
46
 
35
47
  ```bash
36
48
  openclaw gateway restart
37
49
  ```
38
50
 
39
- 4. Confirm plugin is loaded:
51
+ 5. Confirm plugin is loaded:
40
52
 
41
53
  ```bash
42
54
  openclaw plugins info memory-braid
@@ -56,6 +68,19 @@ openclaw config set plugins.slots.memory memory-braid
56
68
  openclaw gateway restart
57
69
  ```
58
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
+
59
84
  ## Quick start: hybrid capture + multilingual NER
60
85
 
61
86
  Add this under `plugins.entries["memory-braid"].config` in your OpenClaw config:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-braid",
3
- "version": "0.3.1",
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",
package/src/index.ts CHANGED
@@ -593,6 +593,11 @@ const memoryBraidPlugin = {
593
593
  log,
594
594
  targets,
595
595
  runId,
596
+ }).catch((err) => {
597
+ log.warn("memory_braid.bootstrap.error", {
598
+ runId,
599
+ error: err instanceof Error ? err.message : String(err),
600
+ });
596
601
  });
597
602
 
598
603
  // One startup reconcile pass (non-blocking).
@@ -603,6 +608,13 @@ const memoryBraidPlugin = {
603
608
  log,
604
609
  targets,
605
610
  reason: "startup",
611
+ runId,
612
+ }).catch((err) => {
613
+ log.warn("memory_braid.reconcile.error", {
614
+ runId,
615
+ reason: "startup",
616
+ error: err instanceof Error ? err.message : String(err),
617
+ });
606
618
  });
607
619
 
608
620
  if (cfg.entityExtraction.enabled && cfg.entityExtraction.startup.downloadOnStartup) {
@@ -44,6 +44,8 @@ type OssClientLike = {
44
44
  delete: (memoryId: string) => Promise<{ message: string }>;
45
45
  };
46
46
 
47
+ type OssMemoryCtor = new (config?: Record<string, unknown>) => OssClientLike;
48
+
47
49
  function extractCloudText(memory: CloudRecord): string {
48
50
  const byData = memory.data?.memory;
49
51
  if (typeof byData === "string" && byData.trim()) {
@@ -102,6 +104,30 @@ function asNonEmptyString(value: unknown): string | undefined {
102
104
  return trimmed ? trimmed : undefined;
103
105
  }
104
106
 
107
+ function asOssMemoryCtor(value: unknown): OssMemoryCtor | undefined {
108
+ if (typeof value !== "function") {
109
+ return undefined;
110
+ }
111
+ return value as OssMemoryCtor;
112
+ }
113
+
114
+ export function resolveOssMemoryCtor(moduleValue: unknown): OssMemoryCtor | undefined {
115
+ if (!moduleValue) {
116
+ return undefined;
117
+ }
118
+
119
+ const mod = asRecord(moduleValue);
120
+ const defaultMod = asRecord(mod.default);
121
+
122
+ return (
123
+ asOssMemoryCtor(mod.Memory) ??
124
+ asOssMemoryCtor(mod.MemoryClient) ??
125
+ asOssMemoryCtor(defaultMod.Memory) ??
126
+ asOssMemoryCtor(defaultMod.MemoryClient) ??
127
+ asOssMemoryCtor(mod.default)
128
+ );
129
+ }
130
+
105
131
  function resolveStateDir(explicitStateDir?: string): string {
106
132
  const resolved =
107
133
  explicitStateDir?.trim() ||
@@ -319,10 +345,13 @@ export class Mem0Adapter {
319
345
 
320
346
  try {
321
347
  const mod = await import("mem0ai/oss");
322
- const Memory = (mod as { Memory?: new (config?: Record<string, unknown>) => OssClientLike })
323
- .Memory;
348
+ const Memory = resolveOssMemoryCtor(mod);
324
349
  if (!Memory) {
325
- throw new Error("mem0ai/oss Memory export not found");
350
+ const exportKeys = Object.keys(asRecord(mod));
351
+ const defaultKeys = Object.keys(asRecord(asRecord(mod).default));
352
+ throw new Error(
353
+ `mem0ai/oss Memory export not found (exports=${exportKeys.join(",") || "none"}; default=${defaultKeys.join(",") || "none"})`,
354
+ );
326
355
  }
327
356
 
328
357
  const providedConfig = this.cfg.mem0.ossConfig;
package/src/state.ts CHANGED
@@ -97,22 +97,38 @@ export async function writeCaptureDedupeState(
97
97
  export async function withStateLock<T>(
98
98
  lockFilePath: string,
99
99
  fn: () => Promise<T>,
100
- options?: { retries?: number; retryDelayMs?: number },
100
+ options?: { retries?: number; retryDelayMs?: number; staleLockMs?: number },
101
101
  ): Promise<T> {
102
102
  const retries = options?.retries ?? 12;
103
103
  const retryDelayMs = options?.retryDelayMs ?? 150;
104
+ const staleLockMs = options?.staleLockMs ?? 30_000;
104
105
  await fs.mkdir(path.dirname(lockFilePath), { recursive: true });
105
106
 
106
107
  let handle: fs.FileHandle | null = null;
107
108
  for (let attempt = 0; attempt <= retries; attempt += 1) {
108
109
  try {
109
110
  handle = await fs.open(lockFilePath, "wx");
111
+ await handle.writeFile(
112
+ `${JSON.stringify({
113
+ pid: process.pid,
114
+ startedAt: new Date().toISOString(),
115
+ })}\n`,
116
+ "utf8",
117
+ );
110
118
  break;
111
119
  } catch (err) {
112
120
  const code = (err as { code?: string }).code;
113
- if (code !== "EEXIST" || attempt >= retries) {
121
+ if (code !== "EEXIST") {
114
122
  throw err;
115
123
  }
124
+ const recovered = await recoverStaleLock(lockFilePath, staleLockMs);
125
+ if (recovered) {
126
+ attempt -= 1;
127
+ continue;
128
+ }
129
+ if (attempt >= retries) {
130
+ throw new Error(`Failed to acquire lock for ${lockFilePath}: lock file already exists`);
131
+ }
116
132
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
117
133
  }
118
134
  }
@@ -128,3 +144,56 @@ export async function withStateLock<T>(
128
144
  await fs.unlink(lockFilePath).catch(() => undefined);
129
145
  }
130
146
  }
147
+
148
+ function isProcessAlive(pid: number): boolean {
149
+ try {
150
+ process.kill(pid, 0);
151
+ return true;
152
+ } catch (err) {
153
+ const code = (err as { code?: string }).code;
154
+ if (code === "ESRCH") {
155
+ return false;
156
+ }
157
+ return true;
158
+ }
159
+ }
160
+
161
+ async function recoverStaleLock(lockFilePath: string, staleLockMs: number): Promise<boolean> {
162
+ let stat: Awaited<ReturnType<typeof fs.stat>>;
163
+ try {
164
+ stat = await fs.stat(lockFilePath);
165
+ } catch {
166
+ return false;
167
+ }
168
+
169
+ const ageMs = Date.now() - stat.mtimeMs;
170
+
171
+ let raw: string | null = null;
172
+ try {
173
+ raw = await fs.readFile(lockFilePath, "utf8");
174
+ } catch {
175
+ raw = null;
176
+ }
177
+
178
+ if (raw) {
179
+ try {
180
+ const parsed = JSON.parse(raw) as { pid?: unknown };
181
+ if (typeof parsed.pid === "number" && Number.isFinite(parsed.pid)) {
182
+ if (isProcessAlive(parsed.pid)) {
183
+ return false;
184
+ }
185
+ await fs.unlink(lockFilePath).catch(() => undefined);
186
+ return true;
187
+ }
188
+ } catch {
189
+ // Legacy lock file format, handled by age-based fallback below.
190
+ }
191
+ }
192
+
193
+ if (ageMs < staleLockMs) {
194
+ return false;
195
+ }
196
+
197
+ await fs.unlink(lockFilePath).catch(() => undefined);
198
+ return true;
199
+ }