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 +561 -129
- package/openclaw.plugin.json +55 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
730
|
-
const resp = await openai().chat.completions.create({
|
|
731
|
-
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
|
1221
|
-
|
|
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
|
-
|
|
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/
|
|
1862
|
-
var
|
|
1863
|
-
var
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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 =
|
|
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.
|
|
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,
|
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": {
|