@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 +6 -6
- package/backend.ts +3 -1
- package/hooks.ts +8 -1
- package/index.test.ts +70 -58
- package/index.ts +38 -58
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
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
|
|
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,
|
|
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
|
|
180
|
-
| `provisionQueryParams` | object | Optional `utm_*` map forwarded only to the initial `POST /v1alpha1/mem9s` request made
|
|
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
|
|
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
|
|
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(
|
|
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?: {
|
|
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(
|
|
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("
|
|
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
|
-
|
|
224
|
-
|
|
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
|
|
241
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
301
|
-
const
|
|
298
|
+
const hookA = apiA.getHook("before_prompt_build");
|
|
299
|
+
const hookB = apiB.getHook("before_prompt_build");
|
|
302
300
|
|
|
303
|
-
const promiseA =
|
|
304
|
-
const promiseB =
|
|
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
|
-
|
|
310
|
-
|
|
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
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
const secondResult =
|
|
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.
|
|
373
|
-
assert.equal(
|
|
384
|
+
assert.deepEqual(secondResult, { data: [], total: 0 });
|
|
385
|
+
assert.equal(searchRequests, 1);
|
|
374
386
|
assert.equal(
|
|
375
|
-
infoLogsB.includes("[mem9] reusing
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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]
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 {
|
package/openclaw.plugin.json
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"provisionToken": {
|
|
18
18
|
"type": "string",
|
|
19
|
-
"description": "One-time create-new token used locally to ensure
|
|
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
|
|
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