@mem9/mem9 0.3.7 → 0.4.0-rc.0
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 +50 -18
- package/hooks.ts +17 -30
- package/index.ts +83 -347
- package/openclaw.plugin.json +25 -0
- package/package.json +4 -2
- package/server-backend.test.ts +82 -0
- package/server-backend.ts +49 -7
- package/types.ts +4 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# OpenClaw Plugin for
|
|
1
|
+
# OpenClaw Plugin for mem9
|
|
2
2
|
|
|
3
|
-
Memory plugin for [OpenClaw](https://github.com/openclaw) — replaces the built-in memory slot with cloud-persistent shared memory. Runs in server mode only, connecting to `mnemo-server` via `apiUrl` + `apiKey` (preferred) or legacy `tenantID`.
|
|
3
|
+
Memory plugin for [OpenClaw](https://github.com/openclaw) — replaces the built-in memory slot with cloud-persistent shared memory. Runs in server mode only, connecting to `mnemo-server` via `apiUrl` + `apiKey` (preferred) or legacy `tenantID`. Optional `provisionQueryParams` can forward `utm_*` attribution only during first-time auto-provisioning.
|
|
4
4
|
|
|
5
5
|
## 🚀 Quick Start (Server Mode)
|
|
6
6
|
|
|
@@ -20,18 +20,19 @@ curl -s -X POST http://localhost:8080/v1alpha1/mem9s \
|
|
|
20
20
|
# {"id": "uuid"}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Add
|
|
23
|
+
Add mem9 to your project's `openclaw.json`:
|
|
24
24
|
|
|
25
25
|
```json
|
|
26
26
|
{
|
|
27
27
|
"plugins": {
|
|
28
|
-
"slots": { "memory": "
|
|
28
|
+
"slots": { "memory": "mem9" },
|
|
29
29
|
"entries": {
|
|
30
|
-
"
|
|
30
|
+
"mem9": {
|
|
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
|
}
|
|
@@ -63,8 +64,8 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
63
64
|
|
|
64
65
|
| Hook | Trigger | What it does |
|
|
65
66
|
|---|---|---|
|
|
66
|
-
| `before_prompt_build` | Every LLM call | Searches memories by current prompt
|
|
67
|
-
| `after_compaction` | After `/compact` |
|
|
67
|
+
| `before_prompt_build` | Every LLM call | Searches memories by current prompt and injects relevant ones as context |
|
|
68
|
+
| `after_compaction` | After `/compact` | Logs compaction so the next prompt re-queries memories from the server |
|
|
68
69
|
| `before_reset` | Before `/reset` | Saves a session summary (last 3 user messages) as memory before context is wiped |
|
|
69
70
|
| `agent_end` | Agent finishes | Auto-captures the last assistant response as memory (if substantial) |
|
|
70
71
|
|
|
@@ -72,7 +73,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
72
73
|
|
|
73
74
|
| Tool | Description |
|
|
74
75
|
|---|---|
|
|
75
|
-
| `memory_store` | Store a new memory
|
|
76
|
+
| `memory_store` | Store a new memory |
|
|
76
77
|
| `memory_search` | Hybrid vector + keyword search (or keyword-only) |
|
|
77
78
|
| `memory_get` | Retrieve a single memory by ID |
|
|
78
79
|
| `memory_update` | Update an existing memory |
|
|
@@ -90,7 +91,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
90
91
|
### Method A: npm install (Recommended)
|
|
91
92
|
|
|
92
93
|
```bash
|
|
93
|
-
openclaw plugins install @mem9/
|
|
94
|
+
openclaw plugins install @mem9/mem9
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
### Method B: From source
|
|
@@ -103,7 +104,7 @@ npm install
|
|
|
103
104
|
|
|
104
105
|
### Configure OpenClaw
|
|
105
106
|
|
|
106
|
-
Add
|
|
107
|
+
Add mem9 to your project's `openclaw.json`:
|
|
107
108
|
|
|
108
109
|
OpenClaw is often deployed across teams with multiple agents. Server mode gives you:
|
|
109
110
|
|
|
@@ -138,10 +139,10 @@ Each agent uses the same `apiKey` for the shared memory pool. The plugin sends t
|
|
|
138
139
|
{
|
|
139
140
|
"plugins": {
|
|
140
141
|
"slots": {
|
|
141
|
-
"memory": "
|
|
142
|
+
"memory": "mem9"
|
|
142
143
|
},
|
|
143
144
|
"entries": {
|
|
144
|
-
"
|
|
145
|
+
"mem9": {
|
|
145
146
|
"enabled": true,
|
|
146
147
|
"config": {
|
|
147
148
|
"apiUrl": "http://your-server:8080",
|
|
@@ -159,8 +160,8 @@ That's it. The server handles scoping and conflict resolution. Conceptually, the
|
|
|
159
160
|
|
|
160
161
|
Start OpenClaw. You should see:
|
|
161
162
|
|
|
162
|
-
```
|
|
163
|
-
[mem9] Server mode
|
|
163
|
+
```text
|
|
164
|
+
[mem9] Server mode (v1alpha2)
|
|
164
165
|
```
|
|
165
166
|
|
|
166
167
|
If you see `[mem9] No mode configured...`, check your `openclaw.json` config.
|
|
@@ -173,9 +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
|
+
| `provisionQueryParams` | object | Optional `utm_*` map forwarded only to the initial `POST /v1alpha1/mem9s` auto-provision request when `apiKey` is absent |
|
|
178
|
+
| `defaultTimeoutMs` | number | Default timeout for non-search mem9 API requests in milliseconds. Default: `8000` |
|
|
179
|
+
| `searchTimeoutMs` | number | Timeout for `memory_search` and automatic recall search in milliseconds. Default: `15000` |
|
|
176
180
|
| `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
|
|
177
181
|
|
|
178
|
-
> **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.
|
|
182
|
+
> **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. `provisionQueryParams` is ignored after an `apiKey` is already configured, and non-`utm_*` keys are dropped before the provision request is sent.
|
|
183
|
+
|
|
184
|
+
## Timeout Behavior
|
|
185
|
+
|
|
186
|
+
The plugin uses two timeout buckets:
|
|
187
|
+
|
|
188
|
+
- `searchTimeoutMs` applies to `memory_search` and the automatic recall search in `before_prompt_build`
|
|
189
|
+
- `defaultTimeoutMs` applies to all other mem9 HTTP requests, including register, store, get, update, delete, and ingest
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"plugins": {
|
|
196
|
+
"entries": {
|
|
197
|
+
"openclaw": {
|
|
198
|
+
"enabled": true,
|
|
199
|
+
"config": {
|
|
200
|
+
"apiUrl": "http://your-server:8080",
|
|
201
|
+
"apiKey": "uuid",
|
|
202
|
+
"defaultTimeoutMs": 8000,
|
|
203
|
+
"searchTimeoutMs": 15000
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
179
210
|
|
|
180
211
|
## File Structure
|
|
181
212
|
|
|
@@ -183,7 +214,7 @@ Defined in `openclaw.plugin.json`:
|
|
|
183
214
|
openclaw-plugin/
|
|
184
215
|
├── README.md # This file
|
|
185
216
|
├── openclaw.plugin.json # Plugin metadata + config schema
|
|
186
|
-
├── package.json # npm package (@mem9/
|
|
217
|
+
├── package.json # npm package (@mem9/mem9)
|
|
187
218
|
├── index.ts # Plugin entry point + tool registration
|
|
188
219
|
├── backend.ts # MemoryBackend interface
|
|
189
220
|
├── server-backend.ts # Server mode: fetch → mnemo API
|
|
@@ -197,4 +228,5 @@ openclaw-plugin/
|
|
|
197
228
|
|---|---|---|
|
|
198
229
|
| `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
|
|
199
230
|
| `Server mode requires...` | Missing key | Add `apiKey` (or legacy `tenantID`) to config |
|
|
200
|
-
|
|
|
231
|
+
| Search requests time out | Hybrid/vector search exceeds plugin timeout | Increase `searchTimeoutMs` in plugin config |
|
|
232
|
+
| Plugin not loading | Not in memory slot | Set `"slots": {"memory": "mem9"}` 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,15 @@ 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(
|
|
317
|
+
const backend = new ServerBackend(
|
|
318
|
+
effectiveApiUrl,
|
|
319
|
+
"",
|
|
320
|
+
agentName,
|
|
321
|
+
{
|
|
322
|
+
timeouts: timeoutConfig,
|
|
323
|
+
provisionQueryParams: cfg.provisionQueryParams ?? {},
|
|
324
|
+
},
|
|
325
|
+
);
|
|
573
326
|
const result = await backend.register();
|
|
574
327
|
api.logger.info(
|
|
575
328
|
`[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
|
|
@@ -589,52 +342,32 @@ const mnemoPlugin = {
|
|
|
589
342
|
|
|
590
343
|
const hookAgentId = cfg.agentName ?? "agent";
|
|
591
344
|
|
|
592
|
-
const
|
|
593
|
-
|
|
345
|
+
const factory: ToolFactory = (ctx: ToolContext) => {
|
|
346
|
+
const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
|
|
347
|
+
const backend = new LazyServerBackend(
|
|
594
348
|
effectiveApiUrl,
|
|
595
349
|
() => resolveAPIKey(agentId),
|
|
596
350
|
agentId,
|
|
351
|
+
timeoutConfig,
|
|
597
352
|
);
|
|
598
|
-
|
|
599
|
-
const factory: ToolFactory = (ctx: ToolContext) => {
|
|
600
|
-
const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
|
|
601
|
-
const backend = createBackend(agentId);
|
|
602
353
|
return buildTools(backend);
|
|
603
354
|
};
|
|
604
355
|
|
|
605
356
|
api.registerTool(factory, { names: toolNames });
|
|
606
357
|
|
|
607
358
|
// 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
|
-
}
|
|
359
|
+
const hookBackend = new LazyServerBackend(
|
|
360
|
+
effectiveApiUrl,
|
|
361
|
+
() => resolveAPIKey(hookAgentId),
|
|
362
|
+
hookAgentId,
|
|
363
|
+
timeoutConfig,
|
|
364
|
+
);
|
|
632
365
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
) {
|
|
366
|
+
// Register memory capability so OpenClaw 2026.4.2+ binds this plugin to
|
|
367
|
+
// the memory slot. Without this, the plugin is treated as a legacy
|
|
368
|
+
// hook-only plugin and automatic context injection won't work.
|
|
369
|
+
// Guard with typeof check for backward compatibility with older hosts.
|
|
370
|
+
if (typeof api.registerCapability === "function") {
|
|
638
371
|
api.registerCapability("memory", {
|
|
639
372
|
search: async (query, opts) => {
|
|
640
373
|
const result = await hookBackend.search({ q: query, limit: opts?.limit });
|
|
@@ -672,6 +405,7 @@ class LazyServerBackend implements MemoryBackend {
|
|
|
672
405
|
private apiUrl: string,
|
|
673
406
|
private apiKeyProvider: () => Promise<string>,
|
|
674
407
|
private agentId: string,
|
|
408
|
+
private timeouts: BackendTimeouts,
|
|
675
409
|
) {}
|
|
676
410
|
|
|
677
411
|
private async resolve(): Promise<ServerBackend> {
|
|
@@ -680,7 +414,9 @@ class LazyServerBackend implements MemoryBackend {
|
|
|
680
414
|
|
|
681
415
|
this.resolving = this.apiKeyProvider().then((apiKey) =>
|
|
682
416
|
Promise.resolve().then(() => {
|
|
683
|
-
this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId
|
|
417
|
+
this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, {
|
|
418
|
+
timeouts: this.timeouts,
|
|
419
|
+
});
|
|
684
420
|
return this.resolved;
|
|
685
421
|
})
|
|
686
422
|
).catch((err) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -14,6 +14,23 @@
|
|
|
14
14
|
"type": "string",
|
|
15
15
|
"description": "mem9 API key (secret — do not share)"
|
|
16
16
|
},
|
|
17
|
+
"provisionQueryParams": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"description": "Optional utm_* params forwarded only during first-time auto-provisioning",
|
|
20
|
+
"additionalProperties": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"defaultTimeoutMs": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"minimum": 1,
|
|
27
|
+
"description": "Default timeout in milliseconds for non-search mem9 API requests (default 8000)"
|
|
28
|
+
},
|
|
29
|
+
"searchTimeoutMs": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"minimum": 1,
|
|
32
|
+
"description": "Timeout in milliseconds for memory search and automatic recall search (default 15000)"
|
|
33
|
+
},
|
|
17
34
|
"tenantID": {
|
|
18
35
|
"type": "string",
|
|
19
36
|
"description": "Deprecated: use apiKey"
|
|
@@ -30,6 +47,14 @@
|
|
|
30
47
|
"placeholder": "key...",
|
|
31
48
|
"sensitive": true
|
|
32
49
|
},
|
|
50
|
+
"defaultTimeoutMs": {
|
|
51
|
+
"label": "Default Timeout (ms)",
|
|
52
|
+
"placeholder": "8000"
|
|
53
|
+
},
|
|
54
|
+
"searchTimeoutMs": {
|
|
55
|
+
"label": "Search Timeout (ms)",
|
|
56
|
+
"placeholder": "15000"
|
|
57
|
+
},
|
|
33
58
|
"tenantID": {
|
|
34
59
|
"label": "Tenant ID (legacy)",
|
|
35
60
|
"placeholder": "uuid...",
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mem9/mem9",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-rc.0",
|
|
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",
|
|
7
7
|
"author": "mem9-ai",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/mem9-ai/mem9.git",
|
|
10
|
+
"url": "git+https://github.com/mem9-ai/mem9.git",
|
|
11
11
|
"directory": "openclaw-plugin"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/mem9-ai/mem9/tree/main/openclaw-plugin#readme",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
+
"test": "node --experimental-strip-types --test ./server-backend.test.ts",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
38
39
|
"prepublishOnly": "npm run typecheck"
|
|
39
40
|
},
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.15.30",
|
|
45
47
|
"typescript": "^5.5.0"
|
|
46
48
|
},
|
|
47
49
|
"openclaw": {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { ServerBackend } from "./server-backend.ts";
|
|
5
|
+
|
|
6
|
+
test("register forwards only utm_* params during auto-provision", async () => {
|
|
7
|
+
const originalFetch = globalThis.fetch;
|
|
8
|
+
let requestedURL = "";
|
|
9
|
+
|
|
10
|
+
globalThis.fetch = async (input, init) => {
|
|
11
|
+
requestedURL = String(input);
|
|
12
|
+
assert.equal(init?.method, "POST");
|
|
13
|
+
|
|
14
|
+
return new Response(JSON.stringify({ id: "space-1" }), {
|
|
15
|
+
status: 201,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const backend = new ServerBackend("https://api.mem9.ai", "", "agent-1", {
|
|
24
|
+
provisionQueryParams: {
|
|
25
|
+
utm_source: "bosn",
|
|
26
|
+
foo: "bar",
|
|
27
|
+
utm_campaign: "spring",
|
|
28
|
+
utm_medium: "",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await backend.register();
|
|
33
|
+
assert.equal(result.id, "space-1");
|
|
34
|
+
|
|
35
|
+
const url = new URL(requestedURL);
|
|
36
|
+
assert.equal(url.origin + url.pathname, "https://api.mem9.ai/v1alpha1/mem9s");
|
|
37
|
+
assert.equal(url.searchParams.get("utm_source"), "bosn");
|
|
38
|
+
assert.equal(url.searchParams.get("utm_campaign"), "spring");
|
|
39
|
+
assert.equal(url.searchParams.has("foo"), false);
|
|
40
|
+
assert.equal(url.searchParams.has("utm_medium"), false);
|
|
41
|
+
} finally {
|
|
42
|
+
globalThis.fetch = originalFetch;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("normal memory requests do not append provision query params", async () => {
|
|
47
|
+
const originalFetch = globalThis.fetch;
|
|
48
|
+
let requestedURL = "";
|
|
49
|
+
|
|
50
|
+
globalThis.fetch = async (input) => {
|
|
51
|
+
requestedURL = String(input);
|
|
52
|
+
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
id: "mem-1",
|
|
56
|
+
content: "remember this",
|
|
57
|
+
created_at: "2026-04-05T00:00:00Z",
|
|
58
|
+
updated_at: "2026-04-05T00:00:00Z",
|
|
59
|
+
}),
|
|
60
|
+
{
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const backend = new ServerBackend("https://api.mem9.ai", "space-key", "agent-1", {
|
|
71
|
+
provisionQueryParams: {
|
|
72
|
+
utm_source: "bosn",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await backend.store({ content: "remember this" });
|
|
77
|
+
|
|
78
|
+
assert.equal(requestedURL, "https://api.mem9.ai/v1alpha2/mem9s/memories");
|
|
79
|
+
} finally {
|
|
80
|
+
globalThis.fetch = originalFetch;
|
|
81
|
+
}
|
|
82
|
+
});
|
package/server-backend.ts
CHANGED
|
@@ -14,25 +14,60 @@ 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 ServerBackendOptions {
|
|
26
|
+
timeouts?: BackendTimeouts;
|
|
27
|
+
provisionQueryParams?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RequestOptions {
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
export class ServerBackend implements MemoryBackend {
|
|
18
35
|
private baseUrl: string;
|
|
19
36
|
private apiKey: string;
|
|
20
37
|
private agentName: string;
|
|
38
|
+
private provisionQueryParams: Record<string, string>;
|
|
39
|
+
private timeouts: Required<BackendTimeouts>;
|
|
21
40
|
|
|
22
41
|
constructor(
|
|
23
42
|
apiUrl: string,
|
|
24
43
|
apiKey: string,
|
|
25
44
|
agentName: string,
|
|
45
|
+
options: ServerBackendOptions = {},
|
|
26
46
|
) {
|
|
27
47
|
this.baseUrl = apiUrl.replace(/\/+$/, "");
|
|
28
48
|
this.apiKey = apiKey;
|
|
29
49
|
this.agentName = agentName;
|
|
50
|
+
this.provisionQueryParams = options.provisionQueryParams ?? {};
|
|
51
|
+
this.timeouts = {
|
|
52
|
+
defaultTimeoutMs: options.timeouts?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
53
|
+
searchTimeoutMs: options.timeouts?.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
54
|
+
};
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
async register(): Promise<ProvisionMem9sResponse> {
|
|
33
|
-
const
|
|
58
|
+
const query = new URLSearchParams();
|
|
59
|
+
for (const [key, value] of Object.entries(this.provisionQueryParams)) {
|
|
60
|
+
if (!key.startsWith("utm_") || typeof value !== "string" || value === "") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
query.set(key, value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const qs = query.toString();
|
|
68
|
+
const resp = await fetch(this.baseUrl + "/v1alpha1/mem9s" + (qs ? `?${qs}` : ""), {
|
|
34
69
|
method: "POST",
|
|
35
|
-
signal: AbortSignal.timeout(
|
|
70
|
+
signal: AbortSignal.timeout(this.timeouts.defaultTimeoutMs),
|
|
36
71
|
});
|
|
37
72
|
|
|
38
73
|
if (!resp.ok) {
|
|
@@ -75,7 +110,12 @@ export class ServerBackend implements MemoryBackend {
|
|
|
75
110
|
total: number;
|
|
76
111
|
limit: number;
|
|
77
112
|
offset: number;
|
|
78
|
-
}>(
|
|
113
|
+
}>(
|
|
114
|
+
"GET",
|
|
115
|
+
`${this.memoryPath("/memories")}${qs ? "?" + qs : ""}`,
|
|
116
|
+
undefined,
|
|
117
|
+
{ timeoutMs: this.timeouts.searchTimeoutMs },
|
|
118
|
+
);
|
|
79
119
|
return {
|
|
80
120
|
data: raw.memories ?? [],
|
|
81
121
|
total: raw.total,
|
|
@@ -116,7 +156,8 @@ export class ServerBackend implements MemoryBackend {
|
|
|
116
156
|
private async requestRaw(
|
|
117
157
|
method: string,
|
|
118
158
|
path: string,
|
|
119
|
-
body?: unknown
|
|
159
|
+
body?: unknown,
|
|
160
|
+
options?: RequestOptions,
|
|
120
161
|
): Promise<Response> {
|
|
121
162
|
const url = this.baseUrl + path;
|
|
122
163
|
const headers: Record<string, string> = {
|
|
@@ -128,16 +169,17 @@ export class ServerBackend implements MemoryBackend {
|
|
|
128
169
|
method,
|
|
129
170
|
headers,
|
|
130
171
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
131
|
-
signal: AbortSignal.timeout(
|
|
172
|
+
signal: AbortSignal.timeout(options?.timeoutMs ?? this.timeouts.defaultTimeoutMs),
|
|
132
173
|
});
|
|
133
174
|
}
|
|
134
175
|
|
|
135
176
|
private async request<T>(
|
|
136
177
|
method: string,
|
|
137
178
|
path: string,
|
|
138
|
-
body?: unknown
|
|
179
|
+
body?: unknown,
|
|
180
|
+
options?: RequestOptions,
|
|
139
181
|
): Promise<T> {
|
|
140
|
-
const resp = await this.requestRaw(method, path, body);
|
|
182
|
+
const resp = await this.requestRaw(method, path, body, options);
|
|
141
183
|
|
|
142
184
|
if (resp.status === 204) {
|
|
143
185
|
return undefined as T;
|
package/types.ts
CHANGED
|
@@ -2,7 +2,10 @@ export interface PluginConfig {
|
|
|
2
2
|
// Server mode (apiUrl present → server)
|
|
3
3
|
apiUrl?: string;
|
|
4
4
|
apiKey?: string;
|
|
5
|
+
provisionQueryParams?: Record<string, string>;
|
|
5
6
|
tenantID?: string;
|
|
7
|
+
defaultTimeoutMs?: number;
|
|
8
|
+
searchTimeoutMs?: number;
|
|
6
9
|
|
|
7
10
|
tenantName?: string;
|
|
8
11
|
|
|
@@ -25,6 +28,7 @@ export interface Memory {
|
|
|
25
28
|
created_at: string;
|
|
26
29
|
updated_at: string;
|
|
27
30
|
score?: number;
|
|
31
|
+
confidence?: number;
|
|
28
32
|
|
|
29
33
|
// Smart memory pipeline (server mode)
|
|
30
34
|
memory_type?: string;
|