@mnemoai/core 1.1.0 → 1.1.1

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.
Files changed (220) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +7 -0
  4. package/dist/cli.js.map +7 -0
  5. package/dist/index.d.ts +128 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/{index.ts → dist/index.js} +526 -1333
  8. package/dist/index.js.map +7 -0
  9. package/dist/src/access-tracker.d.ts +97 -0
  10. package/dist/src/access-tracker.d.ts.map +1 -0
  11. package/dist/src/access-tracker.js +184 -0
  12. package/dist/src/access-tracker.js.map +7 -0
  13. package/dist/src/adapters/chroma.d.ts +31 -0
  14. package/dist/src/adapters/chroma.d.ts.map +1 -0
  15. package/{src/adapters/chroma.ts → dist/src/adapters/chroma.js} +45 -107
  16. package/dist/src/adapters/chroma.js.map +7 -0
  17. package/dist/src/adapters/lancedb.d.ts +29 -0
  18. package/dist/src/adapters/lancedb.d.ts.map +1 -0
  19. package/{src/adapters/lancedb.ts → dist/src/adapters/lancedb.js} +41 -109
  20. package/dist/src/adapters/lancedb.js.map +7 -0
  21. package/dist/src/adapters/pgvector.d.ts +33 -0
  22. package/dist/src/adapters/pgvector.d.ts.map +1 -0
  23. package/{src/adapters/pgvector.ts → dist/src/adapters/pgvector.js} +42 -104
  24. package/dist/src/adapters/pgvector.js.map +7 -0
  25. package/dist/src/adapters/qdrant.d.ts +34 -0
  26. package/dist/src/adapters/qdrant.d.ts.map +1 -0
  27. package/dist/src/adapters/qdrant.js +132 -0
  28. package/dist/src/adapters/qdrant.js.map +7 -0
  29. package/dist/src/adaptive-retrieval.d.ts +14 -0
  30. package/dist/src/adaptive-retrieval.d.ts.map +1 -0
  31. package/dist/src/adaptive-retrieval.js +52 -0
  32. package/dist/src/adaptive-retrieval.js.map +7 -0
  33. package/dist/src/audit-log.d.ts +56 -0
  34. package/dist/src/audit-log.d.ts.map +1 -0
  35. package/dist/src/audit-log.js +139 -0
  36. package/dist/src/audit-log.js.map +7 -0
  37. package/dist/src/chunker.d.ts +45 -0
  38. package/dist/src/chunker.d.ts.map +1 -0
  39. package/dist/src/chunker.js +157 -0
  40. package/dist/src/chunker.js.map +7 -0
  41. package/dist/src/config.d.ts +70 -0
  42. package/dist/src/config.d.ts.map +1 -0
  43. package/dist/src/config.js +142 -0
  44. package/dist/src/config.js.map +7 -0
  45. package/dist/src/decay-engine.d.ts +73 -0
  46. package/dist/src/decay-engine.d.ts.map +1 -0
  47. package/dist/src/decay-engine.js +119 -0
  48. package/dist/src/decay-engine.js.map +7 -0
  49. package/dist/src/embedder.d.ts +94 -0
  50. package/dist/src/embedder.d.ts.map +1 -0
  51. package/{src/embedder.ts → dist/src/embedder.js} +119 -317
  52. package/dist/src/embedder.js.map +7 -0
  53. package/dist/src/extraction-prompts.d.ts +12 -0
  54. package/dist/src/extraction-prompts.d.ts.map +1 -0
  55. package/dist/src/extraction-prompts.js +311 -0
  56. package/dist/src/extraction-prompts.js.map +7 -0
  57. package/dist/src/license.d.ts +29 -0
  58. package/dist/src/license.d.ts.map +1 -0
  59. package/{src/license.ts → dist/src/license.js} +42 -113
  60. package/dist/src/license.js.map +7 -0
  61. package/dist/src/llm-client.d.ts +23 -0
  62. package/dist/src/llm-client.d.ts.map +1 -0
  63. package/{src/llm-client.ts → dist/src/llm-client.js} +22 -55
  64. package/dist/src/llm-client.js.map +7 -0
  65. package/dist/src/logger.d.ts +33 -0
  66. package/dist/src/logger.d.ts.map +1 -0
  67. package/dist/src/logger.js +35 -0
  68. package/dist/src/logger.js.map +7 -0
  69. package/dist/src/mcp-server.d.ts +16 -0
  70. package/dist/src/mcp-server.d.ts.map +1 -0
  71. package/{src/mcp-server.ts → dist/src/mcp-server.js} +81 -181
  72. package/dist/src/mcp-server.js.map +7 -0
  73. package/dist/src/memory-categories.d.ts +40 -0
  74. package/dist/src/memory-categories.d.ts.map +1 -0
  75. package/dist/src/memory-categories.js +33 -0
  76. package/dist/src/memory-categories.js.map +7 -0
  77. package/dist/src/memory-upgrader.d.ts +71 -0
  78. package/dist/src/memory-upgrader.d.ts.map +1 -0
  79. package/dist/src/memory-upgrader.js +238 -0
  80. package/dist/src/memory-upgrader.js.map +7 -0
  81. package/dist/src/migrate.d.ts +47 -0
  82. package/dist/src/migrate.d.ts.map +1 -0
  83. package/{src/migrate.ts → dist/src/migrate.js} +57 -165
  84. package/dist/src/migrate.js.map +7 -0
  85. package/dist/src/mnemo.d.ts +67 -0
  86. package/dist/src/mnemo.d.ts.map +1 -0
  87. package/dist/src/mnemo.js +66 -0
  88. package/dist/src/mnemo.js.map +7 -0
  89. package/dist/src/noise-filter.d.ts +23 -0
  90. package/dist/src/noise-filter.d.ts.map +1 -0
  91. package/dist/src/noise-filter.js +62 -0
  92. package/dist/src/noise-filter.js.map +7 -0
  93. package/dist/src/noise-prototypes.d.ts +40 -0
  94. package/dist/src/noise-prototypes.d.ts.map +1 -0
  95. package/dist/src/noise-prototypes.js +116 -0
  96. package/dist/src/noise-prototypes.js.map +7 -0
  97. package/dist/src/observability.d.ts +16 -0
  98. package/dist/src/observability.d.ts.map +1 -0
  99. package/dist/src/observability.js +53 -0
  100. package/dist/src/observability.js.map +7 -0
  101. package/dist/src/query-tracker.d.ts +27 -0
  102. package/dist/src/query-tracker.d.ts.map +1 -0
  103. package/dist/src/query-tracker.js +32 -0
  104. package/dist/src/query-tracker.js.map +7 -0
  105. package/dist/src/reflection-event-store.d.ts +44 -0
  106. package/dist/src/reflection-event-store.d.ts.map +1 -0
  107. package/dist/src/reflection-event-store.js +50 -0
  108. package/dist/src/reflection-event-store.js.map +7 -0
  109. package/dist/src/reflection-item-store.d.ts +58 -0
  110. package/dist/src/reflection-item-store.d.ts.map +1 -0
  111. package/dist/src/reflection-item-store.js +69 -0
  112. package/dist/src/reflection-item-store.js.map +7 -0
  113. package/dist/src/reflection-mapped-metadata.d.ts +47 -0
  114. package/dist/src/reflection-mapped-metadata.d.ts.map +1 -0
  115. package/dist/src/reflection-mapped-metadata.js +40 -0
  116. package/dist/src/reflection-mapped-metadata.js.map +7 -0
  117. package/dist/src/reflection-metadata.d.ts +11 -0
  118. package/dist/src/reflection-metadata.d.ts.map +1 -0
  119. package/dist/src/reflection-metadata.js +24 -0
  120. package/dist/src/reflection-metadata.js.map +7 -0
  121. package/dist/src/reflection-ranking.d.ts +13 -0
  122. package/dist/src/reflection-ranking.d.ts.map +1 -0
  123. package/{src/reflection-ranking.ts → dist/src/reflection-ranking.js} +12 -21
  124. package/dist/src/reflection-ranking.js.map +7 -0
  125. package/dist/src/reflection-retry.d.ts +30 -0
  126. package/dist/src/reflection-retry.d.ts.map +1 -0
  127. package/{src/reflection-retry.ts → dist/src/reflection-retry.js} +24 -64
  128. package/dist/src/reflection-retry.js.map +7 -0
  129. package/dist/src/reflection-slices.d.ts +42 -0
  130. package/dist/src/reflection-slices.d.ts.map +1 -0
  131. package/{src/reflection-slices.ts → dist/src/reflection-slices.js} +60 -136
  132. package/dist/src/reflection-slices.js.map +7 -0
  133. package/dist/src/reflection-store.d.ts +85 -0
  134. package/dist/src/reflection-store.d.ts.map +1 -0
  135. package/dist/src/reflection-store.js +407 -0
  136. package/dist/src/reflection-store.js.map +7 -0
  137. package/dist/src/resonance-state.d.ts +19 -0
  138. package/dist/src/resonance-state.d.ts.map +1 -0
  139. package/{src/resonance-state.ts → dist/src/resonance-state.js} +13 -42
  140. package/dist/src/resonance-state.js.map +7 -0
  141. package/dist/src/retriever.d.ts +228 -0
  142. package/dist/src/retriever.d.ts.map +1 -0
  143. package/dist/src/retriever.js +1006 -0
  144. package/dist/src/retriever.js.map +7 -0
  145. package/dist/src/scopes.d.ts +58 -0
  146. package/dist/src/scopes.d.ts.map +1 -0
  147. package/dist/src/scopes.js +252 -0
  148. package/dist/src/scopes.js.map +7 -0
  149. package/dist/src/self-improvement-files.d.ts +20 -0
  150. package/dist/src/self-improvement-files.d.ts.map +1 -0
  151. package/{src/self-improvement-files.ts → dist/src/self-improvement-files.js} +24 -49
  152. package/dist/src/self-improvement-files.js.map +7 -0
  153. package/dist/src/semantic-gate.d.ts +24 -0
  154. package/dist/src/semantic-gate.d.ts.map +1 -0
  155. package/dist/src/semantic-gate.js +86 -0
  156. package/dist/src/semantic-gate.js.map +7 -0
  157. package/dist/src/session-recovery.d.ts +9 -0
  158. package/dist/src/session-recovery.d.ts.map +1 -0
  159. package/{src/session-recovery.ts → dist/src/session-recovery.js} +40 -57
  160. package/dist/src/session-recovery.js.map +7 -0
  161. package/dist/src/smart-extractor.d.ts +107 -0
  162. package/dist/src/smart-extractor.d.ts.map +1 -0
  163. package/{src/smart-extractor.ts → dist/src/smart-extractor.js} +130 -383
  164. package/dist/src/smart-extractor.js.map +7 -0
  165. package/dist/src/smart-metadata.d.ts +103 -0
  166. package/dist/src/smart-metadata.d.ts.map +1 -0
  167. package/dist/src/smart-metadata.js +361 -0
  168. package/dist/src/smart-metadata.js.map +7 -0
  169. package/dist/src/storage-adapter.d.ts +102 -0
  170. package/dist/src/storage-adapter.d.ts.map +1 -0
  171. package/dist/src/storage-adapter.js +22 -0
  172. package/dist/src/storage-adapter.js.map +7 -0
  173. package/dist/src/store.d.ts +108 -0
  174. package/dist/src/store.d.ts.map +1 -0
  175. package/dist/src/store.js +939 -0
  176. package/dist/src/store.js.map +7 -0
  177. package/dist/src/tier-manager.d.ts +57 -0
  178. package/dist/src/tier-manager.d.ts.map +1 -0
  179. package/dist/src/tier-manager.js +80 -0
  180. package/dist/src/tier-manager.js.map +7 -0
  181. package/dist/src/tools.d.ts +43 -0
  182. package/dist/src/tools.d.ts.map +1 -0
  183. package/dist/src/tools.js +1075 -0
  184. package/dist/src/tools.js.map +7 -0
  185. package/dist/src/wal-recovery.d.ts +30 -0
  186. package/dist/src/wal-recovery.d.ts.map +1 -0
  187. package/{src/wal-recovery.ts → dist/src/wal-recovery.js} +26 -79
  188. package/dist/src/wal-recovery.js.map +7 -0
  189. package/package.json +21 -2
  190. package/openclaw.plugin.json +0 -815
  191. package/src/access-tracker.ts +0 -341
  192. package/src/adapters/README.md +0 -78
  193. package/src/adapters/qdrant.ts +0 -191
  194. package/src/adaptive-retrieval.ts +0 -90
  195. package/src/audit-log.ts +0 -238
  196. package/src/chunker.ts +0 -254
  197. package/src/config.ts +0 -271
  198. package/src/decay-engine.ts +0 -238
  199. package/src/extraction-prompts.ts +0 -339
  200. package/src/memory-categories.ts +0 -71
  201. package/src/memory-upgrader.ts +0 -388
  202. package/src/mnemo.ts +0 -142
  203. package/src/noise-filter.ts +0 -97
  204. package/src/noise-prototypes.ts +0 -164
  205. package/src/observability.ts +0 -81
  206. package/src/query-tracker.ts +0 -57
  207. package/src/reflection-event-store.ts +0 -98
  208. package/src/reflection-item-store.ts +0 -112
  209. package/src/reflection-mapped-metadata.ts +0 -84
  210. package/src/reflection-metadata.ts +0 -23
  211. package/src/reflection-store.ts +0 -602
  212. package/src/retriever.ts +0 -1510
  213. package/src/scopes.ts +0 -375
  214. package/src/semantic-gate.ts +0 -121
  215. package/src/smart-metadata.ts +0 -561
  216. package/src/storage-adapter.ts +0 -153
  217. package/src/store.ts +0 -1330
  218. package/src/tier-manager.ts +0 -189
  219. package/src/tools.ts +0 -1292
  220. package/test/core.test.mjs +0 -301
@@ -0,0 +1,1006 @@
1
+ import { filterNoise } from "./noise-filter.js";
2
+ import { toLifecycleMemory, getDecayableFromEntry } from "./smart-metadata.js";
3
+ import { getAdaptiveThreshold, recordResonanceScore } from "./resonance-state.js";
4
+ import { requirePro } from "./license.js";
5
+ import { log } from "./logger.js";
6
+ let _parseAccessMetadata = null;
7
+ let _computeEffectiveHalfLife = null;
8
+ let _recordQuery = null;
9
+ if (requirePro("access-tracking")) {
10
+ import("./access-tracker.js").then((mod) => {
11
+ _parseAccessMetadata = mod.parseAccessMetadata;
12
+ _computeEffectiveHalfLife = mod.computeEffectiveHalfLife;
13
+ }).catch(() => {
14
+ });
15
+ }
16
+ if (requirePro("query-tracking")) {
17
+ import("./query-tracker.js").then((mod) => {
18
+ _recordQuery = mod.recordQuery;
19
+ }).catch(() => {
20
+ });
21
+ }
22
+ function parseAccessMetadata(m) {
23
+ if (_parseAccessMetadata) return _parseAccessMetadata(m);
24
+ return { accessCount: 0, lastAccessedAt: 0 };
25
+ }
26
+ function computeEffectiveHalfLife(hl, _ac, _la, _rf, _mx) {
27
+ if (_computeEffectiveHalfLife) return _computeEffectiveHalfLife(hl, _ac, _la, _rf, _mx);
28
+ return hl;
29
+ }
30
+ function recordQuery(data) {
31
+ _recordQuery?.(data);
32
+ }
33
+ import { appendFile } from "node:fs/promises";
34
+ import { homedir } from "node:os";
35
+ import { join } from "node:path";
36
+ function getGraphitiConfig() {
37
+ return {
38
+ enabled: process.env.GRAPHITI_ENABLED === "true",
39
+ baseUrl: process.env.GRAPHITI_BASE_URL || "http://127.0.0.1:18799",
40
+ timeoutMs: 3e3
41
+ };
42
+ }
43
+ async function graphitiSpreadSearch(query, groupId = "default", searchLimit = 3, spreadLimit = 3) {
44
+ const cfg = getGraphitiConfig();
45
+ if (!cfg.enabled) return [];
46
+ try {
47
+ const controller = new AbortController();
48
+ const timeout = setTimeout(() => controller.abort(), cfg.timeoutMs);
49
+ const resp = await fetch(`${cfg.baseUrl}/spread`, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({
53
+ query,
54
+ group_id: groupId,
55
+ search_limit: searchLimit,
56
+ spread_depth: 1,
57
+ spread_limit: spreadLimit
58
+ }),
59
+ signal: controller.signal
60
+ });
61
+ clearTimeout(timeout);
62
+ if (!resp.ok) return [];
63
+ const facts = await resp.json();
64
+ if (!Array.isArray(facts) || facts.length === 0) return [];
65
+ const seen = /* @__PURE__ */ new Set();
66
+ const results = [];
67
+ for (let i = 0; i < facts.length; i++) {
68
+ const f = facts[i];
69
+ const factText = f.fact?.trim();
70
+ if (!factText || factText.length < 5 || seen.has(factText)) continue;
71
+ seen.add(factText);
72
+ const nodes = [f.source_node, f.target_node, f.from_node, f.to_node].filter(Boolean).join(" \u2192 ");
73
+ const text = nodes ? `[\u56FE\u8C31] ${factText} (${nodes})` : `[\u56FE\u8C31] ${factText}`;
74
+ const baseScore = f.source === "search" ? 0.75 : 0.45;
75
+ const degreeBoost = f.degree ? Math.min(0.15, Math.log1p(f.degree) * 0.02) : 0;
76
+ results.push({
77
+ entry: {
78
+ id: `graphiti-${i}-${Date.now()}`,
79
+ text,
80
+ category: "entity",
81
+ importance: 0.8,
82
+ timestamp: f.created_at ? new Date(f.created_at).getTime() : Date.now(),
83
+ scope: "global"
84
+ },
85
+ score: Math.min(1, baseScore + degreeBoost),
86
+ rank: i + 1
87
+ });
88
+ }
89
+ return results;
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+ const DEFAULT_RETRIEVAL_CONFIG = {
95
+ mode: "hybrid",
96
+ vectorWeight: 0.7,
97
+ bm25Weight: 0.3,
98
+ minScore: 0.3,
99
+ rerank: "cross-encoder",
100
+ candidatePoolSize: 20,
101
+ recencyHalfLifeDays: 14,
102
+ recencyWeight: 0.1,
103
+ filterNoise: true,
104
+ rerankModel: "jina-reranker-v3",
105
+ rerankEndpoint: "https://api.jina.ai/v1/rerank",
106
+ lengthNormAnchor: 500,
107
+ hardMinScore: 0.35,
108
+ timeDecayHalfLifeDays: 60,
109
+ reinforcementFactor: 0.5,
110
+ maxHalfLifeMultiplier: 3,
111
+ multiHopRouting: true
112
+ };
113
+ function clampInt(value, min, max) {
114
+ if (!Number.isFinite(value)) return min;
115
+ return Math.min(max, Math.max(min, Math.floor(value)));
116
+ }
117
+ function clamp01(value, fallback) {
118
+ if (!Number.isFinite(value)) return Number.isFinite(fallback) ? fallback : 0;
119
+ return Math.min(1, Math.max(0, value));
120
+ }
121
+ function clamp01WithFloor(value, floor) {
122
+ const safeFloor = clamp01(floor, 0);
123
+ return Math.max(safeFloor, clamp01(value, safeFloor));
124
+ }
125
+ function buildRerankRequest(provider, apiKey, model, query, documents, topN) {
126
+ switch (provider) {
127
+ case "pinecone":
128
+ return {
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ "Api-Key": apiKey,
132
+ "X-Pinecone-API-Version": "2024-10"
133
+ },
134
+ body: {
135
+ model,
136
+ query,
137
+ documents: documents.map((text) => ({ text })),
138
+ top_n: topN,
139
+ rank_fields: ["text"]
140
+ }
141
+ };
142
+ case "voyage":
143
+ return {
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ Authorization: `Bearer ${apiKey}`
147
+ },
148
+ body: {
149
+ model,
150
+ query,
151
+ documents,
152
+ // Voyage uses top_k (not top_n) to limit reranked outputs.
153
+ top_k: topN
154
+ }
155
+ };
156
+ case "ollama":
157
+ return {
158
+ headers: {
159
+ "Content-Type": "application/json"
160
+ },
161
+ body: {
162
+ model,
163
+ query,
164
+ documents,
165
+ top_n: topN
166
+ }
167
+ };
168
+ case "siliconflow":
169
+ case "jina":
170
+ default:
171
+ return {
172
+ headers: {
173
+ "Content-Type": "application/json",
174
+ Authorization: `Bearer ${apiKey}`
175
+ },
176
+ body: {
177
+ model,
178
+ query,
179
+ documents,
180
+ top_n: topN
181
+ }
182
+ };
183
+ }
184
+ }
185
+ function parseRerankResponse(provider, data) {
186
+ const parseItems = (items, scoreKeys) => {
187
+ if (!Array.isArray(items)) return null;
188
+ const parsed = [];
189
+ for (const raw of items) {
190
+ const index = typeof raw?.index === "number" ? raw.index : Number(raw?.index);
191
+ if (!Number.isFinite(index)) continue;
192
+ let score = null;
193
+ for (const key of scoreKeys) {
194
+ const value = raw?.[key];
195
+ const n = typeof value === "number" ? value : Number(value);
196
+ if (Number.isFinite(n)) {
197
+ score = n;
198
+ break;
199
+ }
200
+ }
201
+ if (score === null) continue;
202
+ parsed.push({ index, score });
203
+ }
204
+ return parsed.length > 0 ? parsed : null;
205
+ };
206
+ switch (provider) {
207
+ case "ollama": {
208
+ return parseItems(data.results, ["relevance_score", "score"]) ?? parseItems(data.data, ["relevance_score", "score"]);
209
+ }
210
+ case "pinecone": {
211
+ return parseItems(data.data, ["score", "relevance_score"]) ?? parseItems(data.results, ["score", "relevance_score"]);
212
+ }
213
+ case "voyage": {
214
+ return parseItems(data.data, ["relevance_score", "score"]) ?? parseItems(data.results, ["relevance_score", "score"]);
215
+ }
216
+ case "siliconflow":
217
+ case "jina":
218
+ default: {
219
+ return parseItems(data.results, ["relevance_score", "score"]) ?? parseItems(data.data, ["relevance_score", "score"]);
220
+ }
221
+ }
222
+ }
223
+ function cosineSimilarity(a, b) {
224
+ if (a.length !== b.length) {
225
+ throw new Error("Vector dimensions must match for cosine similarity");
226
+ }
227
+ let dotProduct = 0;
228
+ let normA = 0;
229
+ let normB = 0;
230
+ for (let i = 0; i < a.length; i++) {
231
+ dotProduct += a[i] * b[i];
232
+ normA += a[i] * a[i];
233
+ normB += b[i] * b[i];
234
+ }
235
+ const norm = Math.sqrt(normA) * Math.sqrt(normB);
236
+ return norm === 0 ? 0 : dotProduct / norm;
237
+ }
238
+ const RETRIEVAL_LOG_PATH = join(homedir(), ".openclaw", "memory", "retrieval-log.jsonl");
239
+ class MemoryRetriever {
240
+ constructor(store, embedder, config = DEFAULT_RETRIEVAL_CONFIG, decayEngine = null) {
241
+ this.store = store;
242
+ this.embedder = embedder;
243
+ this.config = config;
244
+ this.decayEngine = decayEngine;
245
+ }
246
+ accessTracker = null;
247
+ tierManager = null;
248
+ setAccessTracker(tracker) {
249
+ this.accessTracker = tracker;
250
+ }
251
+ /**
252
+ * Resonance check: fast vector probe to see if the query "resonates"
253
+ * with any high-salience memories. Returns true if at least one memory
254
+ * has cosine similarity above threshold. This mimics human associative
255
+ * memory — most inputs don't trigger recall, only resonant ones do.
256
+ */
257
+ async resonanceCheck(query, scopeFilter) {
258
+ try {
259
+ const queryVector = await this.embedder.embedQuery(query);
260
+ const threshold = getAdaptiveThreshold();
261
+ const probeResults = await this.store.vectorSearch(
262
+ queryVector,
263
+ 3,
264
+ threshold,
265
+ scopeFilter
266
+ );
267
+ if (probeResults.length === 0) return { resonates: false, topScore: 0 };
268
+ const topScore = Math.max(...probeResults.map((r) => r.score));
269
+ recordResonanceScore(topScore);
270
+ for (const r of probeResults) {
271
+ const importance = r.entry.importance ?? 0.5;
272
+ const similarity = r.score;
273
+ if (similarity >= 0.55 || similarity >= threshold && importance >= 0.7) {
274
+ return { resonates: true, topScore };
275
+ }
276
+ }
277
+ return { resonates: false, topScore };
278
+ } catch {
279
+ return { resonates: true, topScore: 0 };
280
+ }
281
+ }
282
+ /**
283
+ * Strip metadata blocks from auto-recall queries.
284
+ * OpenClaw core may pass the full message including Conversation info,
285
+ * Replied message, and Sender metadata JSON blocks. We only want the
286
+ * actual user text for semantic search.
287
+ */
288
+ cleanAutoRecallQuery(raw) {
289
+ let cleaned = raw.replace(/```json[\s\S]*?```/g, "");
290
+ cleaned = cleaned.replace(/Conversation info \(untrusted metadata\):/g, "");
291
+ cleaned = cleaned.replace(/Sender \(untrusted metadata\):/g, "");
292
+ cleaned = cleaned.replace(/Replied message \(untrusted, for context\):/g, "");
293
+ cleaned = cleaned.replace(/\[Queued messages[^\]]*\]/g, "");
294
+ cleaned = cleaned.replace(/---\s*\nQueued #\d+/g, "");
295
+ cleaned = cleaned.replace(/\n{3,}/g, "\n").trim();
296
+ return cleaned.length > 2 ? cleaned : raw;
297
+ }
298
+ /**
299
+ * Expand date expressions in query to multiple formats for BM25 matching.
300
+ * "3月17日" → "3月17日 3/17 03-17 2026-03-17"
301
+ * "2026年3月17日" → "2026年3月17日 3/17 2026-03-17 03-17"
302
+ * "昨天/前天/上周" → resolved to absolute dates
303
+ */
304
+ expandDateFormats(query) {
305
+ const now = /* @__PURE__ */ new Date();
306
+ const year = now.getFullYear();
307
+ let expanded = query;
308
+ expanded = expanded.replace(/(\d{1,2})月(\d{1,2})[日号]/g, (match, m, d) => {
309
+ const mm = String(m).padStart(2, "0");
310
+ const dd = String(d).padStart(2, "0");
311
+ return `${match} ${m}/${d} ${year}-${mm}-${dd} ${mm}-${dd}`;
312
+ });
313
+ expanded = expanded.replace(/(\d{4})年(\d{1,2})月(\d{1,2})[日号]/g, (match, y, m, d) => {
314
+ const mm = String(m).padStart(2, "0");
315
+ const dd = String(d).padStart(2, "0");
316
+ return `${match} ${m}/${d} ${y}-${mm}-${dd} ${mm}-${dd}`;
317
+ });
318
+ const relMap = {
319
+ "\u4ECA\u5929": 0,
320
+ "\u6628\u5929": -1,
321
+ "\u524D\u5929": -2,
322
+ "\u5927\u524D\u5929": -3
323
+ };
324
+ for (const [word, offset] of Object.entries(relMap)) {
325
+ if (expanded.includes(word)) {
326
+ const d = new Date(now.getTime() + offset * 864e5);
327
+ const iso = d.toISOString().slice(0, 10);
328
+ const m = d.getMonth() + 1;
329
+ const day = d.getDate();
330
+ expanded = expanded.replace(word, `${word} ${iso} ${m}/${day} ${m}\u6708${day}\u65E5`);
331
+ }
332
+ }
333
+ return expanded;
334
+ }
335
+ async retrieve(context) {
336
+ let { query, limit, scopeFilter, category, source } = context;
337
+ if (source === "auto-recall") {
338
+ query = this.cleanAutoRecallQuery(query);
339
+ }
340
+ const safeLimit = clampInt(limit, 1, 20);
341
+ const t0 = performance.now();
342
+ let resonanceTriggered = false;
343
+ let resonanceTopScore = 0;
344
+ if (source === "auto-recall") {
345
+ const { resonates, topScore: probeTopScore } = await this.resonanceCheck(query, scopeFilter);
346
+ resonanceTriggered = resonates;
347
+ resonanceTopScore = probeTopScore;
348
+ if (!resonates) {
349
+ const trackEntry2 = JSON.stringify({
350
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
351
+ query: query.substring(0, 200),
352
+ source: source || "manual",
353
+ queryType: "gated-out",
354
+ resonanceScore: resonanceTopScore,
355
+ resonanceTriggered: false,
356
+ lancedbCount: 0,
357
+ graphitiCount: 0,
358
+ rerankCount: 0,
359
+ finalCount: 0,
360
+ totalLatencyMs: Math.round(performance.now() - t0),
361
+ lancedbLatencyMs: 0,
362
+ graphitiLatencyMs: 0,
363
+ rerankLatencyMs: 0
364
+ }) + "\n";
365
+ appendFile(RETRIEVAL_LOG_PATH, trackEntry2).catch(() => {
366
+ });
367
+ recordQuery({
368
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
369
+ query: query.substring(0, 200),
370
+ source: source === "auto-recall" ? "auto" : source || "manual",
371
+ hitCount: 0,
372
+ topScore: resonanceTopScore,
373
+ latency_ms: Math.round(performance.now() - t0),
374
+ queryType: "gated-out",
375
+ resonancePass: false
376
+ });
377
+ return [];
378
+ }
379
+ }
380
+ const isMultiHop = this.config.multiHopRouting && this.isMultiHopQuery(query);
381
+ const tLance0 = performance.now();
382
+ const lanceDbPromise = (async () => {
383
+ if (this.config.mode === "vector" || !this.store.hasFtsSupport) {
384
+ return this.vectorOnlyRetrieval(query, safeLimit, scopeFilter, category);
385
+ } else {
386
+ return this.hybridRetrieval(query, safeLimit, scopeFilter, category);
387
+ }
388
+ })();
389
+ const tGraphiti0 = performance.now();
390
+ const graphitiPromise = (async () => {
391
+ if (process.env.GRAPHITI_ENABLED !== "true") return [];
392
+ try {
393
+ const graphitiBase = process.env.GRAPHITI_BASE_URL || "http://127.0.0.1:18799";
394
+ const scope = scopeFilter?.[0] || "default";
395
+ const groupId = scope.startsWith("agent:") ? scope.split(":")[1] || "default" : "default";
396
+ const useSpread = source === "auto-recall" && !isMultiHop;
397
+ const endpoint = useSpread ? "/spread" : "/search";
398
+ const body = useSpread ? { query, group_id: groupId, search_limit: 3, spread_depth: 1, spread_limit: 3 } : { query, group_id: groupId, limit: Math.min(safeLimit, 5) };
399
+ const resp = await fetch(`${graphitiBase}${endpoint}`, {
400
+ method: "POST",
401
+ headers: { "Content-Type": "application/json" },
402
+ body: JSON.stringify(body),
403
+ signal: AbortSignal.timeout(5e3)
404
+ });
405
+ if (!resp.ok) return [];
406
+ const facts = await resp.json();
407
+ return facts.filter((f) => f.fact && !f.expired_at).map((f, i) => {
408
+ const isSpread = f.source === "spread";
409
+ const degreeBoost = f.degree ? Math.min(0.15, Math.log1p(f.degree) * 0.03) : 0;
410
+ const baseScore = isSpread ? 0.45 : 0.65;
411
+ return {
412
+ entry: {
413
+ id: `graphiti-${Date.now()}-${i}`,
414
+ text: f.fact,
415
+ vector: [],
416
+ category: "fact",
417
+ scope,
418
+ importance: 0.7,
419
+ timestamp: f.valid_at ? new Date(f.valid_at).getTime() : Date.now(),
420
+ metadata: JSON.stringify({ source: isSpread ? "graphiti-spread" : "graphiti", valid_at: f.valid_at, degree: f.degree })
421
+ },
422
+ score: baseScore + 0.01 * (facts.length - i) + degreeBoost,
423
+ sources: { graphiti: { rank: i + 1 } }
424
+ };
425
+ });
426
+ } catch {
427
+ return [];
428
+ }
429
+ })();
430
+ const [lanceResults, graphitiResults] = await Promise.all([lanceDbPromise, graphitiPromise]);
431
+ const tLanceMs = Math.round(performance.now() - tLance0);
432
+ const tGraphitiMs = Math.round(performance.now() - tGraphiti0);
433
+ const lanceTexts = new Set(lanceResults.map((r) => r.entry.text.slice(0, 80)));
434
+ const uniqueGraphiti = graphitiResults.filter(
435
+ (r) => !lanceTexts.has(r.entry.text.slice(0, 80))
436
+ );
437
+ let merged;
438
+ let rerankCount = 0;
439
+ const tRerank0 = performance.now();
440
+ const combined = [...lanceResults, ...uniqueGraphiti];
441
+ if (this.config.rerank !== "none" && this.config.rerankApiKey && combined.length > 0) {
442
+ const queryVector = await this.embedder.embedQuery(query);
443
+ merged = await this.rerankResults(query, queryVector, combined);
444
+ rerankCount = merged.length;
445
+ merged = merged.slice(0, safeLimit);
446
+ } else {
447
+ merged = combined.slice(0, safeLimit);
448
+ }
449
+ const tRerankMs = Math.round(performance.now() - tRerank0);
450
+ if (this.accessTracker && source === "manual" && merged.length > 0) {
451
+ this.accessTracker.recordAccess(
452
+ merged.filter((r) => !r.entry.id.startsWith("graphiti-")).map((r) => r.entry.id)
453
+ );
454
+ }
455
+ const topScore = merged.length > 0 ? Math.max(...merged.map((r) => r.score)) : 0;
456
+ const trackEntry = JSON.stringify({
457
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
458
+ query: query.substring(0, 200),
459
+ source: source || "manual",
460
+ queryType: isMultiHop ? "multi-hop" : "single",
461
+ resonanceScore: topScore,
462
+ resonanceTriggered,
463
+ lancedbCount: lanceResults.length,
464
+ graphitiCount: graphitiResults.length,
465
+ rerankCount,
466
+ finalCount: merged.length,
467
+ totalLatencyMs: Math.round(performance.now() - t0),
468
+ lancedbLatencyMs: tLanceMs,
469
+ graphitiLatencyMs: tGraphitiMs,
470
+ rerankLatencyMs: tRerankMs
471
+ }) + "\n";
472
+ appendFile(RETRIEVAL_LOG_PATH, trackEntry).catch(() => {
473
+ });
474
+ recordQuery({
475
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
476
+ query: query.substring(0, 200),
477
+ source: source === "auto-recall" ? "auto" : source || "manual",
478
+ hitCount: merged.length,
479
+ topScore,
480
+ latency_ms: Math.round(performance.now() - t0),
481
+ queryType: isMultiHop ? "multi-hop" : "single",
482
+ resonancePass: true
483
+ });
484
+ return merged;
485
+ }
486
+ async vectorOnlyRetrieval(query, limit, scopeFilter, category) {
487
+ const queryVector = await this.embedder.embedQuery(query);
488
+ const results = await this.store.vectorSearch(
489
+ queryVector,
490
+ limit,
491
+ this.config.minScore,
492
+ scopeFilter
493
+ );
494
+ const filtered = category ? results.filter((r) => r.entry.category === category) : results;
495
+ const mapped = filtered.map(
496
+ (result, index) => ({
497
+ ...result,
498
+ sources: {
499
+ vector: { score: result.score, rank: index + 1 }
500
+ }
501
+ })
502
+ );
503
+ const weighted = this.decayEngine ? mapped : this.applyImportanceWeight(this.applyRecencyBoost(mapped));
504
+ const lengthNormalized = this.applyLengthNormalization(weighted);
505
+ const hardFiltered = lengthNormalized.filter((r) => r.score >= this.config.hardMinScore);
506
+ const lifecycleRanked = this.decayEngine ? this.applyDecayBoost(hardFiltered) : this.applyTimeDecay(hardFiltered);
507
+ const denoised = this.config.filterNoise ? filterNoise(lifecycleRanked, (r) => r.entry.text) : lifecycleRanked;
508
+ const deduplicated = this.applyMMRDiversity(denoised);
509
+ return deduplicated.slice(0, limit);
510
+ }
511
+ async hybridRetrieval(query, limit, scopeFilter, category) {
512
+ const candidatePoolSize = Math.max(
513
+ this.config.candidatePoolSize,
514
+ limit * 2
515
+ );
516
+ const queryVector = await this.embedder.embedQuery(query);
517
+ const [vectorResults, bm25Results, graphitiResults] = await Promise.all([
518
+ this.runVectorSearch(
519
+ queryVector,
520
+ candidatePoolSize,
521
+ scopeFilter,
522
+ category
523
+ ),
524
+ this.runBM25Search(query, candidatePoolSize, scopeFilter, category),
525
+ graphitiSpreadSearch(query, "default", 3, 3)
526
+ ]);
527
+ const fusedResults = await this.fuseResults(vectorResults, bm25Results, graphitiResults);
528
+ const filtered = fusedResults.filter(
529
+ (r) => r.score >= this.config.minScore
530
+ );
531
+ const reranked = this.config.rerank !== "none" ? await this.rerankResults(
532
+ query,
533
+ queryVector,
534
+ filtered.slice(0, limit * 2)
535
+ ) : filtered;
536
+ const temporallyRanked = this.decayEngine ? reranked : this.applyImportanceWeight(this.applyRecencyBoost(reranked));
537
+ const lengthNormalized = this.applyLengthNormalization(temporallyRanked);
538
+ const hardFiltered = lengthNormalized.filter((r) => r.score >= this.config.hardMinScore);
539
+ const lifecycleRanked = this.decayEngine ? this.applyDecayBoost(hardFiltered) : this.applyTimeDecay(hardFiltered);
540
+ const denoised = this.config.filterNoise ? filterNoise(lifecycleRanked, (r) => r.entry.text) : lifecycleRanked;
541
+ const deduplicated = this.applyMMRDiversity(denoised);
542
+ return deduplicated.slice(0, limit);
543
+ }
544
+ async runVectorSearch(queryVector, limit, scopeFilter, category) {
545
+ const results = await this.store.vectorSearch(
546
+ queryVector,
547
+ limit,
548
+ 0.1,
549
+ scopeFilter
550
+ );
551
+ const filtered = category ? results.filter((r) => r.entry.category === category) : results;
552
+ return filtered.map((result, index) => ({
553
+ ...result,
554
+ rank: index + 1
555
+ }));
556
+ }
557
+ async runBM25Search(query, limit, scopeFilter, category) {
558
+ const expandedQuery = this.expandDateFormats(query);
559
+ const results = await this.store.bm25Search(expandedQuery, limit, scopeFilter);
560
+ const filtered = category ? results.filter((r) => r.entry.category === category) : results;
561
+ return filtered.map((result, index) => ({
562
+ ...result,
563
+ rank: index + 1
564
+ }));
565
+ }
566
+ async fuseResults(vectorResults, bm25Results, graphitiResults = []) {
567
+ const vectorMap = /* @__PURE__ */ new Map();
568
+ const bm25Map = /* @__PURE__ */ new Map();
569
+ vectorResults.forEach((result) => {
570
+ vectorMap.set(result.entry.id, result);
571
+ });
572
+ bm25Results.forEach((result) => {
573
+ bm25Map.set(result.entry.id, result);
574
+ });
575
+ const allIds = /* @__PURE__ */ new Set([...vectorMap.keys(), ...bm25Map.keys()]);
576
+ const fusedResults = [];
577
+ for (const id of allIds) {
578
+ const vectorResult = vectorMap.get(id);
579
+ const bm25Result = bm25Map.get(id);
580
+ if (!vectorResult && bm25Result) {
581
+ try {
582
+ const exists = await this.store.hasId(id);
583
+ if (!exists) continue;
584
+ } catch {
585
+ }
586
+ }
587
+ const baseResult = vectorResult || bm25Result;
588
+ const vectorScore = vectorResult ? vectorResult.score : 0;
589
+ const bm25Score = bm25Result ? bm25Result.score : 0;
590
+ const weightedFusion = vectorScore * this.config.vectorWeight + bm25Score * this.config.bm25Weight;
591
+ const fusedScore = vectorResult ? clamp01(
592
+ Math.max(
593
+ weightedFusion,
594
+ bm25Score >= 0.75 ? bm25Score * 0.92 : 0
595
+ ),
596
+ 0.1
597
+ ) : clamp01(bm25Result.score, 0.1);
598
+ fusedResults.push({
599
+ entry: baseResult.entry,
600
+ score: fusedScore,
601
+ sources: {
602
+ vector: vectorResult ? { score: vectorResult.score, rank: vectorResult.rank } : void 0,
603
+ bm25: bm25Result ? { score: bm25Result.score, rank: bm25Result.rank } : void 0,
604
+ fused: { score: fusedScore }
605
+ }
606
+ });
607
+ }
608
+ for (const gr of graphitiResults) {
609
+ fusedResults.push({
610
+ entry: gr.entry,
611
+ score: gr.score * 0.85,
612
+ sources: {
613
+ graphiti: { score: gr.score, rank: gr.rank },
614
+ fused: { score: gr.score * 0.85 }
615
+ }
616
+ });
617
+ }
618
+ return fusedResults.sort((a, b) => b.score - a.score);
619
+ }
620
+ /**
621
+ * Rerank results using cross-encoder API (Jina, Pinecone, or compatible).
622
+ * Falls back to cosine similarity if API is unavailable or fails.
623
+ */
624
+ async rerankResults(query, queryVector, results) {
625
+ if (results.length === 0) {
626
+ return results;
627
+ }
628
+ log.warn(`[rerank-debug] rerank=${this.config.rerank}, hasKey=${!!this.config.rerankApiKey}, keyPrefix=${String(this.config.rerankApiKey || "").substring(0, 8)}, provider=${this.config.rerankProvider}, model=${this.config.rerankModel}`);
629
+ const isLocalRerank = this.config.rerankProvider === "ollama";
630
+ if (this.config.rerank === "cross-encoder" && (this.config.rerankApiKey || isLocalRerank)) {
631
+ try {
632
+ const provider = this.config.rerankProvider || "jina";
633
+ const model = this.config.rerankModel || (isLocalRerank ? "bge-reranker-v2-m3" : "jina-reranker-v3");
634
+ const endpoint = this.config.rerankEndpoint || (isLocalRerank ? "http://127.0.0.1:11434/api/rerank" : "https://api.jina.ai/v1/rerank");
635
+ const documents = results.map((r) => r.entry.text);
636
+ const { headers, body } = buildRerankRequest(
637
+ provider,
638
+ this.config.rerankApiKey,
639
+ model,
640
+ query,
641
+ documents,
642
+ results.length
643
+ );
644
+ const controller = new AbortController();
645
+ const timeout = setTimeout(() => controller.abort(), 5e3);
646
+ const response = await fetch(endpoint, {
647
+ method: "POST",
648
+ headers,
649
+ body: JSON.stringify(body),
650
+ signal: controller.signal
651
+ });
652
+ clearTimeout(timeout);
653
+ if (response.ok) {
654
+ const data = await response.json();
655
+ const parsed = parseRerankResponse(provider, data);
656
+ if (!parsed) {
657
+ log.warn(
658
+ "Rerank API: invalid response shape, falling back to cosine"
659
+ );
660
+ } else {
661
+ const returnedIndices = new Set(parsed.map((r) => r.index));
662
+ const reranked = parsed.filter((item) => item.index >= 0 && item.index < results.length).map((item) => {
663
+ const original = results[item.index];
664
+ const floor = this.getRerankPreservationFloor(original, false);
665
+ const blendedScore = clamp01WithFloor(
666
+ item.score * 0.6 + original.score * 0.4,
667
+ floor
668
+ );
669
+ return {
670
+ ...original,
671
+ score: blendedScore,
672
+ sources: {
673
+ ...original.sources,
674
+ reranked: { score: item.score }
675
+ }
676
+ };
677
+ });
678
+ const unreturned = results.filter((_, idx) => !returnedIndices.has(idx)).map((r) => ({
679
+ ...r,
680
+ score: clamp01WithFloor(
681
+ r.score * 0.8,
682
+ this.getRerankPreservationFloor(r, true)
683
+ )
684
+ }));
685
+ return [...reranked, ...unreturned].sort(
686
+ (a, b) => b.score - a.score
687
+ );
688
+ }
689
+ } else {
690
+ const errText = await response.text().catch(() => "");
691
+ log.warn(
692
+ `Rerank API returned ${response.status}: ${errText.slice(0, 200)}, falling back to cosine`
693
+ );
694
+ }
695
+ } catch (error) {
696
+ if (error instanceof Error && error.name === "AbortError") {
697
+ log.warn("Rerank API timed out (5s), falling back to cosine");
698
+ } else {
699
+ log.warn("Rerank API failed, falling back to cosine:", error);
700
+ }
701
+ }
702
+ }
703
+ try {
704
+ const reranked = results.map((result) => {
705
+ const cosineScore = cosineSimilarity(queryVector, result.entry.vector);
706
+ const combinedScore = result.score * 0.7 + cosineScore * 0.3;
707
+ return {
708
+ ...result,
709
+ score: clamp01(combinedScore, result.score),
710
+ sources: {
711
+ ...result.sources,
712
+ reranked: { score: cosineScore }
713
+ }
714
+ };
715
+ });
716
+ return reranked.sort((a, b) => b.score - a.score);
717
+ } catch (error) {
718
+ log.warn("Reranking failed, returning original results:", error);
719
+ return results;
720
+ }
721
+ }
722
+ getRerankPreservationFloor(result, unreturned) {
723
+ const bm25Score = result.sources.bm25?.score ?? 0;
724
+ if (bm25Score >= 0.75) {
725
+ return result.score * (unreturned ? 1 : 0.95);
726
+ }
727
+ if (bm25Score >= 0.6) {
728
+ return result.score * (unreturned ? 0.95 : 0.9);
729
+ }
730
+ return result.score * (unreturned ? 0.8 : 0.5);
731
+ }
732
+ /**
733
+ * Apply recency boost: newer memories get a small score bonus.
734
+ * This ensures corrections/updates naturally outrank older entries
735
+ * when semantic similarity is close.
736
+ * Formula: boost = exp(-ageDays / halfLife) * weight
737
+ */
738
+ applyRecencyBoost(results) {
739
+ const { recencyHalfLifeDays, recencyWeight } = this.config;
740
+ if (!recencyHalfLifeDays || recencyHalfLifeDays <= 0 || !recencyWeight) {
741
+ return results;
742
+ }
743
+ const now = Date.now();
744
+ const boosted = results.map((r) => {
745
+ const ts = r.entry.timestamp && r.entry.timestamp > 0 ? r.entry.timestamp : now;
746
+ const ageDays = (now - ts) / 864e5;
747
+ const boost = Math.exp(-ageDays / recencyHalfLifeDays) * recencyWeight;
748
+ return {
749
+ ...r,
750
+ score: clamp01(r.score + boost, r.score)
751
+ };
752
+ });
753
+ return boosted.sort((a, b) => b.score - a.score);
754
+ }
755
+ /**
756
+ * Apply importance weighting: memories with higher importance get a score boost.
757
+ * This ensures critical memories (importance=1.0) outrank casual ones (importance=0.5)
758
+ * when semantic similarity is close.
759
+ * Formula: score *= (baseWeight + (1 - baseWeight) * importance)
760
+ * With baseWeight=0.7: importance=1.0 → ×1.0, importance=0.5 → ×0.85, importance=0.0 → ×0.7
761
+ */
762
+ applyImportanceWeight(results) {
763
+ const baseWeight = 0.7;
764
+ const weighted = results.map((r) => {
765
+ const importance = r.entry.importance ?? 0.7;
766
+ const factor = baseWeight + (1 - baseWeight) * importance;
767
+ return {
768
+ ...r,
769
+ score: clamp01(r.score * factor, r.score * baseWeight)
770
+ };
771
+ });
772
+ return weighted.sort((a, b) => b.score - a.score);
773
+ }
774
+ applyDecayBoost(results) {
775
+ if (!this.decayEngine || results.length === 0) return results;
776
+ const scored = results.map((result) => ({
777
+ memory: toLifecycleMemory(result.entry.id, result.entry),
778
+ score: result.score
779
+ }));
780
+ this.decayEngine.applySearchBoost(scored);
781
+ const reranked = results.map((result, index) => ({
782
+ ...result,
783
+ score: clamp01(scored[index].score, result.score * 0.3)
784
+ }));
785
+ return reranked.sort((a, b) => b.score - a.score);
786
+ }
787
+ /**
788
+ * Detect multi-hop queries that involve multiple entities or relationships.
789
+ * Multi-hop queries benefit from LanceDB secondary retrieval rather than
790
+ * Graphiti spread (which follows single-entity neighborhoods).
791
+ */
792
+ isMultiHopQuery(query) {
793
+ const capitalizedWords = new Set(
794
+ query.match(/\b[A-Z\u4e00-\u9fff][a-zA-Z\u4e00-\u9fff]{1,}/g) || []
795
+ );
796
+ if (capitalizedWords.size >= 2) return true;
797
+ const relationPatterns = [
798
+ /和.{1,20}的关系/,
799
+ /compared\s+to/i,
800
+ /difference\s+between/i,
801
+ /为什么/,
802
+ /how\s+does\s+.{1,30}\s+relate\s+to/i,
803
+ /between\s+.{1,30}\s+and\s+/i,
804
+ /相比/,
805
+ /区别/,
806
+ /之间/
807
+ ];
808
+ for (const pat of relationPatterns) {
809
+ if (pat.test(query)) return true;
810
+ }
811
+ if (query.length > 100 && /[??]/.test(query)) return true;
812
+ return false;
813
+ }
814
+ /**
815
+ * via sheer keyword density and broad semantic coverage.
816
+ * Short, focused entries (< anchor) get a slight boost.
817
+ * Long, sprawling entries (> anchor) get penalized.
818
+ * Formula: score *= 1 / (1 + log2(charLen / anchor))
819
+ */
820
+ applyLengthNormalization(results) {
821
+ const anchor = this.config.lengthNormAnchor;
822
+ if (!anchor || anchor <= 0) return results;
823
+ const normalized = results.map((r) => {
824
+ const charLen = r.entry.text.length;
825
+ const ratio = charLen / anchor;
826
+ const logRatio = Math.log2(Math.max(ratio, 1));
827
+ const factor = 1 / (1 + 0.5 * logRatio);
828
+ return {
829
+ ...r,
830
+ score: clamp01(r.score * factor, r.score * 0.3)
831
+ };
832
+ });
833
+ return normalized.sort((a, b) => b.score - a.score);
834
+ }
835
+ /**
836
+ * Time decay: multiplicative penalty for old entries.
837
+ * Unlike recencyBoost (additive bonus for new entries), this actively
838
+ * penalizes stale information so recent knowledge wins ties.
839
+ * Formula: score *= 0.5 + 0.5 * exp(-ageDays / halfLife)
840
+ * At 0 days: 1.0x (no penalty)
841
+ * At halfLife: ~0.68x
842
+ * At 2*halfLife: ~0.59x
843
+ * Floor at 0.5x (never penalize more than half)
844
+ */
845
+ applyTimeDecay(results) {
846
+ const halfLife = this.config.timeDecayHalfLifeDays;
847
+ if (!halfLife || halfLife <= 0) return results;
848
+ const now = Date.now();
849
+ const decayed = results.map((r) => {
850
+ const ts = r.entry.timestamp && r.entry.timestamp > 0 ? r.entry.timestamp : now;
851
+ const ageDays = (now - ts) / 864e5;
852
+ const { accessCount, lastAccessedAt } = parseAccessMetadata(
853
+ r.entry.metadata
854
+ );
855
+ const effectiveHL = computeEffectiveHalfLife(
856
+ halfLife,
857
+ accessCount,
858
+ lastAccessedAt,
859
+ this.config.reinforcementFactor,
860
+ this.config.maxHalfLifeMultiplier
861
+ );
862
+ const factor = 0.5 + 0.5 * Math.exp(-ageDays / effectiveHL);
863
+ return {
864
+ ...r,
865
+ score: clamp01(r.score * factor, r.score * 0.5)
866
+ };
867
+ });
868
+ return decayed.sort((a, b) => b.score - a.score);
869
+ }
870
+ /**
871
+ * Apply lifecycle-aware score adjustment (decay + tier floors).
872
+ *
873
+ * This is intentionally lightweight:
874
+ * - reads tier/access metadata (if any)
875
+ * - multiplies scores by max(tierFloor, decayComposite)
876
+ */
877
+ applyLifecycleBoost(results) {
878
+ if (!this.decayEngine) return results;
879
+ const now = Date.now();
880
+ const pairs = results.map((r) => {
881
+ const { memory } = getDecayableFromEntry(r.entry);
882
+ return { r, memory };
883
+ });
884
+ const scored = pairs.map((p) => ({ memory: p.memory, score: p.r.score }));
885
+ this.decayEngine.applySearchBoost(scored, now);
886
+ const boosted = pairs.map((p, i) => ({ ...p.r, score: scored[i].score }));
887
+ return boosted.sort((a, b) => b.score - a.score);
888
+ }
889
+ /**
890
+ * Record access stats (access_count, last_accessed_at) and apply tier
891
+ * promotion/demotion for a small number of top results.
892
+ *
893
+ * Note: this writes back to LanceDB via delete+readd; keep it bounded.
894
+ */
895
+ async recordAccessAndMaybeTransition(results) {
896
+ if (!this.decayEngine && !this.tierManager) return;
897
+ const now = Date.now();
898
+ const toUpdate = results.slice(0, 3);
899
+ for (const r of toUpdate) {
900
+ const { memory, meta } = getDecayableFromEntry(r.entry);
901
+ const nextAccess = memory.accessCount + 1;
902
+ meta.access_count = nextAccess;
903
+ meta.last_accessed_at = now;
904
+ if (meta.created_at === void 0 && meta.createdAt === void 0) {
905
+ meta.created_at = memory.createdAt;
906
+ }
907
+ if (meta.tier === void 0) {
908
+ meta.tier = memory.tier;
909
+ }
910
+ if (meta.confidence === void 0) {
911
+ meta.confidence = memory.confidence;
912
+ }
913
+ const updatedMemory = {
914
+ ...memory,
915
+ accessCount: nextAccess,
916
+ lastAccessedAt: now
917
+ };
918
+ if (this.decayEngine && this.tierManager) {
919
+ const ds = this.decayEngine.score(updatedMemory, now);
920
+ const transition = this.tierManager.evaluate(updatedMemory, ds, now);
921
+ if (transition) {
922
+ meta.tier = transition.toTier;
923
+ }
924
+ }
925
+ try {
926
+ await this.store.update(r.entry.id, {
927
+ metadata: JSON.stringify(meta)
928
+ });
929
+ } catch {
930
+ }
931
+ }
932
+ }
933
+ /**
934
+ * MMR-inspired diversity filter: greedily select results that are both
935
+ * relevant (high score) and diverse (low similarity to already-selected).
936
+ *
937
+ * Uses cosine similarity between memory vectors. If two memories have
938
+ * cosine similarity > threshold (default 0.92), the lower-scored one
939
+ * is demoted to the end rather than removed entirely.
940
+ *
941
+ * This prevents top-k from being filled with near-identical entries
942
+ * (e.g. 3 similar "SVG style" memories) while keeping them available
943
+ * if the pool is small.
944
+ */
945
+ applyMMRDiversity(results, similarityThreshold = 0.85) {
946
+ if (results.length <= 1) return results;
947
+ const selected = [];
948
+ const deferred = [];
949
+ for (const candidate of results) {
950
+ const tooSimilar = selected.some((s) => {
951
+ const sVec = s.entry.vector;
952
+ const cVec = candidate.entry.vector;
953
+ if (!sVec?.length || !cVec?.length) return false;
954
+ const sArr = Array.from(sVec);
955
+ const cArr = Array.from(cVec);
956
+ const sim = cosineSimilarity(sArr, cArr);
957
+ return sim > similarityThreshold;
958
+ });
959
+ if (tooSimilar) {
960
+ deferred.push(candidate);
961
+ } else {
962
+ selected.push(candidate);
963
+ }
964
+ }
965
+ return [...selected, ...deferred];
966
+ }
967
+ // Update configuration
968
+ updateConfig(newConfig) {
969
+ this.config = { ...this.config, ...newConfig };
970
+ }
971
+ // Get current configuration
972
+ getConfig() {
973
+ return { ...this.config };
974
+ }
975
+ // Test retrieval system
976
+ async test(query = "test query") {
977
+ try {
978
+ const results = await this.retrieve({
979
+ query,
980
+ limit: 1
981
+ });
982
+ return {
983
+ success: true,
984
+ mode: this.config.mode,
985
+ hasFtsSupport: this.store.hasFtsSupport
986
+ };
987
+ } catch (error) {
988
+ return {
989
+ success: false,
990
+ mode: this.config.mode,
991
+ hasFtsSupport: this.store.hasFtsSupport,
992
+ error: error instanceof Error ? error.message : String(error)
993
+ };
994
+ }
995
+ }
996
+ }
997
+ function createRetriever(store, embedder, config, options) {
998
+ const fullConfig = { ...DEFAULT_RETRIEVAL_CONFIG, ...config };
999
+ return new MemoryRetriever(store, embedder, fullConfig, options?.decayEngine ?? null);
1000
+ }
1001
+ export {
1002
+ DEFAULT_RETRIEVAL_CONFIG,
1003
+ MemoryRetriever,
1004
+ createRetriever
1005
+ };
1006
+ //# sourceMappingURL=retriever.js.map