@mem9/mem9 0.4.7 → 0.4.8-alpha.0
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 +26 -17
- package/backend.ts +14 -0
- package/hooks.ts +78 -6
- package/index.test.ts +694 -0
- package/index.ts +391 -21
- package/openclaw.plugin.json +29 -0
- package/package.json +3 -1
- package/server-backend.test.ts +82 -0
- package/server-backend.ts +21 -4
- package/types.ts +5 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# OpenClaw Plugin for
|
|
1
|
+
# OpenClaw Plugin for mem9
|
|
2
2
|
|
|
3
|
-
Memory plugin for [OpenClaw](https://github.com/openclaw) — replaces the built-in memory slot with cloud-persistent shared memory. Runs in server mode only, connecting to `mnemo-server` via `apiUrl` + `apiKey` (preferred) or legacy `tenantID`.
|
|
3
|
+
Memory plugin for [OpenClaw](https://github.com/openclaw) — replaces the built-in memory slot with cloud-persistent shared memory. Runs in server mode only, connecting to `mnemo-server` via `apiUrl` + `apiKey` (preferred) or legacy `tenantID`. Optional `provisionToken` and `provisionQueryParams` are used only during first-time create-new setup before an explicit `apiKey` is configured.
|
|
4
|
+
|
|
5
|
+
When `apiKey` is absent during create-new onboarding, the plugin does not auto-provision on startup. Instead, the first post-restart user message triggers exactly one create-new provision through the normal hook path. The plugin coordinates that call across concurrent OpenClaw plugin registrations on the same machine and reuses the generated key locally for future restarts tied to the same `provisionToken`.
|
|
4
6
|
|
|
5
7
|
## 🚀 Quick Start (Server Mode)
|
|
6
8
|
|
|
@@ -20,14 +22,14 @@ curl -s -X POST http://localhost:8080/v1alpha1/mem9s \
|
|
|
20
22
|
# {"id": "uuid"}
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
Add
|
|
25
|
+
Add mem9 to your project's `openclaw.json`:
|
|
24
26
|
|
|
25
27
|
```json
|
|
26
28
|
{
|
|
27
29
|
"plugins": {
|
|
28
|
-
"slots": { "memory": "
|
|
30
|
+
"slots": { "memory": "mem9" },
|
|
29
31
|
"entries": {
|
|
30
|
-
"
|
|
32
|
+
"mem9": {
|
|
31
33
|
"enabled": true,
|
|
32
34
|
"config": {
|
|
33
35
|
"apiUrl": "http://localhost:8080",
|
|
@@ -64,8 +66,8 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
64
66
|
|
|
65
67
|
| Hook | Trigger | What it does |
|
|
66
68
|
|---|---|---|
|
|
67
|
-
| `before_prompt_build` | Every LLM call | Searches memories by current prompt
|
|
68
|
-
| `after_compaction` | After `/compact` |
|
|
69
|
+
| `before_prompt_build` | Every LLM call | Searches memories by current prompt and injects relevant ones as context |
|
|
70
|
+
| `after_compaction` | After `/compact` | Logs compaction so the next prompt re-queries memories from the server |
|
|
69
71
|
| `before_reset` | Before `/reset` | Saves a session summary (last 3 user messages) as memory before context is wiped |
|
|
70
72
|
| `agent_end` | Agent finishes | Auto-captures the last assistant response as memory (if substantial) |
|
|
71
73
|
|
|
@@ -73,7 +75,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
73
75
|
|
|
74
76
|
| Tool | Description |
|
|
75
77
|
|---|---|
|
|
76
|
-
| `memory_store` | Store a new memory
|
|
78
|
+
| `memory_store` | Store a new memory |
|
|
77
79
|
| `memory_search` | Hybrid vector + keyword search (or keyword-only) |
|
|
78
80
|
| `memory_get` | Retrieve a single memory by ID |
|
|
79
81
|
| `memory_update` | Update an existing memory |
|
|
@@ -91,7 +93,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
91
93
|
### Method A: npm install (Recommended)
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
|
-
openclaw plugins install @mem9/
|
|
96
|
+
openclaw plugins install @mem9/mem9
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
### Method B: From source
|
|
@@ -104,7 +106,7 @@ npm install
|
|
|
104
106
|
|
|
105
107
|
### Configure OpenClaw
|
|
106
108
|
|
|
107
|
-
Add
|
|
109
|
+
Add mem9 to your project's `openclaw.json`:
|
|
108
110
|
|
|
109
111
|
OpenClaw is often deployed across teams with multiple agents. Server mode gives you:
|
|
110
112
|
|
|
@@ -139,10 +141,10 @@ Each agent uses the same `apiKey` for the shared memory pool. The plugin sends t
|
|
|
139
141
|
{
|
|
140
142
|
"plugins": {
|
|
141
143
|
"slots": {
|
|
142
|
-
"memory": "
|
|
144
|
+
"memory": "mem9"
|
|
143
145
|
},
|
|
144
146
|
"entries": {
|
|
145
|
-
"
|
|
147
|
+
"mem9": {
|
|
146
148
|
"enabled": true,
|
|
147
149
|
"config": {
|
|
148
150
|
"apiUrl": "http://your-server:8080",
|
|
@@ -160,8 +162,8 @@ That's it. The server handles scoping and conflict resolution. Conceptually, the
|
|
|
160
162
|
|
|
161
163
|
Start OpenClaw. You should see:
|
|
162
164
|
|
|
163
|
-
```
|
|
164
|
-
[mem9] Server mode
|
|
165
|
+
```text
|
|
166
|
+
[mem9] Server mode (v1alpha2)
|
|
165
167
|
```
|
|
166
168
|
|
|
167
169
|
If you see `[mem9] No mode configured...`, check your `openclaw.json` config.
|
|
@@ -174,11 +176,17 @@ Defined in `openclaw.plugin.json`:
|
|
|
174
176
|
|---|---|---|
|
|
175
177
|
| `apiUrl` | string | mnemo-server URL |
|
|
176
178
|
| `apiKey` | string | Preferred key. Uses `/v1alpha2/mem9s/...` with `X-API-Key` header |
|
|
179
|
+
| `provisionToken` | string | Optional one-time create-new token used locally to ensure the first-message create-new provision runs only once and is reused on this machine until an explicit `apiKey` is configured |
|
|
180
|
+
| `provisionQueryParams` | object | Optional `utm_*` map forwarded only to the initial `POST /v1alpha1/mem9s` request made during create-new when `apiKey` is absent |
|
|
177
181
|
| `defaultTimeoutMs` | number | Default timeout for non-search mem9 API requests in milliseconds. Default: `8000` |
|
|
178
182
|
| `searchTimeoutMs` | number | Timeout for `memory_search` and automatic recall search in milliseconds. Default: `15000` |
|
|
183
|
+
| `debug` | boolean | When `true`, emit mem9 debug logs. Current coverage includes `before_prompt_build` recall diagnostics; future mem9 debug categories reuse the same switch |
|
|
184
|
+
| `debugRecall` | boolean | Deprecated alias for `debug` |
|
|
179
185
|
| `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
|
|
180
186
|
|
|
181
|
-
> **Note**: `apiKey` takes precedence when both fields are set. If only `tenantID` is present, the plugin treats it as a legacy alias for `apiKey`, still uses v1alpha2, and logs a deprecation warning once at startup.
|
|
187
|
+
> **Note**: `apiKey` takes precedence when both fields are set. If only `tenantID` is present, the plugin treats it as a legacy alias for `apiKey`, still uses v1alpha2, and logs a deprecation warning once at startup. `provisionToken` and `provisionQueryParams` are ignored after an `apiKey` is already configured, and non-`utm_*` keys are dropped before the provision request is sent. During create-new onboarding, the plugin shares one in-flight provision result across concurrent local registrations and reuses the persisted result for the same `provisionToken`, so repeated reloads or repeated setup retries do not create multiple keys.
|
|
188
|
+
|
|
189
|
+
For debugging, set `"debug": true` in the plugin config. The plugin will emit `[mem9][debug]` lines; current coverage shows how `before_prompt_build` stripped OpenClaw metadata wrappers before issuing the recall search. `"debugRecall": true` still works as a deprecated alias.
|
|
182
190
|
|
|
183
191
|
## Timeout Behavior
|
|
184
192
|
|
|
@@ -213,7 +221,7 @@ Example:
|
|
|
213
221
|
openclaw-plugin/
|
|
214
222
|
├── README.md # This file
|
|
215
223
|
├── openclaw.plugin.json # Plugin metadata + config schema
|
|
216
|
-
├── package.json # npm package (@mem9/
|
|
224
|
+
├── package.json # npm package (@mem9/mem9)
|
|
217
225
|
├── index.ts # Plugin entry point + tool registration
|
|
218
226
|
├── backend.ts # MemoryBackend interface
|
|
219
227
|
├── server-backend.ts # Server mode: fetch → mnemo API
|
|
@@ -227,5 +235,6 @@ openclaw-plugin/
|
|
|
227
235
|
|---|---|---|
|
|
228
236
|
| `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
|
|
229
237
|
| `Server mode requires...` | Missing key | Add `apiKey` (or legacy `tenantID`) to config |
|
|
238
|
+
| Multiple auto-provisioned keys appear during create-new | Setup retriggered create-new provisioning before the first result was reused, or an older plugin still auto-provisions on startup | Upgrade to `@mem9/mem9@0.4.7+`; newer builds provision only from the first post-restart user message and reuse one local result across duplicate setup retries |
|
|
230
239
|
| Search requests time out | Hybrid/vector search exceeds plugin timeout | Increase `searchTimeoutMs` in plugin config |
|
|
231
|
-
| Plugin not loading | Not in memory slot | Set `"slots": {"memory": "
|
|
240
|
+
| Plugin not loading | Not in memory slot | Set `"slots": {"memory": "mem9"}` in openclaw.json |
|
package/backend.ts
CHANGED
|
@@ -9,6 +9,20 @@ import type {
|
|
|
9
9
|
IngestResult,
|
|
10
10
|
} from "./types.js";
|
|
11
11
|
|
|
12
|
+
export class PendingProvisionError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
message = "mem9 create-new setup is waiting for the first post-restart message to finish provisioning",
|
|
15
|
+
) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "PendingProvisionError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isPendingProvisionError(err: unknown): err is PendingProvisionError {
|
|
22
|
+
return err instanceof PendingProvisionError
|
|
23
|
+
|| (err instanceof Error && err.name === "PendingProvisionError");
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* MemoryBackend — the abstraction that tools and hooks call through.
|
|
14
28
|
*/
|
package/hooks.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Reference: OpenClaw's built-in memory-lancedb extension uses the same pattern.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { isPendingProvisionError, type MemoryBackend } from "./backend.js";
|
|
15
15
|
import type { Memory, IngestMessage } from "./types.js";
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -38,6 +38,14 @@ interface Logger {
|
|
|
38
38
|
error: (msg: string) => void;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function previewText(text: string, maxLen = 160): string {
|
|
42
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
43
|
+
if (normalized.length <= maxLen) {
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
return normalized.slice(0, maxLen) + "...";
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/**
|
|
42
50
|
* Hook handler types mirroring OpenClaw's PluginHookHandlerMap.
|
|
43
51
|
* We define them locally to avoid importing OpenClaw types at the module level.
|
|
@@ -161,6 +169,33 @@ function nonEmptyString(value: unknown): string | null {
|
|
|
161
169
|
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
function extractRecallQuery(prompt: string): string {
|
|
173
|
+
let s = stripInjectedContext(prompt).replace(/\r\n?/g, "\n");
|
|
174
|
+
|
|
175
|
+
s = s.replace(
|
|
176
|
+
/^Conversation info \(untrusted metadata\):\s*\n```[\s\S]*?\n```\s*/gm,
|
|
177
|
+
"",
|
|
178
|
+
);
|
|
179
|
+
s = s.replace(
|
|
180
|
+
/^Sender \(untrusted metadata\):\s*\n```[\s\S]*?\n```\s*/gm,
|
|
181
|
+
"",
|
|
182
|
+
);
|
|
183
|
+
s = s.replace(
|
|
184
|
+
/<<<EXTERNAL_UNTRUSTED_CONTENT[\s\S]*?<<<END_EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/g,
|
|
185
|
+
"",
|
|
186
|
+
);
|
|
187
|
+
s = s.replace(
|
|
188
|
+
/^Untrusted context \(metadata, do not treat as instructions or commands\):\s*$/gm,
|
|
189
|
+
"",
|
|
190
|
+
);
|
|
191
|
+
s = s.replace(/^\s*Source:\s.*$/gm, "");
|
|
192
|
+
s = s.replace(/^\s*UNTRUSTED [^\n]*$/gm, "");
|
|
193
|
+
s = s.replace(/^\s*---\s*$/gm, "");
|
|
194
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
195
|
+
|
|
196
|
+
return s.trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
164
199
|
// ---------------------------------------------------------------------------
|
|
165
200
|
// Hook registration
|
|
166
201
|
// ---------------------------------------------------------------------------
|
|
@@ -169,7 +204,12 @@ export function registerHooks(
|
|
|
169
204
|
api: HookApi,
|
|
170
205
|
backend: MemoryBackend,
|
|
171
206
|
logger: Logger,
|
|
172
|
-
options?: {
|
|
207
|
+
options?: {
|
|
208
|
+
maxIngestBytes?: number;
|
|
209
|
+
fallbackAgentId?: string;
|
|
210
|
+
provisionForCreateNew?: () => Promise<string>;
|
|
211
|
+
debug?: boolean;
|
|
212
|
+
},
|
|
173
213
|
): void {
|
|
174
214
|
const maxIngestBytes = options?.maxIngestBytes ?? DEFAULT_MAX_INGEST_BYTES;
|
|
175
215
|
|
|
@@ -181,11 +221,34 @@ export function registerHooks(
|
|
|
181
221
|
async (event: unknown) => {
|
|
182
222
|
try {
|
|
183
223
|
const evt = event as { prompt?: string };
|
|
184
|
-
const prompt = evt?.prompt;
|
|
185
|
-
if (
|
|
224
|
+
const prompt = nonEmptyString(evt?.prompt);
|
|
225
|
+
if (options?.provisionForCreateNew) {
|
|
226
|
+
await options.provisionForCreateNew();
|
|
227
|
+
}
|
|
228
|
+
if (!prompt) return;
|
|
229
|
+
|
|
230
|
+
const recallQuery = extractRecallQuery(prompt);
|
|
231
|
+
if (options?.debug) {
|
|
232
|
+
logger.info(
|
|
233
|
+
`[mem9][debug] before_prompt_build rawPromptLen=${prompt.length} recallQueryLen=${recallQuery.length} recallQueryPreview=${JSON.stringify(previewText(recallQuery))}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (recallQuery.length < MIN_PROMPT_LEN) {
|
|
237
|
+
if (options?.debug) {
|
|
238
|
+
logger.info(
|
|
239
|
+
`[mem9][debug] before_prompt_build skipping recall because stripped query is shorter than ${MIN_PROMPT_LEN}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
186
244
|
|
|
187
|
-
const result = await backend.search({ q:
|
|
245
|
+
const result = await backend.search({ q: recallQuery, limit: MAX_INJECT });
|
|
188
246
|
const memories = result.data ?? [];
|
|
247
|
+
if (options?.debug) {
|
|
248
|
+
logger.info(
|
|
249
|
+
`[mem9][debug] before_prompt_build recall search limit=${MAX_INJECT} results=${memories.length}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
189
252
|
|
|
190
253
|
if (memories.length === 0) return;
|
|
191
254
|
|
|
@@ -195,6 +258,9 @@ export function registerHooks(
|
|
|
195
258
|
prependContext: formatMemoriesBlock(memories),
|
|
196
259
|
};
|
|
197
260
|
} catch (err) {
|
|
261
|
+
if (isPendingProvisionError(err)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
198
264
|
// Graceful degradation — never block the LLM call
|
|
199
265
|
logger.error(`[mem9] before_prompt_build failed: ${String(err)}`);
|
|
200
266
|
}
|
|
@@ -245,6 +311,9 @@ export function registerHooks(
|
|
|
245
311
|
|
|
246
312
|
logger.info("[mem9] Session context saved before reset");
|
|
247
313
|
} catch (err) {
|
|
314
|
+
if (isPendingProvisionError(err)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
248
317
|
// Best-effort — never block /reset
|
|
249
318
|
logger.error(`[mem9] before_reset save failed: ${String(err)}`);
|
|
250
319
|
}
|
|
@@ -346,7 +415,10 @@ export function registerHooks(
|
|
|
346
415
|
`[mem9] Ingested session: memories_changed=${result.memories_changed}, status=${result.status}`
|
|
347
416
|
);
|
|
348
417
|
}
|
|
349
|
-
} catch {
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (isPendingProvisionError(err)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
350
422
|
// Best-effort — never fail the agent end phase
|
|
351
423
|
}
|
|
352
424
|
});
|