openclaw-amem 1.2.2 → 1.4.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/dist/index.js CHANGED
@@ -53,17 +53,29 @@ var init_config = __esm({
53
53
  });
54
54
 
55
55
  // ../amem-core/src/embedding.ts
56
+ function getEmbeddingModel() {
57
+ return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
58
+ }
56
59
  async function getExtractor() {
57
- if (extractor) return extractor;
60
+ const wanted = getEmbeddingModel();
61
+ if (extractor && loadedModelName === wanted) return extractor;
58
62
  if (!pipeline) {
59
63
  const mod = await import("@huggingface/transformers");
60
64
  pipeline = mod.pipeline;
61
65
  }
62
- extractor = await pipeline("feature-extraction", MODEL_NAME, {
66
+ extractor = await pipeline("feature-extraction", wanted, {
63
67
  revision: "main"
64
68
  });
69
+ loadedModelName = wanted;
70
+ cachedDim = null;
65
71
  return extractor;
66
72
  }
73
+ async function getEmbeddingDim() {
74
+ if (cachedDim !== null && loadedModelName === getEmbeddingModel()) return cachedDim;
75
+ const probe = await encode("dimension probe");
76
+ cachedDim = probe.length;
77
+ return cachedDim;
78
+ }
67
79
  function meanPoolingNormalize(output, attentionMask) {
68
80
  const seqLen = output.length;
69
81
  const dim = output[0].length;
@@ -117,13 +129,28 @@ function cosineSimilarity(a, b) {
117
129
  for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
118
130
  return dot;
119
131
  }
120
- var pipeline, extractor, MODEL_NAME;
132
+ var pipeline, extractor, loadedModelName, cachedDim, DEFAULT_EMBEDDING_MODEL;
121
133
  var init_embedding = __esm({
122
134
  "../amem-core/src/embedding.ts"() {
123
135
  "use strict";
124
136
  pipeline = null;
125
137
  extractor = null;
126
- MODEL_NAME = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
138
+ loadedModelName = null;
139
+ cachedDim = null;
140
+ DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
141
+ }
142
+ });
143
+
144
+ // ../amem-core/src/auth.ts
145
+ function canWrite(note, callerAgentId) {
146
+ return note.owner === callerAgentId || note.writers.includes(callerAgentId) || note.writers.includes("*");
147
+ }
148
+ function canRead(note, callerAgentId) {
149
+ return note.owner === callerAgentId || note.readers.includes(callerAgentId) || note.readers.includes("*");
150
+ }
151
+ var init_auth = __esm({
152
+ "../amem-core/src/auth.ts"() {
153
+ "use strict";
127
154
  }
128
155
  });
129
156
 
@@ -155,15 +182,26 @@ async function ensureCollection(collectionName) {
155
182
  if (collectionName) _collectionReadyMap.set(col, true);
156
183
  else _collectionReady = true;
157
184
  };
185
+ let existing = null;
158
186
  try {
159
- await qdrant("GET", `/collections/${col}`);
187
+ existing = await qdrant("GET", `/collections/${col}`);
188
+ } catch {
189
+ }
190
+ if (existing) {
191
+ const collectionDim = existing.config?.params?.vectors?.size;
192
+ if (typeof collectionDim === "number") {
193
+ const modelDim = await getEmbeddingDim();
194
+ if (collectionDim !== modelDim) {
195
+ throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
196
+ }
197
+ }
160
198
  markReady();
161
199
  return;
162
- } catch {
163
200
  }
164
201
  try {
202
+ const size = await getEmbeddingDim();
165
203
  await qdrant("PUT", `/collections/${col}`, {
166
- vectors: { size: VECTOR_DIM, distance: "Cosine" }
204
+ vectors: { size, distance: "Cosine" }
167
205
  });
168
206
  } catch (err) {
169
207
  if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
@@ -180,8 +218,46 @@ async function ensureCollection(collectionName) {
180
218
  field_name: "topics",
181
219
  field_schema: "keyword"
182
220
  });
221
+ await qdrant("PUT", `/collections/${col}/index`, {
222
+ field_name: "subjects",
223
+ field_schema: "keyword"
224
+ });
183
225
  markReady();
184
226
  }
227
+ async function scrollAllRaw(collection, limit = 1e4) {
228
+ const out = [];
229
+ let offset = void 0;
230
+ for (; ; ) {
231
+ const body = { with_payload: true, with_vector: true, limit };
232
+ if (offset !== void 0 && offset !== null) body.offset = offset;
233
+ const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
234
+ out.push(...res.points);
235
+ offset = res.next_page_offset;
236
+ if (offset === void 0 || offset === null || res.points.length === 0) break;
237
+ }
238
+ return out;
239
+ }
240
+ async function countPointsRaw(collection) {
241
+ const res = await qdrant("POST", `/collections/${collection}/points/count`, { exact: true });
242
+ return res.count;
243
+ }
244
+ async function collectionDimRaw(collection) {
245
+ try {
246
+ const info = await qdrant("GET", `/collections/${collection}`);
247
+ return info.config?.params?.vectors?.size ?? null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+ async function createCollectionRaw(collection, size) {
253
+ await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
254
+ for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
255
+ await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
256
+ }
257
+ }
258
+ async function upsertPointsRaw(collection, points) {
259
+ await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
260
+ }
185
261
  function noteToPoint(note) {
186
262
  return {
187
263
  id: note.id,
@@ -212,6 +288,10 @@ function noteToPoint(note) {
212
288
  // 30
213
289
  evolution_type: note.evolution_type || "",
214
290
  conflict: note.conflict ?? false,
291
+ conflicts_with: note.conflicts_with ?? [],
292
+ conflict_reason: note.conflict_reason ?? "",
293
+ conflict_scanned_at: note.conflict_scanned_at ?? "",
294
+ subjects: note.subjects ?? [],
215
295
  // 31
216
296
  ephemeral: note.ephemeral ?? false,
217
297
  low_quality: note.low_quality ?? false,
@@ -264,6 +344,10 @@ function pointToNote(point) {
264
344
  // 30
265
345
  evolution_type: typeof p.evolution_type === "string" && ["EVOLVE", "CONFLICT", "EXPAND", "NEW"].includes(p.evolution_type) ? p.evolution_type : void 0,
266
346
  conflict: p.conflict === true,
347
+ conflicts_with: Array.isArray(p.conflicts_with) ? p.conflicts_with.filter((v) => typeof v === "string") : [],
348
+ conflict_reason: typeof p.conflict_reason === "string" ? p.conflict_reason : "",
349
+ conflict_scanned_at: typeof p.conflict_scanned_at === "string" ? p.conflict_scanned_at : "",
350
+ subjects: Array.isArray(p.subjects) ? p.subjects.filter((v) => typeof v === "string") : [],
267
351
  // 31
268
352
  ephemeral: p.ephemeral === true,
269
353
  low_quality: p.low_quality === true,
@@ -273,28 +357,41 @@ function pointToNote(point) {
273
357
  writers: Array.isArray(p.writers) ? p.writers : [p.agent_id || "main"]
274
358
  };
275
359
  }
276
- function agentFilter(agentId) {
360
+ function agentFilter(agentId, subject) {
361
+ const must = [
362
+ {
363
+ should: [
364
+ { key: "agent_id", match: { value: agentId } },
365
+ { key: "agent_id", match: { value: "shared" } }
366
+ ]
367
+ }
368
+ ];
369
+ if (subject !== void 0) {
370
+ must.push({
371
+ should: [{ key: "subjects", match: { value: subject } }, { is_empty: { key: "subjects" } }]
372
+ });
373
+ }
277
374
  return {
278
- must: [
279
- {
280
- should: [
281
- { key: "agent_id", match: { value: agentId } },
282
- { key: "agent_id", match: { value: "shared" } }
283
- ]
284
- }
285
- ],
375
+ must,
286
376
  must_not: [{ key: "is_active", match: { value: false } }]
287
377
  };
288
378
  }
289
379
  function makeCrud(collectionName, modeBIsolated = false) {
290
380
  const col = collectionName;
291
- function scopedAgentFilter(agentId) {
381
+ function scopedAgentFilter(agentId, subject) {
292
382
  if (modeBIsolated) {
383
+ const must = [];
384
+ if (subject !== void 0) {
385
+ must.push({
386
+ should: [{ key: "subjects", match: { value: subject } }, { is_empty: { key: "subjects" } }]
387
+ });
388
+ }
293
389
  return {
390
+ ...must.length > 0 && { must },
294
391
  must_not: [{ key: "is_active", match: { value: false } }]
295
392
  };
296
393
  }
297
- return agentFilter(agentId);
394
+ return agentFilter(agentId, subject);
298
395
  }
299
396
  return {
300
397
  async addNote(note) {
@@ -303,7 +400,14 @@ function makeCrud(collectionName, modeBIsolated = false) {
303
400
  points: [noteToPoint(note)]
304
401
  });
305
402
  },
306
- async getNote(id) {
403
+ /**
404
+ * Story 36: this is the one read that bypasses the agent filter — it fetches
405
+ * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
406
+ * note comes back as `null` (indistinguishable from missing, so nothing leaks,
407
+ * and callers already handle null). Omitting it skips the check, preserving
408
+ * behaviour for internal callers that only ever hold their own ids.
409
+ */
410
+ async getNote(id, readerAgentId) {
307
411
  await ensureCollection(col);
308
412
  try {
309
413
  const result = await qdrant("POST", `/collections/${col}/points`, {
@@ -312,7 +416,9 @@ function makeCrud(collectionName, modeBIsolated = false) {
312
416
  with_vector: true
313
417
  });
314
418
  if (!result.length) return null;
315
- return pointToNote(result[0]);
419
+ const note = pointToNote(result[0]);
420
+ if (readerAgentId !== void 0 && !canRead(note, readerAgentId)) return null;
421
+ return note;
316
422
  } catch {
317
423
  return null;
318
424
  }
@@ -348,17 +454,48 @@ function makeCrud(collectionName, modeBIsolated = false) {
348
454
  if (!result.points.length) return null;
349
455
  return pointToNote(result.points[0]);
350
456
  },
351
- async updateNoteContent(id, content, embedding, hash) {
457
+ /**
458
+ * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
459
+ * hold the note already should prefer checking `canWrite` themselves; this
460
+ * fetch-then-check path exists for callers that only have an id (the plugin's
461
+ * CRUD hook). Returns false — without writing — when the caller may not write.
462
+ * Omitting `callerAgentId` skips the check, preserving existing behaviour for
463
+ * internal callers that are already scoped to their own notes.
464
+ */
465
+ async updateNoteContent(id, content, embedding, hash, callerAgentId) {
352
466
  await ensureCollection(col);
467
+ let existing = null;
468
+ if (callerAgentId !== void 0) {
469
+ existing = await this.getNote(id);
470
+ if (existing && !canWrite(existing, callerAgentId)) return false;
471
+ }
353
472
  await qdrant("PUT", `/collections/${col}/points/vectors?wait=true`, {
354
473
  points: [{ id, vector: embedding }]
355
474
  });
475
+ const payload = { content, hash };
476
+ if (existing) {
477
+ const history = [
478
+ ...existing.evolution_history ?? [],
479
+ {
480
+ triggeredBy: "",
481
+ triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
482
+ oldContext: existing.context,
483
+ newContext: existing.context,
484
+ oldTags: existing.tags,
485
+ newTags: existing.tags,
486
+ action: "crud_update",
487
+ oldContent: existing.content
488
+ }
489
+ ];
490
+ payload.evolution_history = JSON.stringify(history);
491
+ }
356
492
  await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
357
- payload: { content, hash },
493
+ payload,
358
494
  points: [id]
359
495
  });
496
+ return true;
360
497
  },
361
- async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0) {
498
+ async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0, subject) {
362
499
  await ensureCollection(col);
363
500
  const result = await qdrant("POST", `/collections/${col}/points/search`, {
364
501
  vector: embedding,
@@ -366,7 +503,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
366
503
  with_payload: true,
367
504
  with_vector: true,
368
505
  score_threshold: scoreThreshold,
369
- filter: scopedAgentFilter(agentId)
506
+ filter: scopedAgentFilter(agentId, subject)
370
507
  });
371
508
  const queryResults = result.map((r) => ({
372
509
  note: pointToNote(r),
@@ -400,14 +537,14 @@ function makeCrud(collectionName, modeBIsolated = false) {
400
537
  }
401
538
  return queryResults;
402
539
  },
403
- async listNotes(agentId) {
540
+ async listNotes(agentId, subject) {
404
541
  await ensureCollection(col);
405
542
  const body = {
406
543
  with_payload: true,
407
544
  with_vector: true,
408
545
  limit: 1e4
409
546
  };
410
- if (agentId) body.filter = scopedAgentFilter(agentId);
547
+ if (agentId) body.filter = scopedAgentFilter(agentId, subject);
411
548
  const result = await qdrant("POST", `/collections/${col}/points/scroll`, body);
412
549
  return result.points.map(pointToNote);
413
550
  },
@@ -417,12 +554,18 @@ function makeCrud(collectionName, modeBIsolated = false) {
417
554
  points: [id]
418
555
  });
419
556
  },
420
- async invalidateNote(id) {
557
+ /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
558
+ async invalidateNote(id, callerAgentId) {
421
559
  await ensureCollection(col);
560
+ if (callerAgentId !== void 0) {
561
+ const existing = await this.getNote(id);
562
+ if (existing && !canWrite(existing, callerAgentId)) return false;
563
+ }
422
564
  await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
423
565
  payload: { is_active: false },
424
566
  points: [id]
425
567
  });
568
+ return true;
426
569
  },
427
570
  async getNotesByDatePrefix(datePrefix, agentId) {
428
571
  await ensureCollection(col);
@@ -468,6 +611,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
468
611
  async replaceLinkReferences(oldId, newId, agentId) {
469
612
  const notes = await this.listNotes(agentId);
470
613
  for (const note of notes) {
614
+ if (!canWrite(note, agentId)) continue;
471
615
  if (note.links.includes(oldId)) {
472
616
  const newLinks = note.links.map((linkId) => linkId === oldId ? newId : linkId);
473
617
  const filteredLinks = newLinks.filter((linkId) => linkId !== note.id);
@@ -481,31 +625,49 @@ function makeCrud(collectionName, modeBIsolated = false) {
481
625
  function createStorageContext(collectionName, modeBIsolated = false) {
482
626
  return makeCrud(collectionName || getCollection(), modeBIsolated);
483
627
  }
484
- async function getNote(id) {
485
- return makeCrud(getCollection()).getNote(id);
628
+ async function getNote(id, readerAgentId) {
629
+ return makeCrud(getCollection()).getNote(id, readerAgentId);
486
630
  }
487
631
  async function updateNote(note) {
488
632
  return makeCrud(getCollection()).updateNote(note);
489
633
  }
490
- async function listNotes(agentId) {
491
- return makeCrud(getCollection()).listNotes(agentId);
634
+ async function listNotes(agentId, subject) {
635
+ return makeCrud(getCollection()).listNotes(agentId, subject);
492
636
  }
493
637
  async function deleteNote(id) {
494
638
  return makeCrud(getCollection()).deleteNote(id);
495
639
  }
496
- async function invalidateNote(id) {
497
- return makeCrud(getCollection()).invalidateNote(id);
640
+ async function invalidateNote(id, callerAgentId) {
641
+ return makeCrud(getCollection()).invalidateNote(id, callerAgentId);
498
642
  }
499
643
  async function patchNotePayload(id, fields) {
500
644
  return makeCrud(getCollection()).patchNotePayload(id, fields);
501
645
  }
502
- var QDRANT_URL, getCollection, VECTOR_DIM, _collectionReady, _collectionReadyMap;
646
+ var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, _collectionReady, _collectionReadyMap;
503
647
  var init_storage = __esm({
504
648
  "../amem-core/src/storage.ts"() {
505
649
  "use strict";
650
+ init_auth();
651
+ init_embedding();
506
652
  QDRANT_URL = "http://localhost:6333";
507
653
  getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
508
- VECTOR_DIM = 384;
654
+ EmbeddingDimensionMismatchError = class extends Error {
655
+ constructor(collection, collectionDim, modelDim, model) {
656
+ super(
657
+ `Collection "${collection}" stores ${collectionDim}-dimension vectors, but the embedding model "${model}" produces ${modelDim}. Qdrant fixes a collection's vector size at creation and cannot change it, so writes and searches would both fail.
658
+ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or migrate: build a new collection with the new model, backfill it, then point AMEM_COLLECTION at it. See docs/reference/embedding-models.md.`
659
+ );
660
+ this.collection = collection;
661
+ this.collectionDim = collectionDim;
662
+ this.modelDim = modelDim;
663
+ this.model = model;
664
+ this.name = "EmbeddingDimensionMismatchError";
665
+ }
666
+ collection;
667
+ collectionDim;
668
+ modelDim;
669
+ model;
670
+ };
509
671
  _collectionReady = false;
510
672
  _collectionReadyMap = /* @__PURE__ */ new Map();
511
673
  }
@@ -600,7 +762,39 @@ Classification rules:
600
762
  - NEW: Completely unrelated information, no substantive connection to the old memory
601
763
  Return: {"type": "NEW"}
602
764
 
603
- Return only JSON, no other text.`
765
+ Return only JSON, no other text.`,
766
+ conflictScan: (numberedNotes) => `You are auditing a person's memory store for CONTRADICTIONS.
767
+
768
+ Below are numbered memories. Find pairs that CANNOT both be true of the same person at the same time.
769
+
770
+ ${numberedNotes}
771
+
772
+ What counts as a contradiction:
773
+ - The same attribute holding two incompatible values ("lives in Paris" vs "moved to Berlin")
774
+ - A stated preference or constraint that a later memory violates ("is vegetarian" vs "loved the steak")
775
+ - A fact that a later memory supersedes ("uses MySQL" vs "migrated to PostgreSQL")
776
+
777
+ What does NOT count \u2014 be strict, these are the common false positives:
778
+ - Additive facts. Two things can both be true ("has a dog named Buddy" + "adopted a second dog, Scout" is NOT a contradiction)
779
+ - Change over time that both memories already acknowledge
780
+ - Merely similar or related topics
781
+ - Different contexts (likes coffee at work, tea at home)
782
+
783
+ For each contradicting pair, also say which one is SUPERSEDED \u2014 the one that is
784
+ no longer true. Judge this from the WORDING, not from any assumed order: phrases
785
+ like "used to", "back in 2019", "moved last month", "switched to" tell you which
786
+ statement describes the past. The memories are NOT listed in chronological order,
787
+ and the number does not imply age.
788
+
789
+ If you cannot tell which one is superseded, set it to null. That is a normal and
790
+ useful answer \u2014 say null rather than guessing, because a wrong guess retires a
791
+ memory that is still true.
792
+
793
+ Return ONLY a JSON array. Empty array if nothing genuinely contradicts:
794
+ [{"a": 0, "b": 3, "superseded": 0, "reason": "one short sentence naming the incompatible attribute"}]
795
+
796
+ "superseded" must be either the value of "a", the value of "b", or null.
797
+ Use the numbers shown. Report a pair once. Prefer returning nothing over guessing.`
604
798
  };
605
799
  zh = {
606
800
  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
@@ -685,7 +879,36 @@ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
685
879
  - 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
686
880
  \u8FD4\u56DE\uFF1A{"type": "NEW"}
687
881
 
688
- \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`
882
+ \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`,
883
+ conflictScan: (numberedNotes) => `\u4F60\u5728\u5BA1\u8BA1\u4E00\u4E2A\u4EBA\u7684\u8BB0\u5FC6\u5E93\uFF0C\u627E\u51FA\u5176\u4E2D**\u4E92\u76F8\u77DB\u76FE**\u7684\u6761\u76EE\u3002
884
+
885
+ \u4E0B\u9762\u662F\u7F16\u53F7\u7684\u8BB0\u5FC6\u3002\u627E\u51FA\u90A3\u4E9B**\u4E0D\u53EF\u80FD\u540C\u65F6\u4E3A\u771F**\u7684\u914D\u5BF9\u3002
886
+
887
+ ${numberedNotes}
888
+
889
+ \u7B97\u77DB\u76FE\u7684\u60C5\u51B5\uFF1A
890
+ - \u540C\u4E00\u5C5E\u6027\u4E0A\u51FA\u73B0\u4E92\u65A5\u7684\u503C\uFF08\u300C\u4F4F\u5728\u5DF4\u9ECE\u300Dvs\u300C\u642C\u5230\u4E86\u67CF\u6797\u300D\uFF09
891
+ - \u540E\u6765\u7684\u8BB0\u5FC6\u8FDD\u53CD\u4E86\u5148\u524D\u9648\u8FF0\u7684\u504F\u597D\u6216\u7EA6\u675F\uFF08\u300C\u5403\u7D20\u300Dvs\u300C\u90A3\u5757\u725B\u6392\u5F88\u597D\u5403\u300D\uFF09
892
+ - \u540E\u6765\u7684\u4E8B\u5B9E\u53D6\u4EE3\u4E86\u5148\u524D\u7684\uFF08\u300C\u7528 MySQL\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
893
+
894
+ **\u4E0D\u7B97**\u77DB\u76FE \u2014\u2014 \u8BF7\u4E25\u683C\uFF0C\u4EE5\u4E0B\u662F\u6700\u5E38\u89C1\u7684\u8BEF\u5224\uFF1A
895
+ - \u7D2F\u52A0\u7684\u4E8B\u5B9E\u3002\u4E24\u8005\u53EF\u4EE5\u540C\u65F6\u6210\u7ACB\uFF08\u300C\u517B\u4E86\u4E00\u53EA\u72D7\u53EB Buddy\u300D+\u300C\u53C8\u9886\u517B\u4E86\u7B2C\u4E8C\u53EA\u53EB Scout\u300D**\u4E0D\u662F**\u77DB\u76FE\uFF09
896
+ - \u4E24\u6761\u8BB0\u5FC6\u672C\u8EAB\u5DF2\u7ECF\u4F53\u73B0\u4E86\u968F\u65F6\u95F4\u7684\u53D8\u5316
897
+ - \u53EA\u662F\u4E3B\u9898\u76F8\u4F3C\u6216\u76F8\u5173
898
+ - \u573A\u666F\u4E0D\u540C\uFF08\u5728\u516C\u53F8\u559D\u5496\u5561\uFF0C\u5728\u5BB6\u559D\u8336\uFF09
899
+
900
+ \u5BF9\u6BCF\u4E00\u5BF9\u77DB\u76FE\uFF0C\u8FD8\u8981\u6307\u51FA\u54EA\u4E00\u6761\u662F**\u5DF2\u5931\u6548\u7684**\uFF08\u4E0D\u518D\u4E3A\u771F\u7684\u90A3\u6761\uFF09\u3002\u8BF7\u4ECE**\u63AA\u8F9E**\u5224\u65AD\uFF0C\u4E0D\u8981\u5047\u8BBE\u987A\u5E8F\uFF1A
901
+ \u300C\u4EE5\u524D\u300D\u300C2019 \u5E74\u90A3\u4F1A\u513F\u300D\u300C\u4E0A\u4E2A\u6708\u642C\u4E86\u300D\u300C\u6539\u7528\u4E86\u300D\u8FD9\u7C7B\u8BF4\u6CD5\u80FD\u544A\u8BC9\u4F60\u54EA\u6761\u63CF\u8FF0\u7684\u662F\u8FC7\u53BB\u3002
902
+ \u8FD9\u4E9B\u8BB0\u5FC6**\u4E0D\u662F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\u7684**\uFF0C\u7F16\u53F7\u4E5F\u4E0D\u4EE3\u8868\u65B0\u65E7\u3002
903
+
904
+ \u5982\u679C\u65E0\u6CD5\u5224\u65AD\u54EA\u6761\u5DF2\u5931\u6548\uFF0C\u5C31\u586B null\u3002\u8FD9\u662F\u4E00\u4E2A**\u6B63\u5E38\u4E14\u6709\u7528**\u7684\u56DE\u7B54 \u2014\u2014 \u5B81\u53EF\u586B null \u4E5F\u4E0D\u8981\u731C\uFF0C
905
+ \u56E0\u4E3A\u731C\u9519\u4F1A\u8BA9\u4E00\u6761**\u4ECD\u7136\u4E3A\u771F**\u7684\u8BB0\u5FC6\u88AB\u505C\u7528\u3002
906
+
907
+ \u53EA\u8FD4\u56DE JSON \u6570\u7EC4\u3002\u6CA1\u6709\u771F\u6B63\u77DB\u76FE\u5C31\u8FD4\u56DE\u7A7A\u6570\u7EC4\uFF1A
908
+ [{"a": 0, "b": 3, "superseded": 0, "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u662F\u54EA\u4E2A\u5C5E\u6027\u4E92\u65A5"}]
909
+
910
+ "superseded" \u53EA\u80FD\u662F "a" \u7684\u503C\u3001"b" \u7684\u503C\uFF0C\u6216 null\u3002
911
+ \u4F7F\u7528\u4E0A\u9762\u663E\u793A\u7684\u7F16\u53F7\u3002\u540C\u4E00\u5BF9\u53EA\u62A5\u4E00\u6B21\u3002**\u5B81\u53EF\u4E0D\u62A5\uFF0C\u4E5F\u4E0D\u8981\u731C\u3002**`
689
912
  };
690
913
  templates = { en, zh };
691
914
  t = templates[LOCALE];
@@ -693,35 +916,92 @@ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
693
916
  });
694
917
 
695
918
  // ../amem-core/src/llm.ts
696
- function anthropic() {
697
- return _anthropic ??= new import_sdk.default({
698
- ...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
699
- ...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
700
- });
919
+ function configureLlm(cfg) {
920
+ _override = { ...cfg };
921
+ _anthropicClients.clear();
922
+ _openaiClients.clear();
923
+ }
924
+ function warnOnce(key, message) {
925
+ if (_warned.has(key)) return;
926
+ _warned.add(key);
927
+ console.error(message);
928
+ }
929
+ function resolveProvider(role = "fast") {
930
+ const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
931
+ const p = (raw || process.env.AMEM_LLM_PROVIDER || _override.provider || "anthropic").trim().toLowerCase();
932
+ if (p !== "anthropic" && p !== "openai") {
933
+ warnOnce(`provider:${p}`, `[amem] unknown LLM provider "${p}"; falling back to anthropic`);
934
+ }
935
+ return p;
701
936
  }
702
- function openai() {
703
- return _openai ??= new import_openai.default({
704
- // AMEM_LLM_API_KEY first (engine convention), then the SDK's own
705
- // OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
706
- // env fallback, so read it here. Placeholder last, so keyless local servers
707
- // (Ollama, vLLM) still work.
708
- apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
709
- ...process.env.AMEM_LLM_BASE_URL && { baseURL: process.env.AMEM_LLM_BASE_URL }
710
- });
937
+ function resolveModel(role = "fast") {
938
+ const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_MODEL || _override.strong?.model || void 0 : void 0;
939
+ return strong || process.env.AMEM_LLM_MODEL || _override.model || (resolveProvider(role) === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
940
+ }
941
+ function resolveBaseURL(role = "fast") {
942
+ const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_BASE_URL || _override.strong?.baseURL || void 0 : void 0;
943
+ return strong || process.env.AMEM_LLM_BASE_URL || _override.baseURL || void 0;
944
+ }
945
+ function resolveCrudRole() {
946
+ const raw = (process.env.AMEM_LLM_CRUD_ROLE || _override.crudRole || "fast").trim().toLowerCase();
947
+ if (raw === "strong") return "strong";
948
+ if (raw !== "fast") {
949
+ warnOnce(`crudRole:${raw}`, `[amem] unknown AMEM_LLM_CRUD_ROLE "${raw}"; using fast`);
950
+ }
951
+ return "fast";
952
+ }
953
+ function resolveTimeoutMs() {
954
+ const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
955
+ if (Number.isFinite(envVal) && envVal > 0) return envVal;
956
+ if (_override.timeoutMs && _override.timeoutMs > 0) return _override.timeoutMs;
957
+ return DEFAULT_TIMEOUT_MS;
958
+ }
959
+ function anthropic(baseURL) {
960
+ const key = baseURL ?? "";
961
+ let client = _anthropicClients.get(key);
962
+ if (!client) {
963
+ client = new import_sdk.default({
964
+ ...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
965
+ ...baseURL && { baseURL },
966
+ timeout: resolveTimeoutMs()
967
+ });
968
+ _anthropicClients.set(key, client);
969
+ }
970
+ return client;
971
+ }
972
+ function openai(baseURL) {
973
+ const key = baseURL ?? "";
974
+ let client = _openaiClients.get(key);
975
+ if (!client) {
976
+ client = new import_openai.default({
977
+ // AMEM_LLM_API_KEY first (engine convention), then the SDK's own
978
+ // OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
979
+ // env fallback, so read it here. Placeholder last, so keyless local servers
980
+ // (Ollama, vLLM) still work.
981
+ apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
982
+ ...baseURL && { baseURL },
983
+ timeout: resolveTimeoutMs()
984
+ });
985
+ _openaiClients.set(key, client);
986
+ }
987
+ return client;
711
988
  }
712
- async function llmCall(prompt, maxTokens = 500) {
713
- const isThinking = MODEL.includes("gemini") || MODEL.includes("pro-agent");
989
+ async function llmCall(prompt, maxTokens = 500, role = "fast") {
990
+ const provider = resolveProvider(role);
991
+ const model = resolveModel(role);
992
+ const baseURL = resolveBaseURL(role);
993
+ const isThinking = model.includes("gemini") || model.includes("pro-agent");
714
994
  const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
715
995
  try {
716
- return PROVIDER === "openai" ? await openaiCall(prompt, effectiveMaxTokens) : await anthropicCall(prompt, effectiveMaxTokens);
996
+ return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
717
997
  } catch (e) {
718
998
  console.error(`[amem] LLM call failed: ${e.message}`);
719
999
  return null;
720
1000
  }
721
1001
  }
722
- async function anthropicCall(prompt, maxTokens) {
723
- const resp = await anthropic().messages.create({
724
- model: MODEL,
1002
+ async function anthropicCall(prompt, model, maxTokens, baseURL) {
1003
+ const resp = await anthropic(baseURL).messages.create({
1004
+ model,
725
1005
  max_tokens: maxTokens,
726
1006
  messages: [{ role: "user", content: prompt }]
727
1007
  });
@@ -730,17 +1010,20 @@ async function anthropicCall(prompt, maxTokens) {
730
1010
  }
731
1011
  return null;
732
1012
  }
733
- async function openaiCall(prompt, maxTokens) {
734
- const isReasoning = /^o\d/.test(MODEL) || MODEL.startsWith("gpt-5");
735
- const resp = await openai().chat.completions.create({
736
- model: MODEL,
1013
+ async function openaiCall(prompt, model, maxTokens, baseURL) {
1014
+ const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
1015
+ const resp = await openai(baseURL).chat.completions.create({
1016
+ model,
737
1017
  ...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
738
1018
  messages: [{ role: "user", content: prompt }]
739
1019
  });
740
1020
  return resp.choices[0]?.message?.content?.trim() ?? null;
741
1021
  }
1022
+ function stripReasoning(raw) {
1023
+ return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<\|(?:eot_id|im_start|im_end|begin_of_text|end_of_text|endoftext)\|>/g, "").trim();
1024
+ }
742
1025
  function stripFences(raw) {
743
- raw = raw.trim();
1026
+ raw = stripReasoning(raw);
744
1027
  if (raw.startsWith("```")) {
745
1028
  const lines = raw.split("\n");
746
1029
  lines.shift();
@@ -755,6 +1038,16 @@ function stripFences(raw) {
755
1038
  }
756
1039
  return raw;
757
1040
  }
1041
+ function parseJsonLoose(raw) {
1042
+ const cleaned = stripFences(raw);
1043
+ try {
1044
+ return JSON.parse(cleaned);
1045
+ } catch (e) {
1046
+ const m = cleaned.match(/\{[\s\S]*\}/);
1047
+ if (m) return JSON.parse(m[0]);
1048
+ throw e;
1049
+ }
1050
+ }
758
1051
  async function llmConstructNote(content) {
759
1052
  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:
760
1053
  {
@@ -802,7 +1095,7 @@ Text: ${content}`;
802
1095
  confidence: "medium"
803
1096
  };
804
1097
  try {
805
- const data = JSON.parse(stripFences(raw));
1098
+ const data = parseJsonLoose(raw);
806
1099
  const rawCategory = typeof data.category === "string" ? data.category : "General";
807
1100
  const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
808
1101
  const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
@@ -845,9 +1138,9 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
845
1138
  const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
846
1139
  const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
847
1140
  try {
848
- const raw = await llmCall(prompt, 400);
1141
+ const raw = await llmCall(prompt, 400, resolveCrudRole());
849
1142
  if (!raw) return [];
850
- const match = raw.match(/\[.*\]/s);
1143
+ const match = stripReasoning(raw).match(/\[.*\]/s);
851
1144
  if (!match) return [];
852
1145
  const parsed = JSON.parse(match[0]);
853
1146
  if (!Array.isArray(parsed)) return [];
@@ -875,10 +1168,10 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
875
1168
  }
876
1169
  async function llmShouldMerge(contentA, contentB) {
877
1170
  const prompt = t.shouldMerge(contentA, contentB);
878
- const raw = await llmCall(prompt, 300);
1171
+ const raw = await llmCall(prompt, 300, "strong");
879
1172
  if (!raw) return { shouldMerge: false };
880
1173
  try {
881
- const data = JSON.parse(stripFences(raw));
1174
+ const data = parseJsonLoose(raw);
882
1175
  if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
883
1176
  if (data.shouldMerge && typeof data.merged === "string") {
884
1177
  return { shouldMerge: true, merged: data.merged };
@@ -891,10 +1184,10 @@ async function llmShouldMerge(contentA, contentB) {
891
1184
  }
892
1185
  async function llmEvolutionJudge(oldContent, newContent) {
893
1186
  const prompt = t.evolutionJudge(oldContent, newContent);
894
- const raw = await llmCall(prompt, 300);
1187
+ const raw = await llmCall(prompt, 300, "strong");
895
1188
  if (!raw) return { type: "NEW" };
896
1189
  try {
897
- const data = JSON.parse(stripFences(raw));
1190
+ const data = parseJsonLoose(raw);
898
1191
  const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
899
1192
  return {
900
1193
  type,
@@ -931,7 +1224,7 @@ ${linkedStr}`;
931
1224
  const raw = await llmCall(prompt, 500);
932
1225
  if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
933
1226
  try {
934
- const data = JSON.parse(stripFences(raw));
1227
+ const data = parseJsonLoose(raw);
935
1228
  return {
936
1229
  tags: Array.isArray(data.tags) ? data.tags : null,
937
1230
  context: typeof data.context === "string" ? data.context : null,
@@ -944,20 +1237,56 @@ ${linkedStr}`;
944
1237
  return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
945
1238
  }
946
1239
  }
947
- var import_sdk, import_openai, PROVIDER, MODEL, _anthropic, _openai, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
1240
+ async function llmConflictScan(contents) {
1241
+ if (contents.length < 2) return [];
1242
+ const numbered = contents.map((c, i) => `[${i}] ${c}`).join("\n");
1243
+ try {
1244
+ const raw = await llmCall(t.conflictScan(numbered), 600, "strong");
1245
+ if (!raw) return [];
1246
+ const cleaned = stripReasoning(raw);
1247
+ const match = cleaned.match(/\[[\s\S]*\]/);
1248
+ if (!match) return [];
1249
+ const parsed = JSON.parse(match[0]);
1250
+ if (!Array.isArray(parsed)) return [];
1251
+ const pairs = [];
1252
+ const seen = /* @__PURE__ */ new Set();
1253
+ for (const item of parsed) {
1254
+ if (!item || typeof item !== "object") continue;
1255
+ const { a, b } = item;
1256
+ if (typeof a !== "number" || typeof b !== "number") continue;
1257
+ if (!Number.isInteger(a) || !Number.isInteger(b)) continue;
1258
+ if (a < 0 || b < 0 || a >= contents.length || b >= contents.length) continue;
1259
+ if (a === b) continue;
1260
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
1261
+ if (seen.has(key)) continue;
1262
+ seen.add(key);
1263
+ const rawSup = item.superseded;
1264
+ const supersededIndex = rawSup === a || rawSup === b ? rawSup : null;
1265
+ pairs.push({
1266
+ a,
1267
+ b,
1268
+ reason: typeof item.reason === "string" ? item.reason : "",
1269
+ supersededIndex
1270
+ });
1271
+ }
1272
+ return pairs;
1273
+ } catch (e) {
1274
+ console.error(`[amem] llmConflictScan failed: ${e.message}`);
1275
+ return [];
1276
+ }
1277
+ }
1278
+ var import_sdk, import_openai, _override, _warned, DEFAULT_TIMEOUT_MS, _anthropicClients, _openaiClients, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
948
1279
  var init_llm = __esm({
949
1280
  "../amem-core/src/llm.ts"() {
950
1281
  "use strict";
951
1282
  import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
952
1283
  import_openai = __toESM(require("openai"), 1);
953
1284
  init_prompts();
954
- PROVIDER = (process.env.AMEM_LLM_PROVIDER ?? "anthropic").trim().toLowerCase();
955
- if (PROVIDER !== "anthropic" && PROVIDER !== "openai") {
956
- console.error(`[amem] unknown AMEM_LLM_PROVIDER "${PROVIDER}"; falling back to anthropic`);
957
- }
958
- MODEL = process.env.AMEM_LLM_MODEL ?? (PROVIDER === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
959
- _anthropic = null;
960
- _openai = null;
1285
+ _override = {};
1286
+ _warned = /* @__PURE__ */ new Set();
1287
+ DEFAULT_TIMEOUT_MS = 3e4;
1288
+ _anthropicClients = /* @__PURE__ */ new Map();
1289
+ _openaiClients = /* @__PURE__ */ new Map();
961
1290
  VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
962
1291
  VALID_CATEGORIES = /* @__PURE__ */ new Set([
963
1292
  "Technical",
@@ -1077,6 +1406,7 @@ function defaultCtx() {
1077
1406
  }
1078
1407
  async function addMemory(content, agentId = "main", opts) {
1079
1408
  const scope = opts?.scope ?? "private";
1409
+ const subjects = opts?.subjects ?? [];
1080
1410
  const ctx = opts?.storageCtx ?? defaultCtx();
1081
1411
  const quality = checkQuality(content);
1082
1412
  if (!quality.ok) {
@@ -1101,9 +1431,14 @@ async function addMemory(content, agentId = "main", opts) {
1101
1431
  const embedding = await encode(fieldsText);
1102
1432
  const topMatch = await ctx.queryByEmbedding(embedding, 1, agentId, 0);
1103
1433
  if (topMatch.length > 0 && topMatch[0].score >= 0.85) {
1104
- console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
1105
- await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
1106
- return topMatch[0].note.id;
1434
+ if (canWrite(topMatch[0].note, agentId)) {
1435
+ console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
1436
+ await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
1437
+ return topMatch[0].note.id;
1438
+ }
1439
+ console.log(
1440
+ `[add] dedup: high-sim match ${topMatch[0].note.id.slice(0, 8)} is not writable by ${logSafe(agentId)} \u2014 inserting a new note instead`
1441
+ );
1107
1442
  }
1108
1443
  const pendingMerge = topMatch.length > 0 && topMatch[0].score >= 0.72 && topMatch[0].score < 0.85;
1109
1444
  if (pendingMerge) {
@@ -1113,6 +1448,7 @@ async function addMemory(content, agentId = "main", opts) {
1113
1448
  const writers = [agentId];
1114
1449
  const note = {
1115
1450
  id: (0, import_uuid.v4)(),
1451
+ subjects,
1116
1452
  content,
1117
1453
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1118
1454
  keywords,
@@ -1171,6 +1507,10 @@ async function addMemory(content, agentId = "main", opts) {
1171
1507
  for (const lid of linkedIds) {
1172
1508
  const linked = await ctx.getNote(lid);
1173
1509
  if (linked && !linked.links.includes(note.id)) {
1510
+ if (!canWrite(linked, agentId)) {
1511
+ console.log(`[link] back-link into ${lid.slice(0, 8)} skipped \u2014 not writable by ${logSafe(agentId)}`);
1512
+ continue;
1513
+ }
1174
1514
  linked.links.push(note.id);
1175
1515
  await ctx.updateNote(linked);
1176
1516
  }
@@ -1180,10 +1520,14 @@ async function addMemory(content, agentId = "main", opts) {
1180
1520
  for (const lid of linkedIds.slice(0, 3)) {
1181
1521
  const linked = await ctx.getNote(lid);
1182
1522
  if (!linked) continue;
1523
+ if (!canWrite(linked, agentId)) {
1524
+ console.log(` [evo] skipping ${lid.slice(0, 8)} \u2014 not writable by ${logSafe(agentId)}`);
1525
+ continue;
1526
+ }
1183
1527
  const linkedNotes = [];
1184
1528
  for (const llid of linked.links.slice(0, 5)) {
1185
1529
  if (llid === note.id) continue;
1186
- const ln = await ctx.getNote(llid);
1530
+ const ln = await ctx.getNote(llid, agentId);
1187
1531
  if (ln) linkedNotes.push({ id: ln.id, content: ln.content });
1188
1532
  }
1189
1533
  linkedNotes.push({ id: note.id, content });
@@ -1220,10 +1564,14 @@ async function addMemory(content, agentId = "main", opts) {
1220
1564
  note.links.push(targetId);
1221
1565
  noteChanged = true;
1222
1566
  }
1223
- const target = await ctx.getNote(targetId);
1567
+ const target = await ctx.getNote(targetId, agentId);
1224
1568
  if (target && !target.links.includes(note.id)) {
1225
- target.links.push(note.id);
1226
- await ctx.updateNote(target);
1569
+ if (canWrite(target, agentId)) {
1570
+ target.links.push(note.id);
1571
+ await ctx.updateNote(target);
1572
+ } else {
1573
+ console.log(` [evo] strengthen back-link into ${targetId.slice(0, 8)} skipped \u2014 not writable`);
1574
+ }
1227
1575
  }
1228
1576
  }
1229
1577
  if (tagsToUpdate.length > 0) {
@@ -1272,6 +1620,7 @@ async function addMemory(content, agentId = "main", opts) {
1272
1620
  }
1273
1621
  async function addEpisodic(content, agentId = "main", opts) {
1274
1622
  const scope = opts?.scope ?? "private";
1623
+ const subjects = opts?.subjects ?? [];
1275
1624
  const ctx = opts?.storageCtx ?? defaultCtx();
1276
1625
  const quality = checkQuality(content);
1277
1626
  if (!quality.ok) {
@@ -1281,6 +1630,7 @@ async function addEpisodic(content, agentId = "main", opts) {
1281
1630
  const now = (/* @__PURE__ */ new Date()).toISOString();
1282
1631
  const note = {
1283
1632
  id: (0, import_uuid.v4)(),
1633
+ subjects,
1284
1634
  content,
1285
1635
  timestamp: now,
1286
1636
  keywords: [],
@@ -1310,14 +1660,15 @@ async function addEpisodic(content, agentId = "main", opts) {
1310
1660
  }
1311
1661
  async function searchMemory(query, topK = 5, agentId = "main", opts) {
1312
1662
  const useBfs = opts?.useBfs !== false;
1663
+ const subject = opts?.subject;
1313
1664
  const bfsSimThreshold = opts?.bfsSimThreshold ?? 0.25;
1314
1665
  const ctx = opts?.storageCtx ?? defaultCtx();
1315
1666
  const total = await ctx.countNotes(agentId);
1316
1667
  if (total === 0) return [];
1317
1668
  const queryEmbedding = await encode(query);
1318
1669
  const n = Math.min(Math.max(topK * 4, 20), total);
1319
- const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0);
1320
- const allNotes = await ctx.listNotes(agentId);
1670
+ const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0, subject);
1671
+ const allNotes = await ctx.listNotes(agentId, subject);
1321
1672
  const bm25State = buildBM25(allNotes);
1322
1673
  const queryTokens = simpleTokenize(query);
1323
1674
  const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n);
@@ -1628,7 +1979,81 @@ async function consolidateMemories(agentId, logger, storageCtx) {
1628
1979
  log.info(`[Consolidation] Completed consolidation run. Merged ${mergedCount} pairs.`);
1629
1980
  return mergedCount;
1630
1981
  }
1631
- var import_uuid, import_crypto, fs2, path3, import_jieba, _jieba, EPHEMERAL_SIGNALS;
1982
+ function resolveConflictMode(override) {
1983
+ const raw = (process.env.AMEM_CONFLICT_MODE || override || "review").trim().toLowerCase();
1984
+ return raw === "auto" ? "auto" : "review";
1985
+ }
1986
+ async function conflictSweep(agentId, opts) {
1987
+ const ctx = opts?.storageCtx ?? defaultCtx();
1988
+ const force = opts?.force === true;
1989
+ const mode = resolveConflictMode(opts?.mode);
1990
+ const log = opts?.logger?.info ?? ((m) => console.log(m));
1991
+ const raw = await ctx.listNotes(agentId);
1992
+ const notes = raw.filter((n) => n.agent_id !== "shared" && n.note_type !== "knowledge" && n.is_active !== false);
1993
+ const groups = /* @__PURE__ */ new Map();
1994
+ for (const n of notes) {
1995
+ const c = n.category || "General";
1996
+ if (!groups.has(c)) groups.set(c, []);
1997
+ groups.get(c).push(n);
1998
+ }
1999
+ let pairsFound = 0;
2000
+ let retired = 0;
2001
+ let batchesScanned = 0;
2002
+ let batchesSkipped = 0;
2003
+ for (const [category, groupNotes] of groups.entries()) {
2004
+ groupNotes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
2005
+ for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {
2006
+ const batch = groupNotes.slice(start, start + CONFLICT_BATCH_SIZE);
2007
+ if (batch.length < 2) continue;
2008
+ if (!force && batch.every((n) => n.conflict_scanned_at)) {
2009
+ batchesSkipped++;
2010
+ continue;
2011
+ }
2012
+ batchesScanned++;
2013
+ const pairs = await llmConflictScan(batch.map((n) => n.content));
2014
+ for (const { a, b, reason, supersededIndex } of pairs) {
2015
+ const noteA = batch[a];
2016
+ const noteB = batch[b];
2017
+ if (!noteA || !noteB) continue;
2018
+ pairsFound++;
2019
+ await ctx.patchNotePayload(noteA.id, {
2020
+ conflict: true,
2021
+ evolution_type: "CONFLICT",
2022
+ conflicts_with: Array.from(/* @__PURE__ */ new Set([...noteA.conflicts_with ?? [], noteB.id])),
2023
+ conflict_reason: reason
2024
+ });
2025
+ await ctx.patchNotePayload(noteB.id, {
2026
+ conflict: true,
2027
+ evolution_type: "CONFLICT",
2028
+ conflicts_with: Array.from(/* @__PURE__ */ new Set([...noteB.conflicts_with ?? [], noteA.id])),
2029
+ conflict_reason: reason
2030
+ });
2031
+ log(`[conflict] ${category}: ${noteA.id.slice(0, 8)} \u2194 ${noteB.id.slice(0, 8)} \u2014 ${reason}`);
2032
+ if (mode === "auto") {
2033
+ const superseded = supersededIndex === a ? noteA : supersededIndex === b ? noteB : null;
2034
+ if (!superseded) {
2035
+ log(`[conflict] auto: no superseded side identified \u2014 marked only, nothing retired`);
2036
+ } else {
2037
+ const ok = await ctx.invalidateNote(superseded.id, agentId);
2038
+ if (ok) {
2039
+ retired++;
2040
+ log(`[conflict] auto-retired the superseded note ${superseded.id.slice(0, 8)}`);
2041
+ }
2042
+ }
2043
+ }
2044
+ }
2045
+ const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
2046
+ for (const n of batch) {
2047
+ await ctx.patchNotePayload(n.id, { conflict_scanned_at: scannedAt });
2048
+ }
2049
+ }
2050
+ }
2051
+ log(
2052
+ `[conflict] ${batchesScanned} batch(es) scanned, ${batchesSkipped} already up to date; ${pairsFound} pair(s) found, ${retired} retired`
2053
+ );
2054
+ return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped };
2055
+ }
2056
+ var import_uuid, import_crypto, fs2, path3, import_jieba, logSafe, _jieba, EPHEMERAL_SIGNALS, CONFLICT_BATCH_SIZE;
1632
2057
  var init_memory = __esm({
1633
2058
  "../amem-core/src/memory.ts"() {
1634
2059
  "use strict";
@@ -1638,12 +2063,15 @@ var init_memory = __esm({
1638
2063
  path3 = __toESM(require("path"), 1);
1639
2064
  init_embedding();
1640
2065
  init_storage();
2066
+ init_auth();
1641
2067
  init_llm();
1642
2068
  init_evo_counter();
1643
2069
  init_config();
1644
2070
  import_jieba = require("@node-rs/jieba");
2071
+ logSafe = (id) => id.replace(/[\r\n]/g, "");
1645
2072
  _jieba = null;
1646
2073
  EPHEMERAL_SIGNALS = ["\u5F85\u8DD1", "\u7B49\u786E\u8BA4", "\u6628\u65E5", "\u660E\u5929\u5B8C\u6210"];
2074
+ CONFLICT_BATCH_SIZE = 25;
1647
2075
  }
1648
2076
  });
1649
2077
 
@@ -1654,6 +2082,7 @@ async function scanLowQuality(agentId) {
1654
2082
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
1655
2083
  const results = [];
1656
2084
  for (const note of notes) {
2085
+ if (!canWrite(note, agentId)) continue;
1657
2086
  const reasons = [];
1658
2087
  if (note.content.trim().length < 10) {
1659
2088
  reasons.push("too_short");
@@ -1737,7 +2166,7 @@ async function generateReviewBatch(agentId, outputPath) {
1737
2166
  const title = LOCALE2 === "zh" ? "A-MEM \u8D28\u91CF\u5BA1\u6838" : "A-MEM Quality Review";
1738
2167
  const genLabel = LOCALE2 === "zh" ? "\u751F\u6210\u65F6\u95F4" : "Generated";
1739
2168
  const countLabel = LOCALE2 === "zh" ? `\u5171 ${items.length} \u6761\u4F4E\u8D28\u91CF\u6761\u76EE` : `${items.length} low-quality item(s)`;
1740
- const applyHint = LOCALE2 === "zh" ? "\u9009\u597D\u540E\u53EF\u4F7F\u7528 memory_quality_apply \u6279\u91CF\u5904\u7406" : "Use memory_quality_apply to batch-process selected items";
2169
+ const applyHint = LOCALE2 === "zh" ? "\u52FE\u9009\u540E\u4EA4\u7ED9\u52A9\u624B\u5904\u7406\u8FD9\u4E9B\u6761\u76EE" : "Tick your choices, then ask the assistant to act on them";
1741
2170
  lines.push(`# ${title} \u2014 Batch ${batchN || "custom"}`);
1742
2171
  lines.push("");
1743
2172
  lines.push(`> ${genLabel}\uFF1A${now} | ${countLabel}`);
@@ -1746,6 +2175,48 @@ async function generateReviewBatch(agentId, outputPath) {
1746
2175
  if (items.length === 0) {
1747
2176
  lines.push(LOCALE2 === "zh" ? "\u2705 \u6CA1\u6709\u53D1\u73B0\u4F4E\u8D28\u91CF\u6761\u76EE\u3002" : "\u2705 No low-quality items found.");
1748
2177
  }
2178
+ const byId = new Map(items.map((it) => [it.note.id, it.note]));
2179
+ const renderedPairs = /* @__PURE__ */ new Set();
2180
+ const pairLines = [];
2181
+ for (const { note } of items) {
2182
+ for (const otherId of note.conflicts_with ?? []) {
2183
+ const other = byId.get(otherId);
2184
+ if (!other) continue;
2185
+ const key = note.id < otherId ? `${note.id}:${otherId}` : `${otherId}:${note.id}`;
2186
+ if (renderedPairs.has(key)) continue;
2187
+ renderedPairs.add(key);
2188
+ const [newer, older] = Date.parse(note.timestamp) >= Date.parse(other.timestamp) ? [note, other] : [other, note];
2189
+ const zh2 = LOCALE2 === "zh";
2190
+ pairLines.push(`### \u{1F7E0} ${zh2 ? "\u51B2\u7A81" : "CONFLICT"} | ${newer.category || "General"}`);
2191
+ if (newer.conflict_reason) {
2192
+ pairLines.push(`**${zh2 ? "\u5224\u5B9A\u7406\u7531" : "Why"}\uFF1A** ${newer.conflict_reason}`);
2193
+ pairLines.push("");
2194
+ }
2195
+ pairLines.push(`| | ${zh2 ? "\u65F6\u95F4" : "When"} | ${zh2 ? "\u5185\u5BB9" : "Content"} |`);
2196
+ pairLines.push("| :-- | :-- | :-- |");
2197
+ pairLines.push(`| **A** | ${newer.timestamp.slice(0, 10)} | ${newer.content.replace(/\n/g, " ")} |`);
2198
+ pairLines.push(`| **B** | ${older.timestamp.slice(0, 10)} | ${older.content.replace(/\n/g, " ")} |`);
2199
+ pairLines.push("");
2200
+ pairLines.push(`\`A: ${newer.id}\``);
2201
+ pairLines.push(`\`B: ${older.id}\``);
2202
+ pairLines.push("");
2203
+ pairLines.push(
2204
+ zh2 ? `- [ ] \u2705 **A \u662F\u5F53\u524D\u72B6\u6001\uFF0C\u505C\u7528 B**\uFF08\u63A8\u8350\uFF1AA \u66F4\u65B0\uFF09` : `- [ ] \u2705 **A is current \u2014 retire B** (recommended: A is newer)`
2205
+ );
2206
+ pairLines.push(zh2 ? `- [ ] \u21A9\uFE0F B \u662F\u5F53\u524D\u72B6\u6001\uFF0C\u505C\u7528 A` : `- [ ] \u21A9\uFE0F B is current \u2014 retire A`);
2207
+ pairLines.push(zh2 ? `- [ ] \u{1F91D} \u4E24\u8005\u90FD\u6210\u7ACB\uFF08\u8BEF\u5224\uFF09` : `- [ ] \u{1F91D} Both hold \u2014 not a contradiction`);
2208
+ pairLines.push("");
2209
+ pairLines.push("---");
2210
+ pairLines.push("");
2211
+ }
2212
+ }
2213
+ if (pairLines.length > 0) {
2214
+ lines.push(LOCALE2 === "zh" ? "## \u51B2\u7A81\uFF08\u6210\u5BF9\uFF0C\u4E00\u4E2A\u51B2\u7A81\u4E00\u4E2A\u51B3\u5B9A\uFF09" : "## Conflicts (paired \u2014 one decision each)");
2215
+ lines.push("");
2216
+ lines.push(...pairLines);
2217
+ lines.push(LOCALE2 === "zh" ? "## \u5176\u4F59\u6761\u76EE" : "## Other items");
2218
+ lines.push("");
2219
+ }
1749
2220
  for (let i = 0; i < items.length; i++) {
1750
2221
  const { note, reasons } = items[i];
1751
2222
  const badge = severityBadge(reasons);
@@ -1788,34 +2259,155 @@ var init_quality = __esm({
1788
2259
  fs3 = __toESM(require("fs"), 1);
1789
2260
  path4 = __toESM(require("path"), 1);
1790
2261
  init_storage();
2262
+ init_auth();
1791
2263
  LOCALE2 = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
1792
2264
  DEFAULT_OUTPUT_DIR = process.env.AMEM_REVIEW_DIR || process.cwd();
1793
2265
  }
1794
2266
  });
1795
2267
 
2268
+ // ../amem-core/src/migrate.ts
2269
+ function missingDerivedFields(n) {
2270
+ return n.keywords.length === 0 || n.tags.length === 0;
2271
+ }
2272
+ async function migrateCollection(opts) {
2273
+ const { from, to } = opts;
2274
+ const refreshFields = opts.refreshFields !== false;
2275
+ const dryRun = opts.dryRun !== false;
2276
+ const log = opts.logger?.info ?? ((m) => console.log(m));
2277
+ const warn = opts.logger?.warn ?? ((m) => console.warn(m));
2278
+ if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
2279
+ const model = getEmbeddingModel();
2280
+ const targetDim = await getEmbeddingDim();
2281
+ const sourceDim = await collectionDimRaw(from);
2282
+ if (sourceDim === null) throw new Error(`migrate: source collection "${from}" does not exist`);
2283
+ const points = await scrollAllRaw(from);
2284
+ const notes = points.map(pointToNote);
2285
+ const missingDerived = notes.filter(missingDerivedFields).length;
2286
+ log(
2287
+ `[migrate] ${from} (${sourceDim}d, ${notes.length} notes) \u2192 ${to} (${targetDim}d, ${model}); ${missingDerived} note(s) missing keywords/tags`
2288
+ );
2289
+ if (dryRun) {
2290
+ log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
2291
+ return { total: notes.length, missingDerived, refreshed: 0, migrated: 0, sourceDim, targetDim, model, dryRun: true };
2292
+ }
2293
+ const existingTargetDim = await collectionDimRaw(to);
2294
+ if (existingTargetDim === null) {
2295
+ await createCollectionRaw(to, targetDim);
2296
+ log(`[migrate] created ${to} at ${targetDim}d`);
2297
+ } else {
2298
+ if (existingTargetDim !== targetDim) {
2299
+ throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
2300
+ }
2301
+ const existingCount = await countPointsRaw(to);
2302
+ if (existingCount > 0) {
2303
+ throw new Error(`migrate: target "${to}" already holds ${existingCount} point(s); use an empty collection`);
2304
+ }
2305
+ }
2306
+ let refreshed = 0;
2307
+ let migrated = 0;
2308
+ const BATCH = 64;
2309
+ let buffer = [];
2310
+ const flush = async () => {
2311
+ if (!buffer.length) return;
2312
+ await upsertPointsRaw(to, buffer);
2313
+ migrated += buffer.length;
2314
+ buffer = [];
2315
+ };
2316
+ for (const note of notes) {
2317
+ if (refreshFields && missingDerivedFields(note)) {
2318
+ try {
2319
+ const built = await llmConstructNote(note.content);
2320
+ if (note.keywords.length === 0) note.keywords = built.keywords;
2321
+ if (note.tags.length === 0) note.tags = built.tags;
2322
+ if (!note.context) note.context = built.context;
2323
+ refreshed++;
2324
+ } catch (e) {
2325
+ warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
2326
+ }
2327
+ }
2328
+ const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
2329
+ buffer.push(point);
2330
+ if (buffer.length >= BATCH) {
2331
+ await flush();
2332
+ log(`[migrate] ${migrated}/${notes.length}`);
2333
+ }
2334
+ }
2335
+ await flush();
2336
+ const finalCount = await countPointsRaw(to);
2337
+ if (finalCount !== notes.length) {
2338
+ warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
2339
+ }
2340
+ log(
2341
+ `[migrate] done: ${migrated} migrated, ${refreshed} re-extracted. "${from}" is untouched \u2014 switch with AMEM_COLLECTION=${to}, and keep the old one until you are satisfied.`
2342
+ );
2343
+ return { total: notes.length, missingDerived, refreshed, migrated, sourceDim, targetDim, model, dryRun: false };
2344
+ }
2345
+ var init_migrate = __esm({
2346
+ "../amem-core/src/migrate.ts"() {
2347
+ "use strict";
2348
+ init_storage();
2349
+ init_embedding();
2350
+ init_llm();
2351
+ init_memory();
2352
+ }
2353
+ });
2354
+
2355
+ // ../amem-core/src/crud-guard.ts
2356
+ function resolveCrudUpdateMinSim(override) {
2357
+ const envVal = Number(process.env.AMEM_CRUD_UPDATE_MIN_SIM);
2358
+ if (Number.isFinite(envVal) && envVal >= 0) return envVal;
2359
+ if (override !== void 0 && Number.isFinite(override) && override >= 0) return override;
2360
+ return DEFAULT_CRUD_UPDATE_MIN_SIM;
2361
+ }
2362
+ function isPlausibleUpdateTarget(newEmbedding, targetEmbedding, minSimilarity) {
2363
+ if (!newEmbedding?.length || !targetEmbedding?.length) return false;
2364
+ if (newEmbedding.length !== targetEmbedding.length) return false;
2365
+ return cosineSimilarity(newEmbedding, targetEmbedding) >= resolveCrudUpdateMinSim(minSimilarity);
2366
+ }
2367
+ var DEFAULT_CRUD_UPDATE_MIN_SIM;
2368
+ var init_crud_guard = __esm({
2369
+ "../amem-core/src/crud-guard.ts"() {
2370
+ "use strict";
2371
+ init_embedding();
2372
+ DEFAULT_CRUD_UPDATE_MIN_SIM = 0.35;
2373
+ }
2374
+ });
2375
+
1796
2376
  // ../amem-core/src/index.ts
1797
2377
  var src_exports = {};
1798
2378
  __export(src_exports, {
2379
+ DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
2380
+ DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
2381
+ EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
1799
2382
  addEpisodic: () => addEpisodic,
1800
2383
  addMemory: () => addMemory,
2384
+ canRead: () => canRead,
2385
+ canWrite: () => canWrite,
1801
2386
  checkQuality: () => checkQuality,
1802
2387
  configure: () => configure,
2388
+ configureLlm: () => configureLlm,
2389
+ conflictSweep: () => conflictSweep,
1803
2390
  consolidateMemories: () => consolidateMemories,
1804
2391
  createStorageContext: () => createStorageContext,
1805
2392
  deleteNote: () => deleteNote,
1806
2393
  encode: () => encode,
1807
2394
  ensureCollection: () => ensureCollection,
1808
2395
  generateReviewBatch: () => generateReviewBatch,
2396
+ getEmbeddingDim: () => getEmbeddingDim,
2397
+ getEmbeddingModel: () => getEmbeddingModel,
1809
2398
  getNote: () => getNote,
1810
2399
  invalidateNote: () => invalidateNote,
1811
2400
  isModelLoaded: () => isModelLoaded,
2401
+ isPlausibleUpdateTarget: () => isPlausibleUpdateTarget,
1812
2402
  listMemories: () => listMemories,
1813
2403
  listNotes: () => listNotes,
1814
2404
  llmCrudDecision: () => llmCrudDecision,
1815
2405
  loadModel: () => loadModel,
1816
2406
  mergeSimilarNotes: () => mergeSimilarNotes,
2407
+ migrateCollection: () => migrateCollection,
1817
2408
  patchNotePayload: () => patchNotePayload,
1818
2409
  pingQdrant: () => pingQdrant,
2410
+ resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
1819
2411
  scanLowQuality: () => scanLowQuality,
1820
2412
  searchMemory: () => searchMemory,
1821
2413
  updateNote: () => updateNote
@@ -1828,6 +2420,12 @@ var init_src = __esm({
1828
2420
  init_memory();
1829
2421
  init_quality();
1830
2422
  init_storage();
2423
+ init_auth();
2424
+ init_embedding();
2425
+ init_storage();
2426
+ init_migrate();
2427
+ init_memory();
2428
+ init_crud_guard();
1831
2429
  init_llm();
1832
2430
  }
1833
2431
  });
@@ -1901,6 +2499,26 @@ function register(api) {
1901
2499
  const convBlocked = isConvAccessBlocked(api.config, pluginId);
1902
2500
  if (convBlocked) logger.warn(BLOCKED_WARNING_LOG);
1903
2501
  configure({ dataDir: path5.join(os2.homedir(), ".openclaw") });
2502
+ const hasStrong = !!(pluginConfig.llmStrongProvider || pluginConfig.llmStrongModel || pluginConfig.llmStrongBaseURL);
2503
+ if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || hasStrong) {
2504
+ configureLlm({
2505
+ provider: pluginConfig.llmProvider,
2506
+ model: pluginConfig.llmModel,
2507
+ baseURL: pluginConfig.llmBaseURL,
2508
+ crudRole: pluginConfig.llmCrudRole,
2509
+ // Omit the whole block when unset so `strong` transparently falls back to
2510
+ // `fast` — the zero-config path stays byte-for-byte today's behaviour.
2511
+ ...hasStrong && {
2512
+ strong: {
2513
+ provider: pluginConfig.llmStrongProvider,
2514
+ model: pluginConfig.llmStrongModel,
2515
+ baseURL: pluginConfig.llmStrongBaseURL
2516
+ }
2517
+ }
2518
+ });
2519
+ }
2520
+ const conflictSweepEnabled = pluginConfig.conflictSweep !== false;
2521
+ const crudUpdateMinSim = pluginConfig.crudUpdateMinSim;
1904
2522
  const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
1905
2523
  const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
1906
2524
  const defaultScope = buildScope2(resolveAgentId2());
@@ -1908,9 +2526,13 @@ function register(api) {
1908
2526
  logger.info(
1909
2527
  `openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
1910
2528
  );
1911
- ensureCollection(pluginConfig.collection).catch(
1912
- (e) => logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`)
1913
- );
2529
+ ensureCollection(pluginConfig.collection).catch((e) => {
2530
+ if (e instanceof EmbeddingDimensionMismatchError) {
2531
+ logger.error(`openclaw-amem: memory is UNUSABLE \u2014 ${e.message}`);
2532
+ } else {
2533
+ logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`);
2534
+ }
2535
+ });
1914
2536
  if (typeof api.registerMemoryCapability === "function") {
1915
2537
  api.registerMemoryCapability({
1916
2538
  publicArtifacts: {
@@ -2001,17 +2623,22 @@ function register(api) {
2001
2623
  type: "array",
2002
2624
  items: { type: "string" },
2003
2625
  description: "Story 26B: filter knowledge notes by topics (all must match)"
2626
+ },
2627
+ subject: {
2628
+ type: "string",
2629
+ description: "Who you are talking to or about (e.g. a player name). Returns memories about them plus memories about nobody in particular. Omit to search everything."
2004
2630
  }
2005
2631
  },
2006
2632
  required: ["query"]
2007
2633
  },
2008
2634
  async execute(_toolCallId, params) {
2009
- const { query, limit = 5, topicsFilter } = params;
2635
+ const { query, limit = 5, topicsFilter, subject } = params;
2010
2636
  const start = Date.now();
2011
2637
  const hookWarning = convBlocked ? BLOCKED_WARNING_SUFFIX : "";
2012
2638
  try {
2013
2639
  const results = await searchMemory(query, limit, scope.agentId, {
2014
2640
  topicsFilter,
2641
+ subject,
2015
2642
  storageCtx: scope.storageCtx
2016
2643
  });
2017
2644
  logger.info(
@@ -2052,15 +2679,20 @@ ${text}${hookWarning}` }],
2052
2679
  parameters: {
2053
2680
  type: "object",
2054
2681
  properties: {
2055
- text: { type: "string", description: "Fact or information to remember" }
2682
+ text: { type: "string", description: "Fact or information to remember" },
2683
+ subjects: {
2684
+ type: "array",
2685
+ items: { type: "string" },
2686
+ description: "Who this memory is about (e.g. player names). Use several for a shared experience \u2014 it will surface for each of them. Leave empty for a fact about the world or about yourself."
2687
+ }
2056
2688
  },
2057
2689
  required: ["text"]
2058
2690
  },
2059
2691
  async execute(_toolCallId, params) {
2060
- const { text } = params;
2692
+ const { text, subjects } = params;
2061
2693
  const start = Date.now();
2062
2694
  try {
2063
- const id = await addMemory(text, scope.agentId, { storageCtx: scope.storageCtx });
2695
+ const id = await addMemory(text, scope.agentId, { subjects, storageCtx: scope.storageCtx });
2064
2696
  logger.info(`openclaw-amem: memory_add OK id=${id} (${Date.now() - start}ms)`);
2065
2697
  return {
2066
2698
  content: [{ type: "text", text: "Memory saved successfully." }],
@@ -2238,7 +2870,21 @@ ${text}${hookWarning}` }],
2238
2870
  if (target) {
2239
2871
  const newEmbedding = await encode(op.fact);
2240
2872
  const hash = (0, import_crypto2.createHash)("md5").update(op.fact).digest("hex");
2241
- await storageCtx.updateNoteContent(target.id, op.fact, newEmbedding, hash);
2873
+ const targetNote = await storageCtx.getNote(target.id, agentId);
2874
+ if (!targetNote || !isPlausibleUpdateTarget(newEmbedding, targetNote.embedding, crudUpdateMinSim)) {
2875
+ await addMemory(op.fact, agentId, { storageCtx });
2876
+ logger.warn(
2877
+ `openclaw-amem: CRUD UPDATE on ${target.id.slice(0, 8)} looks mis-targeted \u2014 stored as a new memory instead`
2878
+ );
2879
+ continue;
2880
+ }
2881
+ const ok = await storageCtx.updateNoteContent(target.id, op.fact, newEmbedding, hash, agentId);
2882
+ if (!ok) {
2883
+ logger.warn(
2884
+ `openclaw-amem: CRUD UPDATE denied id=${target.id.slice(0, 8)} \u2014 ${agentId} not in writers`
2885
+ );
2886
+ continue;
2887
+ }
2242
2888
  logger.info(
2243
2889
  `openclaw-amem: CRUD UPDATE id=${target.id.slice(0, 8)}: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`
2244
2890
  );
@@ -2246,7 +2892,13 @@ ${text}${hookWarning}` }],
2246
2892
  } else if (op.action === "DELETE" && op.existingIdx !== void 0) {
2247
2893
  const target = existingMemories[op.existingIdx];
2248
2894
  if (target) {
2249
- await storageCtx.invalidateNote(target.id);
2895
+ const ok = await storageCtx.invalidateNote(target.id, agentId);
2896
+ if (!ok) {
2897
+ logger.warn(
2898
+ `openclaw-amem: CRUD DELETE denied id=${target.id.slice(0, 8)} \u2014 ${agentId} not in writers`
2899
+ );
2900
+ continue;
2901
+ }
2250
2902
  logger.info(
2251
2903
  `openclaw-amem: CRUD INVALIDATE id=${target.id.slice(0, 8)}: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`
2252
2904
  );
@@ -2294,6 +2946,21 @@ ${mergeErr.stack}`
2294
2946
  } catch (err) {
2295
2947
  logger.warn(`openclaw-amem: Scheduled daily consolidation failed \u2014 ${err.message}`);
2296
2948
  }
2949
+ if (conflictSweepEnabled) {
2950
+ try {
2951
+ const res = await conflictSweep(defaultScope.agentId, {
2952
+ storageCtx: defaultScope.storageCtx,
2953
+ logger
2954
+ });
2955
+ if (res.pairsFound > 0) {
2956
+ logger.info(
2957
+ `openclaw-amem: Contradiction sweep flagged ${res.pairsFound} pair(s)` + (res.retired > 0 ? `, retired ${res.retired}` : "") + ` (${res.batchesScanned} batch(es) read, ${res.batchesSkipped} unchanged).`
2958
+ );
2959
+ }
2960
+ } catch (err) {
2961
+ logger.warn(`openclaw-amem: Scheduled contradiction sweep failed \u2014 ${err.message}`);
2962
+ }
2963
+ }
2297
2964
  scheduleNextRun();
2298
2965
  }, delay);
2299
2966
  }
@@ -2314,8 +2981,8 @@ ${mergeErr.stack}`
2314
2981
  }
2315
2982
  var plugin = (0, import_plugin_entry.definePluginEntry)({
2316
2983
  id: "openclaw-amem",
2317
- name: "Memory (A-MEM v2)",
2318
- description: "A-MEM agentic memory backend for OpenClaw \u2014 Qdrant + Transformers.js, no Python required.",
2984
+ name: "amem",
2985
+ description: "Agentic memory for OpenClaw \u2014 memories evolve, link into a graph, and stay separated per agent and per person.",
2319
2986
  register
2320
2987
  });
2321
2988
  var index_default = plugin;