@mem9/mem9 0.3.6 → 0.3.7-rc1

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.
Files changed (2) hide show
  1. package/index.ts +344 -18
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -12,6 +12,8 @@ import type {
12
12
  } from "./types.js";
13
13
 
14
14
  const DEFAULT_API_URL = "https://api.mem9.ai";
15
+ const MEM9_MEMORY_PATH_PREFIX = "mem9/";
16
+ const MEM9_MEMORY_PROVIDER = "mem9";
15
17
 
16
18
  function jsonResult(data: unknown) {
17
19
  // Older OpenClaw versions may assume tool results have a normalized
@@ -27,6 +29,69 @@ function jsonResult(data: unknown) {
27
29
  }
28
30
  }
29
31
 
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
+
30
95
  interface MemoryCapability {
31
96
  search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
32
97
  store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
@@ -34,6 +99,71 @@ interface MemoryCapability {
34
99
  remove: (id: string) => Promise<boolean>;
35
100
  }
36
101
 
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
+
37
167
  interface OpenClawPluginApi {
38
168
  pluginConfig?: unknown;
39
169
  logger: {
@@ -44,6 +174,9 @@ interface OpenClawPluginApi {
44
174
  factory: ToolFactory | (() => AnyAgentTool[]),
45
175
  opts: { names: string[] }
46
176
  ) => void;
177
+ registerMemoryCapability?: (capability: MemorySlotCapability) => void;
178
+ registerMemoryPromptSection?: (builder: MemoryPromptSectionBuilder) => void;
179
+ registerMemoryRuntime?: (runtime: MemoryRuntime) => void;
47
180
  registerCapability?: (slot: string, capability: MemoryCapability) => void;
48
181
  on: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
49
182
  }
@@ -69,6 +202,146 @@ interface AnyAgentTool {
69
202
  execute: (_id: string, params: unknown) => Promise<unknown>;
70
203
  }
71
204
 
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
+
72
345
  function buildTools(backend: MemoryBackend): AnyAgentTool[] {
73
346
  return [
74
347
  {
@@ -122,6 +395,10 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
122
395
  type: "object",
123
396
  properties: {
124
397
  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
+ },
125
402
  tags: {
126
403
  type: "string",
127
404
  description: "Comma-separated tags to filter by (AND)",
@@ -141,9 +418,19 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
141
418
  },
142
419
  async execute(_id: string, params: unknown) {
143
420
  try {
144
- const input = (params ?? {}) as SearchInput;
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
+ }
145
425
  const result = await backend.search(input);
146
- return jsonResult({ ok: true, ...result });
426
+ return jsonResult({
427
+ ok: true,
428
+ ...result,
429
+ data: (result.data ?? []).map((memory) => ({
430
+ ...memory,
431
+ path: buildMemoryPath(memory.id),
432
+ })),
433
+ });
147
434
  } catch (err) {
148
435
  return jsonResult({
149
436
  ok: false,
@@ -161,16 +448,31 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
161
448
  type: "object",
162
449
  properties: {
163
450
  id: { type: "string", description: "Memory id (UUID)" },
451
+ path: {
452
+ type: "string",
453
+ description: "Memory lookup path alias, e.g. mem9/<id>",
454
+ },
164
455
  },
165
- required: ["id"],
456
+ required: [],
166
457
  },
167
458
  async execute(_id: string, params: unknown) {
168
459
  try {
169
- const { id } = params as { id: string };
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);
170
466
  const result = await backend.get(id);
171
467
  if (!result)
172
468
  return jsonResult({ ok: false, error: "memory not found" });
173
- return jsonResult({ ok: true, data: result });
469
+ return jsonResult({
470
+ ok: true,
471
+ data: {
472
+ ...result,
473
+ path: buildMemoryPath(result.id),
474
+ },
475
+ });
174
476
  } catch (err) {
175
477
  return jsonResult({
176
478
  ok: false,
@@ -250,6 +552,8 @@ const mnemoPlugin = {
250
552
  name: "Mnemo Memory",
251
553
  description:
252
554
  "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"],
253
557
 
254
558
  register(api: OpenClawPluginApi) {
255
559
  const cfg = (api.pluginConfig ?? {}) as PluginConfig;
@@ -285,30 +589,52 @@ const mnemoPlugin = {
285
589
 
286
590
  const hookAgentId = cfg.agentName ?? "agent";
287
591
 
288
- const factory: ToolFactory = (ctx: ToolContext) => {
289
- const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
290
- const backend = new LazyServerBackend(
592
+ const createBackend = (agentId: string): MemoryBackend =>
593
+ new LazyServerBackend(
291
594
  effectiveApiUrl,
292
595
  () => resolveAPIKey(agentId),
293
596
  agentId,
294
597
  );
598
+
599
+ const factory: ToolFactory = (ctx: ToolContext) => {
600
+ const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
601
+ const backend = createBackend(agentId);
295
602
  return buildTools(backend);
296
603
  };
297
604
 
298
605
  api.registerTool(factory, { names: toolNames });
299
606
 
300
607
  // Shared lazy backend for hooks and capability registration.
301
- const hookBackend = new LazyServerBackend(
302
- effectiveApiUrl,
303
- () => resolveAPIKey(hookAgentId),
304
- hookAgentId,
305
- );
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
+ }
306
632
 
307
- // Register memory capability so OpenClaw 2026.4.2+ binds this plugin to
308
- // the memory slot. Without this, the plugin is treated as a legacy
309
- // hook-only plugin and automatic context injection won't work.
310
- // Guard with typeof check for backward compatibility with older hosts.
311
- if (typeof api.registerCapability === "function") {
633
+ if (
634
+ typeof api.registerMemoryCapability !== "function" &&
635
+ typeof api.registerMemoryRuntime !== "function" &&
636
+ typeof api.registerCapability === "function"
637
+ ) {
312
638
  api.registerCapability("memory", {
313
639
  search: async (query, opts) => {
314
640
  const result = await hookBackend.search({ q: query, limit: opts?.limit });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.3.6",
3
+ "version": "0.3.7-rc1",
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",