@mem9/mem9 0.3.7-rc1 → 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 +32 -1
- package/hooks.ts +17 -30
- package/index.ts +73 -347
- package/openclaw.plugin.json +18 -0
- package/package.json +1 -1
- package/server-backend.ts +31 -6
- package/types.ts +3 -0
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
|
-
* (
|
|
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
|
|
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
|
-
|
|
144
|
-
lines.push(
|
|
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 {
|
|
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
|
|
16
|
-
|
|
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 =
|
|
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
|
|
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
|
|
593
|
-
|
|
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 =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
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(
|
|
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
|
-
}>(
|
|
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(
|
|
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;
|