@cmdoss/memwal-sdk 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +522 -160
  2. package/dist/client/ClientMemoryManager.d.ts.map +1 -1
  3. package/dist/client/ClientMemoryManager.js +25 -8
  4. package/dist/client/ClientMemoryManager.js.map +1 -1
  5. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  6. package/dist/client/SimplePDWClient.d.ts +62 -2
  7. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  8. package/dist/client/SimplePDWClient.js +96 -11
  9. package/dist/client/SimplePDWClient.js.map +1 -1
  10. package/dist/client/namespaces/IndexNamespace.d.ts +1 -1
  11. package/dist/client/namespaces/IndexNamespace.d.ts.map +1 -1
  12. package/dist/client/namespaces/IndexNamespace.js +7 -4
  13. package/dist/client/namespaces/IndexNamespace.js.map +1 -1
  14. package/dist/client/namespaces/MemoryNamespace.d.ts +47 -0
  15. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  16. package/dist/client/namespaces/MemoryNamespace.js +257 -27
  17. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  18. package/dist/client/namespaces/consolidated/AdvancedNamespace.d.ts +215 -0
  19. package/dist/client/namespaces/consolidated/AdvancedNamespace.d.ts.map +1 -0
  20. package/dist/client/namespaces/consolidated/AdvancedNamespace.js +214 -0
  21. package/dist/client/namespaces/consolidated/AdvancedNamespace.js.map +1 -0
  22. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +3 -1
  23. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  24. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  25. package/dist/client/namespaces/consolidated/index.d.ts +1 -0
  26. package/dist/client/namespaces/consolidated/index.d.ts.map +1 -1
  27. package/dist/client/namespaces/consolidated/index.js +1 -0
  28. package/dist/client/namespaces/consolidated/index.js.map +1 -1
  29. package/dist/config/ConfigurationHelper.js +61 -61
  30. package/dist/config/defaults.d.ts.map +1 -1
  31. package/dist/config/defaults.js +9 -4
  32. package/dist/config/defaults.js.map +1 -1
  33. package/dist/config/index.d.ts +1 -0
  34. package/dist/config/index.d.ts.map +1 -1
  35. package/dist/config/index.js +2 -0
  36. package/dist/config/index.js.map +1 -1
  37. package/dist/config/modelDefaults.d.ts +67 -0
  38. package/dist/config/modelDefaults.d.ts.map +1 -0
  39. package/dist/config/modelDefaults.js +91 -0
  40. package/dist/config/modelDefaults.js.map +1 -0
  41. package/dist/core/types/index.d.ts +4 -0
  42. package/dist/core/types/index.d.ts.map +1 -1
  43. package/dist/core/types/index.js.map +1 -1
  44. package/dist/graph/GraphService.d.ts.map +1 -1
  45. package/dist/graph/GraphService.js +22 -21
  46. package/dist/graph/GraphService.js.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/infrastructure/walrus/WalrusStorageService.d.ts +6 -0
  52. package/dist/infrastructure/walrus/WalrusStorageService.d.ts.map +1 -1
  53. package/dist/infrastructure/walrus/WalrusStorageService.js +23 -4
  54. package/dist/infrastructure/walrus/WalrusStorageService.js.map +1 -1
  55. package/dist/langchain/createPDWRAG.js +30 -30
  56. package/dist/pipeline/MemoryPipeline.d.ts.map +1 -1
  57. package/dist/pipeline/MemoryPipeline.js +2 -1
  58. package/dist/pipeline/MemoryPipeline.js.map +1 -1
  59. package/dist/services/EmbeddingService.d.ts +9 -0
  60. package/dist/services/EmbeddingService.d.ts.map +1 -1
  61. package/dist/services/EmbeddingService.js +31 -10
  62. package/dist/services/EmbeddingService.js.map +1 -1
  63. package/dist/services/GeminiAIService.d.ts.map +1 -1
  64. package/dist/services/GeminiAIService.js +311 -310
  65. package/dist/services/GeminiAIService.js.map +1 -1
  66. package/dist/services/MemoryIndexService.d.ts +2 -0
  67. package/dist/services/MemoryIndexService.d.ts.map +1 -1
  68. package/dist/services/MemoryIndexService.js +11 -4
  69. package/dist/services/MemoryIndexService.js.map +1 -1
  70. package/dist/services/StorageService.d.ts +4 -1
  71. package/dist/services/StorageService.d.ts.map +1 -1
  72. package/dist/services/StorageService.js.map +1 -1
  73. package/dist/services/VectorService.js +1 -1
  74. package/dist/services/VectorService.js.map +1 -1
  75. package/dist/services/storage/QuiltBatchManager.d.ts +7 -0
  76. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  77. package/dist/services/storage/QuiltBatchManager.js +24 -5
  78. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  79. package/dist/services/storage/WalrusStorageManager.d.ts +10 -1
  80. package/dist/services/storage/WalrusStorageManager.d.ts.map +1 -1
  81. package/dist/services/storage/WalrusStorageManager.js +53 -12
  82. package/dist/services/storage/WalrusStorageManager.js.map +1 -1
  83. package/dist/vector/BrowserHnswIndexService.js +3 -3
  84. package/dist/vector/BrowserHnswIndexService.js.map +1 -1
  85. package/dist/vector/HnswWasmService.js +1 -1
  86. package/dist/vector/HnswWasmService.js.map +1 -1
  87. package/dist/vector/NodeHnswService.js +5 -5
  88. package/dist/vector/NodeHnswService.js.map +1 -1
  89. package/dist/vector/createHnswService.d.ts +4 -0
  90. package/dist/vector/createHnswService.d.ts.map +1 -1
  91. package/dist/vector/createHnswService.js +15 -3
  92. package/dist/vector/createHnswService.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/access/PermissionService.ts +635 -635
  95. package/src/aggregation/AggregationService.ts +389 -389
  96. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  97. package/src/ai-sdk/index.ts +65 -65
  98. package/src/ai-sdk/tools.ts +460 -460
  99. package/src/ai-sdk/types.ts +404 -404
  100. package/src/batch/BatchManager.ts +597 -597
  101. package/src/batch/BatchingService.ts +429 -429
  102. package/src/batch/MemoryProcessingCache.ts +492 -492
  103. package/src/batch/index.ts +30 -30
  104. package/src/browser.ts +200 -200
  105. package/src/client/ClientMemoryManager.ts +1004 -987
  106. package/src/client/PersonalDataWallet.ts +345 -345
  107. package/src/client/SimplePDWClient.ts +1387 -1289
  108. package/src/client/factory.ts +154 -154
  109. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  110. package/src/client/namespaces/BatchNamespace.ts +356 -356
  111. package/src/client/namespaces/CacheNamespace.ts +123 -123
  112. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  113. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  114. package/src/client/namespaces/ContextNamespace.ts +297 -297
  115. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  116. package/src/client/namespaces/GraphNamespace.ts +468 -468
  117. package/src/client/namespaces/IndexNamespace.ts +364 -361
  118. package/src/client/namespaces/MemoryNamespace.ts +1704 -1422
  119. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  120. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  121. package/src/client/namespaces/StorageNamespace.ts +458 -458
  122. package/src/client/namespaces/TxNamespace.ts +260 -260
  123. package/src/client/namespaces/WalletNamespace.ts +243 -243
  124. package/src/client/namespaces/consolidated/AdvancedNamespace.ts +264 -0
  125. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -607
  126. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  127. package/src/client/namespaces/consolidated/StorageNamespace.ts +1143 -1141
  128. package/src/client/namespaces/consolidated/index.ts +41 -39
  129. package/src/client/signers/KeypairSigner.ts +108 -108
  130. package/src/client/signers/UnifiedSigner.ts +110 -110
  131. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  132. package/src/client/signers/index.ts +26 -26
  133. package/src/config/ConfigurationHelper.ts +412 -412
  134. package/src/config/defaults.ts +56 -51
  135. package/src/config/index.ts +16 -9
  136. package/src/config/modelDefaults.ts +103 -0
  137. package/src/config/validation.ts +70 -70
  138. package/src/core/index.ts +14 -14
  139. package/src/core/interfaces/IService.ts +307 -307
  140. package/src/core/interfaces/index.ts +8 -8
  141. package/src/core/types/capability.ts +297 -297
  142. package/src/core/types/index.ts +874 -870
  143. package/src/core/types/wallet.ts +270 -270
  144. package/src/core/types.ts +9 -9
  145. package/src/core/wallet.ts +222 -222
  146. package/src/embedding/index.ts +19 -19
  147. package/src/embedding/types.ts +357 -357
  148. package/src/errors/index.ts +602 -602
  149. package/src/errors/recovery.ts +461 -461
  150. package/src/errors/validation.ts +567 -567
  151. package/src/generated/pdw/capability.ts +319 -319
  152. package/src/graph/GraphService.ts +888 -887
  153. package/src/graph/KnowledgeGraphManager.ts +728 -728
  154. package/src/graph/index.ts +25 -25
  155. package/src/index.ts +498 -498
  156. package/src/infrastructure/index.ts +22 -22
  157. package/src/infrastructure/seal/EncryptionService.ts +628 -628
  158. package/src/infrastructure/seal/SealService.ts +613 -613
  159. package/src/infrastructure/seal/index.ts +9 -9
  160. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  161. package/src/infrastructure/sui/SuiService.ts +888 -888
  162. package/src/infrastructure/sui/index.ts +9 -9
  163. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  164. package/src/infrastructure/walrus/WalrusStorageService.ts +637 -612
  165. package/src/infrastructure/walrus/index.ts +9 -9
  166. package/src/langchain/createPDWRAG.ts +303 -303
  167. package/src/langchain/index.ts +47 -47
  168. package/src/permissions/ConsentRepository.browser.ts +249 -249
  169. package/src/permissions/ConsentRepository.ts +364 -364
  170. package/src/pipeline/MemoryPipeline.ts +863 -862
  171. package/src/pipeline/PipelineManager.ts +683 -683
  172. package/src/pipeline/index.ts +26 -26
  173. package/src/retrieval/AdvancedSearchService.ts +629 -629
  174. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  175. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -825
  176. package/src/retrieval/index.ts +42 -42
  177. package/src/services/BatchService.ts +352 -352
  178. package/src/services/CapabilityService.ts +464 -464
  179. package/src/services/ClassifierService.ts +465 -465
  180. package/src/services/CrossContextPermissionService.ts +486 -486
  181. package/src/services/EmbeddingService.ts +796 -771
  182. package/src/services/EncryptionService.ts +712 -712
  183. package/src/services/GeminiAIService.ts +754 -753
  184. package/src/services/MemoryIndexService.ts +1009 -1003
  185. package/src/services/MemoryService.ts +369 -369
  186. package/src/services/QueryService.ts +890 -890
  187. package/src/services/StorageService.ts +1185 -1182
  188. package/src/services/TransactionService.ts +838 -838
  189. package/src/services/VectorService.ts +462 -462
  190. package/src/services/ViewService.ts +484 -484
  191. package/src/services/index.ts +25 -25
  192. package/src/services/storage/BlobAttributesManager.ts +333 -333
  193. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  194. package/src/services/storage/MemorySearchManager.ts +387 -387
  195. package/src/services/storage/QuiltBatchManager.ts +1157 -1130
  196. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  197. package/src/services/storage/WalrusStorageManager.ts +333 -287
  198. package/src/services/storage/index.ts +57 -57
  199. package/src/types/index.ts +13 -13
  200. package/src/utils/index.ts +76 -76
  201. package/src/utils/memoryIndexOnChain.ts +507 -507
  202. package/src/vector/BrowserHnswIndexService.ts +758 -758
  203. package/src/vector/HnswWasmService.ts +731 -731
  204. package/src/vector/IHnswService.ts +233 -233
  205. package/src/vector/NodeHnswService.ts +833 -833
  206. package/src/vector/createHnswService.ts +147 -135
  207. package/src/vector/index.ts +56 -56
  208. package/src/wallet/ContextWalletService.ts +656 -656
  209. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,1004 +1,1010 @@
1
- /**
2
- * MemoryIndexService - Enhanced Memory Indexing and Vector Search
3
- *
4
- * Hybrid HNSW-powered memory indexing service providing:
5
- * - O(log N) vector similarity search performance
6
- * - Advanced clustering and graph-based memory organization
7
- * - Intelligent semantic search with relevance scoring
8
- * - Dynamic index optimization and parameter tuning
9
- * - Multi-dimensional vector space analysis
10
- *
11
- * Automatically selects the appropriate HNSW implementation:
12
- * - Browser: hnswlib-wasm (WebAssembly)
13
- * - Node.js: hnswlib-node (native bindings)
14
- */
15
-
16
- import { createHnswService, isBrowser, isNode } from '../vector/createHnswService';
17
- import type { IHnswService, IHnswSearchOptions } from '../vector/IHnswService';
18
- import { EmbeddingService } from './EmbeddingService';
19
- import { StorageService, type MemoryMetadata } from './StorageService';
20
-
21
- export interface MemoryIndexEntry {
22
- memoryId: string;
23
- blobId: string;
24
- vectorId: number;
25
- embedding: number[];
26
- metadata: MemoryMetadata;
27
- indexedAt: Date;
28
- lastAccessed?: Date;
29
- }
30
-
31
- export interface MemoryIndexOptions {
32
- maxElements?: number;
33
- dimension?: number;
34
- efConstruction?: number;
35
- m?: number;
36
- batchSize?: number;
37
- autoFlushInterval?: number;
38
- /** Pre-initialized HNSW service instance (shared singleton) */
39
- hnswService?: IHnswService;
40
- }
41
-
42
- export interface MemorySearchQuery {
43
- query?: string;
44
- vector?: number[];
45
- userAddress: string;
46
- k?: number;
47
- threshold?: number;
48
-
49
- // Memory-specific filters
50
- categories?: string[];
51
- dateRange?: {
52
- start?: Date;
53
- end?: Date;
54
- };
55
- importanceRange?: {
56
- min?: number;
57
- max?: number;
58
- };
59
- tags?: string[];
60
- includeContent?: boolean;
61
-
62
- // Enhanced search features
63
- searchMode?: 'semantic' | 'hybrid' | 'exact'; // Search strategy
64
- boostRecent?: boolean; // Boost recently created memories
65
- diversityFactor?: number; // Result diversity (0-1)
66
- }
67
-
68
- export interface MemorySearchResult {
69
- memoryId: string;
70
- blobId: string;
71
- metadata: MemoryMetadata;
72
- similarity: number;
73
- relevanceScore: number;
74
- content?: string | Uint8Array;
75
- extractedAt?: Date;
76
- embedding?: number[]; // Optional embedding vector for advanced similarity operations
77
- clusterInfo?: {
78
- clusterId: number;
79
- clusterCenter: number[];
80
- intraClusterSimilarity: number;
81
- };
82
- }
83
-
84
- /**
85
- * Memory-focused indexing service providing high-level memory operations
86
- * Uses hybrid HNSW implementation (browser: wasm, node: native) for optimal performance
87
- */
88
- export class MemoryIndexService {
89
- private hnswService: IHnswService | null = null;
90
- private hnswServicePromise: Promise<IHnswService> | null = null;
91
- private embeddingService?: EmbeddingService;
92
- private storageService?: StorageService;
93
- private memoryIndex = new Map<string, Map<string, MemoryIndexEntry>>(); // userAddress -> memoryId -> entry
94
- private nextMemoryId = 1;
95
- private options: MemoryIndexOptions;
96
-
97
- // Performance tracking
98
- private indexStats = new Map<string, {
99
- totalVectors: number;
100
- avgSimilarity: number;
101
- searchLatency: number[];
102
- lastOptimized: Date;
103
- }>();
104
-
105
- constructor(
106
- storageService?: StorageService,
107
- options: MemoryIndexOptions = {}
108
- ) {
109
- this.storageService = storageService;
110
- this.options = options;
111
-
112
- // Use pre-initialized HNSW service if provided (shared singleton pattern)
113
- if (options.hnswService) {
114
- this.hnswService = options.hnswService;
115
- console.log('✅ MemoryIndexService using shared HNSW service instance');
116
- } else {
117
- // Initialize HNSW service asynchronously using factory
118
- this.hnswServicePromise = this.initializeHnswService();
119
-
120
- const envType = isBrowser() ? 'browser (hnswlib-wasm)' : isNode() ? 'Node.js (hnswlib-node)' : 'unknown';
121
- console.log(`✅ MemoryIndexService initializing with hybrid HNSW (${envType})`);
122
- }
123
-
124
- console.log(` Max elements: ${options.maxElements || 10000}`);
125
- console.log(` Embedding dimension: ${options.dimension || 3072}`);
126
- console.log(` HNSW parameters: M=${options.m || 16}, efConstruction=${options.efConstruction || 200}`);
127
- }
128
-
129
- /**
130
- * Initialize HNSW service using factory (auto-detects environment)
131
- */
132
- private async initializeHnswService(): Promise<IHnswService> {
133
- try {
134
- const service = await createHnswService({
135
- indexConfig: {
136
- dimension: this.options.dimension || 3072,
137
- maxElements: this.options.maxElements || 10000,
138
- efConstruction: this.options.efConstruction || 200,
139
- m: this.options.m || 16
140
- },
141
- batchConfig: {
142
- maxBatchSize: this.options.batchSize || 100,
143
- batchDelayMs: this.options.autoFlushInterval || 5000
144
- }
145
- });
146
-
147
- this.hnswService = service;
148
- console.log('✅ HNSW service initialized successfully');
149
- return service;
150
- } catch (error) {
151
- console.error('❌ Failed to initialize HNSW service:', error);
152
- throw error;
153
- }
154
- }
155
-
156
- /**
157
- * Get HNSW service (waits for initialization if needed)
158
- */
159
- private async getHnswService(): Promise<IHnswService> {
160
- if (this.hnswService) {
161
- return this.hnswService;
162
- }
163
- if (this.hnswServicePromise) {
164
- return this.hnswServicePromise;
165
- }
166
- throw new Error('HNSW service not initialized');
167
- }
168
-
169
- /**
170
- * Initialize with embedding service
171
- */
172
- initialize(embeddingService: EmbeddingService, storageService?: StorageService) {
173
- this.embeddingService = embeddingService;
174
- if (storageService) {
175
- this.storageService = storageService;
176
- }
177
- console.log('✅ MemoryIndexService: Embedding service connected');
178
- }
179
-
180
- /**
181
- * Index a memory with its content, metadata, and vector embedding
182
- *
183
- * @param userAddress - User's wallet address
184
- * @param memoryId - Unique memory identifier
185
- * @param blobId - Walrus blob ID for the memory
186
- * @param content - Memory content (stored in index only if not encrypted)
187
- * @param metadata - Memory metadata
188
- * @param embedding - Pre-computed embedding vector (optional)
189
- * @param options - Indexing options
190
- * @param options.isEncrypted - If true, content will NOT be stored in index (security)
191
- */
192
- async indexMemory(
193
- userAddress: string,
194
- memoryId: string,
195
- blobId: string,
196
- content: string,
197
- metadata: MemoryMetadata,
198
- embedding?: number[],
199
- options?: { isEncrypted?: boolean }
200
- ): Promise<{ vectorId: number; indexed: boolean }> {
201
- try {
202
- console.log(`📊 Indexing memory ${memoryId} for user ${userAddress}`);
203
-
204
- // Generate embedding if not provided
205
- let memoryEmbedding = embedding;
206
- if (!memoryEmbedding && this.embeddingService) {
207
- const embeddingResult = await this.embeddingService.embedText({ text: content });
208
- memoryEmbedding = embeddingResult.vector;
209
- }
210
-
211
- if (!memoryEmbedding) {
212
- throw new Error('No embedding provided and no embedding service available');
213
- }
214
-
215
- // Generate vector ID
216
- const vectorId = this.nextMemoryId++;
217
-
218
- // Add to HNSW index with batching
219
- // Option A+: Store content in index ONLY when encryption is OFF (security consideration)
220
- const hnswService = await this.getHnswService();
221
- const isEncrypted = options?.isEncrypted ?? false;
222
-
223
- const vectorMetadata: Record<string, unknown> = {
224
- memoryId,
225
- blobId,
226
- category: metadata.category,
227
- topic: metadata.topic,
228
- importance: metadata.importance,
229
- contentType: metadata.contentType,
230
- createdTimestamp: metadata.createdTimestamp,
231
- customMetadata: metadata.customMetadata
232
- };
233
-
234
- // Only store content if NOT encrypted (privacy-safe)
235
- if (!isEncrypted && content) {
236
- vectorMetadata.content = content;
237
- console.log(' 💾 Content stored in local index (encryption OFF)');
238
- } else if (isEncrypted) {
239
- console.log(' 🔒 Content NOT stored in index (encryption ON - security)');
240
- }
241
-
242
- await hnswService.addVector(
243
- userAddress,
244
- vectorId,
245
- memoryEmbedding,
246
- vectorMetadata
247
- );
248
-
249
- // Update performance statistics
250
- this.updateIndexStats(userAddress);
251
-
252
- // Store in memory index
253
- if (!this.memoryIndex.has(userAddress)) {
254
- this.memoryIndex.set(userAddress, new Map());
255
- }
256
-
257
- const indexEntry: MemoryIndexEntry = {
258
- memoryId,
259
- blobId,
260
- vectorId,
261
- embedding: memoryEmbedding,
262
- metadata,
263
- indexedAt: new Date()
264
- };
265
-
266
- this.memoryIndex.get(userAddress)!.set(memoryId, indexEntry);
267
-
268
- console.log(`✅ Memory indexed: ${memoryId} (vector ${vectorId})`);
269
- console.log(` Category: ${metadata.category}`);
270
- console.log(` Importance: ${metadata.importance}`);
271
- console.log(` Embedding dimension: ${memoryEmbedding.length}`);
272
-
273
- return { vectorId, indexed: true };
274
-
275
- } catch (error) {
276
- console.error('❌ Failed to index memory:', error);
277
- throw error;
278
- }
279
- }
280
-
281
- /**
282
- * Enhanced memory search using native HNSW with advanced features
283
- * Supports semantic search, metadata filtering, and intelligent relevance scoring
284
- */
285
- async searchMemories(query: MemorySearchQuery): Promise<MemorySearchResult[]> {
286
- const startTime = performance.now();
287
-
288
- try {
289
- console.log(`🔍 Memory search for user ${query.userAddress}`);
290
- console.log(` Query: "${query.query || 'vector search'}"`);
291
- console.log(` Mode: ${query.searchMode || 'semantic'}, K: ${query.k || 10}`);
292
-
293
- // Generate query vector if needed
294
- let queryVector = query.vector;
295
- if (!queryVector && query.query && this.embeddingService) {
296
- const embeddingResult = await this.embeddingService.embedText({ text: query.query });
297
- queryVector = embeddingResult.vector;
298
- }
299
-
300
- if (!queryVector) {
301
- throw new Error('No query vector provided and no query text with embedding service');
302
- }
303
-
304
- const userMemories = this.memoryIndex.get(query.userAddress);
305
- if (!userMemories || userMemories.size === 0) {
306
- console.log(' No memories found for user');
307
- return [];
308
- }
309
-
310
- // Configure search parameters based on query mode
311
- const searchK = query.k || 10;
312
- const efSearch = query.searchMode === 'exact' ? searchK * 4 :
313
- query.searchMode === 'hybrid' ? searchK * 2 : searchK;
314
-
315
- // Perform HNSW search using native implementation
316
- const hnswService = await this.getHnswService();
317
- const hnswResults = await hnswService.search(
318
- query.userAddress,
319
- queryVector,
320
- {
321
- k: Math.min(searchK * 3, 100), // Get more candidates for post-filtering
322
- ef: efSearch,
323
- filter: this.createMetadataFilter(query)
324
- }
325
- );
326
-
327
- // Convert to memory search results with enhanced scoring
328
- const results: MemorySearchResult[] = [];
329
-
330
- for (const result of hnswResults) {
331
- const vectorId = result.vectorId;
332
- const distance = result.distance;
333
- // Calculate similarity from distance (cosine distance: similarity = 1 - distance)
334
- const similarity = result.score;
335
-
336
- // Skip results below threshold
337
- if (query.threshold && similarity < query.threshold) {
338
- continue;
339
- }
340
-
341
- // Find memory entry by vector ID
342
- const memoryEntry = Array.from(userMemories.values()).find(entry => entry.vectorId === vectorId);
343
- if (!memoryEntry) continue;
344
-
345
- // Enhanced relevance scoring
346
- let relevanceScore = this.calculateAdvancedRelevanceScore(
347
- similarity,
348
- memoryEntry.metadata,
349
- query,
350
- queryVector,
351
- memoryEntry.embedding || []
352
- );
353
-
354
- // Apply recency boost if requested
355
- if (query.boostRecent) {
356
- const recencyBoost = this.calculateRecencyBoost(memoryEntry.metadata.createdTimestamp || 0);
357
- relevanceScore += recencyBoost * 0.1;
358
- }
359
-
360
- // Option A+: Get content from HNSW metadata if available (avoids Walrus fetch!)
361
- // Content is only stored when encryption is OFF (see indexMemory)
362
- const indexedContent = result.metadata?.content as string | undefined;
363
-
364
- results.push({
365
- memoryId: memoryEntry.memoryId,
366
- blobId: memoryEntry.blobId,
367
- metadata: memoryEntry.metadata,
368
- similarity,
369
- relevanceScore,
370
- content: indexedContent, // ✅ Return content from local index (no Walrus fetch needed!)
371
- extractedAt: memoryEntry.indexedAt
372
- });
373
- }
374
-
375
- // Apply diversity filtering if requested
376
- let finalResults = results;
377
- if (query.diversityFactor && query.diversityFactor > 0) {
378
- finalResults = this.diversifyResults(results, query.diversityFactor);
379
- }
380
-
381
- // Sort by relevance score and limit results
382
- finalResults.sort((a, b) => b.relevanceScore - a.relevanceScore);
383
- finalResults = finalResults.slice(0, searchK);
384
-
385
- // Update search statistics
386
- const searchLatency = performance.now() - startTime;
387
- this.updateSearchStats(query.userAddress, searchLatency);
388
-
389
- // Count how many results have content from local index (Option A+)
390
- const resultsWithLocalContent = finalResults.filter(r => r.content !== undefined).length;
391
-
392
- console.log(`✅ Search completed in ${searchLatency.toFixed(2)}ms`);
393
- console.log(` Found ${finalResults.length} results (similarity range: ${finalResults.length > 0 ? finalResults[finalResults.length-1].similarity.toFixed(3) : 'N/A'} - ${finalResults.length > 0 ? finalResults[0].similarity.toFixed(3) : 'N/A'}`);
394
- if (resultsWithLocalContent > 0) {
395
- console.log(` 📦 ${resultsWithLocalContent}/${finalResults.length} results have content from local index (no Walrus fetch needed!)`);
396
- }
397
-
398
- return finalResults;
399
-
400
- } catch (error) {
401
- console.error('❌ Memory search failed:', error);
402
- throw error;
403
- }
404
- }
405
-
406
- /**
407
- * Get all memories for a user with optional filtering
408
- */
409
- async getUserMemories(
410
- userAddress: string,
411
- filters?: {
412
- categories?: string[];
413
- dateRange?: { start?: Date; end?: Date };
414
- importanceRange?: { min?: number; max?: number };
415
- limit?: number;
416
- }
417
- ): Promise<MemorySearchResult[]> {
418
- const userMemories = this.memoryIndex.get(userAddress);
419
- if (!userMemories) {
420
- return [];
421
- }
422
-
423
- const results: MemorySearchResult[] = [];
424
-
425
- for (const [memoryId, entry] of userMemories) {
426
- // Apply filters
427
- if (filters) {
428
- if (filters.categories && !filters.categories.includes(entry.metadata.category)) {
429
- continue;
430
- }
431
-
432
- if (filters.importanceRange) {
433
- const importance = entry.metadata.importance || 5;
434
- if (filters.importanceRange.min && importance < filters.importanceRange.min) continue;
435
- if (filters.importanceRange.max && importance > filters.importanceRange.max) continue;
436
- }
437
-
438
- if (filters.dateRange) {
439
- const created = new Date(entry.metadata.createdTimestamp || 0);
440
- if (filters.dateRange.start && created < filters.dateRange.start) continue;
441
- if (filters.dateRange.end && created > filters.dateRange.end) continue;
442
- }
443
- }
444
-
445
- results.push({
446
- memoryId: entry.memoryId,
447
- blobId: entry.blobId,
448
- metadata: entry.metadata,
449
- similarity: 1.0, // No similarity for direct listing
450
- relevanceScore: entry.metadata.importance || 5,
451
- extractedAt: entry.indexedAt
452
- });
453
- }
454
-
455
- // Sort by importance and creation time
456
- results.sort((a, b) => {
457
- const importanceDiff = (b.metadata.importance || 5) - (a.metadata.importance || 5);
458
- if (importanceDiff !== 0) return importanceDiff;
459
- return (b.metadata.createdTimestamp || 0) - (a.metadata.createdTimestamp || 0);
460
- });
461
-
462
- // Apply limit
463
- if (filters?.limit) {
464
- return results.slice(0, filters.limit);
465
- }
466
-
467
- return results;
468
- }
469
-
470
- /**
471
- * Remove memory from index
472
- */
473
- async removeMemory(userAddress: string, memoryId: string): Promise<boolean> {
474
- try {
475
- const userMemories = this.memoryIndex.get(userAddress);
476
- if (!userMemories) {
477
- return false;
478
- }
479
-
480
- const entry = userMemories.get(memoryId);
481
- if (!entry) {
482
- return false;
483
- }
484
-
485
- // Remove from HNSW index (if supported)
486
- // Note: hnswlib-node doesn't support deletion, so we just mark as removed
487
-
488
- // Remove from memory index
489
- userMemories.delete(memoryId);
490
-
491
- console.log(`✅ Memory removed from index: ${memoryId}`);
492
- return true;
493
-
494
- } catch (error) {
495
- console.error('❌ Failed to remove memory from index:', error);
496
- return false;
497
- }
498
- }
499
-
500
- /**
501
- * Get index statistics for a user
502
- */
503
- getIndexStats(userAddress: string): {
504
- totalMemories: number;
505
- categoryCounts: Record<string, number>;
506
- importanceDistribution: Record<number, number>;
507
- averageImportance: number;
508
- oldestMemory: Date | null;
509
- newestMemory: Date | null;
510
- indexSize: number;
511
- } {
512
- const userMemories = this.memoryIndex.get(userAddress);
513
- if (!userMemories) {
514
- return {
515
- totalMemories: 0,
516
- categoryCounts: {},
517
- importanceDistribution: {},
518
- averageImportance: 0,
519
- oldestMemory: null,
520
- newestMemory: null,
521
- indexSize: 0
522
- };
523
- }
524
-
525
- const categoryCounts: Record<string, number> = {};
526
- const importanceDistribution: Record<number, number> = {};
527
- let totalImportance = 0;
528
- let oldestMemory: Date | null = null;
529
- let newestMemory: Date | null = null;
530
-
531
- for (const entry of userMemories.values()) {
532
- // Categories
533
- categoryCounts[entry.metadata.category] = (categoryCounts[entry.metadata.category] || 0) + 1;
534
-
535
- // Importance
536
- const importance = entry.metadata.importance || 5;
537
- importanceDistribution[importance] = (importanceDistribution[importance] || 0) + 1;
538
- totalImportance += importance;
539
-
540
- // Dates
541
- const created = new Date(entry.metadata.createdTimestamp || 0);
542
- if (!oldestMemory || created < oldestMemory) {
543
- oldestMemory = created;
544
- }
545
- if (!newestMemory || created > newestMemory) {
546
- newestMemory = created;
547
- }
548
- }
549
-
550
- return {
551
- totalMemories: userMemories.size,
552
- categoryCounts,
553
- importanceDistribution,
554
- averageImportance: userMemories.size > 0 ? totalImportance / userMemories.size : 0,
555
- oldestMemory,
556
- newestMemory,
557
- indexSize: userMemories.size
558
- };
559
- }
560
-
561
- /**
562
- * Flush pending operations and save index
563
- */
564
- async flush(userAddress: string): Promise<void> {
565
- const hnswService = await this.getHnswService();
566
- await hnswService.flushBatch(userAddress);
567
- console.log(`✅ Memory index flushed for user ${userAddress}`);
568
- }
569
-
570
- /**
571
- * Load index from storage (local or Walrus)
572
- *
573
- * @param userAddress - User's wallet address
574
- * @param indexBlobId - Optional Walrus blob ID to load from cloud
575
- */
576
- async loadIndex(userAddress: string, indexBlobId?: string): Promise<void> {
577
- const hnswService = await this.getHnswService();
578
-
579
- // If blobId provided, try to load from Walrus first
580
- if (indexBlobId && 'loadFromWalrus' in hnswService) {
581
- console.log(`📥 Attempting to load index from Walrus: ${indexBlobId}`);
582
- const walrusLoaded = await (hnswService as any).loadFromWalrus(userAddress, indexBlobId);
583
- if (walrusLoaded) {
584
- console.log(`✅ Memory index loaded from Walrus for user ${userAddress}`);
585
- return;
586
- }
587
- console.log(`⚠️ Walrus load failed, falling back to local storage`);
588
- }
589
-
590
- // Fallback to local storage
591
- const loaded = await hnswService.loadIndex(userAddress);
592
- if (loaded) {
593
- console.log(`✅ Memory index loaded from local storage for user ${userAddress}`);
594
- }
595
- }
596
-
597
- /**
598
- * Save index to local storage
599
- */
600
- async saveIndex(userAddress: string): Promise<void> {
601
- const hnswService = await this.getHnswService();
602
- await hnswService.saveIndex(userAddress);
603
- console.log(`✅ Memory index saved for user ${userAddress}`);
604
- }
605
-
606
- /**
607
- * Sync index to Walrus cloud storage
608
- *
609
- * @param userAddress - User's wallet address
610
- * @returns Walrus blob ID if successful, null if Walrus is disabled
611
- */
612
- async syncToWalrus(userAddress: string): Promise<string | null> {
613
- const hnswService = await this.getHnswService();
614
-
615
- if (!('syncToWalrus' in hnswService)) {
616
- console.warn('⚠️ HNSW service does not support Walrus sync');
617
- return null;
618
- }
619
-
620
- const blobId = await (hnswService as any).syncToWalrus(userAddress);
621
- if (blobId) {
622
- console.log(`☁️ Memory index synced to Walrus: ${blobId}`);
623
- }
624
- return blobId;
625
- }
626
-
627
- /**
628
- * Load index directly from Walrus cloud storage
629
- *
630
- * @param userAddress - User's wallet address
631
- * @param blobId - Walrus blob ID
632
- * @returns true if successfully loaded
633
- */
634
- async loadFromWalrus(userAddress: string, blobId: string): Promise<boolean> {
635
- const hnswService = await this.getHnswService();
636
-
637
- if (!('loadFromWalrus' in hnswService)) {
638
- console.warn('⚠️ HNSW service does not support Walrus load');
639
- return false;
640
- }
641
-
642
- const loaded = await (hnswService as any).loadFromWalrus(userAddress, blobId);
643
- if (loaded) {
644
- console.log(`☁️ Memory index loaded from Walrus: ${blobId}`);
645
- }
646
- return loaded;
647
- }
648
-
649
- /**
650
- * Get the Walrus blob ID for a user's index (if backed up)
651
- *
652
- * @param userAddress - User's wallet address
653
- * @returns Blob ID or null if not backed up
654
- */
655
- getWalrusBlobId(userAddress: string): string | null {
656
- if (this.hnswService && 'getWalrusBlobId' in this.hnswService) {
657
- return (this.hnswService as any).getWalrusBlobId(userAddress);
658
- }
659
- return null;
660
- }
661
-
662
- /**
663
- * Check if Walrus backup is enabled
664
- */
665
- isWalrusEnabled(): boolean {
666
- if (this.hnswService && 'isWalrusEnabled' in this.hnswService) {
667
- return (this.hnswService as any).isWalrusEnabled();
668
- }
669
- return false;
670
- }
671
-
672
- /**
673
- * Clear user's index
674
- */
675
- async clearUserIndex(userAddress: string): Promise<void> {
676
- this.memoryIndex.delete(userAddress);
677
- const hnswService = await this.getHnswService();
678
- await hnswService.deleteIndex(userAddress);
679
- console.log(`✅ Memory index cleared for user ${userAddress}`);
680
- }
681
-
682
- /**
683
- * Get overall service statistics
684
- */
685
- async getServiceStats() {
686
- const totalMemories = Array.from(this.memoryIndex.values())
687
- .reduce((sum, userMemories) => sum + userMemories.size, 0);
688
-
689
- let hnswStats = null;
690
- if (this.hnswService) {
691
- hnswStats = this.hnswService.getBatchStats();
692
- }
693
-
694
- return {
695
- totalUsers: this.memoryIndex.size,
696
- totalMemories,
697
- hnswStats,
698
- hasEmbeddingService: !!this.embeddingService,
699
- hasStorageService: !!this.storageService
700
- };
701
- }
702
-
703
- /**
704
- * Destroy service and cleanup resources
705
- */
706
- destroy(): void {
707
- if (this.hnswService) {
708
- this.hnswService.destroy();
709
- }
710
- this.hnswService = null;
711
- this.hnswServicePromise = null;
712
- this.memoryIndex.clear();
713
- console.log('✅ MemoryIndexService destroyed');
714
- }
715
-
716
- // ==================== PRIVATE HELPER METHODS ====================
717
-
718
- private createMemoryFilter(query: MemorySearchQuery) {
719
- return (metadata: any) => {
720
- // Category filter
721
- if (query.categories && query.categories.length > 0) {
722
- if (!query.categories.includes(metadata.category)) {
723
- return false;
724
- }
725
- }
726
-
727
- // Date range filter
728
- if (query.dateRange) {
729
- const created = new Date(metadata.createdTimestamp || 0);
730
- if (query.dateRange.start && created < query.dateRange.start) {
731
- return false;
732
- }
733
- if (query.dateRange.end && created > query.dateRange.end) {
734
- return false;
735
- }
736
- }
737
-
738
- // Importance range filter
739
- if (query.importanceRange) {
740
- const importance = metadata.importance || 5;
741
- if (query.importanceRange.min && importance < query.importanceRange.min) {
742
- return false;
743
- }
744
- if (query.importanceRange.max && importance > query.importanceRange.max) {
745
- return false;
746
- }
747
- }
748
-
749
- // Tags filter (search in custom metadata)
750
- if (query.tags && query.tags.length > 0) {
751
- const metadataText = JSON.stringify(metadata).toLowerCase();
752
- const hasAnyTag = query.tags.some(tag =>
753
- metadataText.includes(tag.toLowerCase())
754
- );
755
- if (!hasAnyTag) {
756
- return false;
757
- }
758
- }
759
-
760
- return true;
761
- };
762
- }
763
-
764
- // ==================== HNSW HELPER METHODS ====================
765
-
766
- /**
767
- * Create metadata filter for HNSW search
768
- */
769
- private createMetadataFilter(query: MemorySearchQuery): ((metadata: any) => boolean) | undefined {
770
- if (!query.categories && !query.dateRange && !query.importanceRange && !query.tags) {
771
- return undefined;
772
- }
773
-
774
- return (metadata: any) => {
775
- // Category filter
776
- if (query.categories && query.categories.length > 0) {
777
- if (!query.categories.includes(metadata.category)) {
778
- return false;
779
- }
780
- }
781
-
782
- // Date range filter
783
- if (query.dateRange) {
784
- const created = new Date(metadata.createdTimestamp || 0);
785
- if (query.dateRange.start && created < query.dateRange.start) {
786
- return false;
787
- }
788
- if (query.dateRange.end && created > query.dateRange.end) {
789
- return false;
790
- }
791
- }
792
-
793
- // Importance range filter
794
- if (query.importanceRange) {
795
- const importance = metadata.importance || 5;
796
- if (query.importanceRange.min && importance < query.importanceRange.min) {
797
- return false;
798
- }
799
- if (query.importanceRange.max && importance > query.importanceRange.max) {
800
- return false;
801
- }
802
- }
803
-
804
- // Tags filter
805
- if (query.tags && query.tags.length > 0) {
806
- const metadataText = JSON.stringify(metadata).toLowerCase();
807
- const hasAnyTag = query.tags.some(tag =>
808
- metadataText.includes(tag.toLowerCase())
809
- );
810
- if (!hasAnyTag) {
811
- return false;
812
- }
813
- }
814
-
815
- return true;
816
- };
817
- }
818
-
819
- /**
820
- * Update index statistics for a user
821
- */
822
- private updateIndexStats(userAddress: string): void {
823
- let stats = this.indexStats.get(userAddress);
824
- if (!stats) {
825
- stats = {
826
- totalVectors: 0,
827
- avgSimilarity: 0,
828
- searchLatency: [],
829
- lastOptimized: new Date()
830
- };
831
- this.indexStats.set(userAddress, stats);
832
- }
833
- stats.totalVectors++;
834
- }
835
-
836
- /**
837
- * Enhanced relevance scoring with multiple factors
838
- */
839
- private calculateAdvancedRelevanceScore(
840
- similarity: number,
841
- metadata: MemoryMetadata,
842
- query: MemorySearchQuery,
843
- queryVector: number[],
844
- documentVector: number[]
845
- ): number {
846
- let score = similarity * 0.7; // Base similarity weight (increased)
847
-
848
- // Importance boost
849
- const importance = metadata.importance || 5;
850
- score += (importance - 5) * 0.02; // -0.1 to +0.1 boost
851
-
852
- // Category exact match boost
853
- if (query.categories && query.categories.includes(metadata.category)) {
854
- score += 0.15;
855
- }
856
-
857
- // Topic relevance boost
858
- if (query.query && metadata.topic) {
859
- const queryLower = query.query.toLowerCase();
860
- const topicLower = metadata.topic.toLowerCase();
861
- if (queryLower.includes(topicLower) || topicLower.includes(queryLower)) {
862
- score += 0.1;
863
- }
864
- }
865
-
866
- // Vector quality boost (based on vector magnitude)
867
- const vectorMagnitude = this.calculateVectorMagnitude(documentVector);
868
- if (vectorMagnitude > 0.1) { // Well-formed embedding
869
- score += 0.05;
870
- }
871
-
872
- // Semantic consistency boost (cosine similarity in different metric)
873
- const semanticConsistency = this.calculateSemanticConsistency(queryVector, documentVector);
874
- score += semanticConsistency * 0.1;
875
-
876
- return Math.min(1.0, Math.max(0.0, score));
877
- }
878
-
879
- /**
880
- * Calculate recency boost based on creation timestamp
881
- */
882
- private calculateRecencyBoost(createdTimestamp: number): number {
883
- const now = Date.now();
884
- const ageInDays = (now - createdTimestamp) / (1000 * 60 * 60 * 24);
885
-
886
- // Exponential decay: more recent = higher boost
887
- if (ageInDays < 1) return 1.0; // Last day: full boost
888
- if (ageInDays < 7) return 0.8; // Last week: 80% boost
889
- if (ageInDays < 30) return 0.5; // Last month: 50% boost
890
- if (ageInDays < 90) return 0.2; // Last quarter: 20% boost
891
- return 0.0; // Older: no boost
892
- }
893
-
894
- /**
895
- * Diversify search results to avoid clustering
896
- */
897
- private diversifyResults(results: MemorySearchResult[], diversityFactor: number): MemorySearchResult[] {
898
- if (diversityFactor <= 0 || results.length <= 1) return results;
899
-
900
- const diversified: MemorySearchResult[] = [];
901
- const remaining = [...results];
902
-
903
- // Always include the top result
904
- if (remaining.length > 0) {
905
- diversified.push(remaining.shift()!);
906
- }
907
-
908
- while (remaining.length > 0 && diversified.length < results.length * 0.8) {
909
- let bestIndex = 0;
910
- let bestScore = 0;
911
-
912
- for (let i = 0; i < remaining.length; i++) {
913
- const candidate = remaining[i];
914
-
915
- // Calculate diversity score (distance from already selected results)
916
- let minSimilarity = 1.0;
917
- for (const selected of diversified) {
918
- const similarity = candidate.similarity; // Could enhance with actual vector similarity
919
- minSimilarity = Math.min(minSimilarity, similarity);
920
- }
921
-
922
- // Combine relevance and diversity
923
- const diversityScore = candidate.relevanceScore * (1 - diversityFactor) +
924
- (1 - minSimilarity) * diversityFactor;
925
-
926
- if (diversityScore > bestScore) {
927
- bestScore = diversityScore;
928
- bestIndex = i;
929
- }
930
- }
931
-
932
- diversified.push(remaining.splice(bestIndex, 1)[0]);
933
- }
934
-
935
- return diversified;
936
- }
937
-
938
- /**
939
- * Update search performance statistics
940
- */
941
- private updateSearchStats(userAddress: string, latency: number): void {
942
- let stats = this.indexStats.get(userAddress);
943
- if (!stats) {
944
- stats = {
945
- totalVectors: 0,
946
- avgSimilarity: 0,
947
- searchLatency: [],
948
- lastOptimized: new Date()
949
- };
950
- this.indexStats.set(userAddress, stats);
951
- }
952
-
953
- stats.searchLatency.push(latency);
954
-
955
- // Keep only last 100 latency measurements
956
- if (stats.searchLatency.length > 100) {
957
- stats.searchLatency = stats.searchLatency.slice(-100);
958
- }
959
- }
960
-
961
- /**
962
- * Calculate vector magnitude
963
- */
964
- private calculateVectorMagnitude(vector: number[]): number {
965
- let sum = 0;
966
- for (const val of vector) {
967
- sum += val * val;
968
- }
969
- return Math.sqrt(sum);
970
- }
971
-
972
- /**
973
- * Calculate cosine similarity between two vectors
974
- * Used for semantic consistency scoring
975
- */
976
- private cosineSimilarity(a: number[], b: number[]): number {
977
- if (a.length !== b.length) return 0;
978
-
979
- let dotProduct = 0;
980
- let normA = 0;
981
- let normB = 0;
982
-
983
- for (let i = 0; i < a.length; i++) {
984
- dotProduct += a[i] * b[i];
985
- normA += a[i] * a[i];
986
- normB += b[i] * b[i];
987
- }
988
-
989
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
990
- return magnitude === 0 ? 0 : dotProduct / magnitude;
991
- }
992
-
993
- /**
994
- * Calculate semantic consistency score
995
- */
996
- private calculateSemanticConsistency(queryVector: number[], documentVector: number[]): number {
997
- // Calculate angle between vectors (semantic consistency)
998
- const similarity = this.cosineSimilarity(queryVector, documentVector);
999
- const angle = Math.acos(Math.max(-1, Math.min(1, similarity)));
1000
-
1001
- // Convert angle to consistency score (0-1, where 1 is perfect alignment)
1002
- return 1 - (angle / Math.PI);
1003
- }
1
+ /**
2
+ * MemoryIndexService - Enhanced Memory Indexing and Vector Search
3
+ *
4
+ * Hybrid HNSW-powered memory indexing service providing:
5
+ * - O(log N) vector similarity search performance
6
+ * - Advanced clustering and graph-based memory organization
7
+ * - Intelligent semantic search with relevance scoring
8
+ * - Dynamic index optimization and parameter tuning
9
+ * - Multi-dimensional vector space analysis
10
+ *
11
+ * Automatically selects the appropriate HNSW implementation:
12
+ * - Browser: hnswlib-wasm (WebAssembly)
13
+ * - Node.js: hnswlib-node (native bindings)
14
+ */
15
+
16
+ import { createHnswService, isBrowser, isNode } from '../vector/createHnswService';
17
+ import type { IHnswService, IHnswSearchOptions } from '../vector/IHnswService';
18
+ import { EmbeddingService } from './EmbeddingService';
19
+ import { StorageService, type MemoryMetadata } from './StorageService';
20
+
21
+ export interface MemoryIndexEntry {
22
+ memoryId: string;
23
+ blobId: string;
24
+ vectorId: number;
25
+ embedding: number[];
26
+ metadata: MemoryMetadata;
27
+ indexedAt: Date;
28
+ lastAccessed?: Date;
29
+ }
30
+
31
+ export interface MemoryIndexOptions {
32
+ maxElements?: number;
33
+ dimension?: number;
34
+ efConstruction?: number;
35
+ m?: number;
36
+ batchSize?: number;
37
+ autoFlushInterval?: number;
38
+ /** Pre-initialized HNSW service instance (shared singleton) */
39
+ hnswService?: IHnswService;
40
+ }
41
+
42
+ export interface MemorySearchQuery {
43
+ query?: string;
44
+ vector?: number[];
45
+ userAddress: string;
46
+ k?: number;
47
+ threshold?: number;
48
+
49
+ // Memory-specific filters
50
+ categories?: string[];
51
+ dateRange?: {
52
+ start?: Date;
53
+ end?: Date;
54
+ };
55
+ importanceRange?: {
56
+ min?: number;
57
+ max?: number;
58
+ };
59
+ tags?: string[];
60
+ includeContent?: boolean;
61
+
62
+ // Enhanced search features
63
+ searchMode?: 'semantic' | 'hybrid' | 'exact'; // Search strategy
64
+ boostRecent?: boolean; // Boost recently created memories
65
+ diversityFactor?: number; // Result diversity (0-1)
66
+ }
67
+
68
+ export interface MemorySearchResult {
69
+ memoryId: string;
70
+ blobId: string;
71
+ metadata: MemoryMetadata;
72
+ similarity: number;
73
+ relevanceScore: number;
74
+ content?: string | Uint8Array;
75
+ extractedAt?: Date;
76
+ embedding?: number[]; // Optional embedding vector for advanced similarity operations
77
+ clusterInfo?: {
78
+ clusterId: number;
79
+ clusterCenter: number[];
80
+ intraClusterSimilarity: number;
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Memory-focused indexing service providing high-level memory operations
86
+ * Uses hybrid HNSW implementation (browser: wasm, node: native) for optimal performance
87
+ */
88
+ export class MemoryIndexService {
89
+ private hnswService: IHnswService | null = null;
90
+ private hnswServicePromise: Promise<IHnswService> | null = null;
91
+ private embeddingService?: EmbeddingService;
92
+ private storageService?: StorageService;
93
+ private memoryIndex = new Map<string, Map<string, MemoryIndexEntry>>(); // userAddress -> memoryId -> entry
94
+ private nextMemoryId = 1;
95
+ private options: MemoryIndexOptions;
96
+
97
+ // Performance tracking
98
+ private indexStats = new Map<string, {
99
+ totalVectors: number;
100
+ avgSimilarity: number;
101
+ searchLatency: number[];
102
+ lastOptimized: Date;
103
+ }>();
104
+
105
+ constructor(
106
+ storageService?: StorageService,
107
+ options: MemoryIndexOptions = {}
108
+ ) {
109
+ this.storageService = storageService;
110
+ this.options = options;
111
+
112
+ // Use pre-initialized HNSW service if provided (shared singleton pattern)
113
+ if (options.hnswService) {
114
+ this.hnswService = options.hnswService;
115
+ console.log('✅ MemoryIndexService using shared HNSW service instance');
116
+ } else {
117
+ // Initialize HNSW service asynchronously using factory
118
+ this.hnswServicePromise = this.initializeHnswService();
119
+
120
+ const envType = isBrowser() ? 'browser (hnswlib-wasm)' : isNode() ? 'Node.js (hnswlib-node)' : 'unknown';
121
+ console.log(`✅ MemoryIndexService initializing with hybrid HNSW (${envType})`);
122
+ }
123
+
124
+ console.log(` Max elements: ${options.maxElements || 10000}`);
125
+ console.log(` Embedding dimension: ${options.dimension || 768}`);
126
+ console.log(` HNSW parameters: M=${options.m || 16}, efConstruction=${options.efConstruction || 200}`);
127
+ }
128
+
129
+ /**
130
+ * Initialize HNSW service using factory (auto-detects environment)
131
+ */
132
+ private async initializeHnswService(): Promise<IHnswService> {
133
+ try {
134
+ const service = await createHnswService({
135
+ indexConfig: {
136
+ dimension: this.options.dimension || 768, // Default 768 for speed (was 3072)
137
+ maxElements: this.options.maxElements || 10000,
138
+ efConstruction: this.options.efConstruction || 200,
139
+ m: this.options.m || 16
140
+ },
141
+ batchConfig: {
142
+ maxBatchSize: this.options.batchSize || 100,
143
+ batchDelayMs: this.options.autoFlushInterval || 5000
144
+ }
145
+ });
146
+
147
+ this.hnswService = service;
148
+ console.log('✅ HNSW service initialized successfully');
149
+ return service;
150
+ } catch (error) {
151
+ console.error('❌ Failed to initialize HNSW service:', error);
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Get HNSW service (waits for initialization if needed)
158
+ */
159
+ private async getHnswService(): Promise<IHnswService> {
160
+ if (this.hnswService) {
161
+ return this.hnswService;
162
+ }
163
+ if (this.hnswServicePromise) {
164
+ return this.hnswServicePromise;
165
+ }
166
+ throw new Error('HNSW service not initialized');
167
+ }
168
+
169
+ /**
170
+ * Initialize with embedding service
171
+ */
172
+ initialize(embeddingService: EmbeddingService, storageService?: StorageService) {
173
+ this.embeddingService = embeddingService;
174
+ if (storageService) {
175
+ this.storageService = storageService;
176
+ }
177
+ console.log('✅ MemoryIndexService: Embedding service connected');
178
+ }
179
+
180
+ /**
181
+ * Index a memory with its content, metadata, and vector embedding
182
+ *
183
+ * @param userAddress - User's wallet address
184
+ * @param memoryId - Unique memory identifier
185
+ * @param blobId - Walrus blob ID for the memory
186
+ * @param content - Memory content (stored in index only if not encrypted)
187
+ * @param metadata - Memory metadata
188
+ * @param embedding - Pre-computed embedding vector (optional)
189
+ * @param options - Indexing options
190
+ * @param options.isEncrypted - If true, content will NOT be stored in index (security)
191
+ * @param options.forceStoreContent - If true, store content even when encrypted (for server-side RAG)
192
+ */
193
+ async indexMemory(
194
+ userAddress: string,
195
+ memoryId: string,
196
+ blobId: string,
197
+ content: string,
198
+ metadata: MemoryMetadata,
199
+ embedding?: number[],
200
+ options?: { isEncrypted?: boolean; forceStoreContent?: boolean }
201
+ ): Promise<{ vectorId: number; indexed: boolean }> {
202
+ try {
203
+ console.log(`📊 Indexing memory ${memoryId} for user ${userAddress}`);
204
+
205
+ // Generate embedding if not provided
206
+ let memoryEmbedding = embedding;
207
+ if (!memoryEmbedding && this.embeddingService) {
208
+ const embeddingResult = await this.embeddingService.embedText({ text: content });
209
+ memoryEmbedding = embeddingResult.vector;
210
+ }
211
+
212
+ if (!memoryEmbedding) {
213
+ throw new Error('No embedding provided and no embedding service available');
214
+ }
215
+
216
+ // Generate vector ID
217
+ const vectorId = this.nextMemoryId++;
218
+
219
+ // Add to HNSW index with batching
220
+ // Option A+: Store content in index ONLY when encryption is OFF (security consideration)
221
+ const hnswService = await this.getHnswService();
222
+ const isEncrypted = options?.isEncrypted ?? false;
223
+
224
+ const vectorMetadata: Record<string, unknown> = {
225
+ memoryId,
226
+ blobId,
227
+ category: metadata.category,
228
+ topic: metadata.topic,
229
+ importance: metadata.importance,
230
+ contentType: metadata.contentType,
231
+ createdTimestamp: metadata.createdTimestamp,
232
+ customMetadata: metadata.customMetadata
233
+ };
234
+
235
+ // Store content in index based on encryption and forceStoreContent settings
236
+ const forceStoreContent = options?.forceStoreContent ?? false;
237
+ if (forceStoreContent && content) {
238
+ // Force store content for server-side RAG (user explicitly opted-in)
239
+ vectorMetadata.content = content;
240
+ console.log(' 💾 Content stored in local index (forceStoreContent=true for RAG)');
241
+ } else if (!isEncrypted && content) {
242
+ vectorMetadata.content = content;
243
+ console.log(' 💾 Content stored in local index (encryption OFF)');
244
+ } else if (isEncrypted) {
245
+ console.log(' 🔒 Content NOT stored in index (encryption ON - security)');
246
+ }
247
+
248
+ await hnswService.addVector(
249
+ userAddress,
250
+ vectorId,
251
+ memoryEmbedding,
252
+ vectorMetadata
253
+ );
254
+
255
+ // Update performance statistics
256
+ this.updateIndexStats(userAddress);
257
+
258
+ // Store in memory index
259
+ if (!this.memoryIndex.has(userAddress)) {
260
+ this.memoryIndex.set(userAddress, new Map());
261
+ }
262
+
263
+ const indexEntry: MemoryIndexEntry = {
264
+ memoryId,
265
+ blobId,
266
+ vectorId,
267
+ embedding: memoryEmbedding,
268
+ metadata,
269
+ indexedAt: new Date()
270
+ };
271
+
272
+ this.memoryIndex.get(userAddress)!.set(memoryId, indexEntry);
273
+
274
+ console.log(`✅ Memory indexed: ${memoryId} (vector ${vectorId})`);
275
+ console.log(` Category: ${metadata.category}`);
276
+ console.log(` Importance: ${metadata.importance}`);
277
+ console.log(` Embedding dimension: ${memoryEmbedding.length}`);
278
+
279
+ return { vectorId, indexed: true };
280
+
281
+ } catch (error) {
282
+ console.error('❌ Failed to index memory:', error);
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Enhanced memory search using native HNSW with advanced features
289
+ * Supports semantic search, metadata filtering, and intelligent relevance scoring
290
+ */
291
+ async searchMemories(query: MemorySearchQuery): Promise<MemorySearchResult[]> {
292
+ const startTime = performance.now();
293
+
294
+ try {
295
+ console.log(`🔍 Memory search for user ${query.userAddress}`);
296
+ console.log(` Query: "${query.query || 'vector search'}"`);
297
+ console.log(` Mode: ${query.searchMode || 'semantic'}, K: ${query.k || 10}`);
298
+
299
+ // Generate query vector if needed
300
+ let queryVector = query.vector;
301
+ if (!queryVector && query.query && this.embeddingService) {
302
+ const embeddingResult = await this.embeddingService.embedText({ text: query.query });
303
+ queryVector = embeddingResult.vector;
304
+ }
305
+
306
+ if (!queryVector) {
307
+ throw new Error('No query vector provided and no query text with embedding service');
308
+ }
309
+
310
+ const userMemories = this.memoryIndex.get(query.userAddress);
311
+ if (!userMemories || userMemories.size === 0) {
312
+ console.log(' No memories found for user');
313
+ return [];
314
+ }
315
+
316
+ // Configure search parameters based on query mode
317
+ const searchK = query.k || 10;
318
+ const efSearch = query.searchMode === 'exact' ? searchK * 4 :
319
+ query.searchMode === 'hybrid' ? searchK * 2 : searchK;
320
+
321
+ // Perform HNSW search using native implementation
322
+ const hnswService = await this.getHnswService();
323
+ const hnswResults = await hnswService.search(
324
+ query.userAddress,
325
+ queryVector,
326
+ {
327
+ k: Math.min(searchK * 3, 100), // Get more candidates for post-filtering
328
+ ef: efSearch,
329
+ filter: this.createMetadataFilter(query)
330
+ }
331
+ );
332
+
333
+ // Convert to memory search results with enhanced scoring
334
+ const results: MemorySearchResult[] = [];
335
+
336
+ for (const result of hnswResults) {
337
+ const vectorId = result.vectorId;
338
+ const distance = result.distance;
339
+ // Calculate similarity from distance (cosine distance: similarity = 1 - distance)
340
+ const similarity = result.score;
341
+
342
+ // Skip results below threshold
343
+ if (query.threshold && similarity < query.threshold) {
344
+ continue;
345
+ }
346
+
347
+ // Find memory entry by vector ID
348
+ const memoryEntry = Array.from(userMemories.values()).find(entry => entry.vectorId === vectorId);
349
+ if (!memoryEntry) continue;
350
+
351
+ // Enhanced relevance scoring
352
+ let relevanceScore = this.calculateAdvancedRelevanceScore(
353
+ similarity,
354
+ memoryEntry.metadata,
355
+ query,
356
+ queryVector,
357
+ memoryEntry.embedding || []
358
+ );
359
+
360
+ // Apply recency boost if requested
361
+ if (query.boostRecent) {
362
+ const recencyBoost = this.calculateRecencyBoost(memoryEntry.metadata.createdTimestamp || 0);
363
+ relevanceScore += recencyBoost * 0.1;
364
+ }
365
+
366
+ // Option A+: Get content from HNSW metadata if available (avoids Walrus fetch!)
367
+ // Content is only stored when encryption is OFF (see indexMemory)
368
+ const indexedContent = result.metadata?.content as string | undefined;
369
+
370
+ results.push({
371
+ memoryId: memoryEntry.memoryId,
372
+ blobId: memoryEntry.blobId,
373
+ metadata: memoryEntry.metadata,
374
+ similarity,
375
+ relevanceScore,
376
+ content: indexedContent, // ✅ Return content from local index (no Walrus fetch needed!)
377
+ extractedAt: memoryEntry.indexedAt
378
+ });
379
+ }
380
+
381
+ // Apply diversity filtering if requested
382
+ let finalResults = results;
383
+ if (query.diversityFactor && query.diversityFactor > 0) {
384
+ finalResults = this.diversifyResults(results, query.diversityFactor);
385
+ }
386
+
387
+ // Sort by relevance score and limit results
388
+ finalResults.sort((a, b) => b.relevanceScore - a.relevanceScore);
389
+ finalResults = finalResults.slice(0, searchK);
390
+
391
+ // Update search statistics
392
+ const searchLatency = performance.now() - startTime;
393
+ this.updateSearchStats(query.userAddress, searchLatency);
394
+
395
+ // Count how many results have content from local index (Option A+)
396
+ const resultsWithLocalContent = finalResults.filter(r => r.content !== undefined).length;
397
+
398
+ console.log(`✅ Search completed in ${searchLatency.toFixed(2)}ms`);
399
+ console.log(` Found ${finalResults.length} results (similarity range: ${finalResults.length > 0 ? finalResults[finalResults.length-1].similarity.toFixed(3) : 'N/A'} - ${finalResults.length > 0 ? finalResults[0].similarity.toFixed(3) : 'N/A'}`);
400
+ if (resultsWithLocalContent > 0) {
401
+ console.log(` 📦 ${resultsWithLocalContent}/${finalResults.length} results have content from local index (no Walrus fetch needed!)`);
402
+ }
403
+
404
+ return finalResults;
405
+
406
+ } catch (error) {
407
+ console.error('❌ Memory search failed:', error);
408
+ throw error;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Get all memories for a user with optional filtering
414
+ */
415
+ async getUserMemories(
416
+ userAddress: string,
417
+ filters?: {
418
+ categories?: string[];
419
+ dateRange?: { start?: Date; end?: Date };
420
+ importanceRange?: { min?: number; max?: number };
421
+ limit?: number;
422
+ }
423
+ ): Promise<MemorySearchResult[]> {
424
+ const userMemories = this.memoryIndex.get(userAddress);
425
+ if (!userMemories) {
426
+ return [];
427
+ }
428
+
429
+ const results: MemorySearchResult[] = [];
430
+
431
+ for (const [memoryId, entry] of userMemories) {
432
+ // Apply filters
433
+ if (filters) {
434
+ if (filters.categories && !filters.categories.includes(entry.metadata.category)) {
435
+ continue;
436
+ }
437
+
438
+ if (filters.importanceRange) {
439
+ const importance = entry.metadata.importance || 5;
440
+ if (filters.importanceRange.min && importance < filters.importanceRange.min) continue;
441
+ if (filters.importanceRange.max && importance > filters.importanceRange.max) continue;
442
+ }
443
+
444
+ if (filters.dateRange) {
445
+ const created = new Date(entry.metadata.createdTimestamp || 0);
446
+ if (filters.dateRange.start && created < filters.dateRange.start) continue;
447
+ if (filters.dateRange.end && created > filters.dateRange.end) continue;
448
+ }
449
+ }
450
+
451
+ results.push({
452
+ memoryId: entry.memoryId,
453
+ blobId: entry.blobId,
454
+ metadata: entry.metadata,
455
+ similarity: 1.0, // No similarity for direct listing
456
+ relevanceScore: entry.metadata.importance || 5,
457
+ extractedAt: entry.indexedAt
458
+ });
459
+ }
460
+
461
+ // Sort by importance and creation time
462
+ results.sort((a, b) => {
463
+ const importanceDiff = (b.metadata.importance || 5) - (a.metadata.importance || 5);
464
+ if (importanceDiff !== 0) return importanceDiff;
465
+ return (b.metadata.createdTimestamp || 0) - (a.metadata.createdTimestamp || 0);
466
+ });
467
+
468
+ // Apply limit
469
+ if (filters?.limit) {
470
+ return results.slice(0, filters.limit);
471
+ }
472
+
473
+ return results;
474
+ }
475
+
476
+ /**
477
+ * Remove memory from index
478
+ */
479
+ async removeMemory(userAddress: string, memoryId: string): Promise<boolean> {
480
+ try {
481
+ const userMemories = this.memoryIndex.get(userAddress);
482
+ if (!userMemories) {
483
+ return false;
484
+ }
485
+
486
+ const entry = userMemories.get(memoryId);
487
+ if (!entry) {
488
+ return false;
489
+ }
490
+
491
+ // Remove from HNSW index (if supported)
492
+ // Note: hnswlib-node doesn't support deletion, so we just mark as removed
493
+
494
+ // Remove from memory index
495
+ userMemories.delete(memoryId);
496
+
497
+ console.log(`✅ Memory removed from index: ${memoryId}`);
498
+ return true;
499
+
500
+ } catch (error) {
501
+ console.error('❌ Failed to remove memory from index:', error);
502
+ return false;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Get index statistics for a user
508
+ */
509
+ getIndexStats(userAddress: string): {
510
+ totalMemories: number;
511
+ categoryCounts: Record<string, number>;
512
+ importanceDistribution: Record<number, number>;
513
+ averageImportance: number;
514
+ oldestMemory: Date | null;
515
+ newestMemory: Date | null;
516
+ indexSize: number;
517
+ } {
518
+ const userMemories = this.memoryIndex.get(userAddress);
519
+ if (!userMemories) {
520
+ return {
521
+ totalMemories: 0,
522
+ categoryCounts: {},
523
+ importanceDistribution: {},
524
+ averageImportance: 0,
525
+ oldestMemory: null,
526
+ newestMemory: null,
527
+ indexSize: 0
528
+ };
529
+ }
530
+
531
+ const categoryCounts: Record<string, number> = {};
532
+ const importanceDistribution: Record<number, number> = {};
533
+ let totalImportance = 0;
534
+ let oldestMemory: Date | null = null;
535
+ let newestMemory: Date | null = null;
536
+
537
+ for (const entry of userMemories.values()) {
538
+ // Categories
539
+ categoryCounts[entry.metadata.category] = (categoryCounts[entry.metadata.category] || 0) + 1;
540
+
541
+ // Importance
542
+ const importance = entry.metadata.importance || 5;
543
+ importanceDistribution[importance] = (importanceDistribution[importance] || 0) + 1;
544
+ totalImportance += importance;
545
+
546
+ // Dates
547
+ const created = new Date(entry.metadata.createdTimestamp || 0);
548
+ if (!oldestMemory || created < oldestMemory) {
549
+ oldestMemory = created;
550
+ }
551
+ if (!newestMemory || created > newestMemory) {
552
+ newestMemory = created;
553
+ }
554
+ }
555
+
556
+ return {
557
+ totalMemories: userMemories.size,
558
+ categoryCounts,
559
+ importanceDistribution,
560
+ averageImportance: userMemories.size > 0 ? totalImportance / userMemories.size : 0,
561
+ oldestMemory,
562
+ newestMemory,
563
+ indexSize: userMemories.size
564
+ };
565
+ }
566
+
567
+ /**
568
+ * Flush pending operations and save index
569
+ */
570
+ async flush(userAddress: string): Promise<void> {
571
+ const hnswService = await this.getHnswService();
572
+ await hnswService.flushBatch(userAddress);
573
+ console.log(`✅ Memory index flushed for user ${userAddress}`);
574
+ }
575
+
576
+ /**
577
+ * Load index from storage (local or Walrus)
578
+ *
579
+ * @param userAddress - User's wallet address
580
+ * @param indexBlobId - Optional Walrus blob ID to load from cloud
581
+ */
582
+ async loadIndex(userAddress: string, indexBlobId?: string): Promise<void> {
583
+ const hnswService = await this.getHnswService();
584
+
585
+ // If blobId provided, try to load from Walrus first
586
+ if (indexBlobId && 'loadFromWalrus' in hnswService) {
587
+ console.log(`📥 Attempting to load index from Walrus: ${indexBlobId}`);
588
+ const walrusLoaded = await (hnswService as any).loadFromWalrus(userAddress, indexBlobId);
589
+ if (walrusLoaded) {
590
+ console.log(`✅ Memory index loaded from Walrus for user ${userAddress}`);
591
+ return;
592
+ }
593
+ console.log(`⚠️ Walrus load failed, falling back to local storage`);
594
+ }
595
+
596
+ // Fallback to local storage
597
+ const loaded = await hnswService.loadIndex(userAddress);
598
+ if (loaded) {
599
+ console.log(`✅ Memory index loaded from local storage for user ${userAddress}`);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Save index to local storage
605
+ */
606
+ async saveIndex(userAddress: string): Promise<void> {
607
+ const hnswService = await this.getHnswService();
608
+ await hnswService.saveIndex(userAddress);
609
+ console.log(`✅ Memory index saved for user ${userAddress}`);
610
+ }
611
+
612
+ /**
613
+ * Sync index to Walrus cloud storage
614
+ *
615
+ * @param userAddress - User's wallet address
616
+ * @returns Walrus blob ID if successful, null if Walrus is disabled
617
+ */
618
+ async syncToWalrus(userAddress: string): Promise<string | null> {
619
+ const hnswService = await this.getHnswService();
620
+
621
+ if (!('syncToWalrus' in hnswService)) {
622
+ console.warn('⚠️ HNSW service does not support Walrus sync');
623
+ return null;
624
+ }
625
+
626
+ const blobId = await (hnswService as any).syncToWalrus(userAddress);
627
+ if (blobId) {
628
+ console.log(`☁️ Memory index synced to Walrus: ${blobId}`);
629
+ }
630
+ return blobId;
631
+ }
632
+
633
+ /**
634
+ * Load index directly from Walrus cloud storage
635
+ *
636
+ * @param userAddress - User's wallet address
637
+ * @param blobId - Walrus blob ID
638
+ * @returns true if successfully loaded
639
+ */
640
+ async loadFromWalrus(userAddress: string, blobId: string): Promise<boolean> {
641
+ const hnswService = await this.getHnswService();
642
+
643
+ if (!('loadFromWalrus' in hnswService)) {
644
+ console.warn('⚠️ HNSW service does not support Walrus load');
645
+ return false;
646
+ }
647
+
648
+ const loaded = await (hnswService as any).loadFromWalrus(userAddress, blobId);
649
+ if (loaded) {
650
+ console.log(`☁️ Memory index loaded from Walrus: ${blobId}`);
651
+ }
652
+ return loaded;
653
+ }
654
+
655
+ /**
656
+ * Get the Walrus blob ID for a user's index (if backed up)
657
+ *
658
+ * @param userAddress - User's wallet address
659
+ * @returns Blob ID or null if not backed up
660
+ */
661
+ getWalrusBlobId(userAddress: string): string | null {
662
+ if (this.hnswService && 'getWalrusBlobId' in this.hnswService) {
663
+ return (this.hnswService as any).getWalrusBlobId(userAddress);
664
+ }
665
+ return null;
666
+ }
667
+
668
+ /**
669
+ * Check if Walrus backup is enabled
670
+ */
671
+ isWalrusEnabled(): boolean {
672
+ if (this.hnswService && 'isWalrusEnabled' in this.hnswService) {
673
+ return (this.hnswService as any).isWalrusEnabled();
674
+ }
675
+ return false;
676
+ }
677
+
678
+ /**
679
+ * Clear user's index
680
+ */
681
+ async clearUserIndex(userAddress: string): Promise<void> {
682
+ this.memoryIndex.delete(userAddress);
683
+ const hnswService = await this.getHnswService();
684
+ await hnswService.deleteIndex(userAddress);
685
+ console.log(`✅ Memory index cleared for user ${userAddress}`);
686
+ }
687
+
688
+ /**
689
+ * Get overall service statistics
690
+ */
691
+ async getServiceStats() {
692
+ const totalMemories = Array.from(this.memoryIndex.values())
693
+ .reduce((sum, userMemories) => sum + userMemories.size, 0);
694
+
695
+ let hnswStats = null;
696
+ if (this.hnswService) {
697
+ hnswStats = this.hnswService.getBatchStats();
698
+ }
699
+
700
+ return {
701
+ totalUsers: this.memoryIndex.size,
702
+ totalMemories,
703
+ hnswStats,
704
+ hasEmbeddingService: !!this.embeddingService,
705
+ hasStorageService: !!this.storageService
706
+ };
707
+ }
708
+
709
+ /**
710
+ * Destroy service and cleanup resources
711
+ */
712
+ destroy(): void {
713
+ if (this.hnswService) {
714
+ this.hnswService.destroy();
715
+ }
716
+ this.hnswService = null;
717
+ this.hnswServicePromise = null;
718
+ this.memoryIndex.clear();
719
+ console.log('✅ MemoryIndexService destroyed');
720
+ }
721
+
722
+ // ==================== PRIVATE HELPER METHODS ====================
723
+
724
+ private createMemoryFilter(query: MemorySearchQuery) {
725
+ return (metadata: any) => {
726
+ // Category filter
727
+ if (query.categories && query.categories.length > 0) {
728
+ if (!query.categories.includes(metadata.category)) {
729
+ return false;
730
+ }
731
+ }
732
+
733
+ // Date range filter
734
+ if (query.dateRange) {
735
+ const created = new Date(metadata.createdTimestamp || 0);
736
+ if (query.dateRange.start && created < query.dateRange.start) {
737
+ return false;
738
+ }
739
+ if (query.dateRange.end && created > query.dateRange.end) {
740
+ return false;
741
+ }
742
+ }
743
+
744
+ // Importance range filter
745
+ if (query.importanceRange) {
746
+ const importance = metadata.importance || 5;
747
+ if (query.importanceRange.min && importance < query.importanceRange.min) {
748
+ return false;
749
+ }
750
+ if (query.importanceRange.max && importance > query.importanceRange.max) {
751
+ return false;
752
+ }
753
+ }
754
+
755
+ // Tags filter (search in custom metadata)
756
+ if (query.tags && query.tags.length > 0) {
757
+ const metadataText = JSON.stringify(metadata).toLowerCase();
758
+ const hasAnyTag = query.tags.some(tag =>
759
+ metadataText.includes(tag.toLowerCase())
760
+ );
761
+ if (!hasAnyTag) {
762
+ return false;
763
+ }
764
+ }
765
+
766
+ return true;
767
+ };
768
+ }
769
+
770
+ // ==================== HNSW HELPER METHODS ====================
771
+
772
+ /**
773
+ * Create metadata filter for HNSW search
774
+ */
775
+ private createMetadataFilter(query: MemorySearchQuery): ((metadata: any) => boolean) | undefined {
776
+ if (!query.categories && !query.dateRange && !query.importanceRange && !query.tags) {
777
+ return undefined;
778
+ }
779
+
780
+ return (metadata: any) => {
781
+ // Category filter
782
+ if (query.categories && query.categories.length > 0) {
783
+ if (!query.categories.includes(metadata.category)) {
784
+ return false;
785
+ }
786
+ }
787
+
788
+ // Date range filter
789
+ if (query.dateRange) {
790
+ const created = new Date(metadata.createdTimestamp || 0);
791
+ if (query.dateRange.start && created < query.dateRange.start) {
792
+ return false;
793
+ }
794
+ if (query.dateRange.end && created > query.dateRange.end) {
795
+ return false;
796
+ }
797
+ }
798
+
799
+ // Importance range filter
800
+ if (query.importanceRange) {
801
+ const importance = metadata.importance || 5;
802
+ if (query.importanceRange.min && importance < query.importanceRange.min) {
803
+ return false;
804
+ }
805
+ if (query.importanceRange.max && importance > query.importanceRange.max) {
806
+ return false;
807
+ }
808
+ }
809
+
810
+ // Tags filter
811
+ if (query.tags && query.tags.length > 0) {
812
+ const metadataText = JSON.stringify(metadata).toLowerCase();
813
+ const hasAnyTag = query.tags.some(tag =>
814
+ metadataText.includes(tag.toLowerCase())
815
+ );
816
+ if (!hasAnyTag) {
817
+ return false;
818
+ }
819
+ }
820
+
821
+ return true;
822
+ };
823
+ }
824
+
825
+ /**
826
+ * Update index statistics for a user
827
+ */
828
+ private updateIndexStats(userAddress: string): void {
829
+ let stats = this.indexStats.get(userAddress);
830
+ if (!stats) {
831
+ stats = {
832
+ totalVectors: 0,
833
+ avgSimilarity: 0,
834
+ searchLatency: [],
835
+ lastOptimized: new Date()
836
+ };
837
+ this.indexStats.set(userAddress, stats);
838
+ }
839
+ stats.totalVectors++;
840
+ }
841
+
842
+ /**
843
+ * Enhanced relevance scoring with multiple factors
844
+ */
845
+ private calculateAdvancedRelevanceScore(
846
+ similarity: number,
847
+ metadata: MemoryMetadata,
848
+ query: MemorySearchQuery,
849
+ queryVector: number[],
850
+ documentVector: number[]
851
+ ): number {
852
+ let score = similarity * 0.7; // Base similarity weight (increased)
853
+
854
+ // Importance boost
855
+ const importance = metadata.importance || 5;
856
+ score += (importance - 5) * 0.02; // -0.1 to +0.1 boost
857
+
858
+ // Category exact match boost
859
+ if (query.categories && query.categories.includes(metadata.category)) {
860
+ score += 0.15;
861
+ }
862
+
863
+ // Topic relevance boost
864
+ if (query.query && metadata.topic) {
865
+ const queryLower = query.query.toLowerCase();
866
+ const topicLower = metadata.topic.toLowerCase();
867
+ if (queryLower.includes(topicLower) || topicLower.includes(queryLower)) {
868
+ score += 0.1;
869
+ }
870
+ }
871
+
872
+ // Vector quality boost (based on vector magnitude)
873
+ const vectorMagnitude = this.calculateVectorMagnitude(documentVector);
874
+ if (vectorMagnitude > 0.1) { // Well-formed embedding
875
+ score += 0.05;
876
+ }
877
+
878
+ // Semantic consistency boost (cosine similarity in different metric)
879
+ const semanticConsistency = this.calculateSemanticConsistency(queryVector, documentVector);
880
+ score += semanticConsistency * 0.1;
881
+
882
+ return Math.min(1.0, Math.max(0.0, score));
883
+ }
884
+
885
+ /**
886
+ * Calculate recency boost based on creation timestamp
887
+ */
888
+ private calculateRecencyBoost(createdTimestamp: number): number {
889
+ const now = Date.now();
890
+ const ageInDays = (now - createdTimestamp) / (1000 * 60 * 60 * 24);
891
+
892
+ // Exponential decay: more recent = higher boost
893
+ if (ageInDays < 1) return 1.0; // Last day: full boost
894
+ if (ageInDays < 7) return 0.8; // Last week: 80% boost
895
+ if (ageInDays < 30) return 0.5; // Last month: 50% boost
896
+ if (ageInDays < 90) return 0.2; // Last quarter: 20% boost
897
+ return 0.0; // Older: no boost
898
+ }
899
+
900
+ /**
901
+ * Diversify search results to avoid clustering
902
+ */
903
+ private diversifyResults(results: MemorySearchResult[], diversityFactor: number): MemorySearchResult[] {
904
+ if (diversityFactor <= 0 || results.length <= 1) return results;
905
+
906
+ const diversified: MemorySearchResult[] = [];
907
+ const remaining = [...results];
908
+
909
+ // Always include the top result
910
+ if (remaining.length > 0) {
911
+ diversified.push(remaining.shift()!);
912
+ }
913
+
914
+ while (remaining.length > 0 && diversified.length < results.length * 0.8) {
915
+ let bestIndex = 0;
916
+ let bestScore = 0;
917
+
918
+ for (let i = 0; i < remaining.length; i++) {
919
+ const candidate = remaining[i];
920
+
921
+ // Calculate diversity score (distance from already selected results)
922
+ let minSimilarity = 1.0;
923
+ for (const selected of diversified) {
924
+ const similarity = candidate.similarity; // Could enhance with actual vector similarity
925
+ minSimilarity = Math.min(minSimilarity, similarity);
926
+ }
927
+
928
+ // Combine relevance and diversity
929
+ const diversityScore = candidate.relevanceScore * (1 - diversityFactor) +
930
+ (1 - minSimilarity) * diversityFactor;
931
+
932
+ if (diversityScore > bestScore) {
933
+ bestScore = diversityScore;
934
+ bestIndex = i;
935
+ }
936
+ }
937
+
938
+ diversified.push(remaining.splice(bestIndex, 1)[0]);
939
+ }
940
+
941
+ return diversified;
942
+ }
943
+
944
+ /**
945
+ * Update search performance statistics
946
+ */
947
+ private updateSearchStats(userAddress: string, latency: number): void {
948
+ let stats = this.indexStats.get(userAddress);
949
+ if (!stats) {
950
+ stats = {
951
+ totalVectors: 0,
952
+ avgSimilarity: 0,
953
+ searchLatency: [],
954
+ lastOptimized: new Date()
955
+ };
956
+ this.indexStats.set(userAddress, stats);
957
+ }
958
+
959
+ stats.searchLatency.push(latency);
960
+
961
+ // Keep only last 100 latency measurements
962
+ if (stats.searchLatency.length > 100) {
963
+ stats.searchLatency = stats.searchLatency.slice(-100);
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Calculate vector magnitude
969
+ */
970
+ private calculateVectorMagnitude(vector: number[]): number {
971
+ let sum = 0;
972
+ for (const val of vector) {
973
+ sum += val * val;
974
+ }
975
+ return Math.sqrt(sum);
976
+ }
977
+
978
+ /**
979
+ * Calculate cosine similarity between two vectors
980
+ * Used for semantic consistency scoring
981
+ */
982
+ private cosineSimilarity(a: number[], b: number[]): number {
983
+ if (a.length !== b.length) return 0;
984
+
985
+ let dotProduct = 0;
986
+ let normA = 0;
987
+ let normB = 0;
988
+
989
+ for (let i = 0; i < a.length; i++) {
990
+ dotProduct += a[i] * b[i];
991
+ normA += a[i] * a[i];
992
+ normB += b[i] * b[i];
993
+ }
994
+
995
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
996
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
997
+ }
998
+
999
+ /**
1000
+ * Calculate semantic consistency score
1001
+ */
1002
+ private calculateSemanticConsistency(queryVector: number[], documentVector: number[]): number {
1003
+ // Calculate angle between vectors (semantic consistency)
1004
+ const similarity = this.cosineSimilarity(queryVector, documentVector);
1005
+ const angle = Math.acos(Math.max(-1, Math.min(1, similarity)));
1006
+
1007
+ // Convert angle to consistency score (0-1, where 1 is perfect alignment)
1008
+ return 1 - (angle / Math.PI);
1009
+ }
1004
1010
  }