@mem9/mem9 0.4.5 → 0.4.7

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,6 @@
1
- # OpenClaw Plugin for mem9
1
+ # OpenClaw Plugin for mnemos
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.
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.
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`.
6
4
 
7
5
  ## 🚀 Quick Start (Server Mode)
8
6
 
@@ -22,14 +20,14 @@ curl -s -X POST http://localhost:8080/v1alpha1/mem9s \
22
20
  # {"id": "uuid"}
23
21
  ```
24
22
 
25
- Add mem9 to your project's `openclaw.json`:
23
+ Add mnemo to your project's `openclaw.json`:
26
24
 
27
25
  ```json
28
26
  {
29
27
  "plugins": {
30
- "slots": { "memory": "mem9" },
28
+ "slots": { "memory": "openclaw" },
31
29
  "entries": {
32
- "mem9": {
30
+ "openclaw": {
33
31
  "enabled": true,
34
32
  "config": {
35
33
  "apiUrl": "http://localhost:8080",
@@ -66,8 +64,8 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
66
64
 
67
65
  | Hook | Trigger | What it does |
68
66
  |---|---|---|
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 |
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 |
71
69
  | `before_reset` | Before `/reset` | Saves a session summary (last 3 user messages) as memory before context is wiped |
72
70
  | `agent_end` | Agent finishes | Auto-captures the last assistant response as memory (if substantial) |
73
71
 
@@ -75,7 +73,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
75
73
 
76
74
  | Tool | Description |
77
75
  |---|---|
78
- | `memory_store` | Store a new memory |
76
+ | `memory_store` | Store a new memory (upsert by key) |
79
77
  | `memory_search` | Hybrid vector + keyword search (or keyword-only) |
80
78
  | `memory_get` | Retrieve a single memory by ID |
81
79
  | `memory_update` | Update an existing memory |
@@ -93,7 +91,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
93
91
  ### Method A: npm install (Recommended)
94
92
 
95
93
  ```bash
96
- openclaw plugins install @mem9/mem9
94
+ openclaw plugins install @mem9/openclaw
97
95
  ```
98
96
 
99
97
  ### Method B: From source
@@ -106,7 +104,7 @@ npm install
106
104
 
107
105
  ### Configure OpenClaw
108
106
 
109
- Add mem9 to your project's `openclaw.json`:
107
+ Add mnemo to your project's `openclaw.json`:
110
108
 
111
109
  OpenClaw is often deployed across teams with multiple agents. Server mode gives you:
112
110
 
@@ -141,10 +139,10 @@ Each agent uses the same `apiKey` for the shared memory pool. The plugin sends t
141
139
  {
142
140
  "plugins": {
143
141
  "slots": {
144
- "memory": "mem9"
142
+ "memory": "openclaw"
145
143
  },
146
144
  "entries": {
147
- "mem9": {
145
+ "openclaw": {
148
146
  "enabled": true,
149
147
  "config": {
150
148
  "apiUrl": "http://your-server:8080",
@@ -162,8 +160,8 @@ That's it. The server handles scoping and conflict resolution. Conceptually, the
162
160
 
163
161
  Start OpenClaw. You should see:
164
162
 
165
- ```text
166
- [mem9] Server mode (v1alpha2)
163
+ ```
164
+ [mem9] Server mode
167
165
  ```
168
166
 
169
167
  If you see `[mem9] No mode configured...`, check your `openclaw.json` config.
@@ -176,13 +174,11 @@ Defined in `openclaw.plugin.json`:
176
174
  |---|---|---|
177
175
  | `apiUrl` | string | mnemo-server URL |
178
176
  | `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 |
181
177
  | `defaultTimeoutMs` | number | Default timeout for non-search mem9 API requests in milliseconds. Default: `8000` |
182
178
  | `searchTimeoutMs` | number | Timeout for `memory_search` and automatic recall search in milliseconds. Default: `15000` |
183
179
  | `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
184
180
 
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.
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.
186
182
 
187
183
  ## Timeout Behavior
188
184
 
@@ -217,7 +213,7 @@ Example:
217
213
  openclaw-plugin/
218
214
  ├── README.md # This file
219
215
  ├── openclaw.plugin.json # Plugin metadata + config schema
220
- ├── package.json # npm package (@mem9/mem9)
216
+ ├── package.json # npm package (@mem9/openclaw)
221
217
  ├── index.ts # Plugin entry point + tool registration
222
218
  ├── backend.ts # MemoryBackend interface
223
219
  ├── server-backend.ts # Server mode: fetch → mnemo API
@@ -231,6 +227,5 @@ openclaw-plugin/
231
227
  |---|---|---|
232
228
  | `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
233
229
  | `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 |
235
230
  | Search requests time out | Hybrid/vector search exceeds plugin timeout | Increase `searchTimeoutMs` in plugin config |
236
- | Plugin not loading | Not in memory slot | Set `"slots": {"memory": "mem9"}` in openclaw.json |
231
+ | Plugin not loading | Not in memory slot | Set `"slots": {"memory": "openclaw"}` in openclaw.json |
package/backend.ts CHANGED
@@ -9,18 +9,6 @@ import type {
9
9
  IngestResult,
10
10
  } from "./types.js";
11
11
 
12
- export class PendingProvisionError extends Error {
13
- constructor(message = "mem9 create-new setup is waiting for an explicit mem9_provision_api_key call") {
14
- super(message);
15
- this.name = "PendingProvisionError";
16
- }
17
- }
18
-
19
- export function isPendingProvisionError(err: unknown): err is PendingProvisionError {
20
- return err instanceof PendingProvisionError
21
- || (err instanceof Error && err.name === "PendingProvisionError");
22
- }
23
-
24
12
  /**
25
13
  * MemoryBackend — the abstraction that tools and hooks call through.
26
14
  */
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 { isPendingProvisionError, type MemoryBackend } from "./backend.js";
14
+ import type { MemoryBackend } from "./backend.js";
15
15
  import type { Memory, IngestMessage } from "./types.js";
16
16
 
17
17
  // ---------------------------------------------------------------------------
@@ -195,9 +195,6 @@ export function registerHooks(
195
195
  prependContext: formatMemoriesBlock(memories),
196
196
  };
197
197
  } catch (err) {
198
- if (isPendingProvisionError(err)) {
199
- return;
200
- }
201
198
  // Graceful degradation — never block the LLM call
202
199
  logger.error(`[mem9] before_prompt_build failed: ${String(err)}`);
203
200
  }
@@ -248,9 +245,6 @@ export function registerHooks(
248
245
 
249
246
  logger.info("[mem9] Session context saved before reset");
250
247
  } catch (err) {
251
- if (isPendingProvisionError(err)) {
252
- return;
253
- }
254
248
  // Best-effort — never block /reset
255
249
  logger.error(`[mem9] before_reset save failed: ${String(err)}`);
256
250
  }
@@ -352,10 +346,7 @@ export function registerHooks(
352
346
  `[mem9] Ingested session: memories_changed=${result.memories_changed}, status=${result.status}`
353
347
  );
354
348
  }
355
- } catch (err) {
356
- if (isPendingProvisionError(err)) {
357
- return;
358
- }
349
+ } catch {
359
350
  // Best-effort — never fail the agent end phase
360
351
  }
361
352
  });
package/index.ts CHANGED
@@ -1,9 +1,4 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- import { PendingProvisionError, isPendingProvisionError, type MemoryBackend } from "./backend.js";
1
+ import type { MemoryBackend } from "./backend.js";
7
2
  import {
8
3
  DEFAULT_SEARCH_TIMEOUT_MS,
9
4
  DEFAULT_TIMEOUT_MS,
@@ -23,29 +18,6 @@ import type {
23
18
 
24
19
  const DEFAULT_API_URL = "https://api.mem9.ai";
25
20
  const TIMEOUT_FIELDS = ["defaultTimeoutMs", "searchTimeoutMs"] as const;
26
- const SHARED_PROVISION_DIR = path.join(os.homedir(), ".openclaw", "mem9", "provision");
27
- const SHARED_PROVISION_POLL_INTERVAL_MS = 250;
28
- const SHARED_PROVISION_RESULT_TTL_MS = 2 * 60 * 1000;
29
- const sharedProvisionPromises = new Map<string, Promise<string>>();
30
-
31
- type SharedProvisionState =
32
- | {
33
- status: "pending";
34
- startedAt: number;
35
- pid: number;
36
- }
37
- | {
38
- status: "done";
39
- startedAt: number;
40
- finishedAt: number;
41
- apiKey: string;
42
- }
43
- | {
44
- status: "error";
45
- startedAt: number;
46
- finishedAt: number;
47
- error: string;
48
- };
49
21
 
50
22
  function normalizeTimeoutMs(
51
23
  value: unknown,
@@ -103,233 +75,6 @@ function jsonResult(data: unknown) {
103
75
  }
104
76
  }
105
77
 
106
- function errorMessage(err: unknown): string {
107
- return err instanceof Error ? err.message : String(err);
108
- }
109
-
110
- function sleep(ms: number): Promise<void> {
111
- return new Promise((resolve) => setTimeout(resolve, ms));
112
- }
113
-
114
- function sharedProvisionKey(
115
- apiUrl: string,
116
- provisionQueryParams: Record<string, string>,
117
- provisionToken: string,
118
- ): string {
119
- const normalizedProvisionQueryParams = Object.fromEntries(
120
- Object.entries(provisionQueryParams).sort(([left], [right]) => left.localeCompare(right)),
121
- );
122
-
123
- return createHash("sha256")
124
- .update(
125
- JSON.stringify({
126
- apiUrl,
127
- provisionToken,
128
- provisionQueryParams: normalizedProvisionQueryParams,
129
- }),
130
- )
131
- .digest("hex");
132
- }
133
-
134
- function sharedProvisionStatePath(sharedKey: string): string {
135
- return path.join(SHARED_PROVISION_DIR, `${sharedKey}.json`);
136
- }
137
-
138
- async function readSharedProvisionState(filePath: string): Promise<SharedProvisionState | null> {
139
- try {
140
- const raw = await readFile(filePath, "utf8");
141
- const parsed = JSON.parse(raw) as Partial<SharedProvisionState>;
142
- if (parsed.status === "pending" && typeof parsed.startedAt === "number") {
143
- return {
144
- status: "pending",
145
- startedAt: parsed.startedAt,
146
- pid: typeof parsed.pid === "number" ? parsed.pid : 0,
147
- };
148
- }
149
- if (
150
- parsed.status === "done"
151
- && typeof parsed.startedAt === "number"
152
- && typeof parsed.finishedAt === "number"
153
- && typeof parsed.apiKey === "string"
154
- ) {
155
- return {
156
- status: "done",
157
- startedAt: parsed.startedAt,
158
- finishedAt: parsed.finishedAt,
159
- apiKey: parsed.apiKey,
160
- };
161
- }
162
- if (
163
- parsed.status === "error"
164
- && typeof parsed.startedAt === "number"
165
- && typeof parsed.finishedAt === "number"
166
- && typeof parsed.error === "string"
167
- ) {
168
- return {
169
- status: "error",
170
- startedAt: parsed.startedAt,
171
- finishedAt: parsed.finishedAt,
172
- error: parsed.error,
173
- };
174
- }
175
- } catch (err) {
176
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
177
- return null;
178
- }
179
- }
180
- return null;
181
- }
182
-
183
- async function writeSharedProvisionState(
184
- filePath: string,
185
- state: SharedProvisionState,
186
- ): Promise<void> {
187
- await mkdir(path.dirname(filePath), { recursive: true });
188
- await writeFile(filePath, JSON.stringify(state), "utf8");
189
- }
190
-
191
- async function createSharedProvisionPendingState(
192
- filePath: string,
193
- startedAt: number,
194
- ): Promise<boolean> {
195
- await mkdir(path.dirname(filePath), { recursive: true });
196
- try {
197
- await writeFile(
198
- filePath,
199
- JSON.stringify({
200
- status: "pending",
201
- startedAt,
202
- pid: process.pid,
203
- } satisfies SharedProvisionState),
204
- { encoding: "utf8", flag: "wx" },
205
- );
206
- return true;
207
- } catch (err) {
208
- if ((err as NodeJS.ErrnoException).code === "EEXIST") {
209
- return false;
210
- }
211
- throw err;
212
- }
213
- }
214
-
215
- async function removeSharedProvisionState(filePath: string): Promise<void> {
216
- await rm(filePath, { force: true });
217
- }
218
-
219
- async function waitForSharedProvisionResult(
220
- filePath: string,
221
- waitTimeoutMs: number,
222
- ): Promise<string | null> {
223
- const deadline = Date.now() + waitTimeoutMs;
224
-
225
- while (true) {
226
- const state = await readSharedProvisionState(filePath);
227
- const now = Date.now();
228
-
229
- if (!state) {
230
- return null;
231
- }
232
-
233
- 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;
239
- }
240
-
241
- if (state.status === "error") {
242
- await removeSharedProvisionState(filePath);
243
- throw new Error(state.error);
244
- }
245
-
246
- if (now - state.startedAt > waitTimeoutMs || now >= deadline) {
247
- await removeSharedProvisionState(filePath);
248
- return null;
249
- }
250
-
251
- await sleep(SHARED_PROVISION_POLL_INTERVAL_MS);
252
- }
253
- }
254
-
255
- async function resolveSharedProvisionedAPIKey(
256
- apiUrl: string,
257
- provisionQueryParams: Record<string, string>,
258
- provisionToken: string,
259
- timeouts: Required<BackendTimeouts>,
260
- logger: OpenClawPluginApi["logger"],
261
- registerTenant: () => Promise<string>,
262
- ): Promise<string> {
263
- const key = sharedProvisionKey(apiUrl, provisionQueryParams, provisionToken);
264
- const existingPromise = sharedProvisionPromises.get(key);
265
- if (existingPromise) {
266
- return existingPromise;
267
- }
268
-
269
- const waitTimeoutMs = Math.max(timeouts.defaultTimeoutMs + 5_000, 30_000);
270
- const filePath = sharedProvisionStatePath(key);
271
- const sharedPromise = (async () => {
272
- while (true) {
273
- const state = await readSharedProvisionState(filePath);
274
- const now = Date.now();
275
-
276
- 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;
283
- }
284
-
285
- if (state?.status === "error") {
286
- await removeSharedProvisionState(filePath);
287
- } else if (state?.status === "pending") {
288
- if (now - state.startedAt > waitTimeoutMs) {
289
- await removeSharedProvisionState(filePath);
290
- continue;
291
- }
292
- logger.info("[mem9] explicit provision already in progress in another mem9 instance; waiting");
293
- const sharedApiKey = await waitForSharedProvisionResult(filePath, waitTimeoutMs);
294
- if (sharedApiKey) {
295
- return sharedApiKey;
296
- }
297
- continue;
298
- }
299
-
300
- const startedAt = Date.now();
301
- const acquired = await createSharedProvisionPendingState(filePath, startedAt);
302
- if (!acquired) {
303
- continue;
304
- }
305
-
306
- try {
307
- const apiKey = await registerTenant();
308
- await writeSharedProvisionState(filePath, {
309
- status: "done",
310
- startedAt,
311
- finishedAt: Date.now(),
312
- apiKey,
313
- });
314
- return apiKey;
315
- } catch (err) {
316
- await writeSharedProvisionState(filePath, {
317
- status: "error",
318
- startedAt,
319
- finishedAt: Date.now(),
320
- error: errorMessage(err),
321
- });
322
- throw err;
323
- }
324
- }
325
- })().finally(() => {
326
- sharedProvisionPromises.delete(key);
327
- });
328
-
329
- sharedProvisionPromises.set(key, sharedPromise);
330
- return sharedPromise;
331
- }
332
-
333
78
  interface MemoryCapability {
334
79
  search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
335
80
  store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
@@ -372,10 +117,7 @@ interface AnyAgentTool {
372
117
  execute: (_id: string, params: unknown) => Promise<unknown>;
373
118
  }
374
119
 
375
- function buildTools(
376
- backend: MemoryBackend,
377
- provisionAPIKey: (() => Promise<string>) | null,
378
- ): AnyAgentTool[] {
120
+ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
379
121
  return [
380
122
  {
381
123
  name: "memory_store",
@@ -548,41 +290,6 @@ function buildTools(
548
290
  }
549
291
  },
550
292
  },
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
293
  ];
587
294
  }
588
295
 
@@ -595,13 +302,7 @@ const mnemoPlugin = {
595
302
  register(api: OpenClawPluginApi) {
596
303
  const cfg = (api.pluginConfig ?? {}) as PluginConfig;
597
304
  const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
598
- const configuredProvisionToken =
599
- typeof cfg.provisionToken === "string" && cfg.provisionToken.trim() !== ""
600
- ? cfg.provisionToken.trim()
601
- : null;
602
- const provisionQueryParams = cfg.provisionQueryParams ?? {};
603
305
  const timeoutConfig = resolveTimeouts(cfg, api.logger);
604
- const hookAgentId = cfg.agentName ?? "agent";
605
306
  if (!cfg.apiUrl) {
606
307
  api.logger.info(`[mem9] apiUrl not configured, using default ${DEFAULT_API_URL}`);
607
308
  }
@@ -613,85 +314,35 @@ const mnemoPlugin = {
613
314
  api.logger.info("[mem9] tenantID is deprecated; treating it as apiKey for v1alpha2");
614
315
  }
615
316
  const registerTenant = async (agentName: string): Promise<string> => {
616
- const backend = new ServerBackend(
617
- effectiveApiUrl,
618
- "",
619
- agentName,
620
- {
621
- timeouts: timeoutConfig,
622
- provisionQueryParams,
623
- },
624
- );
317
+ const backend = new ServerBackend(effectiveApiUrl, "", agentName, timeoutConfig);
625
318
  const result = await backend.register();
626
319
  api.logger.info(
627
320
  `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
628
321
  );
629
322
  return result.id;
630
323
  };
631
- let runtimeProvisionedAPIKey: string | null = null;
632
324
  let registrationPromise: Promise<string> | null = null;
633
- const provisionAPIKey = (agentName: string): Promise<string> => {
634
- if (configuredApiKey) {
635
- return Promise.reject(
636
- new Error(
637
- "mem9_provision_api_key is only available during create-new setup before apiKey is persisted",
638
- ),
639
- );
640
- }
641
- if (runtimeProvisionedAPIKey) {
642
- return Promise.resolve(runtimeProvisionedAPIKey);
643
- }
644
- if (!configuredProvisionToken) {
645
- return Promise.reject(
646
- new Error("mem9 create-new setup cannot provision because provisionToken is missing"),
647
- );
648
- }
325
+ const resolveAPIKey = (agentName: string): Promise<string> => {
326
+ if (configuredApiKey) return Promise.resolve(configuredApiKey);
649
327
  if (!registrationPromise) {
650
- registrationPromise = resolveSharedProvisionedAPIKey(
651
- effectiveApiUrl,
652
- provisionQueryParams,
653
- configuredProvisionToken,
654
- timeoutConfig,
655
- api.logger,
656
- () => registerTenant(agentName),
657
- )
658
- .then((apiKey) => {
659
- runtimeProvisionedAPIKey = apiKey;
660
- return apiKey;
661
- })
662
- .catch((err) => {
663
- registrationPromise = null;
664
- throw err;
665
- });
328
+ registrationPromise = registerTenant(agentName);
666
329
  }
667
330
  return registrationPromise;
668
331
  };
669
- const resolveAPIKey = (): Promise<string> => {
670
- if (configuredApiKey) return Promise.resolve(configuredApiKey);
671
- if (runtimeProvisionedAPIKey) {
672
- return Promise.resolve(runtimeProvisionedAPIKey);
673
- }
674
- return Promise.reject(new PendingProvisionError());
675
- };
676
332
 
677
333
  api.logger.info("[mem9] Server mode (v1alpha2)");
678
- if (!configuredApiKey) {
679
- if (configuredProvisionToken) {
680
- api.logger.info("[mem9] apiKey not configured; waiting for explicit create-new provision");
681
- } else {
682
- api.logger.info("[mem9] apiKey not configured; mem9 will stay idle until apiKey is configured");
683
- }
684
- }
334
+
335
+ const hookAgentId = cfg.agentName ?? "agent";
685
336
 
686
337
  const factory: ToolFactory = (ctx: ToolContext) => {
687
338
  const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
688
339
  const backend = new LazyServerBackend(
689
340
  effectiveApiUrl,
690
- resolveAPIKey,
341
+ () => resolveAPIKey(agentId),
691
342
  agentId,
692
343
  timeoutConfig,
693
344
  );
694
- return buildTools(backend, configuredApiKey ? null : () => provisionAPIKey(agentId));
345
+ return buildTools(backend);
695
346
  };
696
347
 
697
348
  api.registerTool(factory, { names: toolNames });
@@ -699,25 +350,11 @@ const mnemoPlugin = {
699
350
  // Shared lazy backend for hooks and capability registration.
700
351
  const hookBackend = new LazyServerBackend(
701
352
  effectiveApiUrl,
702
- resolveAPIKey,
353
+ () => resolveAPIKey(hookAgentId),
703
354
  hookAgentId,
704
355
  timeoutConfig,
705
356
  );
706
357
 
707
- const withPendingProvisionFallback = async <T>(
708
- action: () => Promise<T>,
709
- fallback: T,
710
- ): Promise<T> => {
711
- try {
712
- return await action();
713
- } catch (err) {
714
- if (isPendingProvisionError(err)) {
715
- return fallback;
716
- }
717
- throw err;
718
- }
719
- };
720
-
721
358
  // Register memory capability so OpenClaw 2026.4.2+ binds this plugin to
722
359
  // the memory slot. Without this, the plugin is treated as a legacy
723
360
  // hook-only plugin and automatic context injection won't work.
@@ -725,24 +362,14 @@ const mnemoPlugin = {
725
362
  if (typeof api.registerCapability === "function") {
726
363
  api.registerCapability("memory", {
727
364
  search: async (query, opts) => {
728
- const result = await withPendingProvisionFallback(
729
- () => hookBackend.search({ q: query, limit: opts?.limit }),
730
- { data: [], total: 0, limit: opts?.limit ?? 20, offset: 0 },
731
- );
365
+ const result = await hookBackend.search({ q: query, limit: opts?.limit });
732
366
  return { data: result.data, total: result.total };
733
367
  },
734
368
  store: async (content, opts) => {
735
- try {
736
- return await hookBackend.store({ content, tags: opts?.tags, source: opts?.source });
737
- } catch (err) {
738
- if (isPendingProvisionError(err)) {
739
- return null;
740
- }
741
- throw err;
742
- }
369
+ return hookBackend.store({ content, tags: opts?.tags, source: opts?.source });
743
370
  },
744
- get: (id) => withPendingProvisionFallback(() => hookBackend.get(id), null),
745
- remove: (id) => withPendingProvisionFallback(() => hookBackend.remove(id), false),
371
+ get: (id) => hookBackend.get(id),
372
+ remove: (id) => hookBackend.remove(id),
746
373
  });
747
374
  }
748
375
 
@@ -760,28 +387,18 @@ const toolNames = [
760
387
  "memory_get",
761
388
  "memory_update",
762
389
  "memory_delete",
763
- "mem9_provision_api_key",
764
390
  ];
765
391
 
766
392
  class LazyServerBackend implements MemoryBackend {
767
- private apiUrl: string;
768
- private apiKeyProvider: () => Promise<string>;
769
- private agentId: string;
770
- private timeouts: BackendTimeouts;
771
393
  private resolved: ServerBackend | null = null;
772
394
  private resolving: Promise<ServerBackend> | null = null;
773
395
 
774
396
  constructor(
775
- apiUrl: string,
776
- apiKeyProvider: () => Promise<string>,
777
- agentId: string,
778
- timeouts: BackendTimeouts,
779
- ) {
780
- this.apiUrl = apiUrl;
781
- this.apiKeyProvider = apiKeyProvider;
782
- this.agentId = agentId;
783
- this.timeouts = timeouts;
784
- }
397
+ private apiUrl: string,
398
+ private apiKeyProvider: () => Promise<string>,
399
+ private agentId: string,
400
+ private timeouts: BackendTimeouts,
401
+ ) {}
785
402
 
786
403
  private async resolve(): Promise<ServerBackend> {
787
404
  if (this.resolved) return this.resolved;
@@ -789,9 +406,7 @@ class LazyServerBackend implements MemoryBackend {
789
406
 
790
407
  this.resolving = this.apiKeyProvider().then((apiKey) =>
791
408
  Promise.resolve().then(() => {
792
- this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, {
793
- timeouts: this.timeouts,
794
- });
409
+ this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, this.timeouts);
795
410
  return this.resolved;
796
411
  })
797
412
  ).catch((err) => {
@@ -14,17 +14,6 @@
14
14
  "type": "string",
15
15
  "description": "mem9 API key (secret — do not share)"
16
16
  },
17
- "provisionToken": {
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"
20
- },
21
- "provisionQueryParams": {
22
- "type": "object",
23
- "description": "Optional utm_* params forwarded only by the explicit first-time create-new provision step",
24
- "additionalProperties": {
25
- "type": "string"
26
- }
27
- },
28
17
  "defaultTimeoutMs": {
29
18
  "type": "number",
30
19
  "minimum": 1,
@@ -51,10 +40,6 @@
51
40
  "placeholder": "key...",
52
41
  "sensitive": true
53
42
  },
54
- "provisionToken": {
55
- "label": "Provision Token",
56
- "placeholder": "create-new only"
57
- },
58
43
  "defaultTimeoutMs": {
59
44
  "label": "Default Timeout (ms)",
60
45
  "placeholder": "8000"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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",
@@ -34,7 +34,6 @@
34
34
  "access": "public"
35
35
  },
36
36
  "scripts": {
37
- "test": "rm -rf ./dist-test && tsc -p ./tsconfig.test.json && node --test ./dist-test/*.test.js",
38
37
  "typecheck": "tsc --noEmit",
39
38
  "prepublishOnly": "npm run typecheck"
40
39
  },
@@ -43,7 +42,6 @@
43
42
  },
44
43
  "dependencies": {},
45
44
  "devDependencies": {
46
- "@types/node": "^22.15.30",
47
45
  "typescript": "^5.5.0"
48
46
  },
49
47
  "openclaw": {
package/server-backend.ts CHANGED
@@ -22,11 +22,6 @@ export interface BackendTimeouts {
22
22
  searchTimeoutMs?: number;
23
23
  }
24
24
 
25
- interface ServerBackendOptions {
26
- timeouts?: BackendTimeouts;
27
- provisionQueryParams?: Record<string, string>;
28
- }
29
-
30
25
  interface RequestOptions {
31
26
  timeoutMs?: number;
32
27
  }
@@ -35,37 +30,25 @@ export class ServerBackend implements MemoryBackend {
35
30
  private baseUrl: string;
36
31
  private apiKey: string;
37
32
  private agentName: string;
38
- private provisionQueryParams: Record<string, string>;
39
33
  private timeouts: Required<BackendTimeouts>;
40
34
 
41
35
  constructor(
42
36
  apiUrl: string,
43
37
  apiKey: string,
44
38
  agentName: string,
45
- options: ServerBackendOptions = {},
39
+ timeouts: BackendTimeouts = {},
46
40
  ) {
47
41
  this.baseUrl = apiUrl.replace(/\/+$/, "");
48
42
  this.apiKey = apiKey;
49
43
  this.agentName = agentName;
50
- this.provisionQueryParams = options.provisionQueryParams ?? {};
51
44
  this.timeouts = {
52
- defaultTimeoutMs: options.timeouts?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
53
- searchTimeoutMs: options.timeouts?.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
45
+ defaultTimeoutMs: timeouts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
46
+ searchTimeoutMs: timeouts.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
54
47
  };
55
48
  }
56
49
 
57
50
  async register(): Promise<ProvisionMem9sResponse> {
58
- const query = new URLSearchParams();
59
- for (const [key, value] of Object.entries(this.provisionQueryParams)) {
60
- if (!key.startsWith("utm_") || typeof value !== "string" || value === "") {
61
- continue;
62
- }
63
-
64
- query.set(key, value);
65
- }
66
-
67
- const qs = query.toString();
68
- const resp = await fetch(this.baseUrl + "/v1alpha1/mem9s" + (qs ? `?${qs}` : ""), {
51
+ const resp = await fetch(this.baseUrl + "/v1alpha1/mem9s", {
69
52
  method: "POST",
70
53
  signal: AbortSignal.timeout(this.timeouts.defaultTimeoutMs),
71
54
  });
package/types.ts CHANGED
@@ -2,8 +2,6 @@ export interface PluginConfig {
2
2
  // Server mode (apiUrl present → server)
3
3
  apiUrl?: string;
4
4
  apiKey?: string;
5
- provisionToken?: string;
6
- provisionQueryParams?: Record<string, string>;
7
5
  tenantID?: string;
8
6
  defaultTimeoutMs?: number;
9
7
  searchTimeoutMs?: number;
package/index.test.ts DELETED
@@ -1,381 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
-
4
- import mnemoPlugin from "./index.js";
5
-
6
- interface RegisteredTool {
7
- name: string;
8
- execute: (_id: string, params: unknown) => Promise<unknown>;
9
- }
10
-
11
- interface SearchCapability {
12
- search: (query: string, opts?: { limit?: number }) => Promise<{ data: unknown[]; total: number }>;
13
- }
14
-
15
- interface StubApi {
16
- pluginConfig?: unknown;
17
- logger: {
18
- info: (...args: unknown[]) => void;
19
- error: (...args: unknown[]) => void;
20
- };
21
- registerTool: (factory: unknown, _opts?: unknown) => void;
22
- registerCapability?: (slot: string, capability: unknown) => void;
23
- on: (...args: unknown[]) => void;
24
- getTools: (ctx?: { agentId?: string }) => RegisteredTool[];
25
- }
26
-
27
- function createStubApi(
28
- pluginConfig: unknown,
29
- options?: {
30
- onRegisterCapability?: (slot: string, capability: unknown) => void;
31
- infoLogs?: string[];
32
- errorLogs?: string[];
33
- },
34
- ): StubApi {
35
- const infoLogs = options?.infoLogs ?? [];
36
- const errorLogs = options?.errorLogs ?? [];
37
- let toolFactory:
38
- | ((ctx?: { agentId?: string }) => RegisteredTool[] | RegisteredTool | null | undefined)
39
- | null = null;
40
-
41
- return {
42
- pluginConfig,
43
- logger: {
44
- info: (...args: unknown[]) => {
45
- infoLogs.push(args.map(String).join(" "));
46
- },
47
- error: (...args: unknown[]) => {
48
- errorLogs.push(args.map(String).join(" "));
49
- },
50
- },
51
- registerTool: (factory: unknown) => {
52
- toolFactory = factory as typeof toolFactory;
53
- },
54
- registerCapability: options?.onRegisterCapability,
55
- on: () => {},
56
- getTools: (ctx = {}) => {
57
- if (!toolFactory) {
58
- return [];
59
- }
60
- const tools = toolFactory(ctx);
61
- if (!tools) {
62
- return [];
63
- }
64
- return Array.isArray(tools) ? tools : [tools];
65
- },
66
- };
67
- }
68
-
69
- async function flushAsyncWork(): Promise<void> {
70
- await new Promise((resolve) => setTimeout(resolve, 0));
71
- }
72
-
73
- async function waitFor(predicate: () => boolean, timeoutMs = 2_000): Promise<void> {
74
- const startedAt = Date.now();
75
-
76
- while (!predicate()) {
77
- if (Date.now() - startedAt > timeoutMs) {
78
- throw new Error("timed out waiting for condition");
79
- }
80
- await new Promise((resolve) => setTimeout(resolve, 10));
81
- }
82
- }
83
-
84
- function uniqueApiUrl(name: string): string {
85
- return `https://api.mem9.ai/${name}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
86
- }
87
-
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
- test("register does not auto-provision on startup during create-new", async () => {
104
- const originalFetch = globalThis.fetch;
105
- const apiUrl = uniqueApiUrl("no-startup-provision");
106
- const requests: string[] = [];
107
- const infoLogs: string[] = [];
108
- const errorLogs: string[] = [];
109
-
110
- globalThis.fetch = async (input) => {
111
- requests.push(String(input));
112
- throw new Error("unexpected fetch");
113
- };
114
-
115
- try {
116
- mnemoPlugin.register(
117
- createStubApi(
118
- {
119
- apiUrl,
120
- provisionToken: "token-startup",
121
- provisionQueryParams: {
122
- utm_source: "bosn",
123
- },
124
- },
125
- { infoLogs, errorLogs },
126
- ),
127
- );
128
-
129
- await flushAsyncWork();
130
-
131
- assert.deepEqual(requests, []);
132
- assert.deepEqual(errorLogs, []);
133
- assert.equal(
134
- infoLogs.includes("[mem9] apiKey not configured; waiting for explicit create-new provision"),
135
- true,
136
- );
137
- } finally {
138
- globalThis.fetch = originalFetch;
139
- }
140
- });
141
-
142
- test("memory capability stays idle until explicit provision runs", async () => {
143
- const originalFetch = globalThis.fetch;
144
- const apiUrl = uniqueApiUrl("pending-capability");
145
- let capability: SearchCapability | null = null;
146
- const requests: string[] = [];
147
-
148
- globalThis.fetch = async (input) => {
149
- requests.push(String(input));
150
- throw new Error("unexpected fetch");
151
- };
152
-
153
- try {
154
- mnemoPlugin.register(
155
- createStubApi(
156
- {
157
- apiUrl,
158
- provisionToken: "token-capability",
159
- },
160
- {
161
- onRegisterCapability: (_slot, registeredCapability) => {
162
- capability = registeredCapability as SearchCapability;
163
- },
164
- },
165
- ),
166
- );
167
-
168
- assert.notEqual(capability, null);
169
-
170
- const result = await capability!.search("hello");
171
-
172
- assert.deepEqual(result, { data: [], total: 0 });
173
- assert.deepEqual(requests, []);
174
- } finally {
175
- globalThis.fetch = originalFetch;
176
- }
177
- });
178
-
179
- test("mem9_provision_api_key provisions once and unlocks memory access", async () => {
180
- const originalFetch = globalThis.fetch;
181
- const apiUrl = uniqueApiUrl("explicit-provision");
182
- let capability: SearchCapability | null = null;
183
- let provisionRequests = 0;
184
- let searchRequests = 0;
185
-
186
- globalThis.fetch = async (input, init) => {
187
- const url = String(input);
188
-
189
- if (url === `${apiUrl}/v1alpha1/mem9s?utm_source=bosn`) {
190
- provisionRequests += 1;
191
- return new Response(JSON.stringify({ id: "space-explicit" }), {
192
- status: 201,
193
- headers: {
194
- "Content-Type": "application/json",
195
- },
196
- });
197
- }
198
-
199
- if (url.includes("/v1alpha2/mem9s/memories")) {
200
- searchRequests += 1;
201
- const headers = init?.headers as Record<string, string> | undefined;
202
- assert.equal(headers?.["X-API-Key"], "space-explicit");
203
-
204
- return new Response(
205
- JSON.stringify({
206
- memories: [],
207
- total: 0,
208
- limit: 20,
209
- offset: 0,
210
- }),
211
- {
212
- status: 200,
213
- headers: {
214
- "Content-Type": "application/json",
215
- },
216
- },
217
- );
218
- }
219
-
220
- throw new Error(`unexpected fetch: ${url}`);
221
- };
222
-
223
- try {
224
- const api = createStubApi(
225
- {
226
- apiUrl,
227
- provisionToken: "token-explicit",
228
- provisionQueryParams: {
229
- utm_source: "bosn",
230
- },
231
- },
232
- {
233
- onRegisterCapability: (_slot, registeredCapability) => {
234
- capability = registeredCapability as SearchCapability;
235
- },
236
- },
237
- );
238
- mnemoPlugin.register(api);
239
-
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
- };
245
-
246
- assert.equal(provisionResult.ok, true);
247
- assert.equal(provisionResult.data?.apiKey, "space-explicit");
248
- assert.equal(provisionRequests, 1);
249
-
250
- assert.notEqual(capability, null);
251
- const searchResult = await capability!.search("hello");
252
-
253
- assert.deepEqual(searchResult, { data: [], total: 0 });
254
- assert.equal(provisionRequests, 1);
255
- assert.equal(searchRequests, 1);
256
- } finally {
257
- globalThis.fetch = originalFetch;
258
- }
259
- });
260
-
261
- test("concurrent explicit provision calls share one server request", async () => {
262
- const originalFetch = globalThis.fetch;
263
- const apiUrl = uniqueApiUrl("shared-explicit");
264
- let provisionRequests = 0;
265
- const provisionControl: { release: () => void } = {
266
- release: () => {},
267
- };
268
- const provisionGate = new Promise<void>((resolve) => {
269
- provisionControl.release = resolve;
270
- });
271
-
272
- globalThis.fetch = async (input) => {
273
- const url = String(input);
274
-
275
- if (url === `${apiUrl}/v1alpha1/mem9s`) {
276
- provisionRequests += 1;
277
- await provisionGate;
278
- return new Response(JSON.stringify({ id: "space-shared-explicit" }), {
279
- status: 201,
280
- headers: {
281
- "Content-Type": "application/json",
282
- },
283
- });
284
- }
285
-
286
- throw new Error(`unexpected fetch: ${url}`);
287
- };
288
-
289
- try {
290
- const pluginConfig = {
291
- apiUrl,
292
- provisionToken: "token-shared-explicit",
293
- };
294
- const apiA = createStubApi(pluginConfig);
295
- const apiB = createStubApi(pluginConfig);
296
-
297
- mnemoPlugin.register(apiA);
298
- mnemoPlugin.register(apiB);
299
-
300
- const toolA = findTool(apiA, "mem9_provision_api_key");
301
- const toolB = findTool(apiB, "mem9_provision_api_key");
302
-
303
- const promiseA = toolA.execute("tool-a", {});
304
- const promiseB = toolB.execute("tool-b", {});
305
-
306
- await waitFor(() => provisionRequests === 1);
307
- provisionControl.release();
308
-
309
- const resultA = parseToolResult(await promiseA) as { ok: boolean; data?: { apiKey?: string } };
310
- const resultB = parseToolResult(await promiseB) as { ok: boolean; data?: { apiKey?: string } };
311
-
312
- 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
- } finally {
318
- provisionControl.release();
319
- globalThis.fetch = originalFetch;
320
- }
321
- });
322
-
323
- test("a second setup retry reuses the shared provisioned key before config write-back", async () => {
324
- const originalFetch = globalThis.fetch;
325
- const apiUrl = uniqueApiUrl("shared-retry");
326
- const infoLogsA: string[] = [];
327
- const infoLogsB: string[] = [];
328
- let provisionRequests = 0;
329
-
330
- globalThis.fetch = async (input) => {
331
- const url = String(input);
332
-
333
- if (url === `${apiUrl}/v1alpha1/mem9s`) {
334
- provisionRequests += 1;
335
- return new Response(JSON.stringify({ id: "space-shared-retry" }), {
336
- status: 201,
337
- headers: {
338
- "Content-Type": "application/json",
339
- },
340
- });
341
- }
342
-
343
- throw new Error(`unexpected fetch: ${url}`);
344
- };
345
-
346
- try {
347
- const pluginConfig = {
348
- apiUrl,
349
- provisionToken: "token-shared-retry",
350
- };
351
-
352
- const apiA = createStubApi(pluginConfig, { infoLogs: infoLogsA });
353
- 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 });
364
- 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
- };
370
-
371
- assert.equal(provisionRequests, 1);
372
- assert.equal(secondResult.ok, true);
373
- assert.equal(secondResult.data?.apiKey, "space-shared-retry");
374
- assert.equal(
375
- infoLogsB.includes("[mem9] reusing shared explicit-provision result pending config write-back"),
376
- true,
377
- );
378
- } finally {
379
- globalThis.fetch = originalFetch;
380
- }
381
- });
@@ -1,82 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
-
4
- import { ServerBackend } from "./server-backend.js";
5
-
6
- test("register forwards only utm_* params during create-new provision", async () => {
7
- const originalFetch = globalThis.fetch;
8
- let requestedURL = "";
9
-
10
- globalThis.fetch = async (input, init) => {
11
- requestedURL = String(input);
12
- assert.equal(init?.method, "POST");
13
-
14
- return new Response(JSON.stringify({ id: "space-1" }), {
15
- status: 201,
16
- headers: {
17
- "Content-Type": "application/json",
18
- },
19
- });
20
- };
21
-
22
- try {
23
- const backend = new ServerBackend("https://api.mem9.ai", "", "agent-1", {
24
- provisionQueryParams: {
25
- utm_source: "bosn",
26
- foo: "bar",
27
- utm_campaign: "spring",
28
- utm_medium: "",
29
- },
30
- });
31
-
32
- const result = await backend.register();
33
- assert.equal(result.id, "space-1");
34
-
35
- const url = new URL(requestedURL);
36
- assert.equal(url.origin + url.pathname, "https://api.mem9.ai/v1alpha1/mem9s");
37
- assert.equal(url.searchParams.get("utm_source"), "bosn");
38
- assert.equal(url.searchParams.get("utm_campaign"), "spring");
39
- assert.equal(url.searchParams.has("foo"), false);
40
- assert.equal(url.searchParams.has("utm_medium"), false);
41
- } finally {
42
- globalThis.fetch = originalFetch;
43
- }
44
- });
45
-
46
- test("normal memory requests do not append provision query params", async () => {
47
- const originalFetch = globalThis.fetch;
48
- let requestedURL = "";
49
-
50
- globalThis.fetch = async (input) => {
51
- requestedURL = String(input);
52
-
53
- return new Response(
54
- JSON.stringify({
55
- id: "mem-1",
56
- content: "remember this",
57
- created_at: "2026-04-05T00:00:00Z",
58
- updated_at: "2026-04-05T00:00:00Z",
59
- }),
60
- {
61
- status: 200,
62
- headers: {
63
- "Content-Type": "application/json",
64
- },
65
- },
66
- );
67
- };
68
-
69
- try {
70
- const backend = new ServerBackend("https://api.mem9.ai", "space-key", "agent-1", {
71
- provisionQueryParams: {
72
- utm_source: "bosn",
73
- },
74
- });
75
-
76
- await backend.store({ content: "remember this" });
77
-
78
- assert.equal(requestedURL, "https://api.mem9.ai/v1alpha2/mem9s/memories");
79
- } finally {
80
- globalThis.fetch = originalFetch;
81
- }
82
- });