memory-braid 0.3.1 → 0.3.4
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 +29 -4
- package/package.json +1 -1
- package/src/index.ts +12 -0
- package/src/mem0-client.ts +50 -3
- package/src/state.ts +71 -2
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.
|
|
23
|
+
openclaw plugins install memory-braid@0.3.4
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
2.
|
|
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
|
-
|
|
45
|
+
4. Restart gateway:
|
|
34
46
|
|
|
35
47
|
```bash
|
|
36
48
|
openclaw gateway restart
|
|
37
49
|
```
|
|
38
50
|
|
|
39
|
-
|
|
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
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) {
|
package/src/mem0-client.ts
CHANGED
|
@@ -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,48 @@ 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
|
+
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
115
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveCtorFromCandidate(candidate: unknown, depth = 0): OssMemoryCtor | undefined {
|
|
119
|
+
if (depth > 6 || !candidate) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const direct = asOssMemoryCtor(candidate);
|
|
124
|
+
if (direct) {
|
|
125
|
+
return direct;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!isObjectLike(candidate)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const record = candidate as Record<string, unknown>;
|
|
133
|
+
return (
|
|
134
|
+
resolveCtorFromCandidate(record.Memory, depth + 1) ??
|
|
135
|
+
resolveCtorFromCandidate(record.MemoryClient, depth + 1) ??
|
|
136
|
+
resolveCtorFromCandidate(record.default, depth + 1)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveOssMemoryCtor(moduleValue: unknown): OssMemoryCtor | undefined {
|
|
141
|
+
return (
|
|
142
|
+
resolveCtorFromCandidate(moduleValue) ??
|
|
143
|
+
resolveCtorFromCandidate(asRecord(moduleValue).Memory) ??
|
|
144
|
+
resolveCtorFromCandidate(asRecord(moduleValue).MemoryClient) ??
|
|
145
|
+
resolveCtorFromCandidate(asRecord(moduleValue).default)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
105
149
|
function resolveStateDir(explicitStateDir?: string): string {
|
|
106
150
|
const resolved =
|
|
107
151
|
explicitStateDir?.trim() ||
|
|
@@ -319,10 +363,13 @@ export class Mem0Adapter {
|
|
|
319
363
|
|
|
320
364
|
try {
|
|
321
365
|
const mod = await import("mem0ai/oss");
|
|
322
|
-
const Memory = (mod
|
|
323
|
-
.Memory;
|
|
366
|
+
const Memory = resolveOssMemoryCtor(mod);
|
|
324
367
|
if (!Memory) {
|
|
325
|
-
|
|
368
|
+
const exportKeys = Object.keys(asRecord(mod));
|
|
369
|
+
const defaultKeys = Object.keys(asRecord(asRecord(mod).default));
|
|
370
|
+
throw new Error(
|
|
371
|
+
`mem0ai/oss Memory export not found (exports=${exportKeys.join(",") || "none"}; default=${defaultKeys.join(",") || "none"})`,
|
|
372
|
+
);
|
|
326
373
|
}
|
|
327
374
|
|
|
328
375
|
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"
|
|
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
|
+
}
|