@cmdoss/memwal-sdk 0.9.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 (174) hide show
  1. package/README.md +157 -52
  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 +2 -1
  7. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  8. package/dist/client/SimplePDWClient.js +23 -6
  9. package/dist/client/SimplePDWClient.js.map +1 -1
  10. package/dist/client/namespaces/MemoryNamespace.d.ts +6 -0
  11. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  12. package/dist/client/namespaces/MemoryNamespace.js +131 -18
  13. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  14. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +3 -1
  15. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  16. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  17. package/dist/config/ConfigurationHelper.js +61 -61
  18. package/dist/config/index.d.ts +1 -0
  19. package/dist/config/index.d.ts.map +1 -1
  20. package/dist/config/index.js +2 -0
  21. package/dist/config/index.js.map +1 -1
  22. package/dist/config/modelDefaults.d.ts +67 -0
  23. package/dist/config/modelDefaults.d.ts.map +1 -0
  24. package/dist/config/modelDefaults.js +91 -0
  25. package/dist/config/modelDefaults.js.map +1 -0
  26. package/dist/graph/GraphService.d.ts.map +1 -1
  27. package/dist/graph/GraphService.js +22 -21
  28. package/dist/graph/GraphService.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/langchain/createPDWRAG.js +30 -30
  34. package/dist/pipeline/MemoryPipeline.d.ts.map +1 -1
  35. package/dist/pipeline/MemoryPipeline.js +2 -1
  36. package/dist/pipeline/MemoryPipeline.js.map +1 -1
  37. package/dist/services/GeminiAIService.d.ts.map +1 -1
  38. package/dist/services/GeminiAIService.js +311 -310
  39. package/dist/services/GeminiAIService.js.map +1 -1
  40. package/dist/services/StorageService.d.ts +4 -1
  41. package/dist/services/StorageService.d.ts.map +1 -1
  42. package/dist/services/StorageService.js.map +1 -1
  43. package/dist/services/storage/QuiltBatchManager.d.ts +7 -0
  44. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  45. package/dist/services/storage/QuiltBatchManager.js +24 -5
  46. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  47. package/dist/services/storage/WalrusStorageManager.d.ts +10 -1
  48. package/dist/services/storage/WalrusStorageManager.d.ts.map +1 -1
  49. package/dist/services/storage/WalrusStorageManager.js +53 -12
  50. package/dist/services/storage/WalrusStorageManager.js.map +1 -1
  51. package/dist/vector/BrowserHnswIndexService.js +2 -2
  52. package/dist/vector/BrowserHnswIndexService.js.map +1 -1
  53. package/dist/vector/NodeHnswService.js +4 -4
  54. package/dist/vector/NodeHnswService.js.map +1 -1
  55. package/dist/vector/createHnswService.d.ts +4 -0
  56. package/dist/vector/createHnswService.d.ts.map +1 -1
  57. package/dist/vector/createHnswService.js +15 -3
  58. package/dist/vector/createHnswService.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/access/PermissionService.ts +635 -635
  61. package/src/aggregation/AggregationService.ts +389 -389
  62. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  63. package/src/ai-sdk/index.ts +65 -65
  64. package/src/ai-sdk/tools.ts +460 -460
  65. package/src/ai-sdk/types.ts +404 -404
  66. package/src/batch/BatchManager.ts +597 -597
  67. package/src/batch/BatchingService.ts +429 -429
  68. package/src/batch/MemoryProcessingCache.ts +492 -492
  69. package/src/batch/index.ts +30 -30
  70. package/src/browser.ts +200 -200
  71. package/src/client/ClientMemoryManager.ts +1004 -987
  72. package/src/client/PersonalDataWallet.ts +345 -345
  73. package/src/client/SimplePDWClient.ts +1387 -1369
  74. package/src/client/factory.ts +154 -154
  75. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  76. package/src/client/namespaces/BatchNamespace.ts +356 -356
  77. package/src/client/namespaces/CacheNamespace.ts +123 -123
  78. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  79. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  80. package/src/client/namespaces/ContextNamespace.ts +297 -297
  81. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  82. package/src/client/namespaces/GraphNamespace.ts +468 -468
  83. package/src/client/namespaces/IndexNamespace.ts +364 -364
  84. package/src/client/namespaces/MemoryNamespace.ts +1704 -1569
  85. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  86. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  87. package/src/client/namespaces/StorageNamespace.ts +458 -458
  88. package/src/client/namespaces/TxNamespace.ts +260 -260
  89. package/src/client/namespaces/WalletNamespace.ts +243 -243
  90. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -607
  91. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  92. package/src/client/namespaces/consolidated/StorageNamespace.ts +1143 -1141
  93. package/src/client/namespaces/consolidated/index.ts +41 -41
  94. package/src/client/signers/KeypairSigner.ts +108 -108
  95. package/src/client/signers/UnifiedSigner.ts +110 -110
  96. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  97. package/src/client/signers/index.ts +26 -26
  98. package/src/config/ConfigurationHelper.ts +412 -412
  99. package/src/config/defaults.ts +56 -56
  100. package/src/config/index.ts +16 -9
  101. package/src/config/modelDefaults.ts +103 -0
  102. package/src/config/validation.ts +70 -70
  103. package/src/core/index.ts +14 -14
  104. package/src/core/interfaces/IService.ts +307 -307
  105. package/src/core/interfaces/index.ts +8 -8
  106. package/src/core/types/capability.ts +297 -297
  107. package/src/core/types/index.ts +874 -874
  108. package/src/core/types/wallet.ts +270 -270
  109. package/src/core/types.ts +9 -9
  110. package/src/core/wallet.ts +222 -222
  111. package/src/embedding/index.ts +19 -19
  112. package/src/embedding/types.ts +357 -357
  113. package/src/errors/index.ts +602 -602
  114. package/src/errors/recovery.ts +461 -461
  115. package/src/errors/validation.ts +567 -567
  116. package/src/generated/pdw/capability.ts +319 -319
  117. package/src/graph/GraphService.ts +888 -887
  118. package/src/graph/KnowledgeGraphManager.ts +728 -728
  119. package/src/graph/index.ts +25 -25
  120. package/src/index.ts +498 -498
  121. package/src/infrastructure/index.ts +22 -22
  122. package/src/infrastructure/seal/EncryptionService.ts +628 -628
  123. package/src/infrastructure/seal/SealService.ts +613 -613
  124. package/src/infrastructure/seal/index.ts +9 -9
  125. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  126. package/src/infrastructure/sui/SuiService.ts +888 -888
  127. package/src/infrastructure/sui/index.ts +9 -9
  128. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  129. package/src/infrastructure/walrus/WalrusStorageService.ts +637 -637
  130. package/src/infrastructure/walrus/index.ts +9 -9
  131. package/src/langchain/createPDWRAG.ts +303 -303
  132. package/src/langchain/index.ts +47 -47
  133. package/src/permissions/ConsentRepository.browser.ts +249 -249
  134. package/src/permissions/ConsentRepository.ts +364 -364
  135. package/src/pipeline/MemoryPipeline.ts +863 -862
  136. package/src/pipeline/PipelineManager.ts +683 -683
  137. package/src/pipeline/index.ts +26 -26
  138. package/src/retrieval/AdvancedSearchService.ts +629 -629
  139. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  140. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -825
  141. package/src/retrieval/index.ts +42 -42
  142. package/src/services/BatchService.ts +352 -352
  143. package/src/services/CapabilityService.ts +464 -464
  144. package/src/services/ClassifierService.ts +465 -465
  145. package/src/services/CrossContextPermissionService.ts +486 -486
  146. package/src/services/EmbeddingService.ts +796 -796
  147. package/src/services/EncryptionService.ts +712 -712
  148. package/src/services/GeminiAIService.ts +754 -753
  149. package/src/services/MemoryIndexService.ts +1009 -1009
  150. package/src/services/MemoryService.ts +369 -369
  151. package/src/services/QueryService.ts +890 -890
  152. package/src/services/StorageService.ts +1185 -1182
  153. package/src/services/TransactionService.ts +838 -838
  154. package/src/services/VectorService.ts +462 -462
  155. package/src/services/ViewService.ts +484 -484
  156. package/src/services/index.ts +25 -25
  157. package/src/services/storage/BlobAttributesManager.ts +333 -333
  158. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  159. package/src/services/storage/MemorySearchManager.ts +387 -387
  160. package/src/services/storage/QuiltBatchManager.ts +1157 -1130
  161. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  162. package/src/services/storage/WalrusStorageManager.ts +333 -287
  163. package/src/services/storage/index.ts +57 -57
  164. package/src/types/index.ts +13 -13
  165. package/src/utils/index.ts +76 -76
  166. package/src/utils/memoryIndexOnChain.ts +507 -507
  167. package/src/vector/BrowserHnswIndexService.ts +758 -758
  168. package/src/vector/HnswWasmService.ts +731 -731
  169. package/src/vector/IHnswService.ts +233 -233
  170. package/src/vector/NodeHnswService.ts +833 -833
  171. package/src/vector/createHnswService.ts +147 -135
  172. package/src/vector/index.ts +56 -56
  173. package/src/wallet/ContextWalletService.ts +656 -656
  174. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,1010 +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 || 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
- }
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
+ }
1010
1010
  }