@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 CHANGED
@@ -1,6 +1,8 @@
1
- # OpenClaw Plugin for mnemos
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 mnemo to your project's `openclaw.json`:
25
+ Add mem9 to your project's `openclaw.json`:
24
26
 
25
27
  ```json
26
28
  {
27
29
  "plugins": {
28
- "slots": { "memory": "openclaw" },
30
+ "slots": { "memory": "mem9" },
29
31
  "entries": {
30
- "openclaw": {
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, injects relevant ones as context (3-min TTL cache) |
68
- | `after_compaction` | After `/compact` | Invalidates cache so the next prompt gets fresh memories from the database |
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 (upsert by key) |
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/openclaw
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 mnemo to your project's `openclaw.json`:
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": "openclaw"
144
+ "memory": "mem9"
143
145
  },
144
146
  "entries": {
145
- "openclaw": {
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/openclaw)
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": "openclaw"}` in openclaw.json |
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 { MemoryBackend } from "./backend.js";
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?: { maxIngestBytes?: number; fallbackAgentId?: string },
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 (!prompt || prompt.length < MIN_PROMPT_LEN) return;
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: prompt, limit: MAX_INJECT });
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
  });