@amemhq/core 2.1.0 → 2.1.2
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 +23 -23
- package/dist/{chunk-XEMQZNLD.js → chunk-Z7G7KOJK.js} +77 -39
- package/dist/chunk-Z7G7KOJK.js.map +1 -0
- package/dist/cli-migrate.cjs +28 -16
- package/dist/cli-migrate.cjs.map +1 -1
- package/dist/cli-migrate.js +1 -1
- package/dist/index.cjs +64 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +2 -3
- package/dist/chunk-XEMQZNLD.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Framework-agnostic **A-MEM agentic memory engine** — memories that **evolve**,
|
|
|
6
6
|
npm i @amemhq/core
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
We extracted this package from [`openclaw-amem`](https://github.com/amemhq/amem/tree/main/packages/openclaw-amem) so that any host can share one memory engine: an OpenClaw plugin, a standalone service ([`amem-api`](../amem-api)), or a game agent. It is part of the [amem monorepo](../../).
|
|
10
10
|
|
|
11
11
|
> Based on _A-MEM: Agentic Memory for LLM Agents_ ([arXiv:2502.12110](https://arxiv.org/abs/2502.12110), NeurIPS 2025). For the original research implementation, see [agiresearch/A-MEM](https://github.com/agiresearch/A-MEM).
|
|
12
12
|
|
|
@@ -14,30 +14,30 @@ Extracted from [`openclaw-amem`](https://github.com/amemhq/amem/tree/main/packag
|
|
|
14
14
|
|
|
15
15
|
Unlike a flat vector store, A-MEM maintains memory as a living, self-evolving semantic graph. On every write:
|
|
16
16
|
|
|
17
|
-
1. **Note Construction** — an LLM extracts keywords, tags, and a context summary
|
|
18
|
-
2. **Link Generation** — retrieves top-6 candidates
|
|
19
|
-
3. **Memory Evolution** — up to 3 linked notes
|
|
20
|
-
4. **Hybrid Retrieval** — fuses dense vectors (Transformers.js `bge-m3`, 1024-dim) and BM25 via Reciprocal Rank Fusion (RRF)
|
|
21
|
-
5. **2-hop BFS Graph Expansion** — after RRF top-K, BFS walks the link graph up to 2 hops
|
|
17
|
+
1. **Note Construction** — an LLM extracts keywords, tags, and a context summary, categorizes the note, and classifies it as `memory` (episodic) or `knowledge` (durable). For knowledge notes, the LLM extracts 1–5 `topics`.
|
|
18
|
+
2. **Link Generation** — the engine retrieves the top-6 candidates. The LLM judges whether to link them bidirectionally (similarity > 0.3).
|
|
19
|
+
3. **Memory Evolution** — the engine evolves the attributes of up to 3 linked notes from the new context. This can trigger further links.
|
|
20
|
+
4. **Hybrid Retrieval** — the engine fuses dense vectors (Transformers.js `bge-m3`, 1024-dim) and BM25 via Reciprocal Rank Fusion (RRF). Retrieval heat boosts the result.
|
|
21
|
+
5. **2-hop BFS Graph Expansion** — after the RRF top-K, BFS walks the link graph up to 2 hops and admits up to 8 graph-connected notes that pass an embedding relevance gate (cos-sim ≥ 0.25). This is the key advantage over flat vector systems.
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
25
|
- 🔄 **Dynamic memory network** (Zettelkasten-inspired) — notes are graph nodes with bidirectional links, not flat rows.
|
|
26
|
-
- 🧬 **Evolution & strengthening** — linked notes update context/tags/embeddings when new details arrive
|
|
27
|
-
- 🚦 **LLM CRUD gate** — analyzes a user↔assistant exchange and decides `NEW` / `UPDATE` / `DELETE` / `NONE` to keep memory clean.
|
|
28
|
-
- 🧹 **Same-day merge + daily consolidation** — merges semantic duplicates (≥ 0.80 same-day
|
|
29
|
-
- ⏳ **Temporal soft-delete** — outdated
|
|
30
|
-
- 🔥 **Heat tracking with time decay** — `retrieval_count` + `last_accessed` give a logarithmic boost
|
|
26
|
+
- 🧬 **Evolution & strengthening** — linked notes update context/tags/embeddings when new details arrive. The `evolution_history` is a full audit trail.
|
|
27
|
+
- 🚦 **LLM CRUD gate** — the LLM analyzes a user↔assistant exchange and decides `NEW` / `UPDATE` / `DELETE` / `NONE` to keep memory clean.
|
|
28
|
+
- 🧹 **Same-day merge + daily consolidation** — the engine merges semantic duplicates (≥ 0.80 same-day, ≥ 0.75 in the 02:30 sweep) and **cascades link references** to preserve graph topology.
|
|
29
|
+
- ⏳ **Temporal soft-delete** — the engine marks outdated and conflicting notes `is_active: false` (zero-migration Qdrant filter) and excludes them from search.
|
|
30
|
+
- 🔥 **Heat tracking with time decay** — `retrieval_count` + `last_accessed` give a logarithmic boost. Age dampens the boost so stale notes do not permanently outrank fresh ones:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
Final Score = RRF Score × (1 + 0.05 × ln(1 + retrieval_count) / (age_days + 1))
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
- 🔍 **2-hop graph traversal with relevance gate** — BFS from anchors
|
|
37
|
-
- 🀄 **Chinese-optimized BM25** — [Jieba](https://github.com/fxsjy/jieba) (`@node-rs/jieba`) word segmentation for
|
|
38
|
-
- 🧠 **Knowledge vs episodic** — `note_type` separates durable `knowledge` (skips consolidation-merge + time-decay) from `memory
|
|
39
|
-
- 🔐 **Multi-agent isolation** — explicit `owner` / `readers` / `writers`
|
|
40
|
-
- 📊 **Quality controls** — write-time gate rejects
|
|
36
|
+
- 🔍 **2-hop graph traversal with relevance gate** — BFS walks from anchors and admits only nodes with cos-sim ≥ 0.25 to the query.
|
|
37
|
+
- 🀄 **Chinese-optimized BM25** — [Jieba](https://github.com/fxsjy/jieba) (`@node-rs/jieba`) provides word segmentation for Chinese. Whitespace segmentation is the fallback elsewhere. Japanese and Korean get no lexical tokens, but dense retrieval still covers them.
|
|
38
|
+
- 🧠 **Knowledge vs episodic** — `note_type` separates durable `knowledge` (skips consolidation-merge + time-decay) from `memory`. The `topics` tags and `topicsFilter` enable subject-level recall.
|
|
39
|
+
- 🔐 **Multi-agent isolation** — every note has explicit `owner` / `readers` / `writers` fields. Mode A uses a shared collection filtered by `agent_id`. Mode B uses a dedicated collection.
|
|
40
|
+
- 📊 **Quality controls** — the write-time gate rejects content shorter than 10 characters and flags ephemeral notes. `scanLowQuality` finds too-short, expired, and conflicting notes.
|
|
41
41
|
|
|
42
42
|
## Architecture
|
|
43
43
|
|
|
@@ -53,7 +53,7 @@ host (OpenClaw plugin / amem-api / game agent)
|
|
|
53
53
|
|
|
54
54
|
## Memory Evolution
|
|
55
55
|
|
|
56
|
-
When a new note is borderline-similar to an existing one (cosine 0.72–0.85), @amemhq/core routes it through an **LLM evolution judgment** instead of naive dedup
|
|
56
|
+
When a new note is borderline-similar to an existing one (cosine 0.72–0.85), @amemhq/core routes it through an **LLM evolution judgment** instead of naive dedup. The LLM classifies the relationship:
|
|
57
57
|
|
|
58
58
|
| Type | Meaning | Action |
|
|
59
59
|
| --- | --- | --- |
|
|
@@ -62,12 +62,12 @@ When a new note is borderline-similar to an existing one (cosine 0.72–0.85), @
|
|
|
62
62
|
| **EXPAND** | New info complements the old | Content merged into old note, history appended, new note absorbed |
|
|
63
63
|
| **NEW** | Unrelated | Both kept as-is |
|
|
64
64
|
|
|
65
|
-
Memories **evolve** rather than being silently overwritten
|
|
65
|
+
Memories **evolve** rather than being silently overwritten. The `evolution_history` is a full audit trail. (Taxonomy per the SSGM framework, arXiv:2603.11768.)
|
|
66
66
|
|
|
67
67
|
## Quality Scoring
|
|
68
68
|
|
|
69
|
-
- **Write-time gate** (`checkQuality`) — content
|
|
70
|
-
- **Periodic scan** (`scanLowQuality` / `generateReviewBatch`) — flags `too_short`, `expired_ephemeral` (> 7 days), and `pending_conflict
|
|
69
|
+
- **Write-time gate** (`checkQuality`) — the gate rejects content shorter than 10 chars. Temporal signal words (`待跑`, `等确认`, `昨日`, `明天完成`) flag the note `ephemeral: true`.
|
|
70
|
+
- **Periodic scan** (`scanLowQuality` / `generateReviewBatch`) — flags `too_short`, `expired_ephemeral` (> 7 days), and `pending_conflict`. It patches `low_quality: true` and emits an Obsidian-compatible review batch.
|
|
71
71
|
|
|
72
72
|
## Multi-Agent Isolation
|
|
73
73
|
|
|
@@ -77,10 +77,10 @@ Every `MemoryNote` carries access fields:
|
|
|
77
77
|
{ owner: 'main', readers: ['main'], writers: ['main'] } // readers: ['*'] = shared with all agents
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
- **Mode A** (default) — one shared Qdrant collection, isolated by `agent_id` at query time
|
|
80
|
+
- **Mode A** (default) — one shared Qdrant collection, isolated by `agent_id` at query time. Setting `agent_id="shared"` (explicit, auditable) publishes a note to all agents.
|
|
81
81
|
- **Mode B** — a dedicated collection per agent for full physical isolation.
|
|
82
82
|
|
|
83
|
-
Isolation is the default
|
|
83
|
+
Isolation is the default. Sharing is an explicit exception (per arXiv:2604.16548). Consolidation runs per-agent scope.
|
|
84
84
|
|
|
85
85
|
## Usage
|
|
86
86
|
|
|
@@ -98,7 +98,7 @@ const hits = await searchMemory('what does the player like to build with?', 5, '
|
|
|
98
98
|
|
|
99
99
|
- Node.js 24 (18+ works)
|
|
100
100
|
- [Qdrant](https://qdrant.tech) on `:6333`
|
|
101
|
-
- An LLM for note
|
|
101
|
+
- An LLM for note, link, and evolution calls — `ANTHROPIC_API_KEY` is the default. To use any OpenAI-compatible endpoint (OpenAI, DeepSeek, OpenRouter, Ollama, vLLM…), set `AMEM_LLM_PROVIDER=openai` with `AMEM_LLM_BASE_URL` and `AMEM_LLM_API_KEY`.
|
|
102
102
|
|
|
103
103
|
## References & Citation
|
|
104
104
|
|
|
@@ -7,6 +7,22 @@ function canRead(note, callerAgentId) {
|
|
|
7
7
|
return note.owner === callerAgentId || note.readers.includes(callerAgentId) || note.readers.includes("*");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import * as os from "os";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
var _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), ".amem");
|
|
14
|
+
var _warn = (msg) => console.warn(msg);
|
|
15
|
+
function configure(opts) {
|
|
16
|
+
if (opts.dataDir) _dataDir = opts.dataDir;
|
|
17
|
+
if (opts.warn) _warn = opts.warn;
|
|
18
|
+
}
|
|
19
|
+
function getDataDir() {
|
|
20
|
+
return _dataDir;
|
|
21
|
+
}
|
|
22
|
+
function warn(msg) {
|
|
23
|
+
_warn(msg);
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
// src/embedding.ts
|
|
11
27
|
var pipeline = null;
|
|
12
28
|
var extractor = null;
|
|
@@ -63,6 +79,12 @@ function applyModelPaths(env) {
|
|
|
63
79
|
env.allowLocalModels = true;
|
|
64
80
|
}
|
|
65
81
|
}
|
|
82
|
+
function humanBytes(n) {
|
|
83
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)} GB`;
|
|
84
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)} MB`;
|
|
85
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)} kB`;
|
|
86
|
+
return `${n} B`;
|
|
87
|
+
}
|
|
66
88
|
function makeProgressReporter() {
|
|
67
89
|
const lastPct = /* @__PURE__ */ new Map();
|
|
68
90
|
return (e) => {
|
|
@@ -71,7 +93,7 @@ function makeProgressReporter() {
|
|
|
71
93
|
const pct = Math.floor(e.progress / 10) * 10;
|
|
72
94
|
if (lastPct.get(e.file) === pct) return;
|
|
73
95
|
lastPct.set(e.file, pct);
|
|
74
|
-
const size = e.total ? ` of ${(e.total
|
|
96
|
+
const size = e.total ? ` of ${humanBytes(e.total)}` : "";
|
|
75
97
|
console.log(`[amem] downloading ${e.file}: ${pct}%${size}`);
|
|
76
98
|
};
|
|
77
99
|
}
|
|
@@ -283,7 +305,7 @@ async function ensureCollection(collectionName) {
|
|
|
283
305
|
if (inUse !== null && inUse !== wanted) throw new MixedEmbeddingModelsError(col, wanted, inUse);
|
|
284
306
|
pinEmbeddingModel(wanted);
|
|
285
307
|
if (wanted === LEGACY_DEFAULT_EMBEDDING_MODEL) {
|
|
286
|
-
|
|
308
|
+
warn(
|
|
287
309
|
`[amem] "${col}" is on ${LEGACY_DEFAULT_EMBEDDING_MODEL} (${LEGACY_DEFAULT_DIM}-dim).
|
|
288
310
|
[amem] ${DEFAULT_EMBEDDING_MODEL} reads 8192 tokens where that one stops at 128, so anything longer is being truncated before it reaches the vector.
|
|
289
311
|
[amem] To move: npx --package=@amemhq/core amem-migrate --from-collection ${col}`
|
|
@@ -687,7 +709,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
687
709
|
})
|
|
688
710
|
)
|
|
689
711
|
]).catch((err) => {
|
|
690
|
-
|
|
712
|
+
warn(`[amem] retrieval tracking patch failed: ${err.message}`);
|
|
691
713
|
});
|
|
692
714
|
for (const r of queryResults) {
|
|
693
715
|
r.note.retrieval_count = (r.note.retrieval_count || 0) + 1;
|
|
@@ -1054,7 +1076,7 @@ var _warned = /* @__PURE__ */ new Set();
|
|
|
1054
1076
|
function warnOnce(key, message) {
|
|
1055
1077
|
if (_warned.has(key)) return;
|
|
1056
1078
|
_warned.add(key);
|
|
1057
|
-
|
|
1079
|
+
warn(message);
|
|
1058
1080
|
}
|
|
1059
1081
|
function resolveProvider(role = "fast") {
|
|
1060
1082
|
const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
|
|
@@ -1126,9 +1148,11 @@ async function llmCall(prompt, maxTokens = 500, role = "fast") {
|
|
|
1126
1148
|
const isThinking = model.includes("gemini") || model.includes("pro-agent");
|
|
1127
1149
|
const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
|
|
1128
1150
|
try {
|
|
1129
|
-
|
|
1151
|
+
const text = provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
|
|
1152
|
+
if (text === null) warn(`[amem] ${provider} answered with no text (model ${model})`);
|
|
1153
|
+
return text;
|
|
1130
1154
|
} catch (e) {
|
|
1131
|
-
|
|
1155
|
+
warn(`[amem] LLM call failed: ${e.message}`);
|
|
1132
1156
|
return null;
|
|
1133
1157
|
}
|
|
1134
1158
|
}
|
|
@@ -1227,7 +1251,8 @@ confidence guide (Story 27):
|
|
|
1227
1251
|
|
|
1228
1252
|
Text: ${content}`;
|
|
1229
1253
|
const raw = await llmCall(prompt, 400);
|
|
1230
|
-
if (!raw)
|
|
1254
|
+
if (!raw) {
|
|
1255
|
+
warn("[amem] note construction got nothing back; storing with no keywords, tags or context");
|
|
1231
1256
|
return {
|
|
1232
1257
|
keywords: [],
|
|
1233
1258
|
tags: [],
|
|
@@ -1237,6 +1262,7 @@ Text: ${content}`;
|
|
|
1237
1262
|
topics: [],
|
|
1238
1263
|
confidence: "medium"
|
|
1239
1264
|
};
|
|
1265
|
+
}
|
|
1240
1266
|
try {
|
|
1241
1267
|
const data = parseJsonLoose(raw);
|
|
1242
1268
|
const rawCategory = typeof data.category === "string" ? data.category : "General";
|
|
@@ -1255,7 +1281,7 @@ Text: ${content}`;
|
|
|
1255
1281
|
confidence
|
|
1256
1282
|
};
|
|
1257
1283
|
} catch (e) {
|
|
1258
|
-
|
|
1284
|
+
warn(`[amem] Note construction parse failed: ${e.message}`);
|
|
1259
1285
|
return {
|
|
1260
1286
|
keywords: [],
|
|
1261
1287
|
tags: [],
|
|
@@ -1284,9 +1310,15 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
|
1284
1310
|
const raw = await llmCall(prompt, 400, resolveCrudRole());
|
|
1285
1311
|
if (!raw) return [];
|
|
1286
1312
|
const match = stripReasoning(raw).match(/\[.*\]/s);
|
|
1287
|
-
if (!match)
|
|
1313
|
+
if (!match) {
|
|
1314
|
+
warn("[amem] llmCrudDecision found no array in the response; nothing from this turn is stored");
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1288
1317
|
const parsed = JSON.parse(match[0]);
|
|
1289
|
-
if (!Array.isArray(parsed))
|
|
1318
|
+
if (!Array.isArray(parsed)) {
|
|
1319
|
+
warn("[amem] llmCrudDecision parsed a non-array; nothing from this turn is stored");
|
|
1320
|
+
return [];
|
|
1321
|
+
}
|
|
1290
1322
|
const ops = [];
|
|
1291
1323
|
for (const item of parsed) {
|
|
1292
1324
|
if (!item || typeof item !== "object") continue;
|
|
@@ -1305,7 +1337,7 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
|
|
|
1305
1337
|
}
|
|
1306
1338
|
return ops.slice(0, 3);
|
|
1307
1339
|
} catch (e) {
|
|
1308
|
-
|
|
1340
|
+
warn(`[amem] llmCrudDecision failed: ${e.message}`);
|
|
1309
1341
|
return [];
|
|
1310
1342
|
}
|
|
1311
1343
|
}
|
|
@@ -1315,13 +1347,16 @@ async function llmShouldMerge(contentA, contentB) {
|
|
|
1315
1347
|
if (!raw) return { shouldMerge: false };
|
|
1316
1348
|
try {
|
|
1317
1349
|
const data = parseJsonLoose(raw);
|
|
1318
|
-
if (typeof data.shouldMerge !== "boolean")
|
|
1350
|
+
if (typeof data.shouldMerge !== "boolean") {
|
|
1351
|
+
warn("[amem] llmShouldMerge got no boolean verdict; treating the pair as distinct");
|
|
1352
|
+
return { shouldMerge: false };
|
|
1353
|
+
}
|
|
1319
1354
|
if (data.shouldMerge && typeof data.merged === "string") {
|
|
1320
1355
|
return { shouldMerge: true, merged: data.merged };
|
|
1321
1356
|
}
|
|
1322
1357
|
return { shouldMerge: false };
|
|
1323
1358
|
} catch (e) {
|
|
1324
|
-
|
|
1359
|
+
warn(`[amem] llmShouldMerge parse failed: ${e.message}`);
|
|
1325
1360
|
return { shouldMerge: false };
|
|
1326
1361
|
}
|
|
1327
1362
|
}
|
|
@@ -1329,7 +1364,10 @@ var VALID_EVOLUTION_TYPES = /* @__PURE__ */ new Set(["EVOLVE", "CONFLICT", "EXPA
|
|
|
1329
1364
|
async function llmEvolutionJudge(oldContent, newContent) {
|
|
1330
1365
|
const prompt = t.evolutionJudge(oldContent, newContent);
|
|
1331
1366
|
const raw = await llmCall(prompt, 300, "strong");
|
|
1332
|
-
if (!raw)
|
|
1367
|
+
if (!raw) {
|
|
1368
|
+
warn("[amem] evolution judge got nothing back; defaulting the pair to NEW");
|
|
1369
|
+
return { type: "NEW" };
|
|
1370
|
+
}
|
|
1333
1371
|
try {
|
|
1334
1372
|
const data = parseJsonLoose(raw);
|
|
1335
1373
|
const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
|
|
@@ -1338,7 +1376,7 @@ async function llmEvolutionJudge(oldContent, newContent) {
|
|
|
1338
1376
|
mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
|
|
1339
1377
|
};
|
|
1340
1378
|
} catch (e) {
|
|
1341
|
-
|
|
1379
|
+
warn(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
|
|
1342
1380
|
return { type: "NEW" };
|
|
1343
1381
|
}
|
|
1344
1382
|
}
|
|
@@ -1377,7 +1415,7 @@ ${linkedStr}`;
|
|
|
1377
1415
|
tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
|
|
1378
1416
|
};
|
|
1379
1417
|
} catch (e) {
|
|
1380
|
-
|
|
1418
|
+
warn(`[amem] Evolution parse failed: ${e.message}`);
|
|
1381
1419
|
return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
|
|
1382
1420
|
}
|
|
1383
1421
|
}
|
|
@@ -1389,9 +1427,15 @@ async function llmConflictScan(contents) {
|
|
|
1389
1427
|
if (!raw) return [];
|
|
1390
1428
|
const cleaned = stripReasoning(raw);
|
|
1391
1429
|
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
1392
|
-
if (!match)
|
|
1430
|
+
if (!match) {
|
|
1431
|
+
warn("[amem] llmConflictScan found no array in the response; this sweep reports no pairs");
|
|
1432
|
+
return [];
|
|
1433
|
+
}
|
|
1393
1434
|
const parsed = JSON.parse(match[0]);
|
|
1394
|
-
if (!Array.isArray(parsed))
|
|
1435
|
+
if (!Array.isArray(parsed)) {
|
|
1436
|
+
warn("[amem] llmConflictScan parsed a non-array; this sweep reports no pairs");
|
|
1437
|
+
return [];
|
|
1438
|
+
}
|
|
1395
1439
|
const pairs = [];
|
|
1396
1440
|
const seen = /* @__PURE__ */ new Set();
|
|
1397
1441
|
for (const item of parsed) {
|
|
@@ -1415,22 +1459,11 @@ async function llmConflictScan(contents) {
|
|
|
1415
1459
|
}
|
|
1416
1460
|
return pairs;
|
|
1417
1461
|
} catch (e) {
|
|
1418
|
-
|
|
1462
|
+
warn(`[amem] llmConflictScan failed: ${e.message}`);
|
|
1419
1463
|
return [];
|
|
1420
1464
|
}
|
|
1421
1465
|
}
|
|
1422
1466
|
|
|
1423
|
-
// src/config.ts
|
|
1424
|
-
import * as os from "os";
|
|
1425
|
-
import * as path from "path";
|
|
1426
|
-
var _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), ".amem");
|
|
1427
|
-
function configure(opts) {
|
|
1428
|
-
if (opts.dataDir) _dataDir = opts.dataDir;
|
|
1429
|
-
}
|
|
1430
|
-
function getDataDir() {
|
|
1431
|
-
return _dataDir;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
1467
|
// src/memory.ts
|
|
1435
1468
|
import { v4 as uuidv4 } from "uuid";
|
|
1436
1469
|
import { createHash } from "crypto";
|
|
@@ -1746,7 +1779,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1746
1779
|
}
|
|
1747
1780
|
}
|
|
1748
1781
|
} catch (e) {
|
|
1749
|
-
|
|
1782
|
+
warn(`[amem] link/evolution phase failed: ${e.message}`);
|
|
1750
1783
|
}
|
|
1751
1784
|
console.log(`[done] Note added: ${note.id}`);
|
|
1752
1785
|
return note.id;
|
|
@@ -1871,7 +1904,12 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1871
1904
|
keywords: note.keywords,
|
|
1872
1905
|
links: note.links,
|
|
1873
1906
|
timestamp: note.timestamp,
|
|
1874
|
-
|
|
1907
|
+
// Neither map covers a note that got here on BM25 alone: it was never in
|
|
1908
|
+
// the dense results, and it was not expanded into. That is a real cosine
|
|
1909
|
+
// nobody had measured, not a zero — and reporting 0 made a lexical match
|
|
1910
|
+
// look like the least relevant row in the list. Both vectors are already
|
|
1911
|
+
// in hand, so measuring it is one dot product.
|
|
1912
|
+
similarity: embSimMap.get(id) ?? bfsSimMap.get(id) ?? cosineSimilarity(queryEmbedding, note.embedding),
|
|
1875
1913
|
rrf: rrfMap.get(id) ?? 0,
|
|
1876
1914
|
via,
|
|
1877
1915
|
topics: note.topics ?? [],
|
|
@@ -2007,8 +2045,8 @@ async function consolidateMemories(agentId, logger, storageCtx) {
|
|
|
2007
2045
|
const ctx = storageCtx ?? defaultCtx();
|
|
2008
2046
|
const log = {
|
|
2009
2047
|
info: (msg) => logger ? logger.info(msg) : console.log(msg),
|
|
2010
|
-
warn: (msg) => logger ? logger.warn(msg) :
|
|
2011
|
-
error: (msg) => logger ? logger.error(msg) :
|
|
2048
|
+
warn: (msg) => logger ? logger.warn(msg) : warn(msg),
|
|
2049
|
+
error: (msg) => logger ? logger.error(msg) : warn(msg)
|
|
2012
2050
|
};
|
|
2013
2051
|
log.info(`[Consolidation] Starting consolidation for agentId: ${agentId}`);
|
|
2014
2052
|
const rawNotes = await ctx.listNotes(agentId);
|
|
@@ -2202,7 +2240,7 @@ async function migrateCollection(opts) {
|
|
|
2202
2240
|
const refreshFields = opts.refreshFields !== false;
|
|
2203
2241
|
const dryRun = opts.dryRun !== false;
|
|
2204
2242
|
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
2205
|
-
const
|
|
2243
|
+
const warn2 = opts.logger?.warn ?? warn;
|
|
2206
2244
|
if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
|
|
2207
2245
|
const model = getEmbeddingModel();
|
|
2208
2246
|
const targetDim = await getEmbeddingDim();
|
|
@@ -2270,7 +2308,7 @@ async function migrateCollection(opts) {
|
|
|
2270
2308
|
if (!note.context) note.context = built.context;
|
|
2271
2309
|
refreshed++;
|
|
2272
2310
|
} catch (e) {
|
|
2273
|
-
|
|
2311
|
+
warn2(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
|
|
2274
2312
|
}
|
|
2275
2313
|
}
|
|
2276
2314
|
const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
|
|
@@ -2283,7 +2321,7 @@ async function migrateCollection(opts) {
|
|
|
2283
2321
|
await flush();
|
|
2284
2322
|
const finalCount = await countPointsRaw(to);
|
|
2285
2323
|
if (finalCount !== notes.length) {
|
|
2286
|
-
|
|
2324
|
+
warn2(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
|
|
2287
2325
|
}
|
|
2288
2326
|
log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
|
|
2289
2327
|
return {
|
|
@@ -2336,6 +2374,7 @@ export {
|
|
|
2336
2374
|
SYSTEM_ACTOR,
|
|
2337
2375
|
canWrite,
|
|
2338
2376
|
canRead,
|
|
2377
|
+
configure,
|
|
2339
2378
|
DEFAULT_EMBEDDING_MODEL,
|
|
2340
2379
|
LEGACY_DEFAULT_EMBEDDING_MODEL,
|
|
2341
2380
|
LEGACY_DEFAULT_DIM,
|
|
@@ -2367,7 +2406,6 @@ export {
|
|
|
2367
2406
|
patchNotePayload,
|
|
2368
2407
|
configureLlm,
|
|
2369
2408
|
llmCrudDecision,
|
|
2370
|
-
configure,
|
|
2371
2409
|
checkQuality,
|
|
2372
2410
|
addMemory,
|
|
2373
2411
|
addEpisodic,
|
|
@@ -2379,4 +2417,4 @@ export {
|
|
|
2379
2417
|
migrateCollection,
|
|
2380
2418
|
switchToMigrated
|
|
2381
2419
|
};
|
|
2382
|
-
//# sourceMappingURL=chunk-
|
|
2420
|
+
//# sourceMappingURL=chunk-Z7G7KOJK.js.map
|