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