@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 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
- Extracted from [`openclaw-amem`](https://github.com/amemhq/amem/tree/main/packages/openclaw-amem) so any host can share one memory engine: an OpenClaw plugin, a standalone service ([`amem-api`](../amem-api)), or a game agent. Part of the [amem monorepo](../../).
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; categorizes the note; and classifies it as `memory` (episodic) or `knowledge` (durable), extracting 1–5 `topics` for knowledge notes.
18
- 2. **Link Generation** — retrieves top-6 candidates; the LLM judges whether to link bidirectionally (similarity > 0.3).
19
- 3. **Memory Evolution** — up to 3 linked notes have their attributes evolved from the new context, possibly triggering further links.
20
- 4. **Hybrid Retrieval** — fuses dense vectors (Transformers.js `bge-m3`, 1024-dim) and BM25 via Reciprocal Rank Fusion (RRF), boosted by retrieval heat.
21
- 5. **2-hop BFS Graph Expansion** — after RRF top-K, BFS walks the link graph up to 2 hops, admitting 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.
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; `evolution_history` audit trail.
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; ≥ 0.75 in the 02:30 sweep) and **cascades link references** to preserve graph topology.
29
- - ⏳ **Temporal soft-delete** — outdated/conflicting notes are marked `is_active: false` (zero-migration Qdrant filter) and excluded from search.
30
- - 🔥 **Heat tracking with time decay** — `retrieval_count` + `last_accessed` give a logarithmic boost, dampened by age so stale notes don't permanently outrank fresh ones:
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, admitting only nodes with cos-sim ≥ 0.25 to the query.
37
- - 🀄 **Chinese-optimized BM25** — [Jieba](https://github.com/fxsjy/jieba) (`@node-rs/jieba`) word segmentation for CJK; whitespace fallback for other languages.
38
- - 🧠 **Knowledge vs episodic** — `note_type` separates durable `knowledge` (skips consolidation-merge + time-decay) from `memory`; `topics` tags + `topicsFilter` enable subject-level recall.
39
- - 🔐 **Multi-agent isolation** — explicit `owner` / `readers` / `writers` on every note; Mode A (shared collection filtered by `agent_id`) or Mode B (dedicated collection).
40
- - 📊 **Quality controls** — write-time gate rejects < 10-char content and flags ephemeral notes; `scanLowQuality` finds too-short/expired/conflicting notes.
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, classifying the relationship:
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; `evolution_history` is a full audit trail. (Taxonomy per the SSGM framework, arXiv:2603.11768.)
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 < 10 chars is rejected; temporal signal words (`待跑`, `等确认`, `昨日`, `明天完成`) flag the note `ephemeral: true`.
70
- - **Periodic scan** (`scanLowQuality` / `generateReviewBatch`) — flags `too_short`, `expired_ephemeral` (> 7 days), and `pending_conflict`, patching `low_quality: true` and emitting an Obsidian-compatible review batch.
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; `agent_id="shared"` (explicit, auditable) publishes a note to all agents.
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; sharing is an explicit exception (per arXiv:2604.16548). Consolidation runs per-agent scope.
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/link/evolution calls: `ANTHROPIC_API_KEY` by default, or set `AMEM_LLM_PROVIDER=openai` with `AMEM_LLM_BASE_URL` + `AMEM_LLM_API_KEY` to use any OpenAI-compatible endpoint (OpenAI, DeepSeek, OpenRouter, Ollama, vLLM…)
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 / 1e9).toFixed(2)} GB` : "";
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
- console.warn(
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
- console.error(`[amem] retrieval tracking patch failed: ${err.message}`);
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
- console.error(message);
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
- return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
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
- console.error(`[amem] LLM call failed: ${e.message}`);
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
- console.error(`[amem] Note construction parse failed: ${e.message}`);
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) return [];
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)) return [];
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
- console.error(`[amem] llmCrudDecision failed: ${e.message}`);
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") return { shouldMerge: false };
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
- console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
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) return { type: "NEW" };
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
- console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
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
- console.error(`[amem] Evolution parse failed: ${e.message}`);
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) return [];
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)) return [];
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
- console.error(`[amem] llmConflictScan failed: ${e.message}`);
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
- console.error(`[warn] Link/Evolution phase failed: ${e.message}`);
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
- similarity: embSimMap.get(id) ?? bfsSimMap.get(id) ?? 0,
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) : console.warn(msg),
2011
- error: (msg) => logger ? logger.error(msg) : console.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 warn = opts.logger?.warn ?? ((m) => console.warn(m));
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
- warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
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
- warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
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-XEMQZNLD.js.map
2420
+ //# sourceMappingURL=chunk-Z7G7KOJK.js.map