@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/index.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { MemoryBackend } from "./backend.js";
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";
2
7
  import {
3
8
  DEFAULT_SEARCH_TIMEOUT_MS,
4
9
  DEFAULT_TIMEOUT_MS,
@@ -18,6 +23,28 @@ import type {
18
23
 
19
24
  const DEFAULT_API_URL = "https://api.mem9.ai";
20
25
  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 sharedProvisionPromises = new Map<string, Promise<string>>();
29
+
30
+ type SharedProvisionState =
31
+ | {
32
+ status: "pending";
33
+ startedAt: number;
34
+ pid: number;
35
+ }
36
+ | {
37
+ status: "done";
38
+ startedAt: number;
39
+ finishedAt: number;
40
+ apiKey: string;
41
+ }
42
+ | {
43
+ status: "error";
44
+ startedAt: number;
45
+ finishedAt: number;
46
+ error: string;
47
+ };
21
48
 
22
49
  function normalizeTimeoutMs(
23
50
  value: unknown,
@@ -75,6 +102,225 @@ function jsonResult(data: unknown) {
75
102
  }
76
103
  }
77
104
 
105
+ function errorMessage(err: unknown): string {
106
+ return err instanceof Error ? err.message : String(err);
107
+ }
108
+
109
+ function sleep(ms: number): Promise<void> {
110
+ return new Promise((resolve) => setTimeout(resolve, ms));
111
+ }
112
+
113
+ function sharedProvisionKey(
114
+ apiUrl: string,
115
+ provisionQueryParams: Record<string, string>,
116
+ provisionToken: string,
117
+ ): string {
118
+ const normalizedProvisionQueryParams = Object.fromEntries(
119
+ Object.entries(provisionQueryParams).sort(([left], [right]) => left.localeCompare(right)),
120
+ );
121
+
122
+ return createHash("sha256")
123
+ .update(
124
+ JSON.stringify({
125
+ apiUrl,
126
+ provisionToken,
127
+ provisionQueryParams: normalizedProvisionQueryParams,
128
+ }),
129
+ )
130
+ .digest("hex");
131
+ }
132
+
133
+ function sharedProvisionStatePath(sharedKey: string): string {
134
+ return path.join(SHARED_PROVISION_DIR, `${sharedKey}.json`);
135
+ }
136
+
137
+ async function readSharedProvisionState(filePath: string): Promise<SharedProvisionState | null> {
138
+ try {
139
+ const raw = await readFile(filePath, "utf8");
140
+ const parsed = JSON.parse(raw) as Partial<SharedProvisionState>;
141
+ if (parsed.status === "pending" && typeof parsed.startedAt === "number") {
142
+ return {
143
+ status: "pending",
144
+ startedAt: parsed.startedAt,
145
+ pid: typeof parsed.pid === "number" ? parsed.pid : 0,
146
+ };
147
+ }
148
+ if (
149
+ parsed.status === "done"
150
+ && typeof parsed.startedAt === "number"
151
+ && typeof parsed.finishedAt === "number"
152
+ && typeof parsed.apiKey === "string"
153
+ ) {
154
+ return {
155
+ status: "done",
156
+ startedAt: parsed.startedAt,
157
+ finishedAt: parsed.finishedAt,
158
+ apiKey: parsed.apiKey,
159
+ };
160
+ }
161
+ if (
162
+ parsed.status === "error"
163
+ && typeof parsed.startedAt === "number"
164
+ && typeof parsed.finishedAt === "number"
165
+ && typeof parsed.error === "string"
166
+ ) {
167
+ return {
168
+ status: "error",
169
+ startedAt: parsed.startedAt,
170
+ finishedAt: parsed.finishedAt,
171
+ error: parsed.error,
172
+ };
173
+ }
174
+ } catch (err) {
175
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
176
+ return null;
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+
182
+ async function writeSharedProvisionState(
183
+ filePath: string,
184
+ state: SharedProvisionState,
185
+ ): Promise<void> {
186
+ await mkdir(path.dirname(filePath), { recursive: true });
187
+ await writeFile(filePath, JSON.stringify(state), "utf8");
188
+ }
189
+
190
+ async function createSharedProvisionPendingState(
191
+ filePath: string,
192
+ startedAt: number,
193
+ ): Promise<boolean> {
194
+ await mkdir(path.dirname(filePath), { recursive: true });
195
+ try {
196
+ await writeFile(
197
+ filePath,
198
+ JSON.stringify({
199
+ status: "pending",
200
+ startedAt,
201
+ pid: process.pid,
202
+ } satisfies SharedProvisionState),
203
+ { encoding: "utf8", flag: "wx" },
204
+ );
205
+ return true;
206
+ } catch (err) {
207
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") {
208
+ return false;
209
+ }
210
+ throw err;
211
+ }
212
+ }
213
+
214
+ async function removeSharedProvisionState(filePath: string): Promise<void> {
215
+ await rm(filePath, { force: true });
216
+ }
217
+
218
+ async function waitForSharedProvisionResult(
219
+ filePath: string,
220
+ waitTimeoutMs: number,
221
+ ): Promise<string | null> {
222
+ const deadline = Date.now() + waitTimeoutMs;
223
+
224
+ while (true) {
225
+ const state = await readSharedProvisionState(filePath);
226
+ const now = Date.now();
227
+
228
+ if (!state) {
229
+ return null;
230
+ }
231
+
232
+ if (state.status === "done") {
233
+ return state.apiKey;
234
+ }
235
+
236
+ if (state.status === "error") {
237
+ await removeSharedProvisionState(filePath);
238
+ throw new Error(state.error);
239
+ }
240
+
241
+ if (now - state.startedAt > waitTimeoutMs || now >= deadline) {
242
+ await removeSharedProvisionState(filePath);
243
+ return null;
244
+ }
245
+
246
+ await sleep(SHARED_PROVISION_POLL_INTERVAL_MS);
247
+ }
248
+ }
249
+
250
+ async function resolveSharedProvisionedAPIKey(
251
+ apiUrl: string,
252
+ provisionQueryParams: Record<string, string>,
253
+ provisionToken: string,
254
+ timeouts: Required<BackendTimeouts>,
255
+ logger: OpenClawPluginApi["logger"],
256
+ registerTenant: () => Promise<string>,
257
+ ): Promise<string> {
258
+ const key = sharedProvisionKey(apiUrl, provisionQueryParams, provisionToken);
259
+ const existingPromise = sharedProvisionPromises.get(key);
260
+ if (existingPromise) {
261
+ return existingPromise;
262
+ }
263
+
264
+ const waitTimeoutMs = Math.max(timeouts.defaultTimeoutMs + 5_000, 30_000);
265
+ const filePath = sharedProvisionStatePath(key);
266
+ const sharedPromise = (async () => {
267
+ while (true) {
268
+ const state = await readSharedProvisionState(filePath);
269
+
270
+ if (state?.status === "done") {
271
+ logger.info("[mem9] reusing locally persisted create-new API key for this provisionToken");
272
+ return state.apiKey;
273
+ }
274
+
275
+ if (state?.status === "error") {
276
+ await removeSharedProvisionState(filePath);
277
+ } else if (state?.status === "pending") {
278
+ const now = Date.now();
279
+ if (now - state.startedAt > waitTimeoutMs) {
280
+ await removeSharedProvisionState(filePath);
281
+ continue;
282
+ }
283
+ logger.info("[mem9] create-new provision already in progress in another mem9 instance; waiting");
284
+ const sharedApiKey = await waitForSharedProvisionResult(filePath, waitTimeoutMs);
285
+ if (sharedApiKey) {
286
+ return sharedApiKey;
287
+ }
288
+ continue;
289
+ }
290
+
291
+ const startedAt = Date.now();
292
+ const acquired = await createSharedProvisionPendingState(filePath, startedAt);
293
+ if (!acquired) {
294
+ continue;
295
+ }
296
+
297
+ try {
298
+ const apiKey = await registerTenant();
299
+ await writeSharedProvisionState(filePath, {
300
+ status: "done",
301
+ startedAt,
302
+ finishedAt: Date.now(),
303
+ apiKey,
304
+ });
305
+ return apiKey;
306
+ } catch (err) {
307
+ await writeSharedProvisionState(filePath, {
308
+ status: "error",
309
+ startedAt,
310
+ finishedAt: Date.now(),
311
+ error: errorMessage(err),
312
+ });
313
+ throw err;
314
+ }
315
+ }
316
+ })().finally(() => {
317
+ sharedProvisionPromises.delete(key);
318
+ });
319
+
320
+ sharedProvisionPromises.set(key, sharedPromise);
321
+ return sharedPromise;
322
+ }
323
+
78
324
  interface MemoryCapability {
79
325
  search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
80
326
  store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
@@ -117,7 +363,9 @@ interface AnyAgentTool {
117
363
  execute: (_id: string, params: unknown) => Promise<unknown>;
118
364
  }
119
365
 
120
- function buildTools(backend: MemoryBackend): AnyAgentTool[] {
366
+ function buildTools(
367
+ backend: MemoryBackend,
368
+ ): AnyAgentTool[] {
121
369
  return [
122
370
  {
123
371
  name: "memory_store",
@@ -302,43 +550,126 @@ const mnemoPlugin = {
302
550
  register(api: OpenClawPluginApi) {
303
551
  const cfg = (api.pluginConfig ?? {}) as PluginConfig;
304
552
  const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
553
+ const configuredProvisionToken =
554
+ typeof cfg.provisionToken === "string" && cfg.provisionToken.trim() !== ""
555
+ ? cfg.provisionToken.trim()
556
+ : null;
557
+ const provisionQueryParams = cfg.provisionQueryParams ?? {};
305
558
  const timeoutConfig = resolveTimeouts(cfg, api.logger);
559
+ const hookAgentId = cfg.agentName ?? "agent";
560
+ const debugEnabled = cfg.debug === true || cfg.debugRecall === true;
306
561
  if (!cfg.apiUrl) {
307
562
  api.logger.info(`[mem9] apiUrl not configured, using default ${DEFAULT_API_URL}`);
308
563
  }
564
+ if (cfg.debugRecall === true && cfg.debug !== true) {
565
+ api.logger.info("[mem9] debugRecall is deprecated; use debug instead");
566
+ }
309
567
 
310
568
  const configuredApiKey = cfg.apiKey ?? cfg.tenantID;
569
+ const provisionWaitTimeoutMs = Math.max(timeoutConfig.defaultTimeoutMs + 5_000, 30_000);
311
570
  if (cfg.apiKey && cfg.tenantID) {
312
571
  api.logger.info("[mem9] both apiKey and tenantID set; using apiKey");
313
572
  } else if (cfg.tenantID) {
314
573
  api.logger.info("[mem9] tenantID is deprecated; treating it as apiKey for v1alpha2");
315
574
  }
316
575
  const registerTenant = async (agentName: string): Promise<string> => {
317
- const backend = new ServerBackend(effectiveApiUrl, "", agentName, timeoutConfig);
576
+ const backend = new ServerBackend(
577
+ effectiveApiUrl,
578
+ "",
579
+ agentName,
580
+ {
581
+ timeouts: timeoutConfig,
582
+ provisionQueryParams,
583
+ },
584
+ );
318
585
  const result = await backend.register();
319
586
  api.logger.info(
320
- `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
587
+ `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this for recovery or reconnect as apiKey`
321
588
  );
322
589
  return result.id;
323
590
  };
591
+ let runtimeProvisionedAPIKey: string | null = null;
592
+ let loggedPersistedProvisionedAPIKeyReuse = false;
324
593
  let registrationPromise: Promise<string> | null = null;
325
- const resolveAPIKey = (agentName: string): Promise<string> => {
326
- if (configuredApiKey) return Promise.resolve(configuredApiKey);
594
+ const provisionAPIKey = (agentName: string): Promise<string> => {
595
+ if (configuredApiKey) {
596
+ return Promise.reject(
597
+ new Error(
598
+ "mem9 create-new auto-provision is only available before apiKey is configured",
599
+ ),
600
+ );
601
+ }
602
+ if (runtimeProvisionedAPIKey) {
603
+ return Promise.resolve(runtimeProvisionedAPIKey);
604
+ }
605
+ if (!configuredProvisionToken) {
606
+ return Promise.reject(
607
+ new Error("mem9 create-new setup cannot provision because provisionToken is missing"),
608
+ );
609
+ }
327
610
  if (!registrationPromise) {
328
- registrationPromise = registerTenant(agentName);
611
+ registrationPromise = resolveSharedProvisionedAPIKey(
612
+ effectiveApiUrl,
613
+ provisionQueryParams,
614
+ configuredProvisionToken,
615
+ timeoutConfig,
616
+ api.logger,
617
+ () => registerTenant(agentName),
618
+ )
619
+ .then((apiKey) => {
620
+ runtimeProvisionedAPIKey = apiKey;
621
+ return apiKey;
622
+ })
623
+ .catch((err) => {
624
+ registrationPromise = null;
625
+ throw err;
626
+ });
329
627
  }
330
628
  return registrationPromise;
331
629
  };
630
+ const resolveAPIKey = async (): Promise<string> => {
631
+ if (configuredApiKey) return Promise.resolve(configuredApiKey);
632
+ if (runtimeProvisionedAPIKey) {
633
+ return runtimeProvisionedAPIKey;
634
+ }
635
+ if (configuredProvisionToken) {
636
+ const sharedKey = sharedProvisionKey(
637
+ effectiveApiUrl,
638
+ provisionQueryParams,
639
+ configuredProvisionToken,
640
+ );
641
+ const sharedApiKey = await waitForSharedProvisionResult(
642
+ sharedProvisionStatePath(sharedKey),
643
+ provisionWaitTimeoutMs,
644
+ );
645
+ if (sharedApiKey) {
646
+ runtimeProvisionedAPIKey = sharedApiKey;
647
+ if (!loggedPersistedProvisionedAPIKeyReuse) {
648
+ api.logger.info("[mem9] reusing locally persisted create-new API key for this provisionToken");
649
+ loggedPersistedProvisionedAPIKeyReuse = true;
650
+ }
651
+ return sharedApiKey;
652
+ }
653
+ }
654
+ throw new PendingProvisionError();
655
+ };
332
656
 
333
657
  api.logger.info("[mem9] Server mode (v1alpha2)");
334
-
335
- const hookAgentId = cfg.agentName ?? "agent";
658
+ if (!configuredApiKey) {
659
+ if (configuredProvisionToken) {
660
+ api.logger.info(
661
+ "[mem9] apiKey not configured; waiting for the first post-restart message to finish create-new provision",
662
+ );
663
+ } else {
664
+ api.logger.info("[mem9] apiKey not configured; mem9 will stay idle until apiKey is configured");
665
+ }
666
+ }
336
667
 
337
668
  const factory: ToolFactory = (ctx: ToolContext) => {
338
669
  const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
339
670
  const backend = new LazyServerBackend(
340
671
  effectiveApiUrl,
341
- () => resolveAPIKey(agentId),
672
+ resolveAPIKey,
342
673
  agentId,
343
674
  timeoutConfig,
344
675
  );
@@ -350,11 +681,25 @@ const mnemoPlugin = {
350
681
  // Shared lazy backend for hooks and capability registration.
351
682
  const hookBackend = new LazyServerBackend(
352
683
  effectiveApiUrl,
353
- () => resolveAPIKey(hookAgentId),
684
+ resolveAPIKey,
354
685
  hookAgentId,
355
686
  timeoutConfig,
356
687
  );
357
688
 
689
+ const withPendingProvisionFallback = async <T>(
690
+ action: () => Promise<T>,
691
+ fallback: T,
692
+ ): Promise<T> => {
693
+ try {
694
+ return await action();
695
+ } catch (err) {
696
+ if (isPendingProvisionError(err)) {
697
+ return fallback;
698
+ }
699
+ throw err;
700
+ }
701
+ };
702
+
358
703
  // Register memory capability so OpenClaw 2026.4.2+ binds this plugin to
359
704
  // the memory slot. Without this, the plugin is treated as a legacy
360
705
  // hook-only plugin and automatic context injection won't work.
@@ -362,14 +707,24 @@ const mnemoPlugin = {
362
707
  if (typeof api.registerCapability === "function") {
363
708
  api.registerCapability("memory", {
364
709
  search: async (query, opts) => {
365
- const result = await hookBackend.search({ q: query, limit: opts?.limit });
710
+ const result = await withPendingProvisionFallback(
711
+ () => hookBackend.search({ q: query, limit: opts?.limit }),
712
+ { data: [], total: 0, limit: opts?.limit ?? 20, offset: 0 },
713
+ );
366
714
  return { data: result.data, total: result.total };
367
715
  },
368
716
  store: async (content, opts) => {
369
- return hookBackend.store({ content, tags: opts?.tags, source: opts?.source });
717
+ try {
718
+ return await hookBackend.store({ content, tags: opts?.tags, source: opts?.source });
719
+ } catch (err) {
720
+ if (isPendingProvisionError(err)) {
721
+ return null;
722
+ }
723
+ throw err;
724
+ }
370
725
  },
371
- get: (id) => hookBackend.get(id),
372
- remove: (id) => hookBackend.remove(id),
726
+ get: (id) => withPendingProvisionFallback(() => hookBackend.get(id), null),
727
+ remove: (id) => withPendingProvisionFallback(() => hookBackend.remove(id), false),
373
728
  });
374
729
  }
375
730
 
@@ -377,6 +732,10 @@ const mnemoPlugin = {
377
732
  registerHooks(api, hookBackend, api.logger, {
378
733
  maxIngestBytes: cfg.maxIngestBytes,
379
734
  fallbackAgentId: hookAgentId,
735
+ debug: debugEnabled,
736
+ provisionForCreateNew: configuredApiKey || !configuredProvisionToken
737
+ ? undefined
738
+ : () => provisionAPIKey(hookAgentId),
380
739
  });
381
740
  },
382
741
  };
@@ -390,15 +749,24 @@ const toolNames = [
390
749
  ];
391
750
 
392
751
  class LazyServerBackend implements MemoryBackend {
752
+ private apiUrl: string;
753
+ private apiKeyProvider: () => Promise<string>;
754
+ private agentId: string;
755
+ private timeouts: BackendTimeouts;
393
756
  private resolved: ServerBackend | null = null;
394
757
  private resolving: Promise<ServerBackend> | null = null;
395
758
 
396
759
  constructor(
397
- private apiUrl: string,
398
- private apiKeyProvider: () => Promise<string>,
399
- private agentId: string,
400
- private timeouts: BackendTimeouts,
401
- ) {}
760
+ apiUrl: string,
761
+ apiKeyProvider: () => Promise<string>,
762
+ agentId: string,
763
+ timeouts: BackendTimeouts,
764
+ ) {
765
+ this.apiUrl = apiUrl;
766
+ this.apiKeyProvider = apiKeyProvider;
767
+ this.agentId = agentId;
768
+ this.timeouts = timeouts;
769
+ }
402
770
 
403
771
  private async resolve(): Promise<ServerBackend> {
404
772
  if (this.resolved) return this.resolved;
@@ -406,7 +774,9 @@ class LazyServerBackend implements MemoryBackend {
406
774
 
407
775
  this.resolving = this.apiKeyProvider().then((apiKey) =>
408
776
  Promise.resolve().then(() => {
409
- this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, this.timeouts);
777
+ this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, {
778
+ timeouts: this.timeouts,
779
+ });
410
780
  return this.resolved;
411
781
  })
412
782
  ).catch((err) => {
@@ -14,6 +14,17 @@
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 first-message API-key provisioning runs only once and can be reused on this machine before apiKey is configured explicitly"
20
+ },
21
+ "provisionQueryParams": {
22
+ "type": "object",
23
+ "description": "Optional utm_* params forwarded only by the first create-new provision request triggered after the initial restart",
24
+ "additionalProperties": {
25
+ "type": "string"
26
+ }
27
+ },
17
28
  "defaultTimeoutMs": {
18
29
  "type": "number",
19
30
  "minimum": 1,
@@ -24,6 +35,14 @@
24
35
  "minimum": 1,
25
36
  "description": "Timeout in milliseconds for memory search and automatic recall search (default 15000)"
26
37
  },
38
+ "debug": {
39
+ "type": "boolean",
40
+ "description": "When true, emit mem9 debug logs. Current coverage includes before_prompt_build recall diagnostics; future mem9 debug categories reuse the same switch"
41
+ },
42
+ "debugRecall": {
43
+ "type": "boolean",
44
+ "description": "Deprecated alias for debug. When true, enables mem9 debug logs including before_prompt_build recall diagnostics"
45
+ },
27
46
  "tenantID": {
28
47
  "type": "string",
29
48
  "description": "Deprecated: use apiKey"
@@ -40,6 +59,10 @@
40
59
  "placeholder": "key...",
41
60
  "sensitive": true
42
61
  },
62
+ "provisionToken": {
63
+ "label": "Provision Token",
64
+ "placeholder": "create-new only"
65
+ },
43
66
  "defaultTimeoutMs": {
44
67
  "label": "Default Timeout (ms)",
45
68
  "placeholder": "8000"
@@ -48,6 +71,12 @@
48
71
  "label": "Search Timeout (ms)",
49
72
  "placeholder": "15000"
50
73
  },
74
+ "debug": {
75
+ "label": "Debug"
76
+ },
77
+ "debugRecall": {
78
+ "label": "Debug Recall (Deprecated)"
79
+ },
51
80
  "tenantID": {
52
81
  "label": "Tenant ID (legacy)",
53
82
  "placeholder": "uuid...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.4.7",
3
+ "version": "0.4.8-alpha.0",
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,6 +34,7 @@
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",
37
38
  "typecheck": "tsc --noEmit",
38
39
  "prepublishOnly": "npm run typecheck"
39
40
  },
@@ -42,6 +43,7 @@
42
43
  },
43
44
  "dependencies": {},
44
45
  "devDependencies": {
46
+ "@types/node": "^22.15.30",
45
47
  "typescript": "^5.5.0"
46
48
  },
47
49
  "openclaw": {
@@ -0,0 +1,82 @@
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
+ });