@mem9/mem9 0.4.5 → 0.4.6

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,8 +1,8 @@
1
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`. Optional `provisionToken` and `provisionQueryParams` are used only during first-time create-new setup before `apiKey` is written back.
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
4
 
5
- When `apiKey` is absent during create-new onboarding, the plugin does not auto-provision on startup. Instead, setup must call the explicit `mem9_provision_api_key` tool once after the first restart. The plugin coordinates that call across concurrent OpenClaw plugin registrations on the same machine and briefly reuses the generated key until config write-back completes.
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`.
6
6
 
7
7
  ## 🚀 Quick Start (Server Mode)
8
8
 
@@ -176,13 +176,13 @@ Defined in `openclaw.plugin.json`:
176
176
  |---|---|---|
177
177
  | `apiUrl` | string | mnemo-server URL |
178
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 explicit `mem9_provision_api_key` step runs only once before `apiKey` is persisted |
180
- | `provisionQueryParams` | object | Optional `utm_*` map forwarded only to the initial `POST /v1alpha1/mem9s` request made by `mem9_provision_api_key` when `apiKey` is absent |
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 |
181
181
  | `defaultTimeoutMs` | number | Default timeout for non-search mem9 API requests in milliseconds. Default: `8000` |
182
182
  | `searchTimeoutMs` | number | Timeout for `memory_search` and automatic recall search in milliseconds. Default: `15000` |
183
183
  | `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
184
184
 
185
- > **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 explicit provision result across concurrent local registrations so repeated reloads or repeated setup retries do not create multiple keys before config write-back.
185
+ > **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.
186
186
 
187
187
  ## Timeout Behavior
188
188
 
@@ -231,6 +231,6 @@ openclaw-plugin/
231
231
  |---|---|---|
232
232
  | `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
233
233
  | `Server mode requires...` | Missing key | Add `apiKey` (or legacy `tenantID`) to config |
234
- | Multiple auto-provisioned keys appear during create-new | Setup called provision more than once before apiKey write-back, or an older plugin still auto-provisions on startup | Upgrade to `@mem9/mem9@0.4.6+`; newer builds only provision through `mem9_provision_api_key` and share one local result across duplicate setup retries |
234
+ | 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 |
235
235
  | Search requests time out | Hybrid/vector search exceeds plugin timeout | Increase `searchTimeoutMs` in plugin config |
236
236
  | Plugin not loading | Not in memory slot | Set `"slots": {"memory": "mem9"}` in openclaw.json |
package/backend.ts CHANGED
@@ -10,7 +10,9 @@ import type {
10
10
  } from "./types.js";
11
11
 
12
12
  export class PendingProvisionError extends Error {
13
- constructor(message = "mem9 create-new setup is waiting for an explicit mem9_provision_api_key call") {
13
+ constructor(
14
+ message = "mem9 create-new setup is waiting for the first post-restart message to finish provisioning",
15
+ ) {
14
16
  super(message);
15
17
  this.name = "PendingProvisionError";
16
18
  }
package/hooks.ts CHANGED
@@ -169,7 +169,11 @@ export function registerHooks(
169
169
  api: HookApi,
170
170
  backend: MemoryBackend,
171
171
  logger: Logger,
172
- options?: { maxIngestBytes?: number; fallbackAgentId?: string },
172
+ options?: {
173
+ maxIngestBytes?: number;
174
+ fallbackAgentId?: string;
175
+ provisionForCreateNew?: () => Promise<string>;
176
+ },
173
177
  ): void {
174
178
  const maxIngestBytes = options?.maxIngestBytes ?? DEFAULT_MAX_INGEST_BYTES;
175
179
 
@@ -182,6 +186,9 @@ export function registerHooks(
182
186
  try {
183
187
  const evt = event as { prompt?: string };
184
188
  const prompt = evt?.prompt;
189
+ if (options?.provisionForCreateNew) {
190
+ await options.provisionForCreateNew();
191
+ }
185
192
  if (!prompt || prompt.length < MIN_PROMPT_LEN) return;
186
193
 
187
194
  const result = await backend.search({ q: prompt, limit: MAX_INJECT });
package/index.test.ts CHANGED
@@ -12,6 +12,8 @@ interface SearchCapability {
12
12
  search: (query: string, opts?: { limit?: number }) => Promise<{ data: unknown[]; total: number }>;
13
13
  }
14
14
 
15
+ type HookHandler = (...args: unknown[]) => unknown;
16
+
15
17
  interface StubApi {
16
18
  pluginConfig?: unknown;
17
19
  logger: {
@@ -22,6 +24,7 @@ interface StubApi {
22
24
  registerCapability?: (slot: string, capability: unknown) => void;
23
25
  on: (...args: unknown[]) => void;
24
26
  getTools: (ctx?: { agentId?: string }) => RegisteredTool[];
27
+ getHook: (name: string) => HookHandler;
25
28
  }
26
29
 
27
30
  function createStubApi(
@@ -34,6 +37,7 @@ function createStubApi(
34
37
  ): StubApi {
35
38
  const infoLogs = options?.infoLogs ?? [];
36
39
  const errorLogs = options?.errorLogs ?? [];
40
+ const hooks = new Map<string, HookHandler>();
37
41
  let toolFactory:
38
42
  | ((ctx?: { agentId?: string }) => RegisteredTool[] | RegisteredTool | null | undefined)
39
43
  | null = null;
@@ -52,7 +56,11 @@ function createStubApi(
52
56
  toolFactory = factory as typeof toolFactory;
53
57
  },
54
58
  registerCapability: options?.onRegisterCapability,
55
- on: () => {},
59
+ on: (hookName: unknown, handler: unknown) => {
60
+ if (typeof hookName === "string" && typeof handler === "function") {
61
+ hooks.set(hookName, handler as HookHandler);
62
+ }
63
+ },
56
64
  getTools: (ctx = {}) => {
57
65
  if (!toolFactory) {
58
66
  return [];
@@ -63,6 +71,13 @@ function createStubApi(
63
71
  }
64
72
  return Array.isArray(tools) ? tools : [tools];
65
73
  },
74
+ getHook: (name: string) => {
75
+ const hook = hooks.get(name);
76
+ if (!hook) {
77
+ throw new Error(`missing hook: ${name}`);
78
+ }
79
+ return hook;
80
+ },
66
81
  };
67
82
  }
68
83
 
@@ -85,21 +100,6 @@ function uniqueApiUrl(name: string): string {
85
100
  return `https://api.mem9.ai/${name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
86
101
  }
87
102
 
88
- function findTool(api: StubApi, name: string): RegisteredTool {
89
- const tool = api.getTools({ agentId: "agent-test" }).find((item) => item.name === name);
90
- if (!tool) {
91
- throw new Error(`missing tool: ${name}`);
92
- }
93
- return tool;
94
- }
95
-
96
- function parseToolResult(result: unknown): unknown {
97
- if (typeof result !== "string") {
98
- return result;
99
- }
100
- return JSON.parse(result);
101
- }
102
-
103
103
  test("register does not auto-provision on startup during create-new", async () => {
104
104
  const originalFetch = globalThis.fetch;
105
105
  const apiUrl = uniqueApiUrl("no-startup-provision");
@@ -131,7 +131,9 @@ test("register does not auto-provision on startup during create-new", async () =
131
131
  assert.deepEqual(requests, []);
132
132
  assert.deepEqual(errorLogs, []);
133
133
  assert.equal(
134
- infoLogs.includes("[mem9] apiKey not configured; waiting for explicit create-new provision"),
134
+ infoLogs.includes(
135
+ "[mem9] apiKey not configured; waiting for the first post-restart message to finish create-new provision",
136
+ ),
135
137
  true,
136
138
  );
137
139
  } finally {
@@ -176,7 +178,7 @@ test("memory capability stays idle until explicit provision runs", async () => {
176
178
  }
177
179
  });
178
180
 
179
- test("mem9_provision_api_key provisions once and unlocks memory access", async () => {
181
+ test("first post-restart prompt provisions once and unlocks memory access", async () => {
180
182
  const originalFetch = globalThis.fetch;
181
183
  const apiUrl = uniqueApiUrl("explicit-provision");
182
184
  let capability: SearchCapability | null = null;
@@ -220,8 +222,8 @@ test("mem9_provision_api_key provisions once and unlocks memory access", async (
220
222
  throw new Error(`unexpected fetch: ${url}`);
221
223
  };
222
224
 
223
- try {
224
- const api = createStubApi(
225
+ try {
226
+ const api = createStubApi(
225
227
  {
226
228
  apiUrl,
227
229
  provisionToken: "token-explicit",
@@ -237,14 +239,10 @@ test("mem9_provision_api_key provisions once and unlocks memory access", async (
237
239
  );
238
240
  mnemoPlugin.register(api);
239
241
 
240
- const tool = findTool(api, "mem9_provision_api_key");
241
- const provisionResult = parseToolResult(await tool.execute("tool-1", {})) as {
242
- ok: boolean;
243
- data?: { apiKey?: string };
244
- };
242
+ const beforePromptBuild = api.getHook("before_prompt_build");
243
+ const hookResult = await beforePromptBuild({ prompt: "hi" });
245
244
 
246
- assert.equal(provisionResult.ok, true);
247
- assert.equal(provisionResult.data?.apiKey, "space-explicit");
245
+ assert.equal(hookResult, undefined);
248
246
  assert.equal(provisionRequests, 1);
249
247
 
250
248
  assert.notEqual(capability, null);
@@ -258,7 +256,7 @@ test("mem9_provision_api_key provisions once and unlocks memory access", async (
258
256
  }
259
257
  });
260
258
 
261
- test("concurrent explicit provision calls share one server request", async () => {
259
+ test("concurrent first-post-restart prompts share one server request", async () => {
262
260
  const originalFetch = globalThis.fetch;
263
261
  const apiUrl = uniqueApiUrl("shared-explicit");
264
262
  let provisionRequests = 0;
@@ -297,37 +295,34 @@ test("concurrent explicit provision calls share one server request", async () =>
297
295
  mnemoPlugin.register(apiA);
298
296
  mnemoPlugin.register(apiB);
299
297
 
300
- const toolA = findTool(apiA, "mem9_provision_api_key");
301
- const toolB = findTool(apiB, "mem9_provision_api_key");
298
+ const hookA = apiA.getHook("before_prompt_build");
299
+ const hookB = apiB.getHook("before_prompt_build");
302
300
 
303
- const promiseA = toolA.execute("tool-a", {});
304
- const promiseB = toolB.execute("tool-b", {});
301
+ const promiseA = hookA({ prompt: "hi" });
302
+ const promiseB = hookB({ prompt: "hi" });
305
303
 
306
304
  await waitFor(() => provisionRequests === 1);
307
305
  provisionControl.release();
308
306
 
309
- const resultA = parseToolResult(await promiseA) as { ok: boolean; data?: { apiKey?: string } };
310
- const resultB = parseToolResult(await promiseB) as { ok: boolean; data?: { apiKey?: string } };
307
+ await promiseA;
308
+ await promiseB;
311
309
 
312
310
  assert.equal(provisionRequests, 1);
313
- assert.equal(resultA.ok, true);
314
- assert.equal(resultB.ok, true);
315
- assert.equal(resultA.data?.apiKey, "space-shared-explicit");
316
- assert.equal(resultB.data?.apiKey, "space-shared-explicit");
317
311
  } finally {
318
312
  provisionControl.release();
319
313
  globalThis.fetch = originalFetch;
320
314
  }
321
315
  });
322
316
 
323
- test("a second setup retry reuses the shared provisioned key before config write-back", async () => {
317
+ test("a second setup retry reuses the locally persisted provisioned key before config write-back", async () => {
324
318
  const originalFetch = globalThis.fetch;
325
319
  const apiUrl = uniqueApiUrl("shared-retry");
326
320
  const infoLogsA: string[] = [];
327
321
  const infoLogsB: string[] = [];
328
322
  let provisionRequests = 0;
323
+ let searchRequests = 0;
329
324
 
330
- globalThis.fetch = async (input) => {
325
+ globalThis.fetch = async (input, init) => {
331
326
  const url = String(input);
332
327
 
333
328
  if (url === `${apiUrl}/v1alpha1/mem9s`) {
@@ -340,6 +335,26 @@ test("a second setup retry reuses the shared provisioned key before config write
340
335
  });
341
336
  }
342
337
 
338
+ if (url.includes("/v1alpha2/mem9s/memories")) {
339
+ searchRequests += 1;
340
+ const headers = init?.headers as Record<string, string> | undefined;
341
+ assert.equal(headers?.["X-API-Key"], "space-shared-retry");
342
+ return new Response(
343
+ JSON.stringify({
344
+ memories: [],
345
+ total: 0,
346
+ limit: 20,
347
+ offset: 0,
348
+ }),
349
+ {
350
+ status: 200,
351
+ headers: {
352
+ "Content-Type": "application/json",
353
+ },
354
+ },
355
+ );
356
+ }
357
+
343
358
  throw new Error(`unexpected fetch: ${url}`);
344
359
  };
345
360
 
@@ -351,28 +366,25 @@ test("a second setup retry reuses the shared provisioned key before config write
351
366
 
352
367
  const apiA = createStubApi(pluginConfig, { infoLogs: infoLogsA });
353
368
  mnemoPlugin.register(apiA);
354
- const firstTool = findTool(apiA, "mem9_provision_api_key");
355
- const firstResult = parseToolResult(await firstTool.execute("tool-a", {})) as {
356
- ok: boolean;
357
- data?: { apiKey?: string };
358
- };
359
-
360
- assert.equal(firstResult.ok, true);
361
- assert.equal(firstResult.data?.apiKey, "space-shared-retry");
362
-
363
- const apiB = createStubApi(pluginConfig, { infoLogs: infoLogsB });
369
+ const firstHook = apiA.getHook("before_prompt_build");
370
+ await firstHook({ prompt: "hi" });
371
+
372
+ let capabilityB: SearchCapability | null = null;
373
+ const apiB = createStubApi(pluginConfig, {
374
+ infoLogs: infoLogsB,
375
+ onRegisterCapability: (_slot, registeredCapability) => {
376
+ capabilityB = registeredCapability as SearchCapability;
377
+ },
378
+ });
364
379
  mnemoPlugin.register(apiB);
365
- const secondTool = findTool(apiB, "mem9_provision_api_key");
366
- const secondResult = parseToolResult(await secondTool.execute("tool-b", {})) as {
367
- ok: boolean;
368
- data?: { apiKey?: string };
369
- };
380
+ assert.notEqual(capabilityB, null);
381
+ const secondResult = await capabilityB!.search("hello");
370
382
 
371
383
  assert.equal(provisionRequests, 1);
372
- assert.equal(secondResult.ok, true);
373
- assert.equal(secondResult.data?.apiKey, "space-shared-retry");
384
+ assert.deepEqual(secondResult, { data: [], total: 0 });
385
+ assert.equal(searchRequests, 1);
374
386
  assert.equal(
375
- infoLogsB.includes("[mem9] reusing shared explicit-provision result pending config write-back"),
387
+ infoLogsB.includes("[mem9] reusing locally persisted create-new API key for this provisionToken"),
376
388
  true,
377
389
  );
378
390
  } finally {
package/index.ts CHANGED
@@ -25,7 +25,6 @@ const DEFAULT_API_URL = "https://api.mem9.ai";
25
25
  const TIMEOUT_FIELDS = ["defaultTimeoutMs", "searchTimeoutMs"] as const;
26
26
  const SHARED_PROVISION_DIR = path.join(os.homedir(), ".openclaw", "mem9", "provision");
27
27
  const SHARED_PROVISION_POLL_INTERVAL_MS = 250;
28
- const SHARED_PROVISION_RESULT_TTL_MS = 2 * 60 * 1000;
29
28
  const sharedProvisionPromises = new Map<string, Promise<string>>();
30
29
 
31
30
  type SharedProvisionState =
@@ -231,11 +230,7 @@ async function waitForSharedProvisionResult(
231
230
  }
232
231
 
233
232
  if (state.status === "done") {
234
- if (now - state.finishedAt <= SHARED_PROVISION_RESULT_TTL_MS) {
235
- return state.apiKey;
236
- }
237
- await removeSharedProvisionState(filePath);
238
- return null;
233
+ return state.apiKey;
239
234
  }
240
235
 
241
236
  if (state.status === "error") {
@@ -271,25 +266,21 @@ async function resolveSharedProvisionedAPIKey(
271
266
  const sharedPromise = (async () => {
272
267
  while (true) {
273
268
  const state = await readSharedProvisionState(filePath);
274
- const now = Date.now();
275
269
 
276
270
  if (state?.status === "done") {
277
- if (now - state.finishedAt <= SHARED_PROVISION_RESULT_TTL_MS) {
278
- logger.info("[mem9] reusing shared explicit-provision result pending config write-back");
279
- return state.apiKey;
280
- }
281
- await removeSharedProvisionState(filePath);
282
- continue;
271
+ logger.info("[mem9] reusing locally persisted create-new API key for this provisionToken");
272
+ return state.apiKey;
283
273
  }
284
274
 
285
275
  if (state?.status === "error") {
286
276
  await removeSharedProvisionState(filePath);
287
277
  } else if (state?.status === "pending") {
278
+ const now = Date.now();
288
279
  if (now - state.startedAt > waitTimeoutMs) {
289
280
  await removeSharedProvisionState(filePath);
290
281
  continue;
291
282
  }
292
- logger.info("[mem9] explicit provision already in progress in another mem9 instance; waiting");
283
+ logger.info("[mem9] create-new provision already in progress in another mem9 instance; waiting");
293
284
  const sharedApiKey = await waitForSharedProvisionResult(filePath, waitTimeoutMs);
294
285
  if (sharedApiKey) {
295
286
  return sharedApiKey;
@@ -374,7 +365,6 @@ interface AnyAgentTool {
374
365
 
375
366
  function buildTools(
376
367
  backend: MemoryBackend,
377
- provisionAPIKey: (() => Promise<string>) | null,
378
368
  ): AnyAgentTool[] {
379
369
  return [
380
370
  {
@@ -548,41 +538,6 @@ function buildTools(
548
538
  }
549
539
  },
550
540
  },
551
-
552
- {
553
- name: "mem9_provision_api_key",
554
- label: "Provision mem9 API Key",
555
- description:
556
- "Create exactly one mem9 API key during create-new setup after the first restart. Use only when apiKey is absent.",
557
- parameters: {
558
- type: "object",
559
- properties: {},
560
- required: [],
561
- },
562
- async execute(_id: string, _params: unknown) {
563
- if (!provisionAPIKey) {
564
- return jsonResult({
565
- ok: false,
566
- error: "mem9_provision_api_key is only available during create-new setup before apiKey is persisted",
567
- });
568
- }
569
-
570
- try {
571
- const apiKey = await provisionAPIKey();
572
- return jsonResult({
573
- ok: true,
574
- data: {
575
- apiKey,
576
- },
577
- });
578
- } catch (err) {
579
- return jsonResult({
580
- ok: false,
581
- error: err instanceof Error ? err.message : String(err),
582
- });
583
- }
584
- },
585
- },
586
541
  ];
587
542
  }
588
543
 
@@ -607,6 +562,7 @@ const mnemoPlugin = {
607
562
  }
608
563
 
609
564
  const configuredApiKey = cfg.apiKey ?? cfg.tenantID;
565
+ const provisionWaitTimeoutMs = Math.max(timeoutConfig.defaultTimeoutMs + 5_000, 30_000);
610
566
  if (cfg.apiKey && cfg.tenantID) {
611
567
  api.logger.info("[mem9] both apiKey and tenantID set; using apiKey");
612
568
  } else if (cfg.tenantID) {
@@ -624,17 +580,18 @@ const mnemoPlugin = {
624
580
  );
625
581
  const result = await backend.register();
626
582
  api.logger.info(
627
- `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
583
+ `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this for recovery or reconnect as apiKey`
628
584
  );
629
585
  return result.id;
630
586
  };
631
587
  let runtimeProvisionedAPIKey: string | null = null;
588
+ let loggedPersistedProvisionedAPIKeyReuse = false;
632
589
  let registrationPromise: Promise<string> | null = null;
633
590
  const provisionAPIKey = (agentName: string): Promise<string> => {
634
591
  if (configuredApiKey) {
635
592
  return Promise.reject(
636
593
  new Error(
637
- "mem9_provision_api_key is only available during create-new setup before apiKey is persisted",
594
+ "mem9 create-new auto-provision is only available before apiKey is configured",
638
595
  ),
639
596
  );
640
597
  }
@@ -666,18 +623,39 @@ const mnemoPlugin = {
666
623
  }
667
624
  return registrationPromise;
668
625
  };
669
- const resolveAPIKey = (): Promise<string> => {
626
+ const resolveAPIKey = async (): Promise<string> => {
670
627
  if (configuredApiKey) return Promise.resolve(configuredApiKey);
671
628
  if (runtimeProvisionedAPIKey) {
672
- return Promise.resolve(runtimeProvisionedAPIKey);
629
+ return runtimeProvisionedAPIKey;
630
+ }
631
+ if (configuredProvisionToken) {
632
+ const sharedKey = sharedProvisionKey(
633
+ effectiveApiUrl,
634
+ provisionQueryParams,
635
+ configuredProvisionToken,
636
+ );
637
+ const sharedApiKey = await waitForSharedProvisionResult(
638
+ sharedProvisionStatePath(sharedKey),
639
+ provisionWaitTimeoutMs,
640
+ );
641
+ if (sharedApiKey) {
642
+ runtimeProvisionedAPIKey = sharedApiKey;
643
+ if (!loggedPersistedProvisionedAPIKeyReuse) {
644
+ api.logger.info("[mem9] reusing locally persisted create-new API key for this provisionToken");
645
+ loggedPersistedProvisionedAPIKeyReuse = true;
646
+ }
647
+ return sharedApiKey;
648
+ }
673
649
  }
674
- return Promise.reject(new PendingProvisionError());
650
+ throw new PendingProvisionError();
675
651
  };
676
652
 
677
653
  api.logger.info("[mem9] Server mode (v1alpha2)");
678
654
  if (!configuredApiKey) {
679
655
  if (configuredProvisionToken) {
680
- api.logger.info("[mem9] apiKey not configured; waiting for explicit create-new provision");
656
+ api.logger.info(
657
+ "[mem9] apiKey not configured; waiting for the first post-restart message to finish create-new provision",
658
+ );
681
659
  } else {
682
660
  api.logger.info("[mem9] apiKey not configured; mem9 will stay idle until apiKey is configured");
683
661
  }
@@ -691,7 +669,7 @@ const mnemoPlugin = {
691
669
  agentId,
692
670
  timeoutConfig,
693
671
  );
694
- return buildTools(backend, configuredApiKey ? null : () => provisionAPIKey(agentId));
672
+ return buildTools(backend);
695
673
  };
696
674
 
697
675
  api.registerTool(factory, { names: toolNames });
@@ -750,6 +728,9 @@ const mnemoPlugin = {
750
728
  registerHooks(api, hookBackend, api.logger, {
751
729
  maxIngestBytes: cfg.maxIngestBytes,
752
730
  fallbackAgentId: hookAgentId,
731
+ provisionForCreateNew: configuredApiKey || !configuredProvisionToken
732
+ ? undefined
733
+ : () => provisionAPIKey(hookAgentId),
753
734
  });
754
735
  },
755
736
  };
@@ -760,7 +741,6 @@ const toolNames = [
760
741
  "memory_get",
761
742
  "memory_update",
762
743
  "memory_delete",
763
- "mem9_provision_api_key",
764
744
  ];
765
745
 
766
746
  class LazyServerBackend implements MemoryBackend {
@@ -16,11 +16,11 @@
16
16
  },
17
17
  "provisionToken": {
18
18
  "type": "string",
19
- "description": "One-time create-new token used locally to ensure explicit API-key provisioning runs only once before apiKey is persisted"
19
+ "description": "One-time create-new token used locally to ensure first-message API-key provisioning runs only once and can be reused on this machine before apiKey is configured explicitly"
20
20
  },
21
21
  "provisionQueryParams": {
22
22
  "type": "object",
23
- "description": "Optional utm_* params forwarded only by the explicit first-time create-new provision step",
23
+ "description": "Optional utm_* params forwarded only by the first create-new provision request triggered after the initial restart",
24
24
  "additionalProperties": {
25
25
  "type": "string"
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "OpenClaw shared memory plugin — cloud-persistent memory with hybrid vector + keyword search via mnemo-server",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",