@mem9/mem9 0.3.5 → 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.
package/hooks.ts CHANGED
@@ -130,11 +130,14 @@ function formatMemoriesBlock(memories: Memory[]): string {
130
130
  let idx = 1;
131
131
 
132
132
  const formatMem = (m: Memory): string => {
133
- const tags = m.tags?.length ? ` [${m.tags.join(", ")}]` : "";
133
+ const tagStr = m.tags?.length ? `[${m.tags.join(", ")}]` : "";
134
+ const age = m.relative_age ? `(${m.relative_age})` : "";
135
+ const middle = [tagStr, age].filter(Boolean).join(" ");
136
+ const sep = middle ? " " + middle + " " : " ";
134
137
  const content = m.content.length > MAX_CONTENT_LEN
135
138
  ? m.content.slice(0, MAX_CONTENT_LEN) + "..."
136
139
  : m.content;
137
- return `${idx++}.${tags} ${escapeForPrompt(content)}`;
140
+ return `${idx++}.${sep}${escapeForPrompt(content)}`;
138
141
  };
139
142
 
140
143
  if (pinned.length > 0) {
package/index.ts CHANGED
@@ -3,6 +3,7 @@ import { ServerBackend } from "./server-backend.js";
3
3
  import { registerHooks } from "./hooks.js";
4
4
  import type {
5
5
  PluginConfig,
6
+ Memory,
6
7
  CreateMemoryInput,
7
8
  UpdateMemoryInput,
8
9
  SearchInput,
@@ -11,6 +12,8 @@ import type {
11
12
  } from "./types.js";
12
13
 
13
14
  const DEFAULT_API_URL = "https://api.mem9.ai";
15
+ const MEM9_MEMORY_PATH_PREFIX = "mem9/";
16
+ const MEM9_MEMORY_PROVIDER = "mem9";
14
17
 
15
18
  function jsonResult(data: unknown) {
16
19
  // Older OpenClaw versions may assume tool results have a normalized
@@ -26,6 +29,141 @@ function jsonResult(data: unknown) {
26
29
  }
27
30
  }
28
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
+
95
+ interface MemoryCapability {
96
+ search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
97
+ store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
98
+ get: (id: string) => Promise<Memory | null>;
99
+ remove: (id: string) => Promise<boolean>;
100
+ }
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
+
29
167
  interface OpenClawPluginApi {
30
168
  pluginConfig?: unknown;
31
169
  logger: {
@@ -36,6 +174,10 @@ interface OpenClawPluginApi {
36
174
  factory: ToolFactory | (() => AnyAgentTool[]),
37
175
  opts: { names: string[] }
38
176
  ) => void;
177
+ registerMemoryCapability?: (capability: MemorySlotCapability) => void;
178
+ registerMemoryPromptSection?: (builder: MemoryPromptSectionBuilder) => void;
179
+ registerMemoryRuntime?: (runtime: MemoryRuntime) => void;
180
+ registerCapability?: (slot: string, capability: MemoryCapability) => void;
39
181
  on: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
40
182
  }
41
183
 
@@ -60,6 +202,146 @@ interface AnyAgentTool {
60
202
  execute: (_id: string, params: unknown) => Promise<unknown>;
61
203
  }
62
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
+
63
345
  function buildTools(backend: MemoryBackend): AnyAgentTool[] {
64
346
  return [
65
347
  {
@@ -113,6 +395,10 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
113
395
  type: "object",
114
396
  properties: {
115
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
+ },
116
402
  tags: {
117
403
  type: "string",
118
404
  description: "Comma-separated tags to filter by (AND)",
@@ -123,14 +409,28 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
123
409
  description: "Max results (default 20, max 200)",
124
410
  },
125
411
  offset: { type: "number", description: "Pagination offset" },
412
+ memory_type: {
413
+ type: "string",
414
+ description: "Comma-separated memory types to filter by (e.g. insight,pinned)",
415
+ },
126
416
  },
127
417
  required: [],
128
418
  },
129
419
  async execute(_id: string, params: unknown) {
130
420
  try {
131
- 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
+ }
132
425
  const result = await backend.search(input);
133
- 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
+ });
134
434
  } catch (err) {
135
435
  return jsonResult({
136
436
  ok: false,
@@ -148,16 +448,31 @@ function buildTools(backend: MemoryBackend): AnyAgentTool[] {
148
448
  type: "object",
149
449
  properties: {
150
450
  id: { type: "string", description: "Memory id (UUID)" },
451
+ path: {
452
+ type: "string",
453
+ description: "Memory lookup path alias, e.g. mem9/<id>",
454
+ },
151
455
  },
152
- required: ["id"],
456
+ required: [],
153
457
  },
154
458
  async execute(_id: string, params: unknown) {
155
459
  try {
156
- 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);
157
466
  const result = await backend.get(id);
158
467
  if (!result)
159
468
  return jsonResult({ ok: false, error: "memory not found" });
160
- return jsonResult({ ok: true, data: result });
469
+ return jsonResult({
470
+ ok: true,
471
+ data: {
472
+ ...result,
473
+ path: buildMemoryPath(result.id),
474
+ },
475
+ });
161
476
  } catch (err) {
162
477
  return jsonResult({
163
478
  ok: false,
@@ -237,8 +552,10 @@ const mnemoPlugin = {
237
552
  name: "Mnemo Memory",
238
553
  description:
239
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"],
240
557
 
241
- async register(api: OpenClawPluginApi) {
558
+ register(api: OpenClawPluginApi) {
242
559
  const cfg = (api.pluginConfig ?? {}) as PluginConfig;
243
560
  const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
244
561
  if (!cfg.apiUrl) {
@@ -272,25 +589,66 @@ const mnemoPlugin = {
272
589
 
273
590
  const hookAgentId = cfg.agentName ?? "agent";
274
591
 
275
- const factory: ToolFactory = (ctx: ToolContext) => {
276
- const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
277
- const backend = new LazyServerBackend(
592
+ const createBackend = (agentId: string): MemoryBackend =>
593
+ new LazyServerBackend(
278
594
  effectiveApiUrl,
279
595
  () => resolveAPIKey(agentId),
280
596
  agentId,
281
597
  );
598
+
599
+ const factory: ToolFactory = (ctx: ToolContext) => {
600
+ const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
601
+ const backend = createBackend(agentId);
282
602
  return buildTools(backend);
283
603
  };
284
604
 
285
605
  api.registerTool(factory, { names: toolNames });
286
606
 
287
- // Register hooks with a lazy backend for lifecycle memory management.
288
- // Uses the default workspace/agent context for hook-triggered operations.
289
- const hookBackend = new LazyServerBackend(
290
- effectiveApiUrl,
291
- () => resolveAPIKey(hookAgentId),
292
- hookAgentId,
293
- );
607
+ // 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
+ }
632
+
633
+ if (
634
+ typeof api.registerMemoryCapability !== "function" &&
635
+ typeof api.registerMemoryRuntime !== "function" &&
636
+ typeof api.registerCapability === "function"
637
+ ) {
638
+ api.registerCapability("memory", {
639
+ search: async (query, opts) => {
640
+ const result = await hookBackend.search({ q: query, limit: opts?.limit });
641
+ return { data: result.data, total: result.total };
642
+ },
643
+ store: async (content, opts) => {
644
+ return hookBackend.store({ content, tags: opts?.tags, source: opts?.source });
645
+ },
646
+ get: (id) => hookBackend.get(id),
647
+ remove: (id) => hookBackend.remove(id),
648
+ });
649
+ }
650
+
651
+ // Register hooks with lazy backend for lifecycle memory management.
294
652
  registerHooks(api, hookBackend, api.logger, {
295
653
  maxIngestBytes: cfg.maxIngestBytes,
296
654
  fallbackAgentId: hookAgentId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.3.5",
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",
package/server-backend.ts CHANGED
@@ -67,6 +67,7 @@ export class ServerBackend implements MemoryBackend {
67
67
  if (input.source) params.set("source", input.source);
68
68
  if (input.limit != null) params.set("limit", String(input.limit));
69
69
  if (input.offset != null) params.set("offset", String(input.offset));
70
+ if (input.memory_type) params.set("memory_type", input.memory_type);
70
71
 
71
72
  const qs = params.toString();
72
73
  const raw = await this.request<{
package/types.ts CHANGED
@@ -31,6 +31,8 @@ export interface Memory {
31
31
  state?: string;
32
32
  agent_id?: string;
33
33
  session_id?: string;
34
+
35
+ relative_age?: string;
34
36
  }
35
37
 
36
38
  export interface SearchResult {
@@ -60,6 +62,7 @@ export interface SearchInput {
60
62
  source?: string;
61
63
  limit?: number;
62
64
  offset?: number;
65
+ memory_type?: string;
63
66
  }
64
67
 
65
68
  export interface IngestMessage {