openclaw-amem 1.1.4 → 1.2.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 CHANGED
@@ -110,8 +110,8 @@ A memory plugin's job is to read configuration from the environment and send mem
110
110
 
111
111
  What it actually does — all of it declared in [`openclaw.plugin.json`](openclaw.plugin.json):
112
112
 
113
- - **Environment variables it reads** (its configuration surface, supplied by you): `AMEM_LLM_API_KEY`, `AMEM_LLM_BASE_URL`, `AMEM_LLM_MODEL`, `AMEM_COLLECTION`, `AMEM_DATA_DIR`, `AMEM_EVO_COUNTER_PATH`, `AMEM_REVIEW_DIR`, `AMEM_PROMPT_LOCALE`. No credential is bundled, hardcoded, or logged.
114
- - **Network destinations**: only your **local Qdrant** (`http://localhost:6333`) and your configured **LLM endpoint** (Anthropic by default, or `AMEM_LLM_BASE_URL`). It sends memory text/embeddings there to store and evolve notes — its stated purpose. It does not phone home.
113
+ - **Environment variables it reads** (its configuration surface, supplied by you): `AMEM_LLM_PROVIDER`, `AMEM_LLM_API_KEY`, `AMEM_LLM_BASE_URL`, `AMEM_LLM_MODEL`, `AMEM_COLLECTION`, `AMEM_DATA_DIR`, `AMEM_EVO_COUNTER_PATH`, `AMEM_REVIEW_DIR`, `AMEM_PROMPT_LOCALE`. No credential is bundled, hardcoded, or logged.
114
+ - **Network destinations**: only your **local Qdrant** (`http://localhost:6333`) and your configured **LLM endpoint** (Anthropic by default, or any OpenAI-compatible endpoint via `AMEM_LLM_PROVIDER=openai` + `AMEM_LLM_BASE_URL`). It sends memory text/embeddings there to store and evolve notes — its stated purpose. It does not phone home.
115
115
  - **Conversation content** is processed for memory only when you set `hooks.allowConversationAccess: true`. Keep Qdrant and review-output paths scoped to locations you control.
116
116
 
117
117
  ## Development
package/dist/index.js CHANGED
@@ -122,468 +122,6 @@ var init_embedding = __esm({
122
122
  }
123
123
  });
124
124
 
125
- // ../amem-core/src/evo-counter.ts
126
- function counterFile() {
127
- return process.env.AMEM_EVO_COUNTER_PATH || path2.join(getDataDir(), "amem_evo_cnt.json");
128
- }
129
- function getEvoCount() {
130
- try {
131
- const data = JSON.parse(fs.readFileSync(counterFile(), "utf-8"));
132
- return data.count || 0;
133
- } catch {
134
- return 0;
135
- }
136
- }
137
- function incrementEvoCount() {
138
- const count = getEvoCount() + 1;
139
- fs.writeFileSync(counterFile(), JSON.stringify({ count, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
140
- return count;
141
- }
142
- function shouldRunEvolution() {
143
- const count = incrementEvoCount();
144
- return count % EVO_THRESHOLD === 0;
145
- }
146
- var fs, path2, EVO_THRESHOLD;
147
- var init_evo_counter = __esm({
148
- "../amem-core/src/evo-counter.ts"() {
149
- "use strict";
150
- fs = __toESM(require("fs"), 1);
151
- path2 = __toESM(require("path"), 1);
152
- init_config();
153
- EVO_THRESHOLD = 20;
154
- }
155
- });
156
-
157
- // ../amem-core/src/prompts.ts
158
- var LOCALE, en, zh, templates, t;
159
- var init_prompts = __esm({
160
- "../amem-core/src/prompts.ts"() {
161
- "use strict";
162
- LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
163
- en = {
164
- crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
165
-
166
- ## Conversation
167
-
168
- User: ${userText}
169
- Assistant: ${assistantText}
170
-
171
- ## Existing relevant memories (identified by integer idx)
172
-
173
- ${memoryList}
174
-
175
- ## Task
176
-
177
- Extract only genuinely important long-term facts (decisions, preferences, account info, project status, key insights). Skip small talk, confirmations, and information already captured in existing memories.
178
-
179
- ## Operation types
180
- - NEW: Extract a brand new fact not present in existing memories
181
- - UPDATE: New information refines or supersedes an existing memory; specify existingIdx
182
- - DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
183
- - NONE: Nothing worth recording, or information already fully captured
184
-
185
- ## Output format
186
-
187
- Return a JSON array. Each item:
188
- {"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
189
-
190
- Return at most 3 operations. If nothing is worth recording, return [].
191
- Return only the JSON array, no other text.
192
-
193
- Examples:
194
-
195
- 1. New preference:
196
- [{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
197
-
198
- 2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
199
- [{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
200
-
201
- 3. Conversation is just "Sure, thanks" / "Got it" with no new info:
202
- []`,
203
- shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
204
-
205
- Memory A: ${contentA}
206
- Memory B: ${contentB}
207
-
208
- Rules:
209
- - If both memories express the same core fact (possibly different wording or granularity), return:
210
- {"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
211
- - If the memories are complementary, on different topics, or contain different specific facts, return:
212
- {"shouldMerge": false}
213
-
214
- Return only JSON, no other text.
215
-
216
- Examples:
217
-
218
- 1. Should merge (different granularity):
219
- A: "Project uses PostgreSQL"
220
- B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
221
- -> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
222
-
223
- 2. Should NOT merge (complementary but distinct):
224
- A: "User prefers VS Code"
225
- B: "User's VS Code uses One Dark Pro theme"
226
- -> {"shouldMerge": false}`,
227
- evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
228
-
229
- Old memory: ${oldContent}
230
- New memory: ${newContent}
231
-
232
- Classification rules:
233
-
234
- - EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
235
- Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
236
-
237
- - CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
238
- Return: {"type": "CONFLICT"}
239
-
240
- - EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
241
- Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
242
-
243
- - NEW: Completely unrelated information, no substantive connection to the old memory
244
- Return: {"type": "NEW"}
245
-
246
- Return only JSON, no other text.`
247
- };
248
- zh = {
249
- crudDecision: (userText, assistantText, memoryList) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u7BA1\u7406 agent\uFF0C\u8D1F\u8D23\u5206\u6790\u5BF9\u8BDD\u5185\u5BB9\u5E76\u51B3\u5B9A\u5982\u4F55\u64CD\u4F5C\u8BB0\u5FC6\u5E93\u3002
250
-
251
- ## \u5BF9\u8BDD\u5185\u5BB9
252
-
253
- \u7528\u6237\uFF1A${userText}
254
- \u52A9\u624B\uFF1A${assistantText}
255
-
256
- ## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
257
-
258
- ${memoryList}
259
-
260
- ## \u4EFB\u52A1
261
-
262
- \u5206\u6790\u4E0A\u8FF0\u5BF9\u8BDD\uFF0C\u51B3\u5B9A\u9700\u8981\u54EA\u4E9B\u8BB0\u5FC6\u64CD\u4F5C\u3002\u53EA\u63D0\u53D6\u771F\u6B63\u91CD\u8981\u7684\u957F\u671F\u4E8B\u5B9E\uFF08\u51B3\u7B56\u3001\u504F\u597D\u3001\u8D26\u53F7\u4FE1\u606F\u3001\u9879\u76EE\u72B6\u6001\u3001\u5173\u952E\u6D1E\u5BDF\uFF09\u3002\u8DF3\u8FC7\u95F2\u804A\u3001\u786E\u8BA4\u8BED\u3001\u91CD\u590D\u4FE1\u606F\u3002
263
-
264
- ## \u64CD\u4F5C\u7C7B\u578B
265
- - NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
266
- - UPDATE\uFF1A\u65B0\u4FE1\u606F\u66F4\u65B0\u4E86\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\uFF0C\u7528 existingIdx \u6307\u5B9A\u8981\u66F4\u65B0\u7684\u6761\u76EE
267
- - DELETE\uFF1A\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\u5DF2\u7ECF\u8FC7\u65F6\u3001\u53D1\u751F\u51B2\u7A81\u6216\u9519\u8BEF\uFF0C\u7528 existingIdx \u6307\u5B9A\uFF0Cfact \u586B\u539F\u5185\u5BB9
268
- - NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
269
-
270
- ## \u8F93\u51FA\u683C\u5F0F
271
-
272
- \u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
273
- {"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
274
-
275
- \u6BCF\u6B21\u6700\u591A\u8FD4\u56DE 3 \u6761\u64CD\u4F5C\u3002\u5982\u679C\u6CA1\u6709\u503C\u5F97\u64CD\u4F5C\u7684\u5185\u5BB9\uFF0C\u8FD4\u56DE []\u3002
276
- \u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
277
-
278
- \u793A\u4F8B\uFF1A
279
-
280
- 1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
281
- [{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
282
-
283
- 2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
284
- [{"action": "UPDATE", "fact": "\u7528\u6237\u51B3\u5B9A\u4F7F\u7528 React\uFF08\u653E\u5F03\u4E86 Vue\uFF09", "existingIdx": 0, "reason": "\u51B3\u7B56\u5DF2\u660E\u786E\uFF0C\u66F4\u65B0\u8BC4\u4F30\u72B6\u6001"}]
285
-
286
- 3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
287
- []`,
288
- shouldMerge: (contentA, contentB) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u53BB\u91CD\u52A9\u624B\uFF0C\u8D1F\u8D23\u5224\u65AD\u4E24\u6761\u8BB0\u5FC6\u662F\u5426\u8868\u8FBE\u4E86\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\u3002
289
-
290
- \u8BB0\u5FC6A\uFF1A${contentA}
291
- \u8BB0\u5FC6B\uFF1A${contentB}
292
-
293
- \u5224\u65AD\u89C4\u5219\uFF1A
294
- - \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u8868\u8FBE\u7684\u662F\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\uFF08\u53EF\u80FD\u63AA\u8F9E\u4E0D\u540C\u3001\u7C92\u5EA6\u4E0D\u540C\uFF0C\u4F46\u6838\u5FC3\u4E8B\u5B9E\u4E00\u81F4\uFF09\uFF0C\u8FD4\u56DE JSON\uFF1A
295
- {"shouldMerge": true, "merged": "\u5408\u5E76\u540E\u7684\u7B80\u6D01\u8868\u8FF0\uFF0C\u4FDD\u7559\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u952E\u4FE1\u606F\uFF0C\u6BD4\u4EFB\u4F55\u4E00\u6761\u90FD\u66F4\u5B8C\u6574"}
296
- - \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u662F\u4E92\u8865\u4FE1\u606F\u3001\u4E0D\u540C\u4E3B\u9898\u3001\u6216\u5305\u542B\u4E0D\u540C\u7684\u5177\u4F53\u4E8B\u5B9E\uFF0C\u8FD4\u56DE JSON\uFF1A
297
- {"shouldMerge": false}
298
-
299
- \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
300
-
301
- \u793A\u4F8B\uFF1A
302
-
303
- 1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
304
- A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
305
- B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
306
- \u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
307
-
308
- 2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
309
- A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
310
- B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
311
- \u2192 {"shouldMerge": false}`,
312
- evolutionJudge: (oldContent, newContent) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u6F14\u5316\u5224\u65AD\u52A9\u624B\u3002\u5206\u6790\u4EE5\u4E0B\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u7CFB\u5E76\u8FD4\u56DE JSON\u3002
313
-
314
- \u65E7\u8BB0\u5FC6\uFF1A${oldContent}
315
- \u65B0\u8BB0\u5FC6\uFF1A${newContent}
316
-
317
- \u5224\u65AD\u89C4\u5219\uFF1A
318
-
319
- - EVOLVE\uFF1A\u65B0\u5185\u5BB9\u662F\u5BF9\u65E7\u8BB0\u5FC6\u7684\u6DF1\u5316/\u66F4\u65B0\uFF08\u5982\u300C\u6B63\u5728\u8003\u8651\u7528 Next.js\u300D\u2192\u300C\u51B3\u5B9A\u7528 Next.js 14 App Router\u300D\uFF09
320
- \u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
321
-
322
- - CONFLICT\uFF1A\u65B0\u65E7\u4FE1\u606F\u5728\u540C\u4E00\u5C5E\u6027\u4E0A\u76F4\u63A5\u77DB\u76FE\uFF08\u5982\u300C\u4F7F\u7528 MySQL \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
323
- \u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
324
-
325
- - EXPAND\uFF1A\u65B0\u4FE1\u606F\u662F\u5BF9\u65E7\u8BB0\u5FC6\u540C\u4E00\u4E3B\u9898\u7684\u8865\u5145\u6269\u5C55\uFF08\u5982\u300C\u8D1F\u8D23\u540E\u7AEF\u5F00\u53D1\u300D+\u300C\u540E\u7AEF\u4F7F\u7528 Go \u548C gRPC\u300D\uFF09
326
- \u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
327
-
328
- - NEW\uFF1A\u5168\u65B0\u4FE1\u606F\uFF0C\u4E0E\u65E7\u8BB0\u5FC6\u65E0\u5B9E\u8D28\u5173\u8054\uFF08\u5982\u300C\u559C\u6B22 dark mode\u300Dvs\u300C\u4E0B\u5468\u8981\u53BB\u51FA\u5DEE\u300D\uFF09
329
- \u8FD4\u56DE\uFF1A{"type": "NEW"}
330
-
331
- \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`
332
- };
333
- templates = { en, zh };
334
- t = templates[LOCALE];
335
- }
336
- });
337
-
338
- // ../amem-core/src/llm.ts
339
- async function llmCall(prompt, maxTokens = 500) {
340
- try {
341
- const isThinking = MODEL.includes("gemini") || MODEL.includes("pro-agent");
342
- const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
343
- const resp = await client.messages.create({
344
- model: MODEL,
345
- max_tokens: effectiveMaxTokens,
346
- messages: [{ role: "user", content: prompt }]
347
- });
348
- for (const block of resp.content) {
349
- if (block.type === "text") return block.text.trim();
350
- }
351
- return null;
352
- } catch (e) {
353
- console.error(`[amem] LLM call failed: ${e.message}`);
354
- return null;
355
- }
356
- }
357
- function stripFences(raw) {
358
- raw = raw.trim();
359
- if (raw.startsWith("```")) {
360
- const lines = raw.split("\n");
361
- lines.shift();
362
- if (lines[lines.length - 1] === "```") lines.pop();
363
- raw = lines.join("\n").trim();
364
- }
365
- if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
366
- try {
367
- raw = JSON.parse(raw);
368
- } catch {
369
- }
370
- }
371
- return raw;
372
- }
373
- async function llmConstructNote(content) {
374
- const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:
375
- {
376
- "keywords": ["keyword1", "keyword2"],
377
- "tags": ["tag1", "tag2"],
378
- "context": "one sentence summary in the same language as the input",
379
- "category": "Technical|Business|Personal|Project|Research|System|General",
380
- "note_type": "memory|knowledge",
381
- "topics": ["Topic1", "Topic2"],
382
- "confidence": "high|medium|low"
383
- }
384
-
385
- Category guide:
386
- - Technical: code, tools, configuration, APIs, debugging
387
- - Business: company, finance, compliance, contracts, invoices
388
- - Personal: personal state, habits, preferences, emotions
389
- - Project: project progress, decisions, milestones
390
- - Research: research, literature, evaluation, comparison
391
- - System: system services, monitoring, operations
392
- - General: anything that does not fit the above
393
-
394
- note_type guide:
395
- - knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
396
- - memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
397
-
398
- topics guide (Story 26B):
399
- - Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
400
- - List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
401
-
402
- confidence guide (Story 27):
403
- - high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
404
- - medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
405
- - low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
406
-
407
- Text: ${content}`;
408
- const raw = await llmCall(prompt, 400);
409
- if (!raw)
410
- return {
411
- keywords: [],
412
- tags: [],
413
- context: "",
414
- category: "General",
415
- note_type: "memory",
416
- topics: [],
417
- confidence: "medium"
418
- };
419
- try {
420
- const data = JSON.parse(stripFences(raw));
421
- const rawCategory = typeof data.category === "string" ? data.category : "General";
422
- const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
423
- const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
424
- const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
425
- const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
426
- const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
427
- return {
428
- keywords: Array.isArray(data.keywords) ? data.keywords : [],
429
- tags: Array.isArray(data.tags) ? data.tags : [],
430
- context: typeof data.context === "string" ? data.context : "",
431
- category,
432
- note_type,
433
- topics,
434
- confidence
435
- };
436
- } catch (e) {
437
- console.error(`[amem] Note construction parse failed: ${e.message}`);
438
- return {
439
- keywords: [],
440
- tags: [],
441
- context: "",
442
- category: "General",
443
- note_type: "memory",
444
- topics: [],
445
- confidence: "medium"
446
- };
447
- }
448
- }
449
- async function llmShouldLink(noteContent, candidateContent) {
450
- const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
451
- Reply with only "yes" or "no".
452
-
453
- Note A: ${noteContent}
454
- Note B: ${candidateContent}`;
455
- const raw = await llmCall(prompt, 10);
456
- if (!raw) return false;
457
- return raw.toLowerCase().startsWith("yes");
458
- }
459
- async function llmCrudDecision(userText, assistantText, existingMemories) {
460
- const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
461
- const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
462
- try {
463
- const raw = await llmCall(prompt, 400);
464
- if (!raw) return [];
465
- const match = raw.match(/\[.*\]/s);
466
- if (!match) return [];
467
- const parsed = JSON.parse(match[0]);
468
- if (!Array.isArray(parsed)) return [];
469
- const ops = [];
470
- for (const item of parsed) {
471
- if (!item || typeof item !== "object") continue;
472
- const action = item.action;
473
- if (!["NEW", "UPDATE", "DELETE", "NONE"].includes(action)) continue;
474
- if (action === "NONE") continue;
475
- const op = {
476
- action,
477
- fact: typeof item.fact === "string" ? item.fact : "",
478
- reason: typeof item.reason === "string" ? item.reason : void 0
479
- };
480
- if (typeof item.existingIdx === "number") {
481
- op.existingIdx = item.existingIdx;
482
- }
483
- ops.push(op);
484
- }
485
- return ops.slice(0, 3);
486
- } catch (e) {
487
- console.error(`[amem] llmCrudDecision failed: ${e.message}`);
488
- return [];
489
- }
490
- }
491
- async function llmShouldMerge(contentA, contentB) {
492
- const prompt = t.shouldMerge(contentA, contentB);
493
- const raw = await llmCall(prompt, 300);
494
- if (!raw) return { shouldMerge: false };
495
- try {
496
- const data = JSON.parse(stripFences(raw));
497
- if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
498
- if (data.shouldMerge && typeof data.merged === "string") {
499
- return { shouldMerge: true, merged: data.merged };
500
- }
501
- return { shouldMerge: false };
502
- } catch (e) {
503
- console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
504
- return { shouldMerge: false };
505
- }
506
- }
507
- async function llmEvolutionJudge(oldContent, newContent) {
508
- const prompt = t.evolutionJudge(oldContent, newContent);
509
- const raw = await llmCall(prompt, 300);
510
- if (!raw) return { type: "NEW" };
511
- try {
512
- const data = JSON.parse(stripFences(raw));
513
- const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
514
- return {
515
- type,
516
- mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
517
- };
518
- } catch (e) {
519
- console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
520
- return { type: "NEW" };
521
- }
522
- }
523
- async function llmEvolveNote(content, linkedNotes) {
524
- const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
525
- Content: ${n.content}`).join("\n");
526
- const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
527
- Reply with JSON only (no markdown):
528
- {
529
- "tags": ["tag1", "tag2", ...],
530
- "context": "updated one sentence summary",
531
- "should_strengthen": true|false,
532
- "suggested_connections": ["neighbor_id_1", "neighbor_id_2", ...],
533
- "tags_to_update": ["tag_1", ..., "tag_n"]
534
- }
535
-
536
- Guidelines:
537
- - "tags" and "context" are for updating the original note based on new connections.
538
- - "should_strengthen" is a decision whether this note should strengthen its connections to any of the newly linked notes (neighbors).
539
- - "suggested_connections" must contain only IDs from the newly linked notes (neighbors) listed below.
540
- - "tags_to_update" are updated tags for the original note itself if we strengthen connections.
541
-
542
- Original note content: ${content}
543
-
544
- Newly linked notes (neighbors):
545
- ${linkedStr}`;
546
- const raw = await llmCall(prompt, 500);
547
- if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
548
- try {
549
- const data = JSON.parse(stripFences(raw));
550
- return {
551
- tags: Array.isArray(data.tags) ? data.tags : null,
552
- context: typeof data.context === "string" ? data.context : null,
553
- shouldStrengthen: typeof data.should_strengthen === "boolean" ? data.should_strengthen : false,
554
- suggestedConnections: Array.isArray(data.suggested_connections) ? data.suggested_connections.map(String) : [],
555
- tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
556
- };
557
- } catch (e) {
558
- console.error(`[amem] Evolution parse failed: ${e.message}`);
559
- return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
560
- }
561
- }
562
- var import_sdk, client, MODEL, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
563
- var init_llm = __esm({
564
- "../amem-core/src/llm.ts"() {
565
- "use strict";
566
- import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
567
- init_prompts();
568
- client = new import_sdk.default({
569
- ...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
570
- ...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
571
- });
572
- MODEL = process.env.AMEM_LLM_MODEL ?? "claude-sonnet-4-6";
573
- VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
574
- VALID_CATEGORIES = /* @__PURE__ */ new Set([
575
- "Technical",
576
- "Business",
577
- "Personal",
578
- "Project",
579
- "Research",
580
- "System",
581
- "General"
582
- ]);
583
- VALID_EVOLUTION_TYPES = /* @__PURE__ */ new Set(["EVOLVE", "CONFLICT", "EXPAND", "NEW"]);
584
- }
585
- });
586
-
587
125
  // ../amem-core/src/storage.ts
588
126
  async function qdrant(method, path6, body) {
589
127
  const res = await fetch(`${QDRANT_URL}${path6}`, {
@@ -601,10 +139,6 @@ async function pingQdrant() {
601
139
  const res = await fetch(`${QDRANT_URL}/readyz`);
602
140
  if (!res.ok) throw new Error(`Qdrant GET /readyz failed: ${res.status}`);
603
141
  }
604
- function resetCollectionReady() {
605
- _collectionReady = false;
606
- _collectionReadyMap.clear();
607
- }
608
142
  async function ensureCollection(collectionName) {
609
143
  const col = collectionName || getCollection();
610
144
  if (collectionName) {
@@ -942,57 +476,526 @@ function makeCrud(collectionName, modeBIsolated = false) {
942
476
  function createStorageContext(collectionName, modeBIsolated = false) {
943
477
  return makeCrud(collectionName || getCollection(), modeBIsolated);
944
478
  }
945
- async function addNote(note) {
946
- return makeCrud(getCollection()).addNote(note);
947
- }
948
479
  async function getNote(id) {
949
480
  return makeCrud(getCollection()).getNote(id);
950
481
  }
951
482
  async function updateNote(note) {
952
483
  return makeCrud(getCollection()).updateNote(note);
953
484
  }
954
- async function findByHash(hash, agentId) {
955
- return makeCrud(getCollection()).findByHash(hash, agentId);
485
+ async function listNotes(agentId) {
486
+ return makeCrud(getCollection()).listNotes(agentId);
487
+ }
488
+ async function deleteNote(id) {
489
+ return makeCrud(getCollection()).deleteNote(id);
490
+ }
491
+ async function invalidateNote(id) {
492
+ return makeCrud(getCollection()).invalidateNote(id);
493
+ }
494
+ async function patchNotePayload(id, fields) {
495
+ return makeCrud(getCollection()).patchNotePayload(id, fields);
496
+ }
497
+ var QDRANT_URL, getCollection, VECTOR_DIM, _collectionReady, _collectionReadyMap;
498
+ var init_storage = __esm({
499
+ "../amem-core/src/storage.ts"() {
500
+ "use strict";
501
+ QDRANT_URL = "http://localhost:6333";
502
+ getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
503
+ VECTOR_DIM = 384;
504
+ _collectionReady = false;
505
+ _collectionReadyMap = /* @__PURE__ */ new Map();
506
+ }
507
+ });
508
+
509
+ // ../amem-core/src/prompts.ts
510
+ var LOCALE, en, zh, templates, t;
511
+ var init_prompts = __esm({
512
+ "../amem-core/src/prompts.ts"() {
513
+ "use strict";
514
+ LOCALE = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
515
+ en = {
516
+ crudDecision: (userText, assistantText, memoryList) => `You are a memory management agent. Analyze the conversation and decide what memory operations are needed.
517
+
518
+ ## Conversation
519
+
520
+ User: ${userText}
521
+ Assistant: ${assistantText}
522
+
523
+ ## Existing relevant memories (identified by integer idx)
524
+
525
+ ${memoryList}
526
+
527
+ ## Task
528
+
529
+ Extract only genuinely important long-term facts (decisions, preferences, account info, project status, key insights). Skip small talk, confirmations, and information already captured in existing memories.
530
+
531
+ ## Operation types
532
+ - NEW: Extract a brand new fact not present in existing memories
533
+ - UPDATE: New information refines or supersedes an existing memory; specify existingIdx
534
+ - DELETE: An existing memory is outdated, contradicted, or wrong; specify existingIdx, fact = original content
535
+ - NONE: Nothing worth recording, or information already fully captured
536
+
537
+ ## Output format
538
+
539
+ Return a JSON array. Each item:
540
+ {"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "fact content", "existingIdx": integer or omit, "reason": "optional"}
541
+
542
+ Return at most 3 operations. If nothing is worth recording, return [].
543
+ Return only the JSON array, no other text.
544
+
545
+ Examples:
546
+
547
+ 1. New preference:
548
+ [{"action": "NEW", "fact": "User prefers TypeScript over JavaScript", "reason": "Explicitly stated tech preference"}]
549
+
550
+ 2. Updating an existing memory (idx 0 was "User is evaluating React and Vue"):
551
+ [{"action": "UPDATE", "fact": "User decided to use React (dropped Vue)", "existingIdx": 0, "reason": "Decision finalized, update evaluation status"}]
552
+
553
+ 3. Conversation is just "Sure, thanks" / "Got it" with no new info:
554
+ []`,
555
+ shouldMerge: (contentA, contentB) => `You are a memory deduplication assistant. Determine whether two memories express essentially the same information.
556
+
557
+ Memory A: ${contentA}
558
+ Memory B: ${contentB}
559
+
560
+ Rules:
561
+ - If both memories express the same core fact (possibly different wording or granularity), return:
562
+ {"shouldMerge": true, "merged": "Concise merged statement preserving key details from both, more complete than either alone"}
563
+ - If the memories are complementary, on different topics, or contain different specific facts, return:
564
+ {"shouldMerge": false}
565
+
566
+ Return only JSON, no other text.
567
+
568
+ Examples:
569
+
570
+ 1. Should merge (different granularity):
571
+ A: "Project uses PostgreSQL"
572
+ B: "Project's primary database is PostgreSQL 16, deployed on AWS RDS"
573
+ -> {"shouldMerge": true, "merged": "Project uses PostgreSQL 16 as primary database, deployed on AWS RDS"}
574
+
575
+ 2. Should NOT merge (complementary but distinct):
576
+ A: "User prefers VS Code"
577
+ B: "User's VS Code uses One Dark Pro theme"
578
+ -> {"shouldMerge": false}`,
579
+ evolutionJudge: (oldContent, newContent) => `You are a memory evolution judge. Analyze the relationship between an old and new memory and return JSON.
580
+
581
+ Old memory: ${oldContent}
582
+ New memory: ${newContent}
583
+
584
+ Classification rules:
585
+
586
+ - EVOLVE: New content deepens or updates the old memory (e.g. "Considering Next.js" -> "Decided on Next.js 14 App Router")
587
+ Return: {"type": "EVOLVE", "mergedContent": "Merged content preserving the evolution trajectory"}
588
+
589
+ - CONFLICT: Old and new information directly contradict each other on the same attribute (e.g. "Uses MySQL as primary DB" vs "Migrated to PostgreSQL")
590
+ Return: {"type": "CONFLICT"}
591
+
592
+ - EXPAND: New information supplements the old memory on the same topic (e.g. "Handles backend dev" + "Backend uses Go and gRPC")
593
+ Return: {"type": "EXPAND", "mergedContent": "Merged content integrating both pieces of information"}
594
+
595
+ - NEW: Completely unrelated information, no substantive connection to the old memory
596
+ Return: {"type": "NEW"}
597
+
598
+ Return only JSON, no other text.`
599
+ };
600
+ zh = {
601
+ crudDecision: (userText, assistantText, memoryList) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u7BA1\u7406 agent\uFF0C\u8D1F\u8D23\u5206\u6790\u5BF9\u8BDD\u5185\u5BB9\u5E76\u51B3\u5B9A\u5982\u4F55\u64CD\u4F5C\u8BB0\u5FC6\u5E93\u3002
602
+
603
+ ## \u5BF9\u8BDD\u5185\u5BB9
604
+
605
+ \u7528\u6237\uFF1A${userText}
606
+ \u52A9\u624B\uFF1A${assistantText}
607
+
608
+ ## \u5DF2\u6709\u76F8\u5173\u8BB0\u5FC6\uFF08\u7528\u6574\u6570 idx \u6807\u8BC6\uFF09
609
+
610
+ ${memoryList}
611
+
612
+ ## \u4EFB\u52A1
613
+
614
+ \u5206\u6790\u4E0A\u8FF0\u5BF9\u8BDD\uFF0C\u51B3\u5B9A\u9700\u8981\u54EA\u4E9B\u8BB0\u5FC6\u64CD\u4F5C\u3002\u53EA\u63D0\u53D6\u771F\u6B63\u91CD\u8981\u7684\u957F\u671F\u4E8B\u5B9E\uFF08\u51B3\u7B56\u3001\u504F\u597D\u3001\u8D26\u53F7\u4FE1\u606F\u3001\u9879\u76EE\u72B6\u6001\u3001\u5173\u952E\u6D1E\u5BDF\uFF09\u3002\u8DF3\u8FC7\u95F2\u804A\u3001\u786E\u8BA4\u8BED\u3001\u91CD\u590D\u4FE1\u606F\u3002
615
+
616
+ ## \u64CD\u4F5C\u7C7B\u578B
617
+ - NEW\uFF1A\u63D0\u53D6\u5168\u65B0\u4E8B\u5B9E\uFF08\u5DF2\u6709\u8BB0\u5FC6\u4E2D\u6CA1\u6709\u7684\u4FE1\u606F\uFF09
618
+ - UPDATE\uFF1A\u65B0\u4FE1\u606F\u66F4\u65B0\u4E86\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\uFF0C\u7528 existingIdx \u6307\u5B9A\u8981\u66F4\u65B0\u7684\u6761\u76EE
619
+ - DELETE\uFF1A\u67D0\u6761\u5DF2\u6709\u8BB0\u5FC6\u5DF2\u7ECF\u8FC7\u65F6\u3001\u53D1\u751F\u51B2\u7A81\u6216\u9519\u8BEF\uFF0C\u7528 existingIdx \u6307\u5B9A\uFF0Cfact \u586B\u539F\u5185\u5BB9
620
+ - NONE\uFF1A\u4E0D\u503C\u5F97\u8BB0\u5F55\u6216\u5DF2\u6709\u5B8C\u5168\u76F8\u540C\u7684\u4FE1\u606F
621
+
622
+ ## \u8F93\u51FA\u683C\u5F0F
623
+
624
+ \u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u6BCF\u6761\u683C\u5F0F\uFF1A
625
+ {"action": "NEW"|"UPDATE"|"DELETE"|"NONE", "fact": "\u4E8B\u5B9E\u5185\u5BB9", "existingIdx": \u6574\u6570\u6216\u7701\u7565, "reason": "\u539F\u56E0\uFF08\u53EF\u9009\uFF09"}
626
+
627
+ \u6BCF\u6B21\u6700\u591A\u8FD4\u56DE 3 \u6761\u64CD\u4F5C\u3002\u5982\u679C\u6CA1\u6709\u503C\u5F97\u64CD\u4F5C\u7684\u5185\u5BB9\uFF0C\u8FD4\u56DE []\u3002
628
+ \u53EA\u8FD4\u56DE JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
629
+
630
+ \u793A\u4F8B\uFF1A
631
+
632
+ 1. \u63D0\u53D6\u65B0\u504F\u597D\uFF1A
633
+ [{"action": "NEW", "fact": "\u7528\u6237\u504F\u597D TypeScript \u800C\u975E JavaScript", "reason": "\u660E\u786E\u8868\u8FBE\u7684\u6280\u672F\u504F\u597D"}]
634
+
635
+ 2. \u66F4\u65B0\u5DF2\u6709\u8BB0\u5FC6\uFF08idx 0 \u539F\u4E3A"\u7528\u6237\u6B63\u5728\u8BC4\u4F30 React \u548C Vue"\uFF09\uFF1A
636
+ [{"action": "UPDATE", "fact": "\u7528\u6237\u51B3\u5B9A\u4F7F\u7528 React\uFF08\u653E\u5F03\u4E86 Vue\uFF09", "existingIdx": 0, "reason": "\u51B3\u7B56\u5DF2\u660E\u786E\uFF0C\u66F4\u65B0\u8BC4\u4F30\u72B6\u6001"}]
637
+
638
+ 3. \u5BF9\u8BDD\u4EC5\u4E3A"\u597D\u7684\uFF0C\u8C22\u8C22"/"\u6CA1\u95EE\u9898"\u7B49\u786E\u8BA4\u8BED\uFF0C\u65E0\u65B0\u4FE1\u606F\uFF1A
639
+ []`,
640
+ shouldMerge: (contentA, contentB) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u53BB\u91CD\u52A9\u624B\uFF0C\u8D1F\u8D23\u5224\u65AD\u4E24\u6761\u8BB0\u5FC6\u662F\u5426\u8868\u8FBE\u4E86\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\u3002
641
+
642
+ \u8BB0\u5FC6A\uFF1A${contentA}
643
+ \u8BB0\u5FC6B\uFF1A${contentB}
644
+
645
+ \u5224\u65AD\u89C4\u5219\uFF1A
646
+ - \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u8868\u8FBE\u7684\u662F\u672C\u8D28\u76F8\u540C\u7684\u4FE1\u606F\uFF08\u53EF\u80FD\u63AA\u8F9E\u4E0D\u540C\u3001\u7C92\u5EA6\u4E0D\u540C\uFF0C\u4F46\u6838\u5FC3\u4E8B\u5B9E\u4E00\u81F4\uFF09\uFF0C\u8FD4\u56DE JSON\uFF1A
647
+ {"shouldMerge": true, "merged": "\u5408\u5E76\u540E\u7684\u7B80\u6D01\u8868\u8FF0\uFF0C\u4FDD\u7559\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u952E\u4FE1\u606F\uFF0C\u6BD4\u4EFB\u4F55\u4E00\u6761\u90FD\u66F4\u5B8C\u6574"}
648
+ - \u5982\u679C\u4E24\u6761\u8BB0\u5FC6\u662F\u4E92\u8865\u4FE1\u606F\u3001\u4E0D\u540C\u4E3B\u9898\u3001\u6216\u5305\u542B\u4E0D\u540C\u7684\u5177\u4F53\u4E8B\u5B9E\uFF0C\u8FD4\u56DE JSON\uFF1A
649
+ {"shouldMerge": false}
650
+
651
+ \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002
652
+
653
+ \u793A\u4F8B\uFF1A
654
+
655
+ 1. \u5E94\u5408\u5E76\uFF08\u7C92\u5EA6\u4E0D\u540C\uFF09\uFF1A
656
+ A: "\u9879\u76EE\u4F7F\u7528 PostgreSQL \u6570\u636E\u5E93"
657
+ B: "\u9879\u76EE\u7684\u4E3B\u6570\u636E\u5E93\u662F PostgreSQL 16\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"
658
+ \u2192 {"shouldMerge": true, "merged": "\u9879\u76EE\u4F7F\u7528 PostgreSQL 16 \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\uFF0C\u90E8\u7F72\u5728 AWS RDS \u4E0A"}
659
+
660
+ 2. \u4E0D\u5E94\u5408\u5E76\uFF08\u4E92\u8865\u4F46\u4E0D\u540C\uFF09\uFF1A
661
+ A: "\u7528\u6237\u559C\u6B22\u7528 VS Code"
662
+ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
663
+ \u2192 {"shouldMerge": false}`,
664
+ evolutionJudge: (oldContent, newContent) => `\u4F60\u662F\u4E00\u4E2A\u8BB0\u5FC6\u6F14\u5316\u5224\u65AD\u52A9\u624B\u3002\u5206\u6790\u4EE5\u4E0B\u4E24\u6761\u8BB0\u5FC6\u7684\u5173\u7CFB\u5E76\u8FD4\u56DE JSON\u3002
665
+
666
+ \u65E7\u8BB0\u5FC6\uFF1A${oldContent}
667
+ \u65B0\u8BB0\u5FC6\uFF1A${newContent}
668
+
669
+ \u5224\u65AD\u89C4\u5219\uFF1A
670
+
671
+ - EVOLVE\uFF1A\u65B0\u5185\u5BB9\u662F\u5BF9\u65E7\u8BB0\u5FC6\u7684\u6DF1\u5316/\u66F4\u65B0\uFF08\u5982\u300C\u6B63\u5728\u8003\u8651\u7528 Next.js\u300D\u2192\u300C\u51B3\u5B9A\u7528 Next.js 14 App Router\u300D\uFF09
672
+ \u8FD4\u56DE\uFF1A{"type": "EVOLVE", "mergedContent": "\u878D\u5408\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u4FDD\u7559\u6F14\u5316\u8F68\u8FF9"}
673
+
674
+ - CONFLICT\uFF1A\u65B0\u65E7\u4FE1\u606F\u5728\u540C\u4E00\u5C5E\u6027\u4E0A\u76F4\u63A5\u77DB\u76FE\uFF08\u5982\u300C\u4F7F\u7528 MySQL \u4F5C\u4E3A\u4E3B\u6570\u636E\u5E93\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
675
+ \u8FD4\u56DE\uFF1A{"type": "CONFLICT"}
676
+
677
+ - EXPAND\uFF1A\u65B0\u4FE1\u606F\u662F\u5BF9\u65E7\u8BB0\u5FC6\u540C\u4E00\u4E3B\u9898\u7684\u8865\u5145\u6269\u5C55\uFF08\u5982\u300C\u8D1F\u8D23\u540E\u7AEF\u5F00\u53D1\u300D+\u300C\u540E\u7AEF\u4F7F\u7528 Go \u548C gRPC\u300D\uFF09
678
+ \u8FD4\u56DE\uFF1A{"type": "EXPAND", "mergedContent": "\u5408\u5E76\u540E\u7684\u5B8C\u6574\u5185\u5BB9\uFF0C\u6574\u5408\u53CC\u65B9\u4FE1\u606F"}
679
+
680
+ - NEW\uFF1A\u5168\u65B0\u4FE1\u606F\uFF0C\u4E0E\u65E7\u8BB0\u5FC6\u65E0\u5B9E\u8D28\u5173\u8054\uFF08\u5982\u300C\u559C\u6B22 dark mode\u300Dvs\u300C\u4E0B\u5468\u8981\u53BB\u51FA\u5DEE\u300D\uFF09
681
+ \u8FD4\u56DE\uFF1A{"type": "NEW"}
682
+
683
+ \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`
684
+ };
685
+ templates = { en, zh };
686
+ t = templates[LOCALE];
687
+ }
688
+ });
689
+
690
+ // ../amem-core/src/llm.ts
691
+ function anthropic() {
692
+ return _anthropic ??= new import_sdk.default({
693
+ ...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
694
+ ...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
695
+ });
696
+ }
697
+ function openai() {
698
+ return _openai ??= new import_openai.default({
699
+ // AMEM_LLM_API_KEY first (engine convention), then the SDK's own
700
+ // OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
701
+ // env fallback, so read it here. Placeholder last, so keyless local servers
702
+ // (Ollama, vLLM) still work.
703
+ apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
704
+ ...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
705
+ });
706
+ }
707
+ async function llmCall(prompt, maxTokens = 500) {
708
+ const isThinking = MODEL.includes("gemini") || MODEL.includes("pro-agent");
709
+ const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
710
+ try {
711
+ return PROVIDER === "openai" ? await openaiCall(prompt, effectiveMaxTokens) : await anthropicCall(prompt, effectiveMaxTokens);
712
+ } catch (e) {
713
+ console.error(`[amem] LLM call failed: ${e.message}`);
714
+ return null;
715
+ }
716
+ }
717
+ async function anthropicCall(prompt, maxTokens) {
718
+ const resp = await anthropic().messages.create({
719
+ model: MODEL,
720
+ max_tokens: maxTokens,
721
+ messages: [{ role: "user", content: prompt }]
722
+ });
723
+ for (const block of resp.content) {
724
+ if (block.type === "text") return block.text.trim();
725
+ }
726
+ return null;
727
+ }
728
+ async function openaiCall(prompt, maxTokens) {
729
+ const isReasoning = /^o\d/.test(MODEL) || MODEL.startsWith("gpt-5");
730
+ const resp = await openai().chat.completions.create({
731
+ model: MODEL,
732
+ ...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
733
+ messages: [{ role: "user", content: prompt }]
734
+ });
735
+ return resp.choices[0]?.message?.content?.trim() ?? null;
736
+ }
737
+ function stripFences(raw) {
738
+ raw = raw.trim();
739
+ if (raw.startsWith("```")) {
740
+ const lines = raw.split("\n");
741
+ lines.shift();
742
+ if (lines[lines.length - 1] === "```") lines.pop();
743
+ raw = lines.join("\n").trim();
744
+ }
745
+ if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
746
+ try {
747
+ raw = JSON.parse(raw);
748
+ } catch {
749
+ }
750
+ }
751
+ return raw;
752
+ }
753
+ async function llmConstructNote(content) {
754
+ const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:
755
+ {
756
+ "keywords": ["keyword1", "keyword2"],
757
+ "tags": ["tag1", "tag2"],
758
+ "context": "one sentence summary in the same language as the input",
759
+ "category": "Technical|Business|Personal|Project|Research|System|General",
760
+ "note_type": "memory|knowledge",
761
+ "topics": ["Topic1", "Topic2"],
762
+ "confidence": "high|medium|low"
763
+ }
764
+
765
+ Category guide:
766
+ - Technical: code, tools, configuration, APIs, debugging
767
+ - Business: company, finance, compliance, contracts, invoices
768
+ - Personal: personal state, habits, preferences, emotions
769
+ - Project: project progress, decisions, milestones
770
+ - Research: research, literature, evaluation, comparison
771
+ - System: system services, monitoring, operations
772
+ - General: anything that does not fit the above
773
+
774
+ note_type guide:
775
+ - knowledge: books, methodologies, tools, domain knowledge, reference material \u2014 durable, no strong time component
776
+ - memory: events, decisions, preferences, states, observations \u2014 episodic, time-sensitive
777
+
778
+ topics guide (Story 26B):
779
+ - Only populate for knowledge notes (note_type=knowledge). For memory notes, return [].
780
+ - List 1-5 concise subject tags representing the main topics of this knowledge, e.g. ["TypeScript", "Qdrant", "Vector DB"].
781
+
782
+ confidence guide (Story 27):
783
+ - high: note_type is unambiguous \u2014 clearly episodic (event/decision/state) or clearly durable knowledge (tool doc/methodology)
784
+ - medium: some ambiguity \u2014 e.g. "learned X method" could be either memory or knowledge
785
+ - low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
786
+
787
+ Text: ${content}`;
788
+ const raw = await llmCall(prompt, 400);
789
+ if (!raw)
790
+ return {
791
+ keywords: [],
792
+ tags: [],
793
+ context: "",
794
+ category: "General",
795
+ note_type: "memory",
796
+ topics: [],
797
+ confidence: "medium"
798
+ };
799
+ try {
800
+ const data = JSON.parse(stripFences(raw));
801
+ const rawCategory = typeof data.category === "string" ? data.category : "General";
802
+ const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
803
+ const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
804
+ const topics = note_type === "knowledge" && Array.isArray(data.topics) ? data.topics.filter((v) => typeof v === "string") : [];
805
+ const rawConfidence = typeof data.confidence === "string" ? data.confidence : "medium";
806
+ const confidence = VALID_CONFIDENCE.has(rawConfidence) ? rawConfidence : "medium";
807
+ return {
808
+ keywords: Array.isArray(data.keywords) ? data.keywords : [],
809
+ tags: Array.isArray(data.tags) ? data.tags : [],
810
+ context: typeof data.context === "string" ? data.context : "",
811
+ category,
812
+ note_type,
813
+ topics,
814
+ confidence
815
+ };
816
+ } catch (e) {
817
+ console.error(`[amem] Note construction parse failed: ${e.message}`);
818
+ return {
819
+ keywords: [],
820
+ tags: [],
821
+ context: "",
822
+ category: "General",
823
+ note_type: "memory",
824
+ topics: [],
825
+ confidence: "medium"
826
+ };
827
+ }
956
828
  }
957
- async function updateNoteContent(id, content, embedding, hash) {
958
- return makeCrud(getCollection()).updateNoteContent(id, content, embedding, hash);
829
+ async function llmShouldLink(noteContent, candidateContent) {
830
+ const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
831
+ Reply with only "yes" or "no".
832
+
833
+ Note A: ${noteContent}
834
+ Note B: ${candidateContent}`;
835
+ const raw = await llmCall(prompt, 10);
836
+ if (!raw) return false;
837
+ return raw.toLowerCase().startsWith("yes");
959
838
  }
960
- async function queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0) {
961
- return makeCrud(getCollection()).queryByEmbedding(embedding, topK, agentId, scoreThreshold);
839
+ async function llmCrudDecision(userText, assistantText, existingMemories) {
840
+ const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
841
+ const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
842
+ try {
843
+ const raw = await llmCall(prompt, 400);
844
+ if (!raw) return [];
845
+ const match = raw.match(/\[.*\]/s);
846
+ if (!match) return [];
847
+ const parsed = JSON.parse(match[0]);
848
+ if (!Array.isArray(parsed)) return [];
849
+ const ops = [];
850
+ for (const item of parsed) {
851
+ if (!item || typeof item !== "object") continue;
852
+ const action = item.action;
853
+ if (!["NEW", "UPDATE", "DELETE", "NONE"].includes(action)) continue;
854
+ if (action === "NONE") continue;
855
+ const op = {
856
+ action,
857
+ fact: typeof item.fact === "string" ? item.fact : "",
858
+ reason: typeof item.reason === "string" ? item.reason : void 0
859
+ };
860
+ if (typeof item.existingIdx === "number") {
861
+ op.existingIdx = item.existingIdx;
862
+ }
863
+ ops.push(op);
864
+ }
865
+ return ops.slice(0, 3);
866
+ } catch (e) {
867
+ console.error(`[amem] llmCrudDecision failed: ${e.message}`);
868
+ return [];
869
+ }
962
870
  }
963
- async function listNotes(agentId) {
964
- return makeCrud(getCollection()).listNotes(agentId);
871
+ async function llmShouldMerge(contentA, contentB) {
872
+ const prompt = t.shouldMerge(contentA, contentB);
873
+ const raw = await llmCall(prompt, 300);
874
+ if (!raw) return { shouldMerge: false };
875
+ try {
876
+ const data = JSON.parse(stripFences(raw));
877
+ if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
878
+ if (data.shouldMerge && typeof data.merged === "string") {
879
+ return { shouldMerge: true, merged: data.merged };
880
+ }
881
+ return { shouldMerge: false };
882
+ } catch (e) {
883
+ console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
884
+ return { shouldMerge: false };
885
+ }
965
886
  }
966
- async function deleteNote(id) {
967
- return makeCrud(getCollection()).deleteNote(id);
887
+ async function llmEvolutionJudge(oldContent, newContent) {
888
+ const prompt = t.evolutionJudge(oldContent, newContent);
889
+ const raw = await llmCall(prompt, 300);
890
+ if (!raw) return { type: "NEW" };
891
+ try {
892
+ const data = JSON.parse(stripFences(raw));
893
+ const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
894
+ return {
895
+ type,
896
+ mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
897
+ };
898
+ } catch (e) {
899
+ console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
900
+ return { type: "NEW" };
901
+ }
968
902
  }
969
- async function invalidateNote(id) {
970
- return makeCrud(getCollection()).invalidateNote(id);
903
+ async function llmEvolveNote(content, linkedNotes) {
904
+ const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
905
+ Content: ${n.content}`).join("\n");
906
+ const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
907
+ Reply with JSON only (no markdown):
908
+ {
909
+ "tags": ["tag1", "tag2", ...],
910
+ "context": "updated one sentence summary",
911
+ "should_strengthen": true|false,
912
+ "suggested_connections": ["neighbor_id_1", "neighbor_id_2", ...],
913
+ "tags_to_update": ["tag_1", ..., "tag_n"]
971
914
  }
972
- async function getNotesByDatePrefix(datePrefix, agentId) {
973
- return makeCrud(getCollection()).getNotesByDatePrefix(datePrefix, agentId);
915
+
916
+ Guidelines:
917
+ - "tags" and "context" are for updating the original note based on new connections.
918
+ - "should_strengthen" is a decision whether this note should strengthen its connections to any of the newly linked notes (neighbors).
919
+ - "suggested_connections" must contain only IDs from the newly linked notes (neighbors) listed below.
920
+ - "tags_to_update" are updated tags for the original note itself if we strengthen connections.
921
+
922
+ Original note content: ${content}
923
+
924
+ Newly linked notes (neighbors):
925
+ ${linkedStr}`;
926
+ const raw = await llmCall(prompt, 500);
927
+ if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
928
+ try {
929
+ const data = JSON.parse(stripFences(raw));
930
+ return {
931
+ tags: Array.isArray(data.tags) ? data.tags : null,
932
+ context: typeof data.context === "string" ? data.context : null,
933
+ shouldStrengthen: typeof data.should_strengthen === "boolean" ? data.should_strengthen : false,
934
+ suggestedConnections: Array.isArray(data.suggested_connections) ? data.suggested_connections.map(String) : [],
935
+ tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
936
+ };
937
+ } catch (e) {
938
+ console.error(`[amem] Evolution parse failed: ${e.message}`);
939
+ return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
940
+ }
974
941
  }
975
- async function countNotes(agentId) {
976
- return makeCrud(getCollection()).countNotes(agentId);
942
+ var import_sdk, import_openai, PROVIDER, MODEL, _anthropic, _openai, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
943
+ var init_llm = __esm({
944
+ "../amem-core/src/llm.ts"() {
945
+ "use strict";
946
+ import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
947
+ import_openai = __toESM(require("openai"), 1);
948
+ init_prompts();
949
+ PROVIDER = (process.env.AMEM_LLM_PROVIDER ?? "anthropic").trim().toLowerCase();
950
+ if (PROVIDER !== "anthropic" && PROVIDER !== "openai") {
951
+ console.error(`[amem] unknown AMEM_LLM_PROVIDER "${PROVIDER}"; falling back to anthropic`);
952
+ }
953
+ MODEL = process.env.AMEM_LLM_MODEL ?? (PROVIDER === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
954
+ _anthropic = null;
955
+ _openai = null;
956
+ VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
957
+ VALID_CATEGORIES = /* @__PURE__ */ new Set([
958
+ "Technical",
959
+ "Business",
960
+ "Personal",
961
+ "Project",
962
+ "Research",
963
+ "System",
964
+ "General"
965
+ ]);
966
+ VALID_EVOLUTION_TYPES = /* @__PURE__ */ new Set(["EVOLVE", "CONFLICT", "EXPAND", "NEW"]);
967
+ }
968
+ });
969
+
970
+ // ../amem-core/src/evo-counter.ts
971
+ function counterFile() {
972
+ return process.env.AMEM_EVO_COUNTER_PATH || path2.join(getDataDir(), "amem_evo_cnt.json");
977
973
  }
978
- async function updateNoteLinks(id, links) {
979
- return makeCrud(getCollection()).updateNoteLinks(id, links);
974
+ function getEvoCount() {
975
+ try {
976
+ const data = JSON.parse(fs.readFileSync(counterFile(), "utf-8"));
977
+ return data.count || 0;
978
+ } catch {
979
+ return 0;
980
+ }
980
981
  }
981
- async function patchNotePayload(id, fields) {
982
- return makeCrud(getCollection()).patchNotePayload(id, fields);
982
+ function incrementEvoCount() {
983
+ const count = getEvoCount() + 1;
984
+ fs.writeFileSync(counterFile(), JSON.stringify({ count, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
985
+ return count;
983
986
  }
984
- async function replaceLinkReferences(oldId, newId, agentId) {
985
- return makeCrud(getCollection()).replaceLinkReferences(oldId, newId, agentId);
987
+ function shouldRunEvolution() {
988
+ const count = incrementEvoCount();
989
+ return count % EVO_THRESHOLD === 0;
986
990
  }
987
- var QDRANT_URL, getCollection, VECTOR_DIM, _collectionReady, _collectionReadyMap;
988
- var init_storage = __esm({
989
- "../amem-core/src/storage.ts"() {
991
+ var fs, path2, EVO_THRESHOLD;
992
+ var init_evo_counter = __esm({
993
+ "../amem-core/src/evo-counter.ts"() {
990
994
  "use strict";
991
- QDRANT_URL = "http://localhost:6333";
992
- getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
993
- VECTOR_DIM = 384;
994
- _collectionReady = false;
995
- _collectionReadyMap = /* @__PURE__ */ new Map();
995
+ fs = __toESM(require("fs"), 1);
996
+ path2 = __toESM(require("path"), 1);
997
+ init_config();
998
+ EVO_THRESHOLD = 20;
996
999
  }
997
1000
  });
998
1001
 
@@ -1389,7 +1392,7 @@ async function listMemories(agentId = "main", storageCtx) {
1389
1392
  return { count };
1390
1393
  }
1391
1394
  function sleep(ms) {
1392
- return new Promise((resolve) => setTimeout(resolve, ms));
1395
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1393
1396
  }
1394
1397
  async function mergeSimilarNotes(agentId, storageCtx) {
1395
1398
  const ctx = storageCtx ?? defaultCtx();
@@ -1709,10 +1712,21 @@ function severityBadge(reasons) {
1709
1712
  return "\u{1F7E0} CONFLICT";
1710
1713
  }
1711
1714
  async function generateReviewBatch(agentId, outputPath) {
1715
+ const root = path4.resolve(DEFAULT_OUTPUT_DIR);
1716
+ let filePath;
1717
+ let batchN;
1718
+ if (outputPath) {
1719
+ const name = path4.basename(outputPath);
1720
+ if (name !== outputPath || name === "" || name === "." || name === "..") {
1721
+ throw new Error(`[quality] outputPath \u5FC5\u987B\u662F\u7EAF\u6587\u4EF6\u540D\uFF08\u4E0D\u542B\u76EE\u5F55\uFF09: ${outputPath}`);
1722
+ }
1723
+ filePath = path4.join(root, name);
1724
+ batchN = 0;
1725
+ } else {
1726
+ batchN = nextBatchNumber(root);
1727
+ filePath = path4.join(root, `amem-review-batch${batchN}.md`);
1728
+ }
1712
1729
  const items = await scanLowQuality(agentId);
1713
- const dir = outputPath ? path4.dirname(outputPath) : DEFAULT_OUTPUT_DIR;
1714
- const batchN = outputPath ? 0 : nextBatchNumber(dir);
1715
- const filePath = outputPath || path4.join(dir, `amem-review-batch${batchN}.md`);
1716
1730
  const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1717
1731
  const lines = [];
1718
1732
  const title = LOCALE2 === "zh" ? "A-MEM \u8D28\u91CF\u5BA1\u6838" : "A-MEM Quality Review";
@@ -1779,64 +1793,37 @@ var src_exports = {};
1779
1793
  __export(src_exports, {
1780
1794
  addEpisodic: () => addEpisodic,
1781
1795
  addMemory: () => addMemory,
1782
- addNote: () => addNote,
1783
- bm25Score: () => bm25Score,
1784
- buildBM25: () => buildBM25,
1785
1796
  checkQuality: () => checkQuality,
1786
1797
  configure: () => configure,
1787
1798
  consolidateMemories: () => consolidateMemories,
1788
- cosineSimilarity: () => cosineSimilarity,
1789
- countNotes: () => countNotes,
1790
1799
  createStorageContext: () => createStorageContext,
1791
1800
  deleteNote: () => deleteNote,
1792
1801
  encode: () => encode,
1793
1802
  ensureCollection: () => ensureCollection,
1794
- findByHash: () => findByHash,
1795
1803
  generateReviewBatch: () => generateReviewBatch,
1796
- getDataDir: () => getDataDir,
1797
- getEvoCount: () => getEvoCount,
1798
1804
  getNote: () => getNote,
1799
- getNotesByDatePrefix: () => getNotesByDatePrefix,
1800
- incrementEvoCount: () => incrementEvoCount,
1801
1805
  invalidateNote: () => invalidateNote,
1802
1806
  isModelLoaded: () => isModelLoaded,
1803
1807
  listMemories: () => listMemories,
1804
1808
  listNotes: () => listNotes,
1805
- llmCall: () => llmCall,
1806
- llmConstructNote: () => llmConstructNote,
1807
1809
  llmCrudDecision: () => llmCrudDecision,
1808
- llmEvolutionJudge: () => llmEvolutionJudge,
1809
- llmEvolveNote: () => llmEvolveNote,
1810
- llmShouldLink: () => llmShouldLink,
1811
- llmShouldMerge: () => llmShouldMerge,
1812
1810
  loadModel: () => loadModel,
1813
1811
  mergeSimilarNotes: () => mergeSimilarNotes,
1814
1812
  patchNotePayload: () => patchNotePayload,
1815
1813
  pingQdrant: () => pingQdrant,
1816
- queryByEmbedding: () => queryByEmbedding,
1817
- replaceLinkReferences: () => replaceLinkReferences,
1818
- resetCollectionReady: () => resetCollectionReady,
1819
- rrfMerge: () => rrfMerge,
1820
1814
  scanLowQuality: () => scanLowQuality,
1821
1815
  searchMemory: () => searchMemory,
1822
- shouldRunEvolution: () => shouldRunEvolution,
1823
- simpleTokenize: () => simpleTokenize,
1824
- t: () => t,
1825
- updateNote: () => updateNote,
1826
- updateNoteContent: () => updateNoteContent,
1827
- updateNoteLinks: () => updateNoteLinks
1816
+ updateNote: () => updateNote
1828
1817
  });
1829
1818
  var init_src = __esm({
1830
1819
  "../amem-core/src/index.ts"() {
1831
1820
  "use strict";
1832
1821
  init_config();
1833
1822
  init_embedding();
1834
- init_evo_counter();
1835
- init_llm();
1836
1823
  init_memory();
1837
- init_prompts();
1838
1824
  init_quality();
1839
1825
  init_storage();
1826
+ init_llm();
1840
1827
  }
1841
1828
  });
1842
1829
 
@@ -1896,6 +1883,28 @@ function hookNeverFiredWarning(now = Date.now()) {
1896
1883
  return HOOK_WARNING_TEXT;
1897
1884
  }
1898
1885
 
1886
+ // src/scope.ts
1887
+ function parseAgentIdFromSessionKey(sessionKey) {
1888
+ if (!sessionKey) return void 0;
1889
+ const parts = sessionKey.split(":").filter(Boolean);
1890
+ if (parts.length >= 3 && parts[0] === "agent") return parts[1] || void 0;
1891
+ return void 0;
1892
+ }
1893
+ function resolveAgentId(ctx, pluginConfig) {
1894
+ return ctx?.agentId ?? parseAgentIdFromSessionKey(ctx?.sessionKey) ?? pluginConfig.agentId ?? "main";
1895
+ }
1896
+ function buildScope(rawAgentId, pluginConfig, createStorageContext2) {
1897
+ const agentCfg = pluginConfig.agents?.[rawAgentId] ?? {};
1898
+ const effectiveAgentId = agentCfg.agentId ?? rawAgentId;
1899
+ const effectiveCollection = agentCfg.collection ?? pluginConfig.collection ?? void 0;
1900
+ const modeBIsolated = !!agentCfg.collection;
1901
+ return {
1902
+ agentId: effectiveAgentId,
1903
+ collection: effectiveCollection,
1904
+ storageCtx: createStorageContext2(effectiveCollection, modeBIsolated)
1905
+ };
1906
+ }
1907
+
1899
1908
  // src/index.ts
1900
1909
  init_src();
1901
1910
  var _config = {};
@@ -1905,27 +1914,9 @@ function register(api) {
1905
1914
  _config = api.pluginConfig || {};
1906
1915
  const pluginConfig = _config;
1907
1916
  configure({ dataDir: path5.join(os2.homedir(), ".openclaw") });
1908
- function parseAgentIdFromSessionKey(sessionKey) {
1909
- if (!sessionKey) return void 0;
1910
- const parts = sessionKey.split(":").filter(Boolean);
1911
- if (parts.length >= 3 && parts[0] === "agent") return parts[1] || void 0;
1912
- return void 0;
1913
- }
1914
- function resolveAgentId(ctx) {
1915
- return ctx?.agentId ?? parseAgentIdFromSessionKey(ctx?.sessionKey) ?? pluginConfig.agentId ?? "main";
1916
- }
1917
- function buildScope(rawAgentId) {
1918
- const agentCfg = pluginConfig.agents?.[rawAgentId] ?? {};
1919
- const effectiveAgentId = agentCfg.agentId ?? rawAgentId;
1920
- const effectiveCollection = agentCfg.collection ?? pluginConfig.collection ?? void 0;
1921
- const modeBIsolated = !!agentCfg.collection;
1922
- return {
1923
- agentId: effectiveAgentId,
1924
- collection: effectiveCollection,
1925
- storageCtx: createStorageContext(effectiveCollection, modeBIsolated)
1926
- };
1927
- }
1928
- const defaultScope = buildScope(resolveAgentId());
1917
+ const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
1918
+ const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
1919
+ const defaultScope = buildScope2(resolveAgentId2());
1929
1920
  const dbPath = path5.join(os2.homedir(), ".openclaw", "amem_db");
1930
1921
  logger.info(
1931
1922
  `openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
@@ -1946,7 +1937,7 @@ function register(api) {
1946
1937
  runtime: {
1947
1938
  async getMemorySearchManager(params) {
1948
1939
  try {
1949
- const scope = buildScope(resolveAgentId(params));
1940
+ const scope = buildScope2(resolveAgentId2(params));
1950
1941
  return {
1951
1942
  manager: {
1952
1943
  status() {
@@ -1996,7 +1987,7 @@ function register(api) {
1996
1987
  }
1997
1988
  },
1998
1989
  resolveMemoryBackendConfig(params) {
1999
- const scope = buildScope(resolveAgentId(params));
1990
+ const scope = buildScope2(resolveAgentId2(params));
2000
1991
  return { backend: "amem-qdrant", baseUrl: "", userId: scope.agentId };
2001
1992
  },
2002
1993
  async closeAllMemorySearchManagers() {
@@ -2009,7 +2000,7 @@ function register(api) {
2009
2000
  if (typeof api.registerTool === "function") {
2010
2001
  api.registerTool(
2011
2002
  (ctx) => {
2012
- const scope = buildScope(resolveAgentId(ctx));
2003
+ const scope = buildScope2(resolveAgentId2(ctx));
2013
2004
  return {
2014
2005
  name: "memory_search",
2015
2006
  label: "Memory Search (A-MEM)",
@@ -2066,7 +2057,7 @@ ${text}${hookWarning}` }],
2066
2057
  );
2067
2058
  api.registerTool(
2068
2059
  (ctx) => {
2069
- const scope = buildScope(resolveAgentId(ctx));
2060
+ const scope = buildScope2(resolveAgentId2(ctx));
2070
2061
  return {
2071
2062
  name: "memory_add",
2072
2063
  label: "Memory Add (A-MEM)",
@@ -2102,7 +2093,7 @@ ${text}${hookWarning}` }],
2102
2093
  );
2103
2094
  api.registerTool(
2104
2095
  (ctx) => {
2105
- const scope = buildScope(resolveAgentId(ctx));
2096
+ const scope = buildScope2(resolveAgentId2(ctx));
2106
2097
  return {
2107
2098
  name: "memory_list",
2108
2099
  label: "Memory List (A-MEM)",
@@ -2132,7 +2123,7 @@ ${text}${hookWarning}` }],
2132
2123
  );
2133
2124
  api.registerTool(
2134
2125
  (ctx) => {
2135
- const scope = buildScope(resolveAgentId(ctx));
2126
+ const scope = buildScope2(resolveAgentId2(ctx));
2136
2127
  return {
2137
2128
  name: "memory_consolidate",
2138
2129
  label: "Memory Consolidate (A-MEM)",
@@ -2165,7 +2156,7 @@ ${text}${hookWarning}` }],
2165
2156
  );
2166
2157
  api.registerTool(
2167
2158
  (ctx) => {
2168
- const scope = buildScope(resolveAgentId(ctx));
2159
+ const scope = buildScope2(resolveAgentId2(ctx));
2169
2160
  return {
2170
2161
  name: "memory_quality_scan",
2171
2162
  label: "Memory Quality Scan (A-MEM)",
@@ -2175,7 +2166,7 @@ ${text}${hookWarning}` }],
2175
2166
  properties: {
2176
2167
  outputPath: {
2177
2168
  type: "string",
2178
- description: "Custom output path for the review batch file (optional, auto-generates if omitted)"
2169
+ description: "Custom filename for the review batch (optional, auto-generates if omitted). A bare filename only \u2014 it is written under AMEM_REVIEW_DIR; a path with directories is rejected."
2179
2170
  }
2180
2171
  },
2181
2172
  required: []
@@ -2213,7 +2204,7 @@ ${text}${hookWarning}` }],
2213
2204
  "agent_end",
2214
2205
  async (event, ctx) => {
2215
2206
  markHookFired();
2216
- const scope = buildScope(resolveAgentId(ctx));
2207
+ const scope = buildScope2(resolveAgentId2(ctx));
2217
2208
  const agentId = scope.agentId;
2218
2209
  const storageCtx = scope.storageCtx;
2219
2210
  logger.info(
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-amem",
3
3
  "name": "Memory (A-MEM v2)",
4
4
  "description": "OpenClaw memory plugin implementing A-MEM — memories evolve, not just accumulate. Graph linking, hybrid retrieval, LLM-driven evolution. No Python.",
5
- "version": "1.1.4",
5
+ "version": "1.2.0",
6
6
  "kind": "memory",
7
7
  "openclaw": {
8
8
  "compat": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-amem",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "OpenClaw memory plugin implementing A-MEM — memories evolve, not just accumulate. Graph linking, hybrid retrieval, LLM-driven evolution. No Python.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -21,16 +21,17 @@
21
21
  "@node-rs/jieba": "^2.0.1",
22
22
  "@qdrant/js-client-rest": "^1.18.0",
23
23
  "@types/uuid": "^11.0.0",
24
+ "openai": "^6.48.0",
24
25
  "uuid": "^14.0.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@eslint/js": "^10.0.1",
28
29
  "@types/node": "^26.1.1",
29
- "eslint": "^10.4.1",
30
- "prettier": "^3.8.3",
30
+ "eslint": "^10.7.0",
31
+ "prettier": "^3.9.5",
31
32
  "tsup": "^8.4.0",
32
- "tsx": "^4.21.0",
33
- "typescript": "^5.9.3",
33
+ "tsx": "^4.23.1",
34
+ "typescript": "^6.0.3",
34
35
  "typescript-eslint": "^8.60.1",
35
36
  "vitest": "^4.1.10"
36
37
  },
@@ -73,6 +74,7 @@
73
74
  "format": "prettier --check \"src/**/*.ts\"",
74
75
  "format:fix": "prettier --write \"src/**/*.ts\"",
75
76
  "test": "vitest run",
77
+ "test:unit": "vitest run test/unit",
76
78
  "test:watch": "vitest",
77
79
  "check": "npm run format && npm run lint && npm run test"
78
80
  }