openclaw-amem 1.2.1 → 1.3.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
@@ -5,8 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __export = (target, all) => {
12
17
  for (var name in all)
@@ -122,6 +127,19 @@ var init_embedding = __esm({
122
127
  }
123
128
  });
124
129
 
130
+ // ../amem-core/src/auth.ts
131
+ function canWrite(note, callerAgentId) {
132
+ return note.owner === callerAgentId || note.writers.includes(callerAgentId) || note.writers.includes("*");
133
+ }
134
+ function canRead(note, callerAgentId) {
135
+ return note.owner === callerAgentId || note.readers.includes(callerAgentId) || note.readers.includes("*");
136
+ }
137
+ var init_auth = __esm({
138
+ "../amem-core/src/auth.ts"() {
139
+ "use strict";
140
+ }
141
+ });
142
+
125
143
  // ../amem-core/src/storage.ts
126
144
  async function qdrant(method, path6, body) {
127
145
  const res = await fetch(`${QDRANT_URL}${path6}`, {
@@ -175,6 +193,10 @@ async function ensureCollection(collectionName) {
175
193
  field_name: "topics",
176
194
  field_schema: "keyword"
177
195
  });
196
+ await qdrant("PUT", `/collections/${col}/index`, {
197
+ field_name: "subjects",
198
+ field_schema: "keyword"
199
+ });
178
200
  markReady();
179
201
  }
180
202
  function noteToPoint(note) {
@@ -207,6 +229,9 @@ function noteToPoint(note) {
207
229
  // 30
208
230
  evolution_type: note.evolution_type || "",
209
231
  conflict: note.conflict ?? false,
232
+ conflicts_with: note.conflicts_with ?? [],
233
+ conflict_reason: note.conflict_reason ?? "",
234
+ subjects: note.subjects ?? [],
210
235
  // 31
211
236
  ephemeral: note.ephemeral ?? false,
212
237
  low_quality: note.low_quality ?? false,
@@ -259,6 +284,9 @@ function pointToNote(point) {
259
284
  // 30
260
285
  evolution_type: typeof p.evolution_type === "string" && ["EVOLVE", "CONFLICT", "EXPAND", "NEW"].includes(p.evolution_type) ? p.evolution_type : void 0,
261
286
  conflict: p.conflict === true,
287
+ conflicts_with: Array.isArray(p.conflicts_with) ? p.conflicts_with.filter((v) => typeof v === "string") : [],
288
+ conflict_reason: typeof p.conflict_reason === "string" ? p.conflict_reason : "",
289
+ subjects: Array.isArray(p.subjects) ? p.subjects.filter((v) => typeof v === "string") : [],
262
290
  // 31
263
291
  ephemeral: p.ephemeral === true,
264
292
  low_quality: p.low_quality === true,
@@ -268,28 +296,41 @@ function pointToNote(point) {
268
296
  writers: Array.isArray(p.writers) ? p.writers : [p.agent_id || "main"]
269
297
  };
270
298
  }
271
- function agentFilter(agentId) {
299
+ function agentFilter(agentId, subject) {
300
+ const must = [
301
+ {
302
+ should: [
303
+ { key: "agent_id", match: { value: agentId } },
304
+ { key: "agent_id", match: { value: "shared" } }
305
+ ]
306
+ }
307
+ ];
308
+ if (subject !== void 0) {
309
+ must.push({
310
+ should: [{ key: "subjects", match: { value: subject } }, { is_empty: { key: "subjects" } }]
311
+ });
312
+ }
272
313
  return {
273
- must: [
274
- {
275
- should: [
276
- { key: "agent_id", match: { value: agentId } },
277
- { key: "agent_id", match: { value: "shared" } }
278
- ]
279
- }
280
- ],
314
+ must,
281
315
  must_not: [{ key: "is_active", match: { value: false } }]
282
316
  };
283
317
  }
284
318
  function makeCrud(collectionName, modeBIsolated = false) {
285
319
  const col = collectionName;
286
- function scopedAgentFilter(agentId) {
320
+ function scopedAgentFilter(agentId, subject) {
287
321
  if (modeBIsolated) {
322
+ const must = [];
323
+ if (subject !== void 0) {
324
+ must.push({
325
+ should: [{ key: "subjects", match: { value: subject } }, { is_empty: { key: "subjects" } }]
326
+ });
327
+ }
288
328
  return {
329
+ ...must.length > 0 && { must },
289
330
  must_not: [{ key: "is_active", match: { value: false } }]
290
331
  };
291
332
  }
292
- return agentFilter(agentId);
333
+ return agentFilter(agentId, subject);
293
334
  }
294
335
  return {
295
336
  async addNote(note) {
@@ -298,7 +339,14 @@ function makeCrud(collectionName, modeBIsolated = false) {
298
339
  points: [noteToPoint(note)]
299
340
  });
300
341
  },
301
- async getNote(id) {
342
+ /**
343
+ * Story 36: this is the one read that bypasses the agent filter — it fetches
344
+ * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
345
+ * note comes back as `null` (indistinguishable from missing, so nothing leaks,
346
+ * and callers already handle null). Omitting it skips the check, preserving
347
+ * behaviour for internal callers that only ever hold their own ids.
348
+ */
349
+ async getNote(id, readerAgentId) {
302
350
  await ensureCollection(col);
303
351
  try {
304
352
  const result = await qdrant("POST", `/collections/${col}/points`, {
@@ -307,7 +355,9 @@ function makeCrud(collectionName, modeBIsolated = false) {
307
355
  with_vector: true
308
356
  });
309
357
  if (!result.length) return null;
310
- return pointToNote(result[0]);
358
+ const note = pointToNote(result[0]);
359
+ if (readerAgentId !== void 0 && !canRead(note, readerAgentId)) return null;
360
+ return note;
311
361
  } catch {
312
362
  return null;
313
363
  }
@@ -343,17 +393,48 @@ function makeCrud(collectionName, modeBIsolated = false) {
343
393
  if (!result.points.length) return null;
344
394
  return pointToNote(result.points[0]);
345
395
  },
346
- async updateNoteContent(id, content, embedding, hash) {
396
+ /**
397
+ * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
398
+ * hold the note already should prefer checking `canWrite` themselves; this
399
+ * fetch-then-check path exists for callers that only have an id (the plugin's
400
+ * CRUD hook). Returns false — without writing — when the caller may not write.
401
+ * Omitting `callerAgentId` skips the check, preserving existing behaviour for
402
+ * internal callers that are already scoped to their own notes.
403
+ */
404
+ async updateNoteContent(id, content, embedding, hash, callerAgentId) {
347
405
  await ensureCollection(col);
406
+ let existing = null;
407
+ if (callerAgentId !== void 0) {
408
+ existing = await this.getNote(id);
409
+ if (existing && !canWrite(existing, callerAgentId)) return false;
410
+ }
348
411
  await qdrant("PUT", `/collections/${col}/points/vectors?wait=true`, {
349
412
  points: [{ id, vector: embedding }]
350
413
  });
414
+ const payload = { content, hash };
415
+ if (existing) {
416
+ const history = [
417
+ ...existing.evolution_history ?? [],
418
+ {
419
+ triggeredBy: "",
420
+ triggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
421
+ oldContext: existing.context,
422
+ newContext: existing.context,
423
+ oldTags: existing.tags,
424
+ newTags: existing.tags,
425
+ action: "crud_update",
426
+ oldContent: existing.content
427
+ }
428
+ ];
429
+ payload.evolution_history = JSON.stringify(history);
430
+ }
351
431
  await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
352
- payload: { content, hash },
432
+ payload,
353
433
  points: [id]
354
434
  });
435
+ return true;
355
436
  },
356
- async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0) {
437
+ async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0, subject) {
357
438
  await ensureCollection(col);
358
439
  const result = await qdrant("POST", `/collections/${col}/points/search`, {
359
440
  vector: embedding,
@@ -361,7 +442,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
361
442
  with_payload: true,
362
443
  with_vector: true,
363
444
  score_threshold: scoreThreshold,
364
- filter: scopedAgentFilter(agentId)
445
+ filter: scopedAgentFilter(agentId, subject)
365
446
  });
366
447
  const queryResults = result.map((r) => ({
367
448
  note: pointToNote(r),
@@ -395,14 +476,14 @@ function makeCrud(collectionName, modeBIsolated = false) {
395
476
  }
396
477
  return queryResults;
397
478
  },
398
- async listNotes(agentId) {
479
+ async listNotes(agentId, subject) {
399
480
  await ensureCollection(col);
400
481
  const body = {
401
482
  with_payload: true,
402
483
  with_vector: true,
403
484
  limit: 1e4
404
485
  };
405
- if (agentId) body.filter = scopedAgentFilter(agentId);
486
+ if (agentId) body.filter = scopedAgentFilter(agentId, subject);
406
487
  const result = await qdrant("POST", `/collections/${col}/points/scroll`, body);
407
488
  return result.points.map(pointToNote);
408
489
  },
@@ -412,12 +493,18 @@ function makeCrud(collectionName, modeBIsolated = false) {
412
493
  points: [id]
413
494
  });
414
495
  },
415
- async invalidateNote(id) {
496
+ /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
497
+ async invalidateNote(id, callerAgentId) {
416
498
  await ensureCollection(col);
499
+ if (callerAgentId !== void 0) {
500
+ const existing = await this.getNote(id);
501
+ if (existing && !canWrite(existing, callerAgentId)) return false;
502
+ }
417
503
  await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
418
504
  payload: { is_active: false },
419
505
  points: [id]
420
506
  });
507
+ return true;
421
508
  },
422
509
  async getNotesByDatePrefix(datePrefix, agentId) {
423
510
  await ensureCollection(col);
@@ -463,6 +550,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
463
550
  async replaceLinkReferences(oldId, newId, agentId) {
464
551
  const notes = await this.listNotes(agentId);
465
552
  for (const note of notes) {
553
+ if (!canWrite(note, agentId)) continue;
466
554
  if (note.links.includes(oldId)) {
467
555
  const newLinks = note.links.map((linkId) => linkId === oldId ? newId : linkId);
468
556
  const filteredLinks = newLinks.filter((linkId) => linkId !== note.id);
@@ -476,20 +564,20 @@ function makeCrud(collectionName, modeBIsolated = false) {
476
564
  function createStorageContext(collectionName, modeBIsolated = false) {
477
565
  return makeCrud(collectionName || getCollection(), modeBIsolated);
478
566
  }
479
- async function getNote(id) {
480
- return makeCrud(getCollection()).getNote(id);
567
+ async function getNote(id, readerAgentId) {
568
+ return makeCrud(getCollection()).getNote(id, readerAgentId);
481
569
  }
482
570
  async function updateNote(note) {
483
571
  return makeCrud(getCollection()).updateNote(note);
484
572
  }
485
- async function listNotes(agentId) {
486
- return makeCrud(getCollection()).listNotes(agentId);
573
+ async function listNotes(agentId, subject) {
574
+ return makeCrud(getCollection()).listNotes(agentId, subject);
487
575
  }
488
576
  async function deleteNote(id) {
489
577
  return makeCrud(getCollection()).deleteNote(id);
490
578
  }
491
- async function invalidateNote(id) {
492
- return makeCrud(getCollection()).invalidateNote(id);
579
+ async function invalidateNote(id, callerAgentId) {
580
+ return makeCrud(getCollection()).invalidateNote(id, callerAgentId);
493
581
  }
494
582
  async function patchNotePayload(id, fields) {
495
583
  return makeCrud(getCollection()).patchNotePayload(id, fields);
@@ -498,6 +586,7 @@ var QDRANT_URL, getCollection, VECTOR_DIM, _collectionReady, _collectionReadyMap
498
586
  var init_storage = __esm({
499
587
  "../amem-core/src/storage.ts"() {
500
588
  "use strict";
589
+ init_auth();
501
590
  QDRANT_URL = "http://localhost:6333";
502
591
  getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
503
592
  VECTOR_DIM = 384;
@@ -595,7 +684,39 @@ Classification rules:
595
684
  - NEW: Completely unrelated information, no substantive connection to the old memory
596
685
  Return: {"type": "NEW"}
597
686
 
598
- Return only JSON, no other text.`
687
+ Return only JSON, no other text.`,
688
+ conflictScan: (numberedNotes) => `You are auditing a person's memory store for CONTRADICTIONS.
689
+
690
+ Below are numbered memories. Find pairs that CANNOT both be true of the same person at the same time.
691
+
692
+ ${numberedNotes}
693
+
694
+ What counts as a contradiction:
695
+ - The same attribute holding two incompatible values ("lives in Paris" vs "moved to Berlin")
696
+ - A stated preference or constraint that a later memory violates ("is vegetarian" vs "loved the steak")
697
+ - A fact that a later memory supersedes ("uses MySQL" vs "migrated to PostgreSQL")
698
+
699
+ What does NOT count \u2014 be strict, these are the common false positives:
700
+ - Additive facts. Two things can both be true ("has a dog named Buddy" + "adopted a second dog, Scout" is NOT a contradiction)
701
+ - Change over time that both memories already acknowledge
702
+ - Merely similar or related topics
703
+ - Different contexts (likes coffee at work, tea at home)
704
+
705
+ For each contradicting pair, also say which one is SUPERSEDED \u2014 the one that is
706
+ no longer true. Judge this from the WORDING, not from any assumed order: phrases
707
+ like "used to", "back in 2019", "moved last month", "switched to" tell you which
708
+ statement describes the past. The memories are NOT listed in chronological order,
709
+ and the number does not imply age.
710
+
711
+ If you cannot tell which one is superseded, set it to null. That is a normal and
712
+ useful answer \u2014 say null rather than guessing, because a wrong guess retires a
713
+ memory that is still true.
714
+
715
+ Return ONLY a JSON array. Empty array if nothing genuinely contradicts:
716
+ [{"a": 0, "b": 3, "superseded": 0, "reason": "one short sentence naming the incompatible attribute"}]
717
+
718
+ "superseded" must be either the value of "a", the value of "b", or null.
719
+ Use the numbers shown. Report a pair once. Prefer returning nothing over guessing.`
599
720
  };
600
721
  zh = {
601
722
  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
@@ -680,7 +801,36 @@ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
680
801
  - 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
802
  \u8FD4\u56DE\uFF1A{"type": "NEW"}
682
803
 
683
- \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`
804
+ \u53EA\u8FD4\u56DE JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002`,
805
+ 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
806
+
807
+ \u4E0B\u9762\u662F\u7F16\u53F7\u7684\u8BB0\u5FC6\u3002\u627E\u51FA\u90A3\u4E9B**\u4E0D\u53EF\u80FD\u540C\u65F6\u4E3A\u771F**\u7684\u914D\u5BF9\u3002
808
+
809
+ ${numberedNotes}
810
+
811
+ \u7B97\u77DB\u76FE\u7684\u60C5\u51B5\uFF1A
812
+ - \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
813
+ - \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
814
+ - \u540E\u6765\u7684\u4E8B\u5B9E\u53D6\u4EE3\u4E86\u5148\u524D\u7684\uFF08\u300C\u7528 MySQL\u300Dvs\u300C\u5DF2\u8FC1\u79FB\u5230 PostgreSQL\u300D\uFF09
815
+
816
+ **\u4E0D\u7B97**\u77DB\u76FE \u2014\u2014 \u8BF7\u4E25\u683C\uFF0C\u4EE5\u4E0B\u662F\u6700\u5E38\u89C1\u7684\u8BEF\u5224\uFF1A
817
+ - \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
818
+ - \u4E24\u6761\u8BB0\u5FC6\u672C\u8EAB\u5DF2\u7ECF\u4F53\u73B0\u4E86\u968F\u65F6\u95F4\u7684\u53D8\u5316
819
+ - \u53EA\u662F\u4E3B\u9898\u76F8\u4F3C\u6216\u76F8\u5173
820
+ - \u573A\u666F\u4E0D\u540C\uFF08\u5728\u516C\u53F8\u559D\u5496\u5561\uFF0C\u5728\u5BB6\u559D\u8336\uFF09
821
+
822
+ \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
823
+ \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
824
+ \u8FD9\u4E9B\u8BB0\u5FC6**\u4E0D\u662F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\u7684**\uFF0C\u7F16\u53F7\u4E5F\u4E0D\u4EE3\u8868\u65B0\u65E7\u3002
825
+
826
+ \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
827
+ \u56E0\u4E3A\u731C\u9519\u4F1A\u8BA9\u4E00\u6761**\u4ECD\u7136\u4E3A\u771F**\u7684\u8BB0\u5FC6\u88AB\u505C\u7528\u3002
828
+
829
+ \u53EA\u8FD4\u56DE JSON \u6570\u7EC4\u3002\u6CA1\u6709\u771F\u6B63\u77DB\u76FE\u5C31\u8FD4\u56DE\u7A7A\u6570\u7EC4\uFF1A
830
+ [{"a": 0, "b": 3, "superseded": 0, "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u662F\u54EA\u4E2A\u5C5E\u6027\u4E92\u65A5"}]
831
+
832
+ "superseded" \u53EA\u80FD\u662F "a" \u7684\u503C\u3001"b" \u7684\u503C\uFF0C\u6216 null\u3002
833
+ \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**`
684
834
  };
685
835
  templates = { en, zh };
686
836
  t = templates[LOCALE];
@@ -688,35 +838,92 @@ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
688
838
  });
689
839
 
690
840
  // ../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
- });
841
+ function configureLlm(cfg) {
842
+ _override = { ...cfg };
843
+ _anthropicClients.clear();
844
+ _openaiClients.clear();
845
+ }
846
+ function warnOnce(key, message) {
847
+ if (_warned.has(key)) return;
848
+ _warned.add(key);
849
+ console.error(message);
850
+ }
851
+ function resolveProvider(role = "fast") {
852
+ const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
853
+ const p = (raw || process.env.AMEM_LLM_PROVIDER || _override.provider || "anthropic").trim().toLowerCase();
854
+ if (p !== "anthropic" && p !== "openai") {
855
+ warnOnce(`provider:${p}`, `[amem] unknown LLM provider "${p}"; falling back to anthropic`);
856
+ }
857
+ return p;
696
858
  }
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
- });
859
+ function resolveModel(role = "fast") {
860
+ const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_MODEL || _override.strong?.model || void 0 : void 0;
861
+ return strong || process.env.AMEM_LLM_MODEL || _override.model || (resolveProvider(role) === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-6");
862
+ }
863
+ function resolveBaseURL(role = "fast") {
864
+ const strong = role === "strong" ? process.env.AMEM_LLM_STRONG_BASE_URL || _override.strong?.baseURL || void 0 : void 0;
865
+ return strong || process.env.AMEM_LLM_BASE_URL || _override.baseURL || void 0;
866
+ }
867
+ function resolveCrudRole() {
868
+ const raw = (process.env.AMEM_LLM_CRUD_ROLE || _override.crudRole || "fast").trim().toLowerCase();
869
+ if (raw === "strong") return "strong";
870
+ if (raw !== "fast") {
871
+ warnOnce(`crudRole:${raw}`, `[amem] unknown AMEM_LLM_CRUD_ROLE "${raw}"; using fast`);
872
+ }
873
+ return "fast";
874
+ }
875
+ function resolveTimeoutMs() {
876
+ const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
877
+ if (Number.isFinite(envVal) && envVal > 0) return envVal;
878
+ if (_override.timeoutMs && _override.timeoutMs > 0) return _override.timeoutMs;
879
+ return DEFAULT_TIMEOUT_MS;
880
+ }
881
+ function anthropic(baseURL) {
882
+ const key = baseURL ?? "";
883
+ let client = _anthropicClients.get(key);
884
+ if (!client) {
885
+ client = new import_sdk.default({
886
+ ...process.env.AMEM_LLM_API_KEY && { apiKey: process.env.AMEM_LLM_API_KEY },
887
+ ...baseURL && { baseURL },
888
+ timeout: resolveTimeoutMs()
889
+ });
890
+ _anthropicClients.set(key, client);
891
+ }
892
+ return client;
893
+ }
894
+ function openai(baseURL) {
895
+ const key = baseURL ?? "";
896
+ let client = _openaiClients.get(key);
897
+ if (!client) {
898
+ client = new import_openai.default({
899
+ // AMEM_LLM_API_KEY first (engine convention), then the SDK's own
900
+ // OPENAI_API_KEY (the standard) — passing an explicit key blocks the SDK's
901
+ // env fallback, so read it here. Placeholder last, so keyless local servers
902
+ // (Ollama, vLLM) still work.
903
+ apiKey: process.env.AMEM_LLM_API_KEY || process.env.OPENAI_API_KEY || "sk-no-key-required",
904
+ ...baseURL && { baseURL },
905
+ timeout: resolveTimeoutMs()
906
+ });
907
+ _openaiClients.set(key, client);
908
+ }
909
+ return client;
706
910
  }
707
- async function llmCall(prompt, maxTokens = 500) {
708
- const isThinking = MODEL.includes("gemini") || MODEL.includes("pro-agent");
911
+ async function llmCall(prompt, maxTokens = 500, role = "fast") {
912
+ const provider = resolveProvider(role);
913
+ const model = resolveModel(role);
914
+ const baseURL = resolveBaseURL(role);
915
+ const isThinking = model.includes("gemini") || model.includes("pro-agent");
709
916
  const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
710
917
  try {
711
- return PROVIDER === "openai" ? await openaiCall(prompt, effectiveMaxTokens) : await anthropicCall(prompt, effectiveMaxTokens);
918
+ return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
712
919
  } catch (e) {
713
920
  console.error(`[amem] LLM call failed: ${e.message}`);
714
921
  return null;
715
922
  }
716
923
  }
717
- async function anthropicCall(prompt, maxTokens) {
718
- const resp = await anthropic().messages.create({
719
- model: MODEL,
924
+ async function anthropicCall(prompt, model, maxTokens, baseURL) {
925
+ const resp = await anthropic(baseURL).messages.create({
926
+ model,
720
927
  max_tokens: maxTokens,
721
928
  messages: [{ role: "user", content: prompt }]
722
929
  });
@@ -725,17 +932,20 @@ async function anthropicCall(prompt, maxTokens) {
725
932
  }
726
933
  return null;
727
934
  }
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,
935
+ async function openaiCall(prompt, model, maxTokens, baseURL) {
936
+ const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
937
+ const resp = await openai(baseURL).chat.completions.create({
938
+ model,
732
939
  ...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
733
940
  messages: [{ role: "user", content: prompt }]
734
941
  });
735
942
  return resp.choices[0]?.message?.content?.trim() ?? null;
736
943
  }
944
+ function stripReasoning(raw) {
945
+ return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<\|(?:eot_id|im_start|im_end|begin_of_text|end_of_text|endoftext)\|>/g, "").trim();
946
+ }
737
947
  function stripFences(raw) {
738
- raw = raw.trim();
948
+ raw = stripReasoning(raw);
739
949
  if (raw.startsWith("```")) {
740
950
  const lines = raw.split("\n");
741
951
  lines.shift();
@@ -750,6 +960,16 @@ function stripFences(raw) {
750
960
  }
751
961
  return raw;
752
962
  }
963
+ function parseJsonLoose(raw) {
964
+ const cleaned = stripFences(raw);
965
+ try {
966
+ return JSON.parse(cleaned);
967
+ } catch (e) {
968
+ const m = cleaned.match(/\{[\s\S]*\}/);
969
+ if (m) return JSON.parse(m[0]);
970
+ throw e;
971
+ }
972
+ }
753
973
  async function llmConstructNote(content) {
754
974
  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
975
  {
@@ -797,7 +1017,7 @@ Text: ${content}`;
797
1017
  confidence: "medium"
798
1018
  };
799
1019
  try {
800
- const data = JSON.parse(stripFences(raw));
1020
+ const data = parseJsonLoose(raw);
801
1021
  const rawCategory = typeof data.category === "string" ? data.category : "General";
802
1022
  const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
803
1023
  const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
@@ -840,9 +1060,9 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
840
1060
  const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
841
1061
  const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
842
1062
  try {
843
- const raw = await llmCall(prompt, 400);
1063
+ const raw = await llmCall(prompt, 400, resolveCrudRole());
844
1064
  if (!raw) return [];
845
- const match = raw.match(/\[.*\]/s);
1065
+ const match = stripReasoning(raw).match(/\[.*\]/s);
846
1066
  if (!match) return [];
847
1067
  const parsed = JSON.parse(match[0]);
848
1068
  if (!Array.isArray(parsed)) return [];
@@ -870,10 +1090,10 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
870
1090
  }
871
1091
  async function llmShouldMerge(contentA, contentB) {
872
1092
  const prompt = t.shouldMerge(contentA, contentB);
873
- const raw = await llmCall(prompt, 300);
1093
+ const raw = await llmCall(prompt, 300, "strong");
874
1094
  if (!raw) return { shouldMerge: false };
875
1095
  try {
876
- const data = JSON.parse(stripFences(raw));
1096
+ const data = parseJsonLoose(raw);
877
1097
  if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
878
1098
  if (data.shouldMerge && typeof data.merged === "string") {
879
1099
  return { shouldMerge: true, merged: data.merged };
@@ -886,10 +1106,10 @@ async function llmShouldMerge(contentA, contentB) {
886
1106
  }
887
1107
  async function llmEvolutionJudge(oldContent, newContent) {
888
1108
  const prompt = t.evolutionJudge(oldContent, newContent);
889
- const raw = await llmCall(prompt, 300);
1109
+ const raw = await llmCall(prompt, 300, "strong");
890
1110
  if (!raw) return { type: "NEW" };
891
1111
  try {
892
- const data = JSON.parse(stripFences(raw));
1112
+ const data = parseJsonLoose(raw);
893
1113
  const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
894
1114
  return {
895
1115
  type,
@@ -926,7 +1146,7 @@ ${linkedStr}`;
926
1146
  const raw = await llmCall(prompt, 500);
927
1147
  if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
928
1148
  try {
929
- const data = JSON.parse(stripFences(raw));
1149
+ const data = parseJsonLoose(raw);
930
1150
  return {
931
1151
  tags: Array.isArray(data.tags) ? data.tags : null,
932
1152
  context: typeof data.context === "string" ? data.context : null,
@@ -939,20 +1159,56 @@ ${linkedStr}`;
939
1159
  return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
940
1160
  }
941
1161
  }
942
- var import_sdk, import_openai, PROVIDER, MODEL, _anthropic, _openai, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
1162
+ async function llmConflictScan(contents) {
1163
+ if (contents.length < 2) return [];
1164
+ const numbered = contents.map((c, i) => `[${i}] ${c}`).join("\n");
1165
+ try {
1166
+ const raw = await llmCall(t.conflictScan(numbered), 600, "strong");
1167
+ if (!raw) return [];
1168
+ const cleaned = stripReasoning(raw);
1169
+ const match = cleaned.match(/\[[\s\S]*\]/);
1170
+ if (!match) return [];
1171
+ const parsed = JSON.parse(match[0]);
1172
+ if (!Array.isArray(parsed)) return [];
1173
+ const pairs = [];
1174
+ const seen = /* @__PURE__ */ new Set();
1175
+ for (const item of parsed) {
1176
+ if (!item || typeof item !== "object") continue;
1177
+ const { a, b } = item;
1178
+ if (typeof a !== "number" || typeof b !== "number") continue;
1179
+ if (!Number.isInteger(a) || !Number.isInteger(b)) continue;
1180
+ if (a < 0 || b < 0 || a >= contents.length || b >= contents.length) continue;
1181
+ if (a === b) continue;
1182
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
1183
+ if (seen.has(key)) continue;
1184
+ seen.add(key);
1185
+ const rawSup = item.superseded;
1186
+ const supersededIndex = rawSup === a || rawSup === b ? rawSup : null;
1187
+ pairs.push({
1188
+ a,
1189
+ b,
1190
+ reason: typeof item.reason === "string" ? item.reason : "",
1191
+ supersededIndex
1192
+ });
1193
+ }
1194
+ return pairs;
1195
+ } catch (e) {
1196
+ console.error(`[amem] llmConflictScan failed: ${e.message}`);
1197
+ return [];
1198
+ }
1199
+ }
1200
+ var import_sdk, import_openai, _override, _warned, DEFAULT_TIMEOUT_MS, _anthropicClients, _openaiClients, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
943
1201
  var init_llm = __esm({
944
1202
  "../amem-core/src/llm.ts"() {
945
1203
  "use strict";
946
1204
  import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
947
1205
  import_openai = __toESM(require("openai"), 1);
948
1206
  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;
1207
+ _override = {};
1208
+ _warned = /* @__PURE__ */ new Set();
1209
+ DEFAULT_TIMEOUT_MS = 3e4;
1210
+ _anthropicClients = /* @__PURE__ */ new Map();
1211
+ _openaiClients = /* @__PURE__ */ new Map();
956
1212
  VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
957
1213
  VALID_CATEGORIES = /* @__PURE__ */ new Set([
958
1214
  "Technical",
@@ -1072,6 +1328,7 @@ function defaultCtx() {
1072
1328
  }
1073
1329
  async function addMemory(content, agentId = "main", opts) {
1074
1330
  const scope = opts?.scope ?? "private";
1331
+ const subjects = opts?.subjects ?? [];
1075
1332
  const ctx = opts?.storageCtx ?? defaultCtx();
1076
1333
  const quality = checkQuality(content);
1077
1334
  if (!quality.ok) {
@@ -1096,9 +1353,14 @@ async function addMemory(content, agentId = "main", opts) {
1096
1353
  const embedding = await encode(fieldsText);
1097
1354
  const topMatch = await ctx.queryByEmbedding(embedding, 1, agentId, 0);
1098
1355
  if (topMatch.length > 0 && topMatch[0].score >= 0.85) {
1099
- console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
1100
- await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
1101
- return topMatch[0].note.id;
1356
+ if (canWrite(topMatch[0].note, agentId)) {
1357
+ console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
1358
+ await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
1359
+ return topMatch[0].note.id;
1360
+ }
1361
+ console.log(
1362
+ `[add] dedup: high-sim match ${topMatch[0].note.id.slice(0, 8)} is not writable by ${logSafe(agentId)} \u2014 inserting a new note instead`
1363
+ );
1102
1364
  }
1103
1365
  const pendingMerge = topMatch.length > 0 && topMatch[0].score >= 0.72 && topMatch[0].score < 0.85;
1104
1366
  if (pendingMerge) {
@@ -1108,6 +1370,7 @@ async function addMemory(content, agentId = "main", opts) {
1108
1370
  const writers = [agentId];
1109
1371
  const note = {
1110
1372
  id: (0, import_uuid.v4)(),
1373
+ subjects,
1111
1374
  content,
1112
1375
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1113
1376
  keywords,
@@ -1166,6 +1429,10 @@ async function addMemory(content, agentId = "main", opts) {
1166
1429
  for (const lid of linkedIds) {
1167
1430
  const linked = await ctx.getNote(lid);
1168
1431
  if (linked && !linked.links.includes(note.id)) {
1432
+ if (!canWrite(linked, agentId)) {
1433
+ console.log(`[link] back-link into ${lid.slice(0, 8)} skipped \u2014 not writable by ${logSafe(agentId)}`);
1434
+ continue;
1435
+ }
1169
1436
  linked.links.push(note.id);
1170
1437
  await ctx.updateNote(linked);
1171
1438
  }
@@ -1175,10 +1442,14 @@ async function addMemory(content, agentId = "main", opts) {
1175
1442
  for (const lid of linkedIds.slice(0, 3)) {
1176
1443
  const linked = await ctx.getNote(lid);
1177
1444
  if (!linked) continue;
1445
+ if (!canWrite(linked, agentId)) {
1446
+ console.log(` [evo] skipping ${lid.slice(0, 8)} \u2014 not writable by ${logSafe(agentId)}`);
1447
+ continue;
1448
+ }
1178
1449
  const linkedNotes = [];
1179
1450
  for (const llid of linked.links.slice(0, 5)) {
1180
1451
  if (llid === note.id) continue;
1181
- const ln = await ctx.getNote(llid);
1452
+ const ln = await ctx.getNote(llid, agentId);
1182
1453
  if (ln) linkedNotes.push({ id: ln.id, content: ln.content });
1183
1454
  }
1184
1455
  linkedNotes.push({ id: note.id, content });
@@ -1215,10 +1486,14 @@ async function addMemory(content, agentId = "main", opts) {
1215
1486
  note.links.push(targetId);
1216
1487
  noteChanged = true;
1217
1488
  }
1218
- const target = await ctx.getNote(targetId);
1489
+ const target = await ctx.getNote(targetId, agentId);
1219
1490
  if (target && !target.links.includes(note.id)) {
1220
- target.links.push(note.id);
1221
- await ctx.updateNote(target);
1491
+ if (canWrite(target, agentId)) {
1492
+ target.links.push(note.id);
1493
+ await ctx.updateNote(target);
1494
+ } else {
1495
+ console.log(` [evo] strengthen back-link into ${targetId.slice(0, 8)} skipped \u2014 not writable`);
1496
+ }
1222
1497
  }
1223
1498
  }
1224
1499
  if (tagsToUpdate.length > 0) {
@@ -1267,6 +1542,7 @@ async function addMemory(content, agentId = "main", opts) {
1267
1542
  }
1268
1543
  async function addEpisodic(content, agentId = "main", opts) {
1269
1544
  const scope = opts?.scope ?? "private";
1545
+ const subjects = opts?.subjects ?? [];
1270
1546
  const ctx = opts?.storageCtx ?? defaultCtx();
1271
1547
  const quality = checkQuality(content);
1272
1548
  if (!quality.ok) {
@@ -1276,6 +1552,7 @@ async function addEpisodic(content, agentId = "main", opts) {
1276
1552
  const now = (/* @__PURE__ */ new Date()).toISOString();
1277
1553
  const note = {
1278
1554
  id: (0, import_uuid.v4)(),
1555
+ subjects,
1279
1556
  content,
1280
1557
  timestamp: now,
1281
1558
  keywords: [],
@@ -1305,14 +1582,15 @@ async function addEpisodic(content, agentId = "main", opts) {
1305
1582
  }
1306
1583
  async function searchMemory(query, topK = 5, agentId = "main", opts) {
1307
1584
  const useBfs = opts?.useBfs !== false;
1585
+ const subject = opts?.subject;
1308
1586
  const bfsSimThreshold = opts?.bfsSimThreshold ?? 0.25;
1309
1587
  const ctx = opts?.storageCtx ?? defaultCtx();
1310
1588
  const total = await ctx.countNotes(agentId);
1311
1589
  if (total === 0) return [];
1312
1590
  const queryEmbedding = await encode(query);
1313
1591
  const n = Math.min(Math.max(topK * 4, 20), total);
1314
- const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0);
1315
- const allNotes = await ctx.listNotes(agentId);
1592
+ const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0, subject);
1593
+ const allNotes = await ctx.listNotes(agentId, subject);
1316
1594
  const bm25State = buildBM25(allNotes);
1317
1595
  const queryTokens = simpleTokenize(query);
1318
1596
  const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n);
@@ -1623,7 +1901,66 @@ async function consolidateMemories(agentId, logger, storageCtx) {
1623
1901
  log.info(`[Consolidation] Completed consolidation run. Merged ${mergedCount} pairs.`);
1624
1902
  return mergedCount;
1625
1903
  }
1626
- var import_uuid, import_crypto, fs2, path3, import_jieba, _jieba, EPHEMERAL_SIGNALS;
1904
+ function resolveConflictMode(override) {
1905
+ const raw = (process.env.AMEM_CONFLICT_MODE || override || "review").trim().toLowerCase();
1906
+ return raw === "auto" ? "auto" : "review";
1907
+ }
1908
+ async function conflictSweep(agentId, opts) {
1909
+ const ctx = opts?.storageCtx ?? defaultCtx();
1910
+ const mode = resolveConflictMode(opts?.mode);
1911
+ const log = opts?.logger?.info ?? ((m) => console.log(m));
1912
+ const raw = await ctx.listNotes(agentId);
1913
+ const notes = raw.filter((n) => n.agent_id !== "shared" && n.note_type !== "knowledge" && n.is_active !== false);
1914
+ const groups = /* @__PURE__ */ new Map();
1915
+ for (const n of notes) {
1916
+ const c = n.category || "General";
1917
+ if (!groups.has(c)) groups.set(c, []);
1918
+ groups.get(c).push(n);
1919
+ }
1920
+ let pairsFound = 0;
1921
+ let retired = 0;
1922
+ for (const [category, groupNotes] of groups.entries()) {
1923
+ for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {
1924
+ const batch = groupNotes.slice(start, start + CONFLICT_BATCH_SIZE);
1925
+ if (batch.length < 2) continue;
1926
+ const pairs = await llmConflictScan(batch.map((n) => n.content));
1927
+ for (const { a, b, reason, supersededIndex } of pairs) {
1928
+ const noteA = batch[a];
1929
+ const noteB = batch[b];
1930
+ if (!noteA || !noteB) continue;
1931
+ pairsFound++;
1932
+ await ctx.patchNotePayload(noteA.id, {
1933
+ conflict: true,
1934
+ evolution_type: "CONFLICT",
1935
+ conflicts_with: Array.from(/* @__PURE__ */ new Set([...noteA.conflicts_with ?? [], noteB.id])),
1936
+ conflict_reason: reason
1937
+ });
1938
+ await ctx.patchNotePayload(noteB.id, {
1939
+ conflict: true,
1940
+ evolution_type: "CONFLICT",
1941
+ conflicts_with: Array.from(/* @__PURE__ */ new Set([...noteB.conflicts_with ?? [], noteA.id])),
1942
+ conflict_reason: reason
1943
+ });
1944
+ log(`[conflict] ${category}: ${noteA.id.slice(0, 8)} \u2194 ${noteB.id.slice(0, 8)} \u2014 ${reason}`);
1945
+ if (mode === "auto") {
1946
+ const superseded = supersededIndex === a ? noteA : supersededIndex === b ? noteB : null;
1947
+ if (!superseded) {
1948
+ log(`[conflict] auto: no superseded side identified \u2014 marked only, nothing retired`);
1949
+ } else {
1950
+ const ok = await ctx.invalidateNote(superseded.id, agentId);
1951
+ if (ok) {
1952
+ retired++;
1953
+ log(`[conflict] auto-retired the superseded note ${superseded.id.slice(0, 8)}`);
1954
+ }
1955
+ }
1956
+ }
1957
+ }
1958
+ }
1959
+ }
1960
+ log(`[conflict] scanned ${notes.length} notes, found ${pairsFound} pair(s), retired ${retired}`);
1961
+ return { scanned: notes.length, pairsFound, retired };
1962
+ }
1963
+ var import_uuid, import_crypto, fs2, path3, import_jieba, logSafe, _jieba, EPHEMERAL_SIGNALS, CONFLICT_BATCH_SIZE;
1627
1964
  var init_memory = __esm({
1628
1965
  "../amem-core/src/memory.ts"() {
1629
1966
  "use strict";
@@ -1633,12 +1970,15 @@ var init_memory = __esm({
1633
1970
  path3 = __toESM(require("path"), 1);
1634
1971
  init_embedding();
1635
1972
  init_storage();
1973
+ init_auth();
1636
1974
  init_llm();
1637
1975
  init_evo_counter();
1638
1976
  init_config();
1639
1977
  import_jieba = require("@node-rs/jieba");
1978
+ logSafe = (id) => id.replace(/[\r\n]/g, "");
1640
1979
  _jieba = null;
1641
1980
  EPHEMERAL_SIGNALS = ["\u5F85\u8DD1", "\u7B49\u786E\u8BA4", "\u6628\u65E5", "\u660E\u5929\u5B8C\u6210"];
1981
+ CONFLICT_BATCH_SIZE = 25;
1642
1982
  }
1643
1983
  });
1644
1984
 
@@ -1649,6 +1989,7 @@ async function scanLowQuality(agentId) {
1649
1989
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
1650
1990
  const results = [];
1651
1991
  for (const note of notes) {
1992
+ if (!canWrite(note, agentId)) continue;
1652
1993
  const reasons = [];
1653
1994
  if (note.content.trim().length < 10) {
1654
1995
  reasons.push("too_short");
@@ -1741,6 +2082,48 @@ async function generateReviewBatch(agentId, outputPath) {
1741
2082
  if (items.length === 0) {
1742
2083
  lines.push(LOCALE2 === "zh" ? "\u2705 \u6CA1\u6709\u53D1\u73B0\u4F4E\u8D28\u91CF\u6761\u76EE\u3002" : "\u2705 No low-quality items found.");
1743
2084
  }
2085
+ const byId = new Map(items.map((it) => [it.note.id, it.note]));
2086
+ const renderedPairs = /* @__PURE__ */ new Set();
2087
+ const pairLines = [];
2088
+ for (const { note } of items) {
2089
+ for (const otherId of note.conflicts_with ?? []) {
2090
+ const other = byId.get(otherId);
2091
+ if (!other) continue;
2092
+ const key = note.id < otherId ? `${note.id}:${otherId}` : `${otherId}:${note.id}`;
2093
+ if (renderedPairs.has(key)) continue;
2094
+ renderedPairs.add(key);
2095
+ const [newer, older] = Date.parse(note.timestamp) >= Date.parse(other.timestamp) ? [note, other] : [other, note];
2096
+ const zh2 = LOCALE2 === "zh";
2097
+ pairLines.push(`### \u{1F7E0} ${zh2 ? "\u51B2\u7A81" : "CONFLICT"} | ${newer.category || "General"}`);
2098
+ if (newer.conflict_reason) {
2099
+ pairLines.push(`**${zh2 ? "\u5224\u5B9A\u7406\u7531" : "Why"}\uFF1A** ${newer.conflict_reason}`);
2100
+ pairLines.push("");
2101
+ }
2102
+ pairLines.push(`| | ${zh2 ? "\u65F6\u95F4" : "When"} | ${zh2 ? "\u5185\u5BB9" : "Content"} |`);
2103
+ pairLines.push("| :-- | :-- | :-- |");
2104
+ pairLines.push(`| **A** | ${newer.timestamp.slice(0, 10)} | ${newer.content.replace(/\n/g, " ")} |`);
2105
+ pairLines.push(`| **B** | ${older.timestamp.slice(0, 10)} | ${older.content.replace(/\n/g, " ")} |`);
2106
+ pairLines.push("");
2107
+ pairLines.push(`\`A: ${newer.id}\``);
2108
+ pairLines.push(`\`B: ${older.id}\``);
2109
+ pairLines.push("");
2110
+ pairLines.push(
2111
+ 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)`
2112
+ );
2113
+ pairLines.push(zh2 ? `- [ ] \u21A9\uFE0F B \u662F\u5F53\u524D\u72B6\u6001\uFF0C\u505C\u7528 A` : `- [ ] \u21A9\uFE0F B is current \u2014 retire A`);
2114
+ pairLines.push(zh2 ? `- [ ] \u{1F91D} \u4E24\u8005\u90FD\u6210\u7ACB\uFF08\u8BEF\u5224\uFF09` : `- [ ] \u{1F91D} Both hold \u2014 not a contradiction`);
2115
+ pairLines.push("");
2116
+ pairLines.push("---");
2117
+ pairLines.push("");
2118
+ }
2119
+ }
2120
+ if (pairLines.length > 0) {
2121
+ 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)");
2122
+ lines.push("");
2123
+ lines.push(...pairLines);
2124
+ lines.push(LOCALE2 === "zh" ? "## \u5176\u4F59\u6761\u76EE" : "## Other items");
2125
+ lines.push("");
2126
+ }
1744
2127
  for (let i = 0; i < items.length; i++) {
1745
2128
  const { note, reasons } = items[i];
1746
2129
  const badge = severityBadge(reasons);
@@ -1783,18 +2166,45 @@ var init_quality = __esm({
1783
2166
  fs3 = __toESM(require("fs"), 1);
1784
2167
  path4 = __toESM(require("path"), 1);
1785
2168
  init_storage();
2169
+ init_auth();
1786
2170
  LOCALE2 = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
1787
2171
  DEFAULT_OUTPUT_DIR = process.env.AMEM_REVIEW_DIR || process.cwd();
1788
2172
  }
1789
2173
  });
1790
2174
 
2175
+ // ../amem-core/src/crud-guard.ts
2176
+ function resolveCrudUpdateMinSim(override) {
2177
+ const envVal = Number(process.env.AMEM_CRUD_UPDATE_MIN_SIM);
2178
+ if (Number.isFinite(envVal) && envVal >= 0) return envVal;
2179
+ if (override !== void 0 && Number.isFinite(override) && override >= 0) return override;
2180
+ return DEFAULT_CRUD_UPDATE_MIN_SIM;
2181
+ }
2182
+ function isPlausibleUpdateTarget(newEmbedding, targetEmbedding, minSimilarity) {
2183
+ if (!newEmbedding?.length || !targetEmbedding?.length) return false;
2184
+ if (newEmbedding.length !== targetEmbedding.length) return false;
2185
+ return cosineSimilarity(newEmbedding, targetEmbedding) >= resolveCrudUpdateMinSim(minSimilarity);
2186
+ }
2187
+ var DEFAULT_CRUD_UPDATE_MIN_SIM;
2188
+ var init_crud_guard = __esm({
2189
+ "../amem-core/src/crud-guard.ts"() {
2190
+ "use strict";
2191
+ init_embedding();
2192
+ DEFAULT_CRUD_UPDATE_MIN_SIM = 0.35;
2193
+ }
2194
+ });
2195
+
1791
2196
  // ../amem-core/src/index.ts
1792
2197
  var src_exports = {};
1793
2198
  __export(src_exports, {
2199
+ DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
1794
2200
  addEpisodic: () => addEpisodic,
1795
2201
  addMemory: () => addMemory,
2202
+ canRead: () => canRead,
2203
+ canWrite: () => canWrite,
1796
2204
  checkQuality: () => checkQuality,
1797
2205
  configure: () => configure,
2206
+ configureLlm: () => configureLlm,
2207
+ conflictSweep: () => conflictSweep,
1798
2208
  consolidateMemories: () => consolidateMemories,
1799
2209
  createStorageContext: () => createStorageContext,
1800
2210
  deleteNote: () => deleteNote,
@@ -1804,6 +2214,7 @@ __export(src_exports, {
1804
2214
  getNote: () => getNote,
1805
2215
  invalidateNote: () => invalidateNote,
1806
2216
  isModelLoaded: () => isModelLoaded,
2217
+ isPlausibleUpdateTarget: () => isPlausibleUpdateTarget,
1807
2218
  listMemories: () => listMemories,
1808
2219
  listNotes: () => listNotes,
1809
2220
  llmCrudDecision: () => llmCrudDecision,
@@ -1811,6 +2222,7 @@ __export(src_exports, {
1811
2222
  mergeSimilarNotes: () => mergeSimilarNotes,
1812
2223
  patchNotePayload: () => patchNotePayload,
1813
2224
  pingQdrant: () => pingQdrant,
2225
+ resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
1814
2226
  scanLowQuality: () => scanLowQuality,
1815
2227
  searchMemory: () => searchMemory,
1816
2228
  updateNote: () => updateNote
@@ -1823,6 +2235,9 @@ var init_src = __esm({
1823
2235
  init_memory();
1824
2236
  init_quality();
1825
2237
  init_storage();
2238
+ init_auth();
2239
+ init_memory();
2240
+ init_crud_guard();
1826
2241
  init_llm();
1827
2242
  }
1828
2243
  });
@@ -1838,12 +2253,9 @@ __export(index_exports, {
1838
2253
  ensureCollection: () => ensureCollection,
1839
2254
  generateReviewBatch: () => generateReviewBatch,
1840
2255
  getNote: () => getNote,
1841
- hookLiveness: () => hookLiveness,
1842
- hookNeverFiredWarning: () => hookNeverFiredWarning,
1843
2256
  invalidateNote: () => invalidateNote,
1844
2257
  listMemories: () => listMemories,
1845
2258
  listNotes: () => listNotes,
1846
- markHookFired: () => markHookFired,
1847
2259
  mergeSimilarNotes: () => mergeSimilarNotes,
1848
2260
  patchNotePayload: () => patchNotePayload,
1849
2261
  register: () => register,
@@ -1858,29 +2270,12 @@ var import_plugin_entry = require("openclaw/plugin-sdk/plugin-entry");
1858
2270
  init_src();
1859
2271
  var import_crypto2 = require("crypto");
1860
2272
 
1861
- // src/hook-liveness.ts
1862
- var HOOK_STATE_KEY = /* @__PURE__ */ Symbol.for("openclaw-amem.hookLiveness");
1863
- var HOOK_WARN_DELAY_MS = 10 * 60 * 1e3;
1864
- var HOOK_WARNING_TEXT = "\n\n\u26A0\uFE0F Warning: agent_end hook has never fired. Automatic memory write-back may be disabled. Set plugins.entries.openclaw-amem.hooks.allowConversationAccess=true in openclaw.json.";
1865
- function hookLiveness(now = Date.now()) {
1866
- const g = globalThis;
1867
- let s = g[HOOK_STATE_KEY];
1868
- if (!s) {
1869
- s = { everFired: false, lastFiredAt: 0, firstSeenAt: now };
1870
- g[HOOK_STATE_KEY] = s;
1871
- }
1872
- return s;
1873
- }
1874
- function markHookFired(now = Date.now()) {
1875
- const s = hookLiveness(now);
1876
- s.everFired = true;
1877
- s.lastFiredAt = now;
1878
- }
1879
- function hookNeverFiredWarning(now = Date.now()) {
1880
- const s = hookLiveness(now);
1881
- if (s.everFired) return "";
1882
- if (now - s.firstSeenAt <= HOOK_WARN_DELAY_MS) return "";
1883
- return HOOK_WARNING_TEXT;
2273
+ // src/conv-access.ts
2274
+ var BLOCKED_WARNING_LOG = "openclaw-amem: \u26A0\uFE0F automatic memory write-back is OFF \u2014 the agent_end hook is blocked by OpenClaw's security policy, so conversations are NOT being saved to long-term memory. Enable it: set plugins.entries.openclaw-amem.hooks.allowConversationAccess=true in openclaw.json, then restart the gateway.";
2275
+ var BLOCKED_WARNING_SUFFIX = "\n\n\u26A0\uFE0F NOTE TO ASSISTANT \u2014 please tell the user: openclaw-amem automatic memory write-back is currently DISABLED, so nothing from this conversation is being saved to long-term memory. To fix it, add `plugins.entries.openclaw-amem.hooks.allowConversationAccess: true` to openclaw.json and restart the gateway.";
2276
+ function isConvAccessBlocked(config, pluginId) {
2277
+ const entry = config?.plugins?.entries?.[pluginId];
2278
+ return entry !== void 0 && entry.hooks?.allowConversationAccess !== true;
1884
2279
  }
1885
2280
 
1886
2281
  // src/scope.ts
@@ -1910,10 +2305,31 @@ init_src();
1910
2305
  var _config = {};
1911
2306
  function register(api) {
1912
2307
  const logger = api.logger;
1913
- hookLiveness();
1914
2308
  _config = api.pluginConfig || {};
1915
2309
  const pluginConfig = _config;
2310
+ const pluginId = api.id ?? "openclaw-amem";
2311
+ const convBlocked = isConvAccessBlocked(api.config, pluginId);
2312
+ if (convBlocked) logger.warn(BLOCKED_WARNING_LOG);
1916
2313
  configure({ dataDir: path5.join(os2.homedir(), ".openclaw") });
2314
+ const hasStrong = !!(pluginConfig.llmStrongProvider || pluginConfig.llmStrongModel || pluginConfig.llmStrongBaseURL);
2315
+ if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || hasStrong) {
2316
+ configureLlm({
2317
+ provider: pluginConfig.llmProvider,
2318
+ model: pluginConfig.llmModel,
2319
+ baseURL: pluginConfig.llmBaseURL,
2320
+ crudRole: pluginConfig.llmCrudRole,
2321
+ // Omit the whole block when unset so `strong` transparently falls back to
2322
+ // `fast` — the zero-config path stays byte-for-byte today's behaviour.
2323
+ ...hasStrong && {
2324
+ strong: {
2325
+ provider: pluginConfig.llmStrongProvider,
2326
+ model: pluginConfig.llmStrongModel,
2327
+ baseURL: pluginConfig.llmStrongBaseURL
2328
+ }
2329
+ }
2330
+ });
2331
+ }
2332
+ const crudUpdateMinSim = pluginConfig.crudUpdateMinSim;
1917
2333
  const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
1918
2334
  const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
1919
2335
  const defaultScope = buildScope2(resolveAgentId2());
@@ -2014,17 +2430,22 @@ function register(api) {
2014
2430
  type: "array",
2015
2431
  items: { type: "string" },
2016
2432
  description: "Story 26B: filter knowledge notes by topics (all must match)"
2433
+ },
2434
+ subject: {
2435
+ type: "string",
2436
+ 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."
2017
2437
  }
2018
2438
  },
2019
2439
  required: ["query"]
2020
2440
  },
2021
2441
  async execute(_toolCallId, params) {
2022
- const { query, limit = 5, topicsFilter } = params;
2442
+ const { query, limit = 5, topicsFilter, subject } = params;
2023
2443
  const start = Date.now();
2024
- const hookWarning = hookNeverFiredWarning();
2444
+ const hookWarning = convBlocked ? BLOCKED_WARNING_SUFFIX : "";
2025
2445
  try {
2026
2446
  const results = await searchMemory(query, limit, scope.agentId, {
2027
2447
  topicsFilter,
2448
+ subject,
2028
2449
  storageCtx: scope.storageCtx
2029
2450
  });
2030
2451
  logger.info(
@@ -2065,15 +2486,20 @@ ${text}${hookWarning}` }],
2065
2486
  parameters: {
2066
2487
  type: "object",
2067
2488
  properties: {
2068
- text: { type: "string", description: "Fact or information to remember" }
2489
+ text: { type: "string", description: "Fact or information to remember" },
2490
+ subjects: {
2491
+ type: "array",
2492
+ items: { type: "string" },
2493
+ 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."
2494
+ }
2069
2495
  },
2070
2496
  required: ["text"]
2071
2497
  },
2072
2498
  async execute(_toolCallId, params) {
2073
- const { text } = params;
2499
+ const { text, subjects } = params;
2074
2500
  const start = Date.now();
2075
2501
  try {
2076
- const id = await addMemory(text, scope.agentId, { storageCtx: scope.storageCtx });
2502
+ const id = await addMemory(text, scope.agentId, { subjects, storageCtx: scope.storageCtx });
2077
2503
  logger.info(`openclaw-amem: memory_add OK id=${id} (${Date.now() - start}ms)`);
2078
2504
  return {
2079
2505
  content: [{ type: "text", text: "Memory saved successfully." }],
@@ -2203,7 +2629,6 @@ ${text}${hookWarning}` }],
2203
2629
  hookFn(
2204
2630
  "agent_end",
2205
2631
  async (event, ctx) => {
2206
- markHookFired();
2207
2632
  const scope = buildScope2(resolveAgentId2(ctx));
2208
2633
  const agentId = scope.agentId;
2209
2634
  const storageCtx = scope.storageCtx;
@@ -2252,7 +2677,21 @@ ${text}${hookWarning}` }],
2252
2677
  if (target) {
2253
2678
  const newEmbedding = await encode(op.fact);
2254
2679
  const hash = (0, import_crypto2.createHash)("md5").update(op.fact).digest("hex");
2255
- await storageCtx.updateNoteContent(target.id, op.fact, newEmbedding, hash);
2680
+ const targetNote = await storageCtx.getNote(target.id, agentId);
2681
+ if (!targetNote || !isPlausibleUpdateTarget(newEmbedding, targetNote.embedding, crudUpdateMinSim)) {
2682
+ await addMemory(op.fact, agentId, { storageCtx });
2683
+ logger.warn(
2684
+ `openclaw-amem: CRUD UPDATE on ${target.id.slice(0, 8)} looks mis-targeted \u2014 stored as a new memory instead`
2685
+ );
2686
+ continue;
2687
+ }
2688
+ const ok = await storageCtx.updateNoteContent(target.id, op.fact, newEmbedding, hash, agentId);
2689
+ if (!ok) {
2690
+ logger.warn(
2691
+ `openclaw-amem: CRUD UPDATE denied id=${target.id.slice(0, 8)} \u2014 ${agentId} not in writers`
2692
+ );
2693
+ continue;
2694
+ }
2256
2695
  logger.info(
2257
2696
  `openclaw-amem: CRUD UPDATE id=${target.id.slice(0, 8)}: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`
2258
2697
  );
@@ -2260,7 +2699,13 @@ ${text}${hookWarning}` }],
2260
2699
  } else if (op.action === "DELETE" && op.existingIdx !== void 0) {
2261
2700
  const target = existingMemories[op.existingIdx];
2262
2701
  if (target) {
2263
- await storageCtx.invalidateNote(target.id);
2702
+ const ok = await storageCtx.invalidateNote(target.id, agentId);
2703
+ if (!ok) {
2704
+ logger.warn(
2705
+ `openclaw-amem: CRUD DELETE denied id=${target.id.slice(0, 8)} \u2014 ${agentId} not in writers`
2706
+ );
2707
+ continue;
2708
+ }
2264
2709
  logger.info(
2265
2710
  `openclaw-amem: CRUD INVALIDATE id=${target.id.slice(0, 8)}: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`
2266
2711
  );
@@ -2289,16 +2734,6 @@ ${mergeErr.stack}`
2289
2734
  { timeoutMs: 3e4 }
2290
2735
  );
2291
2736
  logger.info("openclaw-amem: agent_end CRUD decision hook registered");
2292
- setTimeout(
2293
- () => {
2294
- if (!hookLiveness().everFired) {
2295
- logger.warn(
2296
- "\u26A0\uFE0F openclaw-amem: agent_end hook has never fired in 10 minutes. It may be blocked by OpenClaw security policy. Add to openclaw.json: plugins.entries.openclaw-amem.hooks.allowConversationAccess=true"
2297
- );
2298
- }
2299
- },
2300
- 10 * 60 * 1e3
2301
- );
2302
2737
  }
2303
2738
  function scheduleNextRun() {
2304
2739
  const now = /* @__PURE__ */ new Date();
@@ -2352,12 +2787,9 @@ var index_default = plugin;
2352
2787
  ensureCollection,
2353
2788
  generateReviewBatch,
2354
2789
  getNote,
2355
- hookLiveness,
2356
- hookNeverFiredWarning,
2357
2790
  invalidateNote,
2358
2791
  listMemories,
2359
2792
  listNotes,
2360
- markHookFired,
2361
2793
  mergeSimilarNotes,
2362
2794
  patchNotePayload,
2363
2795
  register,
@@ -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.2.1",
5
+ "version": "1.3.0",
6
6
  "kind": "memory",
7
7
  "openclaw": {
8
8
  "compat": {
@@ -31,6 +31,13 @@
31
31
  "OPENAI_API_KEY",
32
32
  "AMEM_LLM_BASE_URL",
33
33
  "AMEM_LLM_MODEL",
34
+ "AMEM_LLM_STRONG_PROVIDER",
35
+ "AMEM_LLM_STRONG_MODEL",
36
+ "AMEM_LLM_STRONG_BASE_URL",
37
+ "AMEM_LLM_CRUD_ROLE",
38
+ "AMEM_CONFLICT_MODE",
39
+ "AMEM_LLM_TIMEOUT",
40
+ "AMEM_CRUD_UPDATE_MIN_SIM",
34
41
  "AMEM_COLLECTION",
35
42
  "AMEM_DATA_DIR",
36
43
  "AMEM_EVO_COUNTER_PATH",
@@ -90,6 +97,53 @@
90
97
  "type": "string",
91
98
  "description": "Qdrant collection name (mode A shared default)."
92
99
  },
100
+ "llmProvider": {
101
+ "type": "string",
102
+ "enum": [
103
+ "anthropic",
104
+ "openai"
105
+ ],
106
+ "default": "anthropic",
107
+ "description": "LLM API dialect for the engine's own calls. Overridden by AMEM_LLM_PROVIDER."
108
+ },
109
+ "llmModel": {
110
+ "type": "string",
111
+ "description": "Model the engine uses for note construction, linking and evolution. Overridden by AMEM_LLM_MODEL. Defaults to claude-sonnet-4-6 (anthropic) or gpt-4o-mini (openai)."
112
+ },
113
+ "llmBaseURL": {
114
+ "type": "string",
115
+ "description": "Base URL for the LLM endpoint, e.g. an OpenAI-compatible gateway. Overridden by AMEM_LLM_BASE_URL. API keys are read from the environment only and cannot be set here."
116
+ },
117
+ "llmStrongProvider": {
118
+ "type": "string",
119
+ "enum": [
120
+ "anthropic",
121
+ "openai"
122
+ ],
123
+ "description": "Optional stronger tier: request format. Falls back to llmProvider. Overridden by AMEM_LLM_STRONG_PROVIDER."
124
+ },
125
+ "llmStrongModel": {
126
+ "type": "string",
127
+ "description": "Optional stronger tier: model for the few genuinely hard judgements (merge adjudication, contradiction classification). Falls back to llmModel, so leaving it unset keeps single-model behaviour. Overridden by AMEM_LLM_STRONG_MODEL."
128
+ },
129
+ "llmStrongBaseURL": {
130
+ "type": "string",
131
+ "description": "Optional stronger tier: endpoint. Falls back to llmBaseURL. Set all three strong fields to run the tiers on entirely different backends, e.g. a local Ollama for fast and a hosted API for strong. Overridden by AMEM_LLM_STRONG_BASE_URL."
132
+ },
133
+ "llmCrudRole": {
134
+ "type": "string",
135
+ "enum": [
136
+ "fast",
137
+ "strong"
138
+ ],
139
+ "default": "fast",
140
+ "description": "Which tier the agent_end CRUD decision runs on. Defaults to fast: it runs every turn, and its destructive failure mode is handled by the update guard rather than by model tier. Overridden by AMEM_LLM_CRUD_ROLE."
141
+ },
142
+ "crudUpdateMinSim": {
143
+ "type": "number",
144
+ "default": 0.35,
145
+ "description": "Similarity floor (0-1) for accepting an LLM-chosen CRUD UPDATE target. Below it the fact is stored as a new memory instead of overwriting, so a mis-picked memory is never destroyed. Raise it for cheaper/weaker models. Overridden by AMEM_CRUD_UPDATE_MIN_SIM."
146
+ },
93
147
  "agents": {
94
148
  "type": "object",
95
149
  "description": "Per-agent overrides keyed by agentId.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-amem",
3
- "version": "1.2.1",
3
+ "version": "1.3.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",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@anthropic-ai/sdk": "^0.110.0",
19
+ "@anthropic-ai/sdk": "^0.112.1",
20
20
  "@huggingface/transformers": "^4.2.0",
21
21
  "@node-rs/jieba": "^2.0.1",
22
22
  "@qdrant/js-client-rest": "^1.18.0",
@@ -32,7 +32,7 @@
32
32
  "tsup": "^8.4.0",
33
33
  "tsx": "^4.23.1",
34
34
  "typescript": "^6.0.3",
35
- "typescript-eslint": "^8.60.1",
35
+ "typescript-eslint": "^8.64.0",
36
36
  "vitest": "^4.1.10"
37
37
  },
38
38
  "optionalDependencies": {