@mem9/mem9 0.4.0 → 0.4.2
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.test.ts +193 -0
- package/index.ts +29 -7
- package/package.json +2 -2
- package/server-backend.test.ts +1 -1
package/index.test.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import mnemoPlugin from "./index.js";
|
|
5
|
+
|
|
6
|
+
interface StubApi {
|
|
7
|
+
pluginConfig?: unknown;
|
|
8
|
+
logger: {
|
|
9
|
+
info: (...args: unknown[]) => void;
|
|
10
|
+
error: (...args: unknown[]) => void;
|
|
11
|
+
};
|
|
12
|
+
registerTool: () => void;
|
|
13
|
+
registerCapability?: (slot: string, capability: unknown) => void;
|
|
14
|
+
on: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createStubApi(
|
|
18
|
+
pluginConfig: unknown,
|
|
19
|
+
options?: {
|
|
20
|
+
onRegisterCapability?: (slot: string, capability: unknown) => void;
|
|
21
|
+
infoLogs?: string[];
|
|
22
|
+
errorLogs?: string[];
|
|
23
|
+
},
|
|
24
|
+
): StubApi {
|
|
25
|
+
const infoLogs = options?.infoLogs ?? [];
|
|
26
|
+
const errorLogs = options?.errorLogs ?? [];
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
pluginConfig,
|
|
30
|
+
logger: {
|
|
31
|
+
info: (...args: unknown[]) => {
|
|
32
|
+
infoLogs.push(args.map(String).join(" "));
|
|
33
|
+
},
|
|
34
|
+
error: (...args: unknown[]) => {
|
|
35
|
+
errorLogs.push(args.map(String).join(" "));
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
registerTool: () => {},
|
|
39
|
+
registerCapability: options?.onRegisterCapability,
|
|
40
|
+
on: () => {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function flushAsyncWork(): Promise<void> {
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("register eagerly auto-provisions when apiKey is absent", async () => {
|
|
49
|
+
const originalFetch = globalThis.fetch;
|
|
50
|
+
const requests: string[] = [];
|
|
51
|
+
const infoLogs: string[] = [];
|
|
52
|
+
const errorLogs: string[] = [];
|
|
53
|
+
|
|
54
|
+
globalThis.fetch = async (input) => {
|
|
55
|
+
requests.push(String(input));
|
|
56
|
+
|
|
57
|
+
return new Response(JSON.stringify({ id: "space-1" }), {
|
|
58
|
+
status: 201,
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
mnemoPlugin.register(
|
|
67
|
+
createStubApi(
|
|
68
|
+
{
|
|
69
|
+
apiUrl: "https://api.mem9.ai",
|
|
70
|
+
provisionQueryParams: {
|
|
71
|
+
utm_source: "bosn",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{ infoLogs, errorLogs },
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await flushAsyncWork();
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(errorLogs, []);
|
|
81
|
+
assert.equal(requests.length, 1);
|
|
82
|
+
assert.equal(
|
|
83
|
+
requests[0],
|
|
84
|
+
"https://api.mem9.ai/v1alpha1/mem9s?utm_source=bosn",
|
|
85
|
+
);
|
|
86
|
+
assert.equal(
|
|
87
|
+
infoLogs.includes("[mem9] apiKey not configured; starting auto-provision"),
|
|
88
|
+
true,
|
|
89
|
+
);
|
|
90
|
+
assert.equal(
|
|
91
|
+
infoLogs.some((msg) => msg.includes("*** Auto-provisioned apiKey=space-1 ***")),
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
globalThis.fetch = originalFetch;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("search retries auto-provision after an eager startup failure", async () => {
|
|
100
|
+
const originalFetch = globalThis.fetch;
|
|
101
|
+
const infoLogs: string[] = [];
|
|
102
|
+
const errorLogs: string[] = [];
|
|
103
|
+
let provisionAttempts = 0;
|
|
104
|
+
let searchRequests = 0;
|
|
105
|
+
let capability: {
|
|
106
|
+
search: (query: string, opts?: { limit?: number }) => Promise<{ data: unknown[]; total: number }>;
|
|
107
|
+
} | null = null;
|
|
108
|
+
|
|
109
|
+
globalThis.fetch = async (input, init) => {
|
|
110
|
+
const url = String(input);
|
|
111
|
+
|
|
112
|
+
if (url.includes("/v1alpha1/mem9s")) {
|
|
113
|
+
provisionAttempts += 1;
|
|
114
|
+
|
|
115
|
+
if (provisionAttempts === 1) {
|
|
116
|
+
return new Response(JSON.stringify({ error: "boom" }), {
|
|
117
|
+
status: 500,
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return new Response(JSON.stringify({ id: "space-2" }), {
|
|
125
|
+
status: 201,
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (url.includes("/v1alpha2/mem9s/memories")) {
|
|
133
|
+
searchRequests += 1;
|
|
134
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
135
|
+
assert.equal(headers?.["X-API-Key"], "space-2");
|
|
136
|
+
|
|
137
|
+
return new Response(
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
memories: [],
|
|
140
|
+
total: 0,
|
|
141
|
+
limit: 20,
|
|
142
|
+
offset: 0,
|
|
143
|
+
}),
|
|
144
|
+
{
|
|
145
|
+
status: 200,
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error(`unexpected fetch: ${url}`);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
mnemoPlugin.register(
|
|
158
|
+
createStubApi(
|
|
159
|
+
{
|
|
160
|
+
apiUrl: "https://api.mem9.ai",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
infoLogs,
|
|
164
|
+
errorLogs,
|
|
165
|
+
onRegisterCapability: (_slot, registeredCapability) => {
|
|
166
|
+
capability = registeredCapability as typeof capability;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await flushAsyncWork();
|
|
173
|
+
|
|
174
|
+
assert.equal(provisionAttempts, 1);
|
|
175
|
+
assert.equal(
|
|
176
|
+
errorLogs.some((msg) => msg.includes("auto-provision failed: mem9s provision failed (500):")),
|
|
177
|
+
true,
|
|
178
|
+
);
|
|
179
|
+
assert.notEqual(capability, null);
|
|
180
|
+
|
|
181
|
+
const result = await capability!.search("hello");
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(result, { data: [], total: 0 });
|
|
184
|
+
assert.equal(provisionAttempts, 2);
|
|
185
|
+
assert.equal(searchRequests, 1);
|
|
186
|
+
assert.equal(
|
|
187
|
+
infoLogs.some((msg) => msg.includes("*** Auto-provisioned apiKey=space-2 ***")),
|
|
188
|
+
true,
|
|
189
|
+
);
|
|
190
|
+
} finally {
|
|
191
|
+
globalThis.fetch = originalFetch;
|
|
192
|
+
}
|
|
193
|
+
});
|
package/index.ts
CHANGED
|
@@ -75,6 +75,10 @@ function jsonResult(data: unknown) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function errorMessage(err: unknown): string {
|
|
79
|
+
return err instanceof Error ? err.message : String(err);
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
interface MemoryCapability {
|
|
79
83
|
search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
|
|
80
84
|
store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
|
|
@@ -303,6 +307,7 @@ const mnemoPlugin = {
|
|
|
303
307
|
const cfg = (api.pluginConfig ?? {}) as PluginConfig;
|
|
304
308
|
const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
|
|
305
309
|
const timeoutConfig = resolveTimeouts(cfg, api.logger);
|
|
310
|
+
const hookAgentId = cfg.agentName ?? "agent";
|
|
306
311
|
if (!cfg.apiUrl) {
|
|
307
312
|
api.logger.info(`[mem9] apiUrl not configured, using default ${DEFAULT_API_URL}`);
|
|
308
313
|
}
|
|
@@ -333,14 +338,22 @@ const mnemoPlugin = {
|
|
|
333
338
|
const resolveAPIKey = (agentName: string): Promise<string> => {
|
|
334
339
|
if (configuredApiKey) return Promise.resolve(configuredApiKey);
|
|
335
340
|
if (!registrationPromise) {
|
|
336
|
-
registrationPromise = registerTenant(agentName)
|
|
341
|
+
registrationPromise = registerTenant(agentName).catch((err) => {
|
|
342
|
+
registrationPromise = null;
|
|
343
|
+
throw err;
|
|
344
|
+
});
|
|
337
345
|
}
|
|
338
346
|
return registrationPromise;
|
|
339
347
|
};
|
|
340
348
|
|
|
341
349
|
api.logger.info("[mem9] Server mode (v1alpha2)");
|
|
342
350
|
|
|
343
|
-
|
|
351
|
+
if (!configuredApiKey) {
|
|
352
|
+
api.logger.info("[mem9] apiKey not configured; starting auto-provision");
|
|
353
|
+
void resolveAPIKey(hookAgentId).catch((err) => {
|
|
354
|
+
api.logger.error(`[mem9] auto-provision failed: ${errorMessage(err)}`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
344
357
|
|
|
345
358
|
const factory: ToolFactory = (ctx: ToolContext) => {
|
|
346
359
|
const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
|
|
@@ -398,15 +411,24 @@ const toolNames = [
|
|
|
398
411
|
];
|
|
399
412
|
|
|
400
413
|
class LazyServerBackend implements MemoryBackend {
|
|
414
|
+
private apiUrl: string;
|
|
415
|
+
private apiKeyProvider: () => Promise<string>;
|
|
416
|
+
private agentId: string;
|
|
417
|
+
private timeouts: BackendTimeouts;
|
|
401
418
|
private resolved: ServerBackend | null = null;
|
|
402
419
|
private resolving: Promise<ServerBackend> | null = null;
|
|
403
420
|
|
|
404
421
|
constructor(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
) {
|
|
422
|
+
apiUrl: string,
|
|
423
|
+
apiKeyProvider: () => Promise<string>,
|
|
424
|
+
agentId: string,
|
|
425
|
+
timeouts: BackendTimeouts,
|
|
426
|
+
) {
|
|
427
|
+
this.apiUrl = apiUrl;
|
|
428
|
+
this.apiKeyProvider = apiKeyProvider;
|
|
429
|
+
this.agentId = agentId;
|
|
430
|
+
this.timeouts = timeouts;
|
|
431
|
+
}
|
|
410
432
|
|
|
411
433
|
private async resolve(): Promise<ServerBackend> {
|
|
412
434
|
if (this.resolved) return this.resolved;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mem9/mem9",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"test": "
|
|
37
|
+
"test": "rm -rf ./dist-test && tsc -p ./tsconfig.test.json && node --test ./dist-test/*.test.js",
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"prepublishOnly": "npm run typecheck"
|
|
40
40
|
},
|
package/server-backend.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
-
import { ServerBackend } from "./server-backend.
|
|
4
|
+
import { ServerBackend } from "./server-backend.js";
|
|
5
5
|
|
|
6
6
|
test("register forwards only utm_* params during auto-provision", async () => {
|
|
7
7
|
const originalFetch = globalThis.fetch;
|