@mem9/mem9 0.3.7 → 0.3.8

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
@@ -31,7 +31,8 @@ Add mnemo to your project's `openclaw.json`:
31
31
  "enabled": true,
32
32
  "config": {
33
33
  "apiUrl": "http://localhost:8080",
34
- "apiKey": "uuid"
34
+ "apiKey": "uuid",
35
+ "searchTimeoutMs": 15000
35
36
  }
36
37
  }
37
38
  }
@@ -173,10 +174,39 @@ Defined in `openclaw.plugin.json`:
173
174
  |---|---|---|
174
175
  | `apiUrl` | string | mnemo-server URL |
175
176
  | `apiKey` | string | Preferred key. Uses `/v1alpha2/mem9s/...` with `X-API-Key` header |
177
+ | `defaultTimeoutMs` | number | Default timeout for non-search mem9 API requests in milliseconds. Default: `8000` |
178
+ | `searchTimeoutMs` | number | Timeout for `memory_search` and automatic recall search in milliseconds. Default: `15000` |
176
179
  | `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
177
180
 
178
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.
179
182
 
183
+ ## Timeout Behavior
184
+
185
+ The plugin uses two timeout buckets:
186
+
187
+ - `searchTimeoutMs` applies to `memory_search` and the automatic recall search in `before_prompt_build`
188
+ - `defaultTimeoutMs` applies to all other mem9 HTTP requests, including register, store, get, update, delete, and ingest
189
+
190
+ Example:
191
+
192
+ ```json
193
+ {
194
+ "plugins": {
195
+ "entries": {
196
+ "openclaw": {
197
+ "enabled": true,
198
+ "config": {
199
+ "apiUrl": "http://your-server:8080",
200
+ "apiKey": "uuid",
201
+ "defaultTimeoutMs": 8000,
202
+ "searchTimeoutMs": 15000
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ ```
209
+
180
210
  ## File Structure
181
211
 
182
212
  ```
@@ -197,4 +227,5 @@ openclaw-plugin/
197
227
  |---|---|---|
198
228
  | `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
199
229
  | `Server mode requires...` | Missing key | Add `apiKey` (or legacy `tenantID`) to config |
230
+ | Search requests time out | Hybrid/vector search exceeds plugin timeout | Increase `searchTimeoutMs` in plugin config |
200
231
  | Plugin not loading | Not in memory slot | Set `"slots": {"memory": "openclaw"}` in openclaw.json |
package/hooks.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides automatic memory recall and capture via OpenClaw's hook system:
5
5
  * - before_prompt_build: inject relevant memories into every LLM call
6
- * (grouped by type: pinned → insights)
6
+ * (preserving the server/backend recall order)
7
7
  * - after_compaction: (no-op placeholder for future use)
8
8
  * - before_reset: save session context before /reset wipes it
9
9
  * - agent_end: auto-capture via smart pipeline with size-aware message selection
@@ -57,6 +57,8 @@ interface HookContext {
57
57
  sessionId?: string;
58
58
  /** Legacy alias for sessionId used by older OpenClaw versions. */
59
59
  sessionKey?: string;
60
+ /** What initiated this agent run: "user", "heartbeat", "cron", or "memory". */
61
+ trigger?: string;
60
62
  }
61
63
 
62
64
  // ---------------------------------------------------------------------------
@@ -105,27 +107,11 @@ function escapeForPrompt(text: string): string {
105
107
  }
106
108
 
107
109
  /**
108
- * Format memories for injection, grouped by type for maximum comprehension:
109
- * 1. Pinned memories first (user-explicit preferences)
110
- * 2. Insights (extracted facts)
110
+ * Format memories for injection while preserving the backend recall order.
111
111
  */
112
112
  function formatMemoriesBlock(memories: Memory[]): string {
113
113
  if (memories.length === 0) return "";
114
114
 
115
- // Group by memory_type, falling back to "pinned" for legacy memories
116
- const pinned: Memory[] = [];
117
- const insights: Memory[] = [];
118
- const other: Memory[] = [];
119
-
120
- for (const m of memories) {
121
- const mtype = m.memory_type ?? "pinned";
122
- switch (mtype) {
123
- case "pinned": pinned.push(m); break;
124
- case "insight": insights.push(m); break;
125
- default: other.push(m); break;
126
- }
127
- }
128
-
129
115
  const lines: string[] = [];
130
116
  let idx = 1;
131
117
 
@@ -140,18 +126,8 @@ function formatMemoriesBlock(memories: Memory[]): string {
140
126
  return `${idx++}.${sep}${escapeForPrompt(content)}`;
141
127
  };
142
128
 
143
- if (pinned.length > 0) {
144
- lines.push("[Preferences]");
145
- for (const m of pinned) lines.push(formatMem(m));
146
- }
147
- if (insights.length > 0) {
148
- if (lines.length > 0) lines.push("");
149
- lines.push("[Knowledge]");
150
- for (const m of insights) lines.push(formatMem(m));
151
- }
152
- if (other.length > 0) {
153
- if (lines.length > 0) lines.push("");
154
- for (const m of other) lines.push(formatMem(m));
129
+ for (const memory of memories) {
130
+ lines.push(formatMem(memory));
155
131
  }
156
132
 
157
133
  return [
@@ -292,6 +268,12 @@ export function registerHooks(
292
268
  const hookCtx = (context ?? {}) as HookContext;
293
269
  if (!evt?.success || !evt.messages || evt.messages.length === 0) return;
294
270
 
271
+ // Skip cron/heartbeat-triggered runs — they produce low-value messages
272
+ if (hookCtx.trigger === "cron" || hookCtx.trigger === "heartbeat") {
273
+ logger.info(`[mem9] Skipping auto-ingest for ${hookCtx.trigger}-triggered run`);
274
+ return;
275
+ }
276
+
295
277
  // Format raw messages into IngestMessage format
296
278
  const formatted: IngestMessage[] = [];
297
279
  for (const msg of evt.messages) {
@@ -300,6 +282,11 @@ export function registerHooks(
300
282
  const role = typeof m.role === "string" ? m.role : "";
301
283
  if (!role) continue;
302
284
 
285
+ // Skip cron tool results — structured JSON job definitions with no memory value
286
+ if (role === "toolResult" && typeof m.toolName === "string" && m.toolName === "cron") {
287
+ continue;
288
+ }
289
+
303
290
  let content = "";
304
291
  if (typeof m.content === "string") {
305
292
  content = m.content;
package/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { MemoryBackend } from "./backend.js";
2
- import { ServerBackend } from "./server-backend.js";
2
+ import {
3
+ DEFAULT_SEARCH_TIMEOUT_MS,
4
+ DEFAULT_TIMEOUT_MS,
5
+ ServerBackend,
6
+ type BackendTimeouts,
7
+ } from "./server-backend.js";
3
8
  import { registerHooks } from "./hooks.js";
4
9
  import type {
5
10
  PluginConfig,
@@ -12,8 +17,49 @@ import type {
12
17
  } from "./types.js";
13
18
 
14
19
  const DEFAULT_API_URL = "https://api.mem9.ai";
15
- const MEM9_MEMORY_PATH_PREFIX = "mem9/";
16
- const MEM9_MEMORY_PROVIDER = "mem9";
20
+ const TIMEOUT_FIELDS = ["defaultTimeoutMs", "searchTimeoutMs"] as const;
21
+
22
+ function normalizeTimeoutMs(
23
+ value: unknown,
24
+ field: (typeof TIMEOUT_FIELDS)[number],
25
+ fallback: number,
26
+ logger: OpenClawPluginApi["logger"],
27
+ ): number {
28
+ if (value == null) return fallback;
29
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
30
+ logger.info(`[mem9] invalid ${field}; using ${fallback}ms`);
31
+ return fallback;
32
+ }
33
+ return Math.floor(value);
34
+ }
35
+
36
+ function resolveTimeouts(
37
+ cfg: PluginConfig,
38
+ logger: OpenClawPluginApi["logger"],
39
+ ): Required<BackendTimeouts> {
40
+ const timeouts = {
41
+ defaultTimeoutMs: normalizeTimeoutMs(
42
+ cfg.defaultTimeoutMs,
43
+ "defaultTimeoutMs",
44
+ DEFAULT_TIMEOUT_MS,
45
+ logger,
46
+ ),
47
+ searchTimeoutMs: normalizeTimeoutMs(
48
+ cfg.searchTimeoutMs,
49
+ "searchTimeoutMs",
50
+ DEFAULT_SEARCH_TIMEOUT_MS,
51
+ logger,
52
+ ),
53
+ };
54
+
55
+ if (TIMEOUT_FIELDS.some((field) => cfg[field] != null)) {
56
+ logger.info(
57
+ `[mem9] timeout config: defaultTimeoutMs=${timeouts.defaultTimeoutMs}, searchTimeoutMs=${timeouts.searchTimeoutMs}`,
58
+ );
59
+ }
60
+
61
+ return timeouts;
62
+ }
17
63
 
18
64
  function jsonResult(data: unknown) {
19
65
  // Older OpenClaw versions may assume tool results have a normalized
@@ -29,69 +75,6 @@ function jsonResult(data: unknown) {
29
75
  }
30
76
  }
31
77
 
32
- function buildMemoryPath(id: string): string {
33
- return `${MEM9_MEMORY_PATH_PREFIX}${id}`;
34
- }
35
-
36
- function normalizeMemoryLookup(value: string): string {
37
- const trimmed = value.trim();
38
- return trimmed.startsWith(MEM9_MEMORY_PATH_PREFIX)
39
- ? trimmed.slice(MEM9_MEMORY_PATH_PREFIX.length)
40
- : trimmed;
41
- }
42
-
43
- function normalizeSnippet(text: string, maxLength = 240): string {
44
- const flattened = text.replace(/\s+/g, " ").trim();
45
- if (flattened.length <= maxLength) {
46
- return flattened;
47
- }
48
- return `${flattened.slice(0, maxLength - 3)}...`;
49
- }
50
-
51
- function countLines(text: string): number {
52
- if (!text) {
53
- return 1;
54
- }
55
- return text.split(/\r?\n/).length;
56
- }
57
-
58
- function buildPromptSection(params: {
59
- availableTools: Set<string>;
60
- citationsMode?: string;
61
- }): string[] {
62
- const hasMemorySearch = params.availableTools.has("memory_search");
63
- const hasMemoryGet = params.availableTools.has("memory_get");
64
-
65
- if (!hasMemorySearch && !hasMemoryGet) {
66
- return [];
67
- }
68
-
69
- let toolGuidance: string;
70
- if (hasMemorySearch && hasMemoryGet) {
71
- toolGuidance =
72
- "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run `memory_search` first, then use `memory_get` to pull only the needed memory. If low confidence after search, say you checked.";
73
- } else if (hasMemorySearch) {
74
- toolGuidance =
75
- "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run `memory_search` first and answer from the matching results. If low confidence after search, say you checked.";
76
- } else {
77
- toolGuidance =
78
- "Before answering anything about prior work, decisions, dates, people, preferences, or todos that already point to a specific memory: run `memory_get` to pull only the needed memory. If low confidence after reading it, say you checked.";
79
- }
80
-
81
- const lines = ["## Memory Recall", toolGuidance];
82
- if (params.citationsMode === "off") {
83
- lines.push(
84
- "Citations are disabled: do not mention memory identifiers unless the user explicitly asks.",
85
- );
86
- } else {
87
- lines.push(
88
- "Citations: include the memory identifier only when it helps the user verify recalled memory.",
89
- );
90
- }
91
- lines.push("");
92
- return lines;
93
- }
94
-
95
78
  interface MemoryCapability {
96
79
  search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
97
80
  store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
@@ -99,71 +82,6 @@ interface MemoryCapability {
99
82
  remove: (id: string) => Promise<boolean>;
100
83
  }
101
84
 
102
- interface MemoryPromptSectionBuilder {
103
- (params: { availableTools: Set<string>; citationsMode?: string }): string[];
104
- }
105
-
106
- interface MemorySearchResult {
107
- path: string;
108
- startLine: number;
109
- endLine: number;
110
- score: number;
111
- snippet: string;
112
- source: "memory";
113
- citation?: string;
114
- }
115
-
116
- interface MemoryProviderStatus {
117
- backend: "builtin" | "qmd";
118
- provider: string;
119
- requestedProvider?: string;
120
- custom?: Record<string, unknown>;
121
- }
122
-
123
- interface MemorySearchManager {
124
- search: (
125
- query: string,
126
- opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
127
- ) => Promise<MemorySearchResult[]>;
128
- readFile: (params: {
129
- relPath: string;
130
- from?: number;
131
- lines?: number;
132
- }) => Promise<{ text: string; path: string }>;
133
- status: () => MemoryProviderStatus;
134
- sync?: (params?: {
135
- reason?: string;
136
- force?: boolean;
137
- sessionFiles?: string[];
138
- progress?: (update: { completed: number; total: number; label?: string }) => void;
139
- }) => Promise<void>;
140
- probeEmbeddingAvailability: () => Promise<{ ok: boolean; error?: string }>;
141
- probeVectorAvailability: () => Promise<boolean>;
142
- close?: () => Promise<void>;
143
- }
144
-
145
- interface MemoryRuntime {
146
- getMemorySearchManager: (params: {
147
- cfg?: unknown;
148
- agentId: string;
149
- purpose?: "default" | "status";
150
- }) => Promise<{ manager: MemorySearchManager | null; error?: string }>;
151
- resolveMemoryBackendConfig: (params: { cfg?: unknown; agentId: string }) => {
152
- backend: "builtin" | "qmd";
153
- provider: string;
154
- requestedProvider?: string;
155
- custom?: Record<string, unknown>;
156
- };
157
- closeAllMemorySearchManagers?: () => Promise<void>;
158
- }
159
-
160
- interface MemorySlotCapability {
161
- promptBuilder?: MemoryPromptSectionBuilder;
162
- runtime?: MemoryRuntime;
163
- flushPlanResolver?: unknown;
164
- publicArtifacts?: unknown;
165
- }
166
-
167
85
  interface OpenClawPluginApi {
168
86
  pluginConfig?: unknown;
169
87
  logger: {
@@ -174,9 +92,6 @@ interface OpenClawPluginApi {
174
92
  factory: ToolFactory | (() => AnyAgentTool[]),
175
93
  opts: { names: string[] }
176
94
  ) => void;
177
- registerMemoryCapability?: (capability: MemorySlotCapability) => void;
178
- registerMemoryPromptSection?: (builder: MemoryPromptSectionBuilder) => void;
179
- registerMemoryRuntime?: (runtime: MemoryRuntime) => void;
180
95
  registerCapability?: (slot: string, capability: MemoryCapability) => void;
181
96
  on: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
182
97
  }
@@ -202,146 +117,6 @@ interface AnyAgentTool {
202
117
  execute: (_id: string, params: unknown) => Promise<unknown>;
203
118
  }
204
119
 
205
- class Mem9MemorySearchManager implements MemorySearchManager {
206
- constructor(
207
- private backend: MemoryBackend,
208
- private apiUrl: string,
209
- ) {}
210
-
211
- async search(
212
- query: string,
213
- opts?: { maxResults?: number; minScore?: number; sessionKey?: string },
214
- ): Promise<MemorySearchResult[]> {
215
- void opts?.sessionKey;
216
- const result = await this.backend.search({
217
- q: query,
218
- limit: opts?.maxResults,
219
- });
220
- return (result.data ?? [])
221
- .map((memory, index) => {
222
- const startLine = 1;
223
- const endLine = countLines(memory.content);
224
- const score =
225
- typeof memory.score === "number"
226
- ? memory.score
227
- : Math.max(0, 1 - index / Math.max(result.data.length, 1));
228
- return {
229
- path: buildMemoryPath(memory.id),
230
- startLine,
231
- endLine,
232
- score,
233
- snippet: normalizeSnippet(memory.content),
234
- source: "memory" as const,
235
- citation: `${buildMemoryPath(memory.id)}#L${startLine}`,
236
- };
237
- })
238
- .filter((entry) => opts?.minScore == null || entry.score >= opts.minScore);
239
- }
240
-
241
- async readFile(params: {
242
- relPath: string;
243
- from?: number;
244
- lines?: number;
245
- }): Promise<{ text: string; path: string }> {
246
- const lookup = normalizeMemoryLookup(params.relPath);
247
- const memory = await this.backend.get(lookup);
248
- if (!memory) {
249
- return { text: "", path: params.relPath };
250
- }
251
-
252
- const contentLines = memory.content.split(/\r?\n/);
253
- const fromLine = Math.max(1, params.from ?? 1);
254
- const lineCount = params.lines == null ? contentLines.length : Math.max(0, params.lines);
255
- const sliced =
256
- params.lines == null
257
- ? contentLines.slice(fromLine - 1)
258
- : contentLines.slice(fromLine - 1, fromLine - 1 + lineCount);
259
-
260
- return {
261
- text: sliced.join("\n"),
262
- path: buildMemoryPath(memory.id),
263
- };
264
- }
265
-
266
- status(): MemoryProviderStatus {
267
- return {
268
- backend: "builtin",
269
- provider: MEM9_MEMORY_PROVIDER,
270
- requestedProvider: MEM9_MEMORY_PROVIDER,
271
- custom: {
272
- mode: "remote",
273
- apiUrl: this.apiUrl,
274
- },
275
- };
276
- }
277
-
278
- async sync(): Promise<void> {}
279
-
280
- async probeEmbeddingAvailability(): Promise<{ ok: boolean; error?: string }> {
281
- try {
282
- await this.backend.search({ q: "mem9-healthcheck", limit: 1 });
283
- return { ok: true };
284
- } catch (err) {
285
- return {
286
- ok: false,
287
- error: err instanceof Error ? err.message : String(err),
288
- };
289
- }
290
- }
291
-
292
- async probeVectorAvailability(): Promise<boolean> {
293
- const probe = await this.probeEmbeddingAvailability();
294
- return probe.ok;
295
- }
296
-
297
- async close(): Promise<void> {}
298
- }
299
-
300
- function createMemoryRuntime(params: {
301
- apiUrl: string;
302
- fallbackAgentId: string;
303
- createBackend: (agentId: string) => MemoryBackend;
304
- }): MemoryRuntime {
305
- const managers = new Map<string, Mem9MemorySearchManager>();
306
-
307
- const getOrCreateManager = (agentId: string): Mem9MemorySearchManager => {
308
- const resolvedAgentId = agentId.trim() || params.fallbackAgentId;
309
- const existing = managers.get(resolvedAgentId);
310
- if (existing) {
311
- return existing;
312
- }
313
- const next = new Mem9MemorySearchManager(
314
- params.createBackend(resolvedAgentId),
315
- params.apiUrl,
316
- );
317
- managers.set(resolvedAgentId, next);
318
- return next;
319
- };
320
-
321
- return {
322
- async getMemorySearchManager({ agentId }) {
323
- return { manager: getOrCreateManager(agentId) };
324
- },
325
- resolveMemoryBackendConfig() {
326
- return {
327
- backend: "builtin",
328
- provider: MEM9_MEMORY_PROVIDER,
329
- requestedProvider: MEM9_MEMORY_PROVIDER,
330
- custom: {
331
- mode: "remote",
332
- apiUrl: params.apiUrl,
333
- },
334
- };
335
- },
336
- async closeAllMemorySearchManagers() {
337
- for (const manager of managers.values()) {
338
- await manager.close?.();
339
- }
340
- managers.clear();
341
- },
342
- };
343
- }
344
-
345
120
  function buildTools(backend: MemoryBackend): AnyAgentTool[] {
346
121
  return [
347
122
  {
@@ -395,10 +170,6 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
395
170
  type: "object",
396
171
  properties: {
397
172
  q: { type: "string", description: "Search query" },
398
- query: {
399
- type: "string",
400
- description: "Search query alias for hosts that expect `query` instead of `q`",
401
- },
402
173
  tags: {
403
174
  type: "string",
404
175
  description: "Comma-separated tags to filter by (AND)",
@@ -418,19 +189,9 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
418
189
  },
419
190
  async execute(_id: string, params: unknown) {
420
191
  try {
421
- const input = { ...((params ?? {}) as SearchInput) };
422
- if (!input.q && typeof (params as { query?: unknown } | null)?.query === "string") {
423
- input.q = (params as { query: string }).query;
424
- }
192
+ const input = (params ?? {}) as SearchInput;
425
193
  const result = await backend.search(input);
426
- return jsonResult({
427
- ok: true,
428
- ...result,
429
- data: (result.data ?? []).map((memory) => ({
430
- ...memory,
431
- path: buildMemoryPath(memory.id),
432
- })),
433
- });
194
+ return jsonResult({ ok: true, ...result });
434
195
  } catch (err) {
435
196
  return jsonResult({
436
197
  ok: false,
@@ -448,31 +209,16 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
448
209
  type: "object",
449
210
  properties: {
450
211
  id: { type: "string", description: "Memory id (UUID)" },
451
- path: {
452
- type: "string",
453
- description: "Memory lookup path alias, e.g. mem9/<id>",
454
- },
455
212
  },
456
- required: [],
213
+ required: ["id"],
457
214
  },
458
215
  async execute(_id: string, params: unknown) {
459
216
  try {
460
- const raw = params as { id?: string; path?: string };
461
- const lookup = typeof raw.id === "string" ? raw.id : raw.path;
462
- if (!lookup) {
463
- return jsonResult({ ok: false, error: "memory id or path is required" });
464
- }
465
- const id = normalizeMemoryLookup(lookup);
217
+ const { id } = params as { id: string };
466
218
  const result = await backend.get(id);
467
219
  if (!result)
468
220
  return jsonResult({ ok: false, error: "memory not found" });
469
- return jsonResult({
470
- ok: true,
471
- data: {
472
- ...result,
473
- path: buildMemoryPath(result.id),
474
- },
475
- });
221
+ return jsonResult({ ok: true, data: result });
476
222
  } catch (err) {
477
223
  return jsonResult({
478
224
  ok: false,
@@ -552,12 +298,11 @@ const mnemoPlugin = {
552
298
  name: "Mnemo Memory",
553
299
  description:
554
300
  "AI agent memory — server mode (mnemo-server) with hybrid vector + keyword search.",
555
- // Static hint for hosts that inspect entry metadata before runtime registration.
556
- capabilities: ["memory"],
557
301
 
558
302
  register(api: OpenClawPluginApi) {
559
303
  const cfg = (api.pluginConfig ?? {}) as PluginConfig;
560
304
  const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
305
+ const timeoutConfig = resolveTimeouts(cfg, api.logger);
561
306
  if (!cfg.apiUrl) {
562
307
  api.logger.info(`[mem9] apiUrl not configured, using default ${DEFAULT_API_URL}`);
563
308
  }
@@ -569,7 +314,7 @@ const mnemoPlugin = {
569
314
  api.logger.info("[mem9] tenantID is deprecated; treating it as apiKey for v1alpha2");
570
315
  }
571
316
  const registerTenant = async (agentName: string): Promise<string> => {
572
- const backend = new ServerBackend(effectiveApiUrl, "", agentName);
317
+ const backend = new ServerBackend(effectiveApiUrl, "", agentName, timeoutConfig);
573
318
  const result = await backend.register();
574
319
  api.logger.info(
575
320
  `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
@@ -589,52 +334,32 @@ const mnemoPlugin = {
589
334
 
590
335
  const hookAgentId = cfg.agentName ?? "agent";
591
336
 
592
- const createBackend = (agentId: string): MemoryBackend =>
593
- new LazyServerBackend(
337
+ const factory: ToolFactory = (ctx: ToolContext) => {
338
+ const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
339
+ const backend = new LazyServerBackend(
594
340
  effectiveApiUrl,
595
341
  () => resolveAPIKey(agentId),
596
342
  agentId,
343
+ timeoutConfig,
597
344
  );
598
-
599
- const factory: ToolFactory = (ctx: ToolContext) => {
600
- const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
601
- const backend = createBackend(agentId);
602
345
  return buildTools(backend);
603
346
  };
604
347
 
605
348
  api.registerTool(factory, { names: toolNames });
606
349
 
607
350
  // Shared lazy backend for hooks and capability registration.
608
- const hookBackend = createBackend(hookAgentId);
609
- const memoryRuntime = createMemoryRuntime({
610
- apiUrl: effectiveApiUrl,
611
- fallbackAgentId: hookAgentId,
612
- createBackend,
613
- });
614
-
615
- // OpenClaw's memory slot API has evolved across versions:
616
- // - current hosts prefer registerMemoryCapability({ promptBuilder, runtime })
617
- // - 2026.4.2 uses registerMemoryPromptSection/registerMemoryRuntime
618
- // - older compatibility shims may still expose registerCapability("memory", ...)
619
- if (typeof api.registerMemoryCapability === "function") {
620
- api.registerMemoryCapability({
621
- promptBuilder: buildPromptSection,
622
- runtime: memoryRuntime,
623
- });
624
- } else {
625
- if (typeof api.registerMemoryPromptSection === "function") {
626
- api.registerMemoryPromptSection(buildPromptSection);
627
- }
628
- if (typeof api.registerMemoryRuntime === "function") {
629
- api.registerMemoryRuntime(memoryRuntime);
630
- }
631
- }
351
+ const hookBackend = new LazyServerBackend(
352
+ effectiveApiUrl,
353
+ () => resolveAPIKey(hookAgentId),
354
+ hookAgentId,
355
+ timeoutConfig,
356
+ );
632
357
 
633
- if (
634
- typeof api.registerMemoryCapability !== "function" &&
635
- typeof api.registerMemoryRuntime !== "function" &&
636
- typeof api.registerCapability === "function"
637
- ) {
358
+ // Register memory capability so OpenClaw 2026.4.2+ binds this plugin to
359
+ // the memory slot. Without this, the plugin is treated as a legacy
360
+ // hook-only plugin and automatic context injection won't work.
361
+ // Guard with typeof check for backward compatibility with older hosts.
362
+ if (typeof api.registerCapability === "function") {
638
363
  api.registerCapability("memory", {
639
364
  search: async (query, opts) => {
640
365
  const result = await hookBackend.search({ q: query, limit: opts?.limit });
@@ -672,6 +397,7 @@ class LazyServerBackend implements MemoryBackend {
672
397
  private apiUrl: string,
673
398
  private apiKeyProvider: () => Promise<string>,
674
399
  private agentId: string,
400
+ private timeouts: BackendTimeouts,
675
401
  ) {}
676
402
 
677
403
  private async resolve(): Promise<ServerBackend> {
@@ -680,7 +406,7 @@ class LazyServerBackend implements MemoryBackend {
680
406
 
681
407
  this.resolving = this.apiKeyProvider().then((apiKey) =>
682
408
  Promise.resolve().then(() => {
683
- this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId);
409
+ this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, this.timeouts);
684
410
  return this.resolved;
685
411
  })
686
412
  ).catch((err) => {
@@ -14,6 +14,16 @@
14
14
  "type": "string",
15
15
  "description": "mem9 API key (secret — do not share)"
16
16
  },
17
+ "defaultTimeoutMs": {
18
+ "type": "number",
19
+ "minimum": 1,
20
+ "description": "Default timeout in milliseconds for non-search mem9 API requests (default 8000)"
21
+ },
22
+ "searchTimeoutMs": {
23
+ "type": "number",
24
+ "minimum": 1,
25
+ "description": "Timeout in milliseconds for memory search and automatic recall search (default 15000)"
26
+ },
17
27
  "tenantID": {
18
28
  "type": "string",
19
29
  "description": "Deprecated: use apiKey"
@@ -30,6 +40,14 @@
30
40
  "placeholder": "key...",
31
41
  "sensitive": true
32
42
  },
43
+ "defaultTimeoutMs": {
44
+ "label": "Default Timeout (ms)",
45
+ "placeholder": "8000"
46
+ },
47
+ "searchTimeoutMs": {
48
+ "label": "Search Timeout (ms)",
49
+ "placeholder": "15000"
50
+ },
33
51
  "tenantID": {
34
52
  "label": "Tenant ID (legacy)",
35
53
  "placeholder": "uuid...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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",
package/server-backend.ts CHANGED
@@ -14,25 +14,43 @@ type ProvisionMem9sResponse = {
14
14
  id: string;
15
15
  };
16
16
 
17
+ export const DEFAULT_TIMEOUT_MS = 8_000;
18
+ export const DEFAULT_SEARCH_TIMEOUT_MS = 15_000;
19
+
20
+ export interface BackendTimeouts {
21
+ defaultTimeoutMs?: number;
22
+ searchTimeoutMs?: number;
23
+ }
24
+
25
+ interface RequestOptions {
26
+ timeoutMs?: number;
27
+ }
28
+
17
29
  export class ServerBackend implements MemoryBackend {
18
30
  private baseUrl: string;
19
31
  private apiKey: string;
20
32
  private agentName: string;
33
+ private timeouts: Required<BackendTimeouts>;
21
34
 
22
35
  constructor(
23
36
  apiUrl: string,
24
37
  apiKey: string,
25
38
  agentName: string,
39
+ timeouts: BackendTimeouts = {},
26
40
  ) {
27
41
  this.baseUrl = apiUrl.replace(/\/+$/, "");
28
42
  this.apiKey = apiKey;
29
43
  this.agentName = agentName;
44
+ this.timeouts = {
45
+ defaultTimeoutMs: timeouts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
46
+ searchTimeoutMs: timeouts.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
47
+ };
30
48
  }
31
49
 
32
50
  async register(): Promise<ProvisionMem9sResponse> {
33
51
  const resp = await fetch(this.baseUrl + "/v1alpha1/mem9s", {
34
52
  method: "POST",
35
- signal: AbortSignal.timeout(8_000),
53
+ signal: AbortSignal.timeout(this.timeouts.defaultTimeoutMs),
36
54
  });
37
55
 
38
56
  if (!resp.ok) {
@@ -75,7 +93,12 @@ export class ServerBackend implements MemoryBackend {
75
93
  total: number;
76
94
  limit: number;
77
95
  offset: number;
78
- }>("GET", `${this.memoryPath("/memories")}${qs ? "?" + qs : ""}`);
96
+ }>(
97
+ "GET",
98
+ `${this.memoryPath("/memories")}${qs ? "?" + qs : ""}`,
99
+ undefined,
100
+ { timeoutMs: this.timeouts.searchTimeoutMs },
101
+ );
79
102
  return {
80
103
  data: raw.memories ?? [],
81
104
  total: raw.total,
@@ -116,7 +139,8 @@ export class ServerBackend implements MemoryBackend {
116
139
  private async requestRaw(
117
140
  method: string,
118
141
  path: string,
119
- body?: unknown
142
+ body?: unknown,
143
+ options?: RequestOptions,
120
144
  ): Promise<Response> {
121
145
  const url = this.baseUrl + path;
122
146
  const headers: Record<string, string> = {
@@ -128,16 +152,17 @@ export class ServerBackend implements MemoryBackend {
128
152
  method,
129
153
  headers,
130
154
  body: body != null ? JSON.stringify(body) : undefined,
131
- signal: AbortSignal.timeout(8_000),
155
+ signal: AbortSignal.timeout(options?.timeoutMs ?? this.timeouts.defaultTimeoutMs),
132
156
  });
133
157
  }
134
158
 
135
159
  private async request<T>(
136
160
  method: string,
137
161
  path: string,
138
- body?: unknown
162
+ body?: unknown,
163
+ options?: RequestOptions,
139
164
  ): Promise<T> {
140
- const resp = await this.requestRaw(method, path, body);
165
+ const resp = await this.requestRaw(method, path, body, options);
141
166
 
142
167
  if (resp.status === 204) {
143
168
  return undefined as T;
package/types.ts CHANGED
@@ -3,6 +3,8 @@ export interface PluginConfig {
3
3
  apiUrl?: string;
4
4
  apiKey?: string;
5
5
  tenantID?: string;
6
+ defaultTimeoutMs?: number;
7
+ searchTimeoutMs?: number;
6
8
 
7
9
  tenantName?: string;
8
10
 
@@ -25,6 +27,7 @@ export interface Memory {
25
27
  created_at: string;
26
28
  updated_at: string;
27
29
  score?: number;
30
+ confidence?: number;
28
31
 
29
32
  // Smart memory pipeline (server mode)
30
33
  memory_type?: string;