openclaw-amem 1.2.2 → 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 +544 -85
- package/openclaw.plugin.json +55 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -127,6 +127,19 @@ var init_embedding = __esm({
|
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
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
|
+
|
|
130
143
|
// ../amem-core/src/storage.ts
|
|
131
144
|
async function qdrant(method, path6, body) {
|
|
132
145
|
const res = await fetch(`${QDRANT_URL}${path6}`, {
|
|
@@ -180,6 +193,10 @@ async function ensureCollection(collectionName) {
|
|
|
180
193
|
field_name: "topics",
|
|
181
194
|
field_schema: "keyword"
|
|
182
195
|
});
|
|
196
|
+
await qdrant("PUT", `/collections/${col}/index`, {
|
|
197
|
+
field_name: "subjects",
|
|
198
|
+
field_schema: "keyword"
|
|
199
|
+
});
|
|
183
200
|
markReady();
|
|
184
201
|
}
|
|
185
202
|
function noteToPoint(note) {
|
|
@@ -212,6 +229,9 @@ function noteToPoint(note) {
|
|
|
212
229
|
// 30
|
|
213
230
|
evolution_type: note.evolution_type || "",
|
|
214
231
|
conflict: note.conflict ?? false,
|
|
232
|
+
conflicts_with: note.conflicts_with ?? [],
|
|
233
|
+
conflict_reason: note.conflict_reason ?? "",
|
|
234
|
+
subjects: note.subjects ?? [],
|
|
215
235
|
// 31
|
|
216
236
|
ephemeral: note.ephemeral ?? false,
|
|
217
237
|
low_quality: note.low_quality ?? false,
|
|
@@ -264,6 +284,9 @@ function pointToNote(point) {
|
|
|
264
284
|
// 30
|
|
265
285
|
evolution_type: typeof p.evolution_type === "string" && ["EVOLVE", "CONFLICT", "EXPAND", "NEW"].includes(p.evolution_type) ? p.evolution_type : void 0,
|
|
266
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") : [],
|
|
267
290
|
// 31
|
|
268
291
|
ephemeral: p.ephemeral === true,
|
|
269
292
|
low_quality: p.low_quality === true,
|
|
@@ -273,28 +296,41 @@ function pointToNote(point) {
|
|
|
273
296
|
writers: Array.isArray(p.writers) ? p.writers : [p.agent_id || "main"]
|
|
274
297
|
};
|
|
275
298
|
}
|
|
276
|
-
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
|
+
}
|
|
277
313
|
return {
|
|
278
|
-
must
|
|
279
|
-
{
|
|
280
|
-
should: [
|
|
281
|
-
{ key: "agent_id", match: { value: agentId } },
|
|
282
|
-
{ key: "agent_id", match: { value: "shared" } }
|
|
283
|
-
]
|
|
284
|
-
}
|
|
285
|
-
],
|
|
314
|
+
must,
|
|
286
315
|
must_not: [{ key: "is_active", match: { value: false } }]
|
|
287
316
|
};
|
|
288
317
|
}
|
|
289
318
|
function makeCrud(collectionName, modeBIsolated = false) {
|
|
290
319
|
const col = collectionName;
|
|
291
|
-
function scopedAgentFilter(agentId) {
|
|
320
|
+
function scopedAgentFilter(agentId, subject) {
|
|
292
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
|
+
}
|
|
293
328
|
return {
|
|
329
|
+
...must.length > 0 && { must },
|
|
294
330
|
must_not: [{ key: "is_active", match: { value: false } }]
|
|
295
331
|
};
|
|
296
332
|
}
|
|
297
|
-
return agentFilter(agentId);
|
|
333
|
+
return agentFilter(agentId, subject);
|
|
298
334
|
}
|
|
299
335
|
return {
|
|
300
336
|
async addNote(note) {
|
|
@@ -303,7 +339,14 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
303
339
|
points: [noteToPoint(note)]
|
|
304
340
|
});
|
|
305
341
|
},
|
|
306
|
-
|
|
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) {
|
|
307
350
|
await ensureCollection(col);
|
|
308
351
|
try {
|
|
309
352
|
const result = await qdrant("POST", `/collections/${col}/points`, {
|
|
@@ -312,7 +355,9 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
312
355
|
with_vector: true
|
|
313
356
|
});
|
|
314
357
|
if (!result.length) return null;
|
|
315
|
-
|
|
358
|
+
const note = pointToNote(result[0]);
|
|
359
|
+
if (readerAgentId !== void 0 && !canRead(note, readerAgentId)) return null;
|
|
360
|
+
return note;
|
|
316
361
|
} catch {
|
|
317
362
|
return null;
|
|
318
363
|
}
|
|
@@ -348,17 +393,48 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
348
393
|
if (!result.points.length) return null;
|
|
349
394
|
return pointToNote(result.points[0]);
|
|
350
395
|
},
|
|
351
|
-
|
|
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) {
|
|
352
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
|
+
}
|
|
353
411
|
await qdrant("PUT", `/collections/${col}/points/vectors?wait=true`, {
|
|
354
412
|
points: [{ id, vector: embedding }]
|
|
355
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
|
+
}
|
|
356
431
|
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
357
|
-
payload
|
|
432
|
+
payload,
|
|
358
433
|
points: [id]
|
|
359
434
|
});
|
|
435
|
+
return true;
|
|
360
436
|
},
|
|
361
|
-
async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0) {
|
|
437
|
+
async queryByEmbedding(embedding, topK, agentId, scoreThreshold = 0, subject) {
|
|
362
438
|
await ensureCollection(col);
|
|
363
439
|
const result = await qdrant("POST", `/collections/${col}/points/search`, {
|
|
364
440
|
vector: embedding,
|
|
@@ -366,7 +442,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
366
442
|
with_payload: true,
|
|
367
443
|
with_vector: true,
|
|
368
444
|
score_threshold: scoreThreshold,
|
|
369
|
-
filter: scopedAgentFilter(agentId)
|
|
445
|
+
filter: scopedAgentFilter(agentId, subject)
|
|
370
446
|
});
|
|
371
447
|
const queryResults = result.map((r) => ({
|
|
372
448
|
note: pointToNote(r),
|
|
@@ -400,14 +476,14 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
400
476
|
}
|
|
401
477
|
return queryResults;
|
|
402
478
|
},
|
|
403
|
-
async listNotes(agentId) {
|
|
479
|
+
async listNotes(agentId, subject) {
|
|
404
480
|
await ensureCollection(col);
|
|
405
481
|
const body = {
|
|
406
482
|
with_payload: true,
|
|
407
483
|
with_vector: true,
|
|
408
484
|
limit: 1e4
|
|
409
485
|
};
|
|
410
|
-
if (agentId) body.filter = scopedAgentFilter(agentId);
|
|
486
|
+
if (agentId) body.filter = scopedAgentFilter(agentId, subject);
|
|
411
487
|
const result = await qdrant("POST", `/collections/${col}/points/scroll`, body);
|
|
412
488
|
return result.points.map(pointToNote);
|
|
413
489
|
},
|
|
@@ -417,12 +493,18 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
417
493
|
points: [id]
|
|
418
494
|
});
|
|
419
495
|
},
|
|
420
|
-
|
|
496
|
+
/** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
|
|
497
|
+
async invalidateNote(id, callerAgentId) {
|
|
421
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
|
+
}
|
|
422
503
|
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
423
504
|
payload: { is_active: false },
|
|
424
505
|
points: [id]
|
|
425
506
|
});
|
|
507
|
+
return true;
|
|
426
508
|
},
|
|
427
509
|
async getNotesByDatePrefix(datePrefix, agentId) {
|
|
428
510
|
await ensureCollection(col);
|
|
@@ -468,6 +550,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
468
550
|
async replaceLinkReferences(oldId, newId, agentId) {
|
|
469
551
|
const notes = await this.listNotes(agentId);
|
|
470
552
|
for (const note of notes) {
|
|
553
|
+
if (!canWrite(note, agentId)) continue;
|
|
471
554
|
if (note.links.includes(oldId)) {
|
|
472
555
|
const newLinks = note.links.map((linkId) => linkId === oldId ? newId : linkId);
|
|
473
556
|
const filteredLinks = newLinks.filter((linkId) => linkId !== note.id);
|
|
@@ -481,20 +564,20 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
481
564
|
function createStorageContext(collectionName, modeBIsolated = false) {
|
|
482
565
|
return makeCrud(collectionName || getCollection(), modeBIsolated);
|
|
483
566
|
}
|
|
484
|
-
async function getNote(id) {
|
|
485
|
-
return makeCrud(getCollection()).getNote(id);
|
|
567
|
+
async function getNote(id, readerAgentId) {
|
|
568
|
+
return makeCrud(getCollection()).getNote(id, readerAgentId);
|
|
486
569
|
}
|
|
487
570
|
async function updateNote(note) {
|
|
488
571
|
return makeCrud(getCollection()).updateNote(note);
|
|
489
572
|
}
|
|
490
|
-
async function listNotes(agentId) {
|
|
491
|
-
return makeCrud(getCollection()).listNotes(agentId);
|
|
573
|
+
async function listNotes(agentId, subject) {
|
|
574
|
+
return makeCrud(getCollection()).listNotes(agentId, subject);
|
|
492
575
|
}
|
|
493
576
|
async function deleteNote(id) {
|
|
494
577
|
return makeCrud(getCollection()).deleteNote(id);
|
|
495
578
|
}
|
|
496
|
-
async function invalidateNote(id) {
|
|
497
|
-
return makeCrud(getCollection()).invalidateNote(id);
|
|
579
|
+
async function invalidateNote(id, callerAgentId) {
|
|
580
|
+
return makeCrud(getCollection()).invalidateNote(id, callerAgentId);
|
|
498
581
|
}
|
|
499
582
|
async function patchNotePayload(id, fields) {
|
|
500
583
|
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
@@ -503,6 +586,7 @@ var QDRANT_URL, getCollection, VECTOR_DIM, _collectionReady, _collectionReadyMap
|
|
|
503
586
|
var init_storage = __esm({
|
|
504
587
|
"../amem-core/src/storage.ts"() {
|
|
505
588
|
"use strict";
|
|
589
|
+
init_auth();
|
|
506
590
|
QDRANT_URL = "http://localhost:6333";
|
|
507
591
|
getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
|
|
508
592
|
VECTOR_DIM = 384;
|
|
@@ -600,7 +684,39 @@ Classification rules:
|
|
|
600
684
|
- NEW: Completely unrelated information, no substantive connection to the old memory
|
|
601
685
|
Return: {"type": "NEW"}
|
|
602
686
|
|
|
603
|
-
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.`
|
|
604
720
|
};
|
|
605
721
|
zh = {
|
|
606
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
|
|
@@ -685,7 +801,36 @@ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
|
|
|
685
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
|
|
686
802
|
\u8FD4\u56DE\uFF1A{"type": "NEW"}
|
|
687
803
|
|
|
688
|
-
\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**`
|
|
689
834
|
};
|
|
690
835
|
templates = { en, zh };
|
|
691
836
|
t = templates[LOCALE];
|
|
@@ -693,35 +838,92 @@ B: "\u7528\u6237\u7684 VS Code \u4F7F\u7528 One Dark Pro \u4E3B\u9898"
|
|
|
693
838
|
});
|
|
694
839
|
|
|
695
840
|
// ../amem-core/src/llm.ts
|
|
696
|
-
function
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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;
|
|
701
858
|
}
|
|
702
|
-
function
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
});
|
|
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");
|
|
711
862
|
}
|
|
712
|
-
|
|
713
|
-
const
|
|
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;
|
|
910
|
+
}
|
|
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");
|
|
714
916
|
const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
|
|
715
917
|
try {
|
|
716
|
-
return
|
|
918
|
+
return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
|
|
717
919
|
} catch (e) {
|
|
718
920
|
console.error(`[amem] LLM call failed: ${e.message}`);
|
|
719
921
|
return null;
|
|
720
922
|
}
|
|
721
923
|
}
|
|
722
|
-
async function anthropicCall(prompt, maxTokens) {
|
|
723
|
-
const resp = await anthropic().messages.create({
|
|
724
|
-
model
|
|
924
|
+
async function anthropicCall(prompt, model, maxTokens, baseURL) {
|
|
925
|
+
const resp = await anthropic(baseURL).messages.create({
|
|
926
|
+
model,
|
|
725
927
|
max_tokens: maxTokens,
|
|
726
928
|
messages: [{ role: "user", content: prompt }]
|
|
727
929
|
});
|
|
@@ -730,17 +932,20 @@ async function anthropicCall(prompt, maxTokens) {
|
|
|
730
932
|
}
|
|
731
933
|
return null;
|
|
732
934
|
}
|
|
733
|
-
async function openaiCall(prompt, maxTokens) {
|
|
734
|
-
const isReasoning = /^o\d/.test(
|
|
735
|
-
const resp = await openai().chat.completions.create({
|
|
736
|
-
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,
|
|
737
939
|
...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
|
|
738
940
|
messages: [{ role: "user", content: prompt }]
|
|
739
941
|
});
|
|
740
942
|
return resp.choices[0]?.message?.content?.trim() ?? null;
|
|
741
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
|
+
}
|
|
742
947
|
function stripFences(raw) {
|
|
743
|
-
raw = raw
|
|
948
|
+
raw = stripReasoning(raw);
|
|
744
949
|
if (raw.startsWith("```")) {
|
|
745
950
|
const lines = raw.split("\n");
|
|
746
951
|
lines.shift();
|
|
@@ -755,6 +960,16 @@ function stripFences(raw) {
|
|
|
755
960
|
}
|
|
756
961
|
return raw;
|
|
757
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
|
+
}
|
|
758
973
|
async function llmConstructNote(content) {
|
|
759
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:
|
|
760
975
|
{
|
|
@@ -802,7 +1017,7 @@ Text: ${content}`;
|
|
|
802
1017
|
confidence: "medium"
|
|
803
1018
|
};
|
|
804
1019
|
try {
|
|
805
|
-
const data =
|
|
1020
|
+
const data = parseJsonLoose(raw);
|
|
806
1021
|
const rawCategory = typeof data.category === "string" ? data.category : "General";
|
|
807
1022
|
const category = VALID_CATEGORIES.has(rawCategory) ? rawCategory : "General";
|
|
808
1023
|
const note_type = data.note_type === "knowledge" ? "knowledge" : "memory";
|
|
@@ -845,9 +1060,9 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
|
845
1060
|
const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
|
|
846
1061
|
const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
|
|
847
1062
|
try {
|
|
848
|
-
const raw = await llmCall(prompt, 400);
|
|
1063
|
+
const raw = await llmCall(prompt, 400, resolveCrudRole());
|
|
849
1064
|
if (!raw) return [];
|
|
850
|
-
const match = raw.match(/\[.*\]/s);
|
|
1065
|
+
const match = stripReasoning(raw).match(/\[.*\]/s);
|
|
851
1066
|
if (!match) return [];
|
|
852
1067
|
const parsed = JSON.parse(match[0]);
|
|
853
1068
|
if (!Array.isArray(parsed)) return [];
|
|
@@ -875,10 +1090,10 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
|
875
1090
|
}
|
|
876
1091
|
async function llmShouldMerge(contentA, contentB) {
|
|
877
1092
|
const prompt = t.shouldMerge(contentA, contentB);
|
|
878
|
-
const raw = await llmCall(prompt, 300);
|
|
1093
|
+
const raw = await llmCall(prompt, 300, "strong");
|
|
879
1094
|
if (!raw) return { shouldMerge: false };
|
|
880
1095
|
try {
|
|
881
|
-
const data =
|
|
1096
|
+
const data = parseJsonLoose(raw);
|
|
882
1097
|
if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
|
|
883
1098
|
if (data.shouldMerge && typeof data.merged === "string") {
|
|
884
1099
|
return { shouldMerge: true, merged: data.merged };
|
|
@@ -891,10 +1106,10 @@ async function llmShouldMerge(contentA, contentB) {
|
|
|
891
1106
|
}
|
|
892
1107
|
async function llmEvolutionJudge(oldContent, newContent) {
|
|
893
1108
|
const prompt = t.evolutionJudge(oldContent, newContent);
|
|
894
|
-
const raw = await llmCall(prompt, 300);
|
|
1109
|
+
const raw = await llmCall(prompt, 300, "strong");
|
|
895
1110
|
if (!raw) return { type: "NEW" };
|
|
896
1111
|
try {
|
|
897
|
-
const data =
|
|
1112
|
+
const data = parseJsonLoose(raw);
|
|
898
1113
|
const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
|
|
899
1114
|
return {
|
|
900
1115
|
type,
|
|
@@ -931,7 +1146,7 @@ ${linkedStr}`;
|
|
|
931
1146
|
const raw = await llmCall(prompt, 500);
|
|
932
1147
|
if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
933
1148
|
try {
|
|
934
|
-
const data =
|
|
1149
|
+
const data = parseJsonLoose(raw);
|
|
935
1150
|
return {
|
|
936
1151
|
tags: Array.isArray(data.tags) ? data.tags : null,
|
|
937
1152
|
context: typeof data.context === "string" ? data.context : null,
|
|
@@ -944,20 +1159,56 @@ ${linkedStr}`;
|
|
|
944
1159
|
return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
945
1160
|
}
|
|
946
1161
|
}
|
|
947
|
-
|
|
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;
|
|
948
1201
|
var init_llm = __esm({
|
|
949
1202
|
"../amem-core/src/llm.ts"() {
|
|
950
1203
|
"use strict";
|
|
951
1204
|
import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
952
1205
|
import_openai = __toESM(require("openai"), 1);
|
|
953
1206
|
init_prompts();
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
_anthropic = null;
|
|
960
|
-
_openai = null;
|
|
1207
|
+
_override = {};
|
|
1208
|
+
_warned = /* @__PURE__ */ new Set();
|
|
1209
|
+
DEFAULT_TIMEOUT_MS = 3e4;
|
|
1210
|
+
_anthropicClients = /* @__PURE__ */ new Map();
|
|
1211
|
+
_openaiClients = /* @__PURE__ */ new Map();
|
|
961
1212
|
VALID_CONFIDENCE = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
962
1213
|
VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
963
1214
|
"Technical",
|
|
@@ -1077,6 +1328,7 @@ function defaultCtx() {
|
|
|
1077
1328
|
}
|
|
1078
1329
|
async function addMemory(content, agentId = "main", opts) {
|
|
1079
1330
|
const scope = opts?.scope ?? "private";
|
|
1331
|
+
const subjects = opts?.subjects ?? [];
|
|
1080
1332
|
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1081
1333
|
const quality = checkQuality(content);
|
|
1082
1334
|
if (!quality.ok) {
|
|
@@ -1101,9 +1353,14 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1101
1353
|
const embedding = await encode(fieldsText);
|
|
1102
1354
|
const topMatch = await ctx.queryByEmbedding(embedding, 1, agentId, 0);
|
|
1103
1355
|
if (topMatch.length > 0 && topMatch[0].score >= 0.85) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
+
);
|
|
1107
1364
|
}
|
|
1108
1365
|
const pendingMerge = topMatch.length > 0 && topMatch[0].score >= 0.72 && topMatch[0].score < 0.85;
|
|
1109
1366
|
if (pendingMerge) {
|
|
@@ -1113,6 +1370,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1113
1370
|
const writers = [agentId];
|
|
1114
1371
|
const note = {
|
|
1115
1372
|
id: (0, import_uuid.v4)(),
|
|
1373
|
+
subjects,
|
|
1116
1374
|
content,
|
|
1117
1375
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1118
1376
|
keywords,
|
|
@@ -1171,6 +1429,10 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1171
1429
|
for (const lid of linkedIds) {
|
|
1172
1430
|
const linked = await ctx.getNote(lid);
|
|
1173
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
|
+
}
|
|
1174
1436
|
linked.links.push(note.id);
|
|
1175
1437
|
await ctx.updateNote(linked);
|
|
1176
1438
|
}
|
|
@@ -1180,10 +1442,14 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1180
1442
|
for (const lid of linkedIds.slice(0, 3)) {
|
|
1181
1443
|
const linked = await ctx.getNote(lid);
|
|
1182
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
|
+
}
|
|
1183
1449
|
const linkedNotes = [];
|
|
1184
1450
|
for (const llid of linked.links.slice(0, 5)) {
|
|
1185
1451
|
if (llid === note.id) continue;
|
|
1186
|
-
const ln = await ctx.getNote(llid);
|
|
1452
|
+
const ln = await ctx.getNote(llid, agentId);
|
|
1187
1453
|
if (ln) linkedNotes.push({ id: ln.id, content: ln.content });
|
|
1188
1454
|
}
|
|
1189
1455
|
linkedNotes.push({ id: note.id, content });
|
|
@@ -1220,10 +1486,14 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1220
1486
|
note.links.push(targetId);
|
|
1221
1487
|
noteChanged = true;
|
|
1222
1488
|
}
|
|
1223
|
-
const target = await ctx.getNote(targetId);
|
|
1489
|
+
const target = await ctx.getNote(targetId, agentId);
|
|
1224
1490
|
if (target && !target.links.includes(note.id)) {
|
|
1225
|
-
target
|
|
1226
|
-
|
|
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
|
+
}
|
|
1227
1497
|
}
|
|
1228
1498
|
}
|
|
1229
1499
|
if (tagsToUpdate.length > 0) {
|
|
@@ -1272,6 +1542,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1272
1542
|
}
|
|
1273
1543
|
async function addEpisodic(content, agentId = "main", opts) {
|
|
1274
1544
|
const scope = opts?.scope ?? "private";
|
|
1545
|
+
const subjects = opts?.subjects ?? [];
|
|
1275
1546
|
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1276
1547
|
const quality = checkQuality(content);
|
|
1277
1548
|
if (!quality.ok) {
|
|
@@ -1281,6 +1552,7 @@ async function addEpisodic(content, agentId = "main", opts) {
|
|
|
1281
1552
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1282
1553
|
const note = {
|
|
1283
1554
|
id: (0, import_uuid.v4)(),
|
|
1555
|
+
subjects,
|
|
1284
1556
|
content,
|
|
1285
1557
|
timestamp: now,
|
|
1286
1558
|
keywords: [],
|
|
@@ -1310,14 +1582,15 @@ async function addEpisodic(content, agentId = "main", opts) {
|
|
|
1310
1582
|
}
|
|
1311
1583
|
async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
1312
1584
|
const useBfs = opts?.useBfs !== false;
|
|
1585
|
+
const subject = opts?.subject;
|
|
1313
1586
|
const bfsSimThreshold = opts?.bfsSimThreshold ?? 0.25;
|
|
1314
1587
|
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1315
1588
|
const total = await ctx.countNotes(agentId);
|
|
1316
1589
|
if (total === 0) return [];
|
|
1317
1590
|
const queryEmbedding = await encode(query);
|
|
1318
1591
|
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);
|
|
1592
|
+
const embResults = await ctx.queryByEmbedding(queryEmbedding, n, agentId, 0, subject);
|
|
1593
|
+
const allNotes = await ctx.listNotes(agentId, subject);
|
|
1321
1594
|
const bm25State = buildBM25(allNotes);
|
|
1322
1595
|
const queryTokens = simpleTokenize(query);
|
|
1323
1596
|
const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n);
|
|
@@ -1628,7 +1901,66 @@ async function consolidateMemories(agentId, logger, storageCtx) {
|
|
|
1628
1901
|
log.info(`[Consolidation] Completed consolidation run. Merged ${mergedCount} pairs.`);
|
|
1629
1902
|
return mergedCount;
|
|
1630
1903
|
}
|
|
1631
|
-
|
|
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;
|
|
1632
1964
|
var init_memory = __esm({
|
|
1633
1965
|
"../amem-core/src/memory.ts"() {
|
|
1634
1966
|
"use strict";
|
|
@@ -1638,12 +1970,15 @@ var init_memory = __esm({
|
|
|
1638
1970
|
path3 = __toESM(require("path"), 1);
|
|
1639
1971
|
init_embedding();
|
|
1640
1972
|
init_storage();
|
|
1973
|
+
init_auth();
|
|
1641
1974
|
init_llm();
|
|
1642
1975
|
init_evo_counter();
|
|
1643
1976
|
init_config();
|
|
1644
1977
|
import_jieba = require("@node-rs/jieba");
|
|
1978
|
+
logSafe = (id) => id.replace(/[\r\n]/g, "");
|
|
1645
1979
|
_jieba = null;
|
|
1646
1980
|
EPHEMERAL_SIGNALS = ["\u5F85\u8DD1", "\u7B49\u786E\u8BA4", "\u6628\u65E5", "\u660E\u5929\u5B8C\u6210"];
|
|
1981
|
+
CONFLICT_BATCH_SIZE = 25;
|
|
1647
1982
|
}
|
|
1648
1983
|
});
|
|
1649
1984
|
|
|
@@ -1654,6 +1989,7 @@ async function scanLowQuality(agentId) {
|
|
|
1654
1989
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1655
1990
|
const results = [];
|
|
1656
1991
|
for (const note of notes) {
|
|
1992
|
+
if (!canWrite(note, agentId)) continue;
|
|
1657
1993
|
const reasons = [];
|
|
1658
1994
|
if (note.content.trim().length < 10) {
|
|
1659
1995
|
reasons.push("too_short");
|
|
@@ -1746,6 +2082,48 @@ async function generateReviewBatch(agentId, outputPath) {
|
|
|
1746
2082
|
if (items.length === 0) {
|
|
1747
2083
|
lines.push(LOCALE2 === "zh" ? "\u2705 \u6CA1\u6709\u53D1\u73B0\u4F4E\u8D28\u91CF\u6761\u76EE\u3002" : "\u2705 No low-quality items found.");
|
|
1748
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
|
+
}
|
|
1749
2127
|
for (let i = 0; i < items.length; i++) {
|
|
1750
2128
|
const { note, reasons } = items[i];
|
|
1751
2129
|
const badge = severityBadge(reasons);
|
|
@@ -1788,18 +2166,45 @@ var init_quality = __esm({
|
|
|
1788
2166
|
fs3 = __toESM(require("fs"), 1);
|
|
1789
2167
|
path4 = __toESM(require("path"), 1);
|
|
1790
2168
|
init_storage();
|
|
2169
|
+
init_auth();
|
|
1791
2170
|
LOCALE2 = process.env.AMEM_PROMPT_LOCALE === "zh" ? "zh" : "en";
|
|
1792
2171
|
DEFAULT_OUTPUT_DIR = process.env.AMEM_REVIEW_DIR || process.cwd();
|
|
1793
2172
|
}
|
|
1794
2173
|
});
|
|
1795
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
|
+
|
|
1796
2196
|
// ../amem-core/src/index.ts
|
|
1797
2197
|
var src_exports = {};
|
|
1798
2198
|
__export(src_exports, {
|
|
2199
|
+
DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
|
|
1799
2200
|
addEpisodic: () => addEpisodic,
|
|
1800
2201
|
addMemory: () => addMemory,
|
|
2202
|
+
canRead: () => canRead,
|
|
2203
|
+
canWrite: () => canWrite,
|
|
1801
2204
|
checkQuality: () => checkQuality,
|
|
1802
2205
|
configure: () => configure,
|
|
2206
|
+
configureLlm: () => configureLlm,
|
|
2207
|
+
conflictSweep: () => conflictSweep,
|
|
1803
2208
|
consolidateMemories: () => consolidateMemories,
|
|
1804
2209
|
createStorageContext: () => createStorageContext,
|
|
1805
2210
|
deleteNote: () => deleteNote,
|
|
@@ -1809,6 +2214,7 @@ __export(src_exports, {
|
|
|
1809
2214
|
getNote: () => getNote,
|
|
1810
2215
|
invalidateNote: () => invalidateNote,
|
|
1811
2216
|
isModelLoaded: () => isModelLoaded,
|
|
2217
|
+
isPlausibleUpdateTarget: () => isPlausibleUpdateTarget,
|
|
1812
2218
|
listMemories: () => listMemories,
|
|
1813
2219
|
listNotes: () => listNotes,
|
|
1814
2220
|
llmCrudDecision: () => llmCrudDecision,
|
|
@@ -1816,6 +2222,7 @@ __export(src_exports, {
|
|
|
1816
2222
|
mergeSimilarNotes: () => mergeSimilarNotes,
|
|
1817
2223
|
patchNotePayload: () => patchNotePayload,
|
|
1818
2224
|
pingQdrant: () => pingQdrant,
|
|
2225
|
+
resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
|
|
1819
2226
|
scanLowQuality: () => scanLowQuality,
|
|
1820
2227
|
searchMemory: () => searchMemory,
|
|
1821
2228
|
updateNote: () => updateNote
|
|
@@ -1828,6 +2235,9 @@ var init_src = __esm({
|
|
|
1828
2235
|
init_memory();
|
|
1829
2236
|
init_quality();
|
|
1830
2237
|
init_storage();
|
|
2238
|
+
init_auth();
|
|
2239
|
+
init_memory();
|
|
2240
|
+
init_crud_guard();
|
|
1831
2241
|
init_llm();
|
|
1832
2242
|
}
|
|
1833
2243
|
});
|
|
@@ -1901,6 +2311,25 @@ function register(api) {
|
|
|
1901
2311
|
const convBlocked = isConvAccessBlocked(api.config, pluginId);
|
|
1902
2312
|
if (convBlocked) logger.warn(BLOCKED_WARNING_LOG);
|
|
1903
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;
|
|
1904
2333
|
const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
|
|
1905
2334
|
const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
|
|
1906
2335
|
const defaultScope = buildScope2(resolveAgentId2());
|
|
@@ -2001,17 +2430,22 @@ function register(api) {
|
|
|
2001
2430
|
type: "array",
|
|
2002
2431
|
items: { type: "string" },
|
|
2003
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."
|
|
2004
2437
|
}
|
|
2005
2438
|
},
|
|
2006
2439
|
required: ["query"]
|
|
2007
2440
|
},
|
|
2008
2441
|
async execute(_toolCallId, params) {
|
|
2009
|
-
const { query, limit = 5, topicsFilter } = params;
|
|
2442
|
+
const { query, limit = 5, topicsFilter, subject } = params;
|
|
2010
2443
|
const start = Date.now();
|
|
2011
2444
|
const hookWarning = convBlocked ? BLOCKED_WARNING_SUFFIX : "";
|
|
2012
2445
|
try {
|
|
2013
2446
|
const results = await searchMemory(query, limit, scope.agentId, {
|
|
2014
2447
|
topicsFilter,
|
|
2448
|
+
subject,
|
|
2015
2449
|
storageCtx: scope.storageCtx
|
|
2016
2450
|
});
|
|
2017
2451
|
logger.info(
|
|
@@ -2052,15 +2486,20 @@ ${text}${hookWarning}` }],
|
|
|
2052
2486
|
parameters: {
|
|
2053
2487
|
type: "object",
|
|
2054
2488
|
properties: {
|
|
2055
|
-
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
|
+
}
|
|
2056
2495
|
},
|
|
2057
2496
|
required: ["text"]
|
|
2058
2497
|
},
|
|
2059
2498
|
async execute(_toolCallId, params) {
|
|
2060
|
-
const { text } = params;
|
|
2499
|
+
const { text, subjects } = params;
|
|
2061
2500
|
const start = Date.now();
|
|
2062
2501
|
try {
|
|
2063
|
-
const id = await addMemory(text, scope.agentId, { storageCtx: scope.storageCtx });
|
|
2502
|
+
const id = await addMemory(text, scope.agentId, { subjects, storageCtx: scope.storageCtx });
|
|
2064
2503
|
logger.info(`openclaw-amem: memory_add OK id=${id} (${Date.now() - start}ms)`);
|
|
2065
2504
|
return {
|
|
2066
2505
|
content: [{ type: "text", text: "Memory saved successfully." }],
|
|
@@ -2238,7 +2677,21 @@ ${text}${hookWarning}` }],
|
|
|
2238
2677
|
if (target) {
|
|
2239
2678
|
const newEmbedding = await encode(op.fact);
|
|
2240
2679
|
const hash = (0, import_crypto2.createHash)("md5").update(op.fact).digest("hex");
|
|
2241
|
-
await storageCtx.
|
|
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
|
+
}
|
|
2242
2695
|
logger.info(
|
|
2243
2696
|
`openclaw-amem: CRUD UPDATE id=${target.id.slice(0, 8)}: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`
|
|
2244
2697
|
);
|
|
@@ -2246,7 +2699,13 @@ ${text}${hookWarning}` }],
|
|
|
2246
2699
|
} else if (op.action === "DELETE" && op.existingIdx !== void 0) {
|
|
2247
2700
|
const target = existingMemories[op.existingIdx];
|
|
2248
2701
|
if (target) {
|
|
2249
|
-
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
|
+
}
|
|
2250
2709
|
logger.info(
|
|
2251
2710
|
`openclaw-amem: CRUD INVALIDATE id=${target.id.slice(0, 8)}: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`
|
|
2252
2711
|
);
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
35
|
+
"typescript-eslint": "^8.64.0",
|
|
36
36
|
"vitest": "^4.1.10"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|