@cmdoss/memwal-sdk 0.7.0 → 0.8.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 (192) hide show
  1. package/README.md +129 -0
  2. package/dist/client/ClientMemoryManager.js +2 -2
  3. package/dist/client/ClientMemoryManager.js.map +1 -1
  4. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  5. package/dist/client/SimplePDWClient.d.ts +28 -0
  6. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  7. package/dist/client/SimplePDWClient.js +29 -6
  8. package/dist/client/SimplePDWClient.js.map +1 -1
  9. package/dist/client/namespaces/MemoryNamespace.d.ts +4 -0
  10. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  11. package/dist/client/namespaces/MemoryNamespace.js +168 -39
  12. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  13. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts +12 -2
  14. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
  15. package/dist/client/namespaces/consolidated/BlockchainNamespace.js +40 -2
  16. package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
  17. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +67 -2
  18. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  19. package/dist/client/namespaces/consolidated/StorageNamespace.js +549 -16
  20. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  21. package/dist/config/ConfigurationHelper.js +61 -61
  22. package/dist/config/defaults.js +2 -2
  23. package/dist/config/defaults.js.map +1 -1
  24. package/dist/graph/GraphService.js +20 -20
  25. package/dist/infrastructure/seal/EncryptionService.d.ts +9 -5
  26. package/dist/infrastructure/seal/EncryptionService.d.ts.map +1 -1
  27. package/dist/infrastructure/seal/EncryptionService.js +37 -15
  28. package/dist/infrastructure/seal/EncryptionService.js.map +1 -1
  29. package/dist/infrastructure/seal/SealService.d.ts +13 -5
  30. package/dist/infrastructure/seal/SealService.d.ts.map +1 -1
  31. package/dist/infrastructure/seal/SealService.js +36 -34
  32. package/dist/infrastructure/seal/SealService.js.map +1 -1
  33. package/dist/langchain/createPDWRAG.js +30 -30
  34. package/dist/retrieval/MemoryDecryptionPipeline.d.ts.map +1 -1
  35. package/dist/retrieval/MemoryDecryptionPipeline.js +2 -1
  36. package/dist/retrieval/MemoryDecryptionPipeline.js.map +1 -1
  37. package/dist/services/CapabilityService.d.ts.map +1 -1
  38. package/dist/services/CapabilityService.js +30 -14
  39. package/dist/services/CapabilityService.js.map +1 -1
  40. package/dist/services/CrossContextPermissionService.d.ts.map +1 -1
  41. package/dist/services/CrossContextPermissionService.js +9 -7
  42. package/dist/services/CrossContextPermissionService.js.map +1 -1
  43. package/dist/services/EncryptionService.d.ts.map +1 -1
  44. package/dist/services/EncryptionService.js +6 -5
  45. package/dist/services/EncryptionService.js.map +1 -1
  46. package/dist/services/GeminiAIService.js +309 -309
  47. package/dist/services/StorageService.d.ts +1 -0
  48. package/dist/services/StorageService.d.ts.map +1 -1
  49. package/dist/services/StorageService.js +60 -10
  50. package/dist/services/StorageService.js.map +1 -1
  51. package/dist/services/TransactionService.d.ts +20 -0
  52. package/dist/services/TransactionService.d.ts.map +1 -1
  53. package/dist/services/TransactionService.js +43 -0
  54. package/dist/services/TransactionService.js.map +1 -1
  55. package/dist/services/ViewService.js +2 -2
  56. package/dist/services/ViewService.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/access/PermissionService.ts +635 -635
  59. package/src/access/index.ts +8 -8
  60. package/src/aggregation/AggregationService.ts +389 -389
  61. package/src/aggregation/index.ts +8 -8
  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 +987 -987
  72. package/src/client/PersonalDataWallet.ts +345 -345
  73. package/src/client/SimplePDWClient.ts +1289 -1237
  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/EmbeddingsNamespace.ts +99 -99
  82. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  83. package/src/client/namespaces/GraphNamespace.ts +468 -468
  84. package/src/client/namespaces/IndexNamespace.ts +361 -361
  85. package/src/client/namespaces/MemoryNamespace.ts +1422 -1272
  86. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  87. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  88. package/src/client/namespaces/SearchNamespace.ts +1049 -1049
  89. package/src/client/namespaces/StorageNamespace.ts +458 -458
  90. package/src/client/namespaces/TxNamespace.ts +260 -260
  91. package/src/client/namespaces/WalletNamespace.ts +243 -243
  92. package/src/client/namespaces/consolidated/AINamespace.ts +449 -449
  93. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -564
  94. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  95. package/src/client/namespaces/consolidated/StorageNamespace.ts +1141 -497
  96. package/src/client/namespaces/consolidated/index.ts +39 -39
  97. package/src/client/signers/DappKitSigner.ts +207 -207
  98. package/src/client/signers/KeypairSigner.ts +108 -108
  99. package/src/client/signers/UnifiedSigner.ts +110 -110
  100. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  101. package/src/client/signers/index.ts +26 -26
  102. package/src/config/ConfigurationHelper.ts +412 -412
  103. package/src/config/defaults.ts +51 -51
  104. package/src/config/index.ts +8 -8
  105. package/src/config/validation.ts +70 -70
  106. package/src/core/index.ts +14 -14
  107. package/src/core/interfaces/IService.ts +307 -307
  108. package/src/core/interfaces/index.ts +8 -8
  109. package/src/core/types/capability.ts +297 -297
  110. package/src/core/types/index.ts +870 -870
  111. package/src/core/types/wallet.ts +270 -270
  112. package/src/core/types.ts +9 -9
  113. package/src/core/wallet.ts +222 -222
  114. package/src/embedding/index.ts +19 -19
  115. package/src/embedding/types.ts +357 -357
  116. package/src/errors/index.ts +602 -602
  117. package/src/errors/recovery.ts +461 -461
  118. package/src/errors/validation.ts +567 -567
  119. package/src/generated/pdw/capability.ts +319 -319
  120. package/src/generated/pdw/deps/sui/object.ts +12 -12
  121. package/src/generated/pdw/deps/sui/vec_map.ts +32 -32
  122. package/src/generated/pdw/memory.ts +1087 -1087
  123. package/src/generated/pdw/wallet.ts +123 -123
  124. package/src/generated/utils/index.ts +159 -159
  125. package/src/graph/GraphService.ts +887 -887
  126. package/src/graph/KnowledgeGraphManager.ts +728 -728
  127. package/src/graph/index.ts +25 -25
  128. package/src/index.ts +498 -498
  129. package/src/infrastructure/index.ts +22 -22
  130. package/src/infrastructure/seal/EncryptionService.ts +628 -603
  131. package/src/infrastructure/seal/SealService.ts +613 -615
  132. package/src/infrastructure/seal/index.ts +9 -9
  133. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  134. package/src/infrastructure/sui/SuiService.ts +888 -888
  135. package/src/infrastructure/sui/index.ts +9 -9
  136. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  137. package/src/infrastructure/walrus/WalrusStorageService.ts +612 -612
  138. package/src/infrastructure/walrus/index.ts +9 -9
  139. package/src/langchain/PDWEmbeddings.ts +145 -145
  140. package/src/langchain/PDWVectorStore.ts +456 -456
  141. package/src/langchain/createPDWRAG.ts +303 -303
  142. package/src/langchain/index.ts +47 -47
  143. package/src/permissions/ConsentRepository.browser.ts +249 -249
  144. package/src/permissions/ConsentRepository.ts +364 -364
  145. package/src/permissions/index.ts +9 -9
  146. package/src/pipeline/MemoryPipeline.ts +862 -862
  147. package/src/pipeline/PipelineManager.ts +683 -683
  148. package/src/pipeline/index.ts +26 -26
  149. package/src/retrieval/AdvancedSearchService.ts +629 -629
  150. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  151. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -824
  152. package/src/retrieval/MemoryRetrievalService.ts +904 -904
  153. package/src/retrieval/index.ts +42 -42
  154. package/src/services/BatchService.ts +352 -352
  155. package/src/services/CapabilityService.ts +464 -448
  156. package/src/services/ClassifierService.ts +465 -465
  157. package/src/services/CrossContextPermissionService.ts +486 -484
  158. package/src/services/EmbeddingService.ts +771 -771
  159. package/src/services/EncryptionService.ts +712 -711
  160. package/src/services/GeminiAIService.ts +753 -753
  161. package/src/services/IndexManager.ts +977 -977
  162. package/src/services/MemoryIndexService.ts +1003 -1003
  163. package/src/services/MemoryService.ts +369 -369
  164. package/src/services/QueryService.ts +890 -890
  165. package/src/services/StorageService.ts +1182 -1126
  166. package/src/services/TransactionService.ts +838 -790
  167. package/src/services/VectorService.ts +462 -462
  168. package/src/services/ViewService.ts +484 -484
  169. package/src/services/index.ts +25 -25
  170. package/src/services/storage/BlobAttributesManager.ts +333 -333
  171. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  172. package/src/services/storage/MemorySearchManager.ts +387 -387
  173. package/src/services/storage/QuiltBatchManager.ts +1130 -1130
  174. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  175. package/src/services/storage/WalrusStorageManager.ts +287 -287
  176. package/src/services/storage/index.ts +57 -57
  177. package/src/types/index.ts +13 -13
  178. package/src/utils/LRUCache.ts +378 -378
  179. package/src/utils/index.ts +76 -76
  180. package/src/utils/memoryIndexOnChain.ts +507 -507
  181. package/src/utils/rebuildIndex.ts +290 -290
  182. package/src/utils/rebuildIndexNode.ts +771 -771
  183. package/src/vector/BrowserHnswIndexService.ts +758 -758
  184. package/src/vector/HnswWasmService.ts +731 -731
  185. package/src/vector/IHnswService.ts +233 -233
  186. package/src/vector/NodeHnswService.ts +833 -833
  187. package/src/vector/VectorManager.ts +478 -478
  188. package/src/vector/createHnswService.ts +135 -135
  189. package/src/vector/index.ts +56 -56
  190. package/src/wallet/ContextWalletService.ts +656 -656
  191. package/src/wallet/MainWalletService.ts +317 -317
  192. package/src/wallet/index.ts +17 -17
@@ -1,977 +1,977 @@
1
- /**
2
- * IndexManager - Hybrid Index Persistence Manager
3
- *
4
- * Provides intelligent index management with:
5
- * - Option 1: Full rebuild from Blockchain + Walrus (fallback)
6
- * - Option 2: Binary serialization to/from Walrus (fast)
7
- * - Hybrid: Combines both with incremental sync
8
- * - On-chain MemoryIndex object for versioned index tracking
9
- *
10
- * Flow:
11
- * 1. Initialize: Check for MemoryIndex on-chain → Load blob from Walrus
12
- * 2. Save: Upload to Walrus → Create/Update MemoryIndex on-chain
13
- *
14
- * @module services/IndexManager
15
- */
16
-
17
- import type { VectorService } from './VectorService';
18
- import type { StorageService } from './StorageService';
19
- import type { EmbeddingService } from './EmbeddingService';
20
- import type { TransactionService } from './TransactionService';
21
- import type { MemoryIndex } from './ViewService';
22
-
23
- /**
24
- * Index state stored in localStorage/persistent storage
25
- */
26
- export interface IndexState {
27
- /** Blob ID of the saved index on Walrus */
28
- blobId: string;
29
- /** Graph blob ID on Walrus (for knowledge graph) */
30
- graphBlobId?: string;
31
- /** Timestamp when index was last saved */
32
- lastSyncTimestamp: number;
33
- /** Number of vectors in the index at save time */
34
- vectorCount: number;
35
- /** Index version for optimistic locking (matches on-chain version) */
36
- version: number;
37
- /** Dimension of vectors in the index */
38
- dimension: number;
39
- /** On-chain MemoryIndex object ID (if created) */
40
- onChainIndexId?: string;
41
- }
42
-
43
- /**
44
- * Serialized index package stored on Walrus
45
- */
46
- export interface SerializedIndexPackage {
47
- /** Package format version */
48
- formatVersion: '1.0';
49
- /** Space ID (user address) */
50
- spaceId: string;
51
- /** Index version */
52
- version: number;
53
- /** Vector dimension */
54
- dimension: number;
55
- /** Timestamp of serialization */
56
- timestamp: number;
57
- /** Serialized HNSW index as base64 (if supported) */
58
- indexBinary?: string;
59
- /** All vectors with their IDs for reconstruction */
60
- vectors: Array<{
61
- vectorId: number;
62
- vector: number[];
63
- }>;
64
- /** Metadata for each vector */
65
- metadata: Array<[number, any]>;
66
- /** HNSW config used */
67
- hnswConfig: {
68
- maxElements: number;
69
- m: number;
70
- efConstruction: number;
71
- };
72
- }
73
-
74
- /**
75
- * Progress callback for long operations
76
- */
77
- export type IndexProgressCallback = (
78
- stage: 'loading' | 'rebuilding' | 'syncing' | 'saving' | 'complete',
79
- progress: number,
80
- message: string
81
- ) => void;
82
-
83
- /**
84
- * Options for IndexManager initialization
85
- */
86
- export interface IndexManagerOptions {
87
- /** Auto-save interval in ms (default: 5 minutes) */
88
- autoSaveInterval?: number;
89
- /** Enable auto-save (default: true) */
90
- enableAutoSave?: boolean;
91
- /** Storage key prefix for localStorage */
92
- storageKeyPrefix?: string;
93
- /** Progress callback */
94
- onProgress?: IndexProgressCallback;
95
- /** TransactionService for on-chain operations (optional) */
96
- transactionService?: TransactionService;
97
- /** Callback to get MemoryIndex from blockchain */
98
- getMemoryIndexFromChain?: (userAddress: string) => Promise<MemoryIndex | null>;
99
- /** Callback to execute transaction (for create/update MemoryIndex) */
100
- executeTransaction?: (tx: any, signer: any) => Promise<{ digest: string; effects?: any; error?: string }>;
101
- }
102
-
103
- /**
104
- * IndexManager - Manages HNSW index persistence with hybrid restore strategy
105
- *
106
- * Strategy:
107
- * 1. Try to load from Walrus cache (fast, ~500ms)
108
- * 2. If failed, rebuild from blockchain + Walrus (slow but complete)
109
- * 3. Sync any new memories since last save
110
- * 4. Auto-save periodically
111
- *
112
- * Memory Optimization:
113
- * - Removed duplicate vectorCache - now uses VectorService.getAllCachedVectors()
114
- * - Vectors are only stored once in VectorService's HnswWasmService LRU cache
115
- */
116
- export class IndexManager {
117
- private vectorService: VectorService;
118
- private storageService: StorageService;
119
- private embeddingService: EmbeddingService;
120
- private options: Required<Omit<IndexManagerOptions, 'transactionService' | 'getMemoryIndexFromChain' | 'executeTransaction'>> & {
121
- transactionService?: TransactionService;
122
- getMemoryIndexFromChain?: (userAddress: string) => Promise<MemoryIndex | null>;
123
- executeTransaction?: (tx: any, signer: any) => Promise<{ digest: string; effects?: any; error?: string }>;
124
- };
125
- private autoSaveTimer?: ReturnType<typeof setInterval>;
126
- private indexStates: Map<string, IndexState> = new Map();
127
- // Note: vectorCache removed - now using VectorService.getAllCachedVectors() to avoid duplication
128
-
129
- // Storage adapter (can be replaced for non-browser environments)
130
- private storage: {
131
- getItem: (key: string) => string | null;
132
- setItem: (key: string, value: string) => void;
133
- removeItem: (key: string) => void;
134
- };
135
-
136
- constructor(
137
- vectorService: VectorService,
138
- storageService: StorageService,
139
- embeddingService: EmbeddingService,
140
- options: IndexManagerOptions = {}
141
- ) {
142
- this.vectorService = vectorService;
143
- this.storageService = storageService;
144
- this.embeddingService = embeddingService;
145
-
146
- this.options = {
147
- autoSaveInterval: options.autoSaveInterval ?? 5 * 60 * 1000, // 5 minutes
148
- enableAutoSave: options.enableAutoSave ?? true,
149
- storageKeyPrefix: options.storageKeyPrefix ?? 'pdw_index_',
150
- onProgress: options.onProgress ?? (() => {}),
151
- transactionService: options.transactionService,
152
- getMemoryIndexFromChain: options.getMemoryIndexFromChain,
153
- executeTransaction: options.executeTransaction,
154
- };
155
-
156
- // Default to localStorage in browser, no-op in Node
157
- this.storage = typeof localStorage !== 'undefined'
158
- ? localStorage
159
- : {
160
- getItem: () => null,
161
- setItem: () => {},
162
- removeItem: () => {},
163
- };
164
- }
165
-
166
- /**
167
- * Set custom storage adapter (for React Native, Node.js, etc.)
168
- */
169
- setStorageAdapter(adapter: {
170
- getItem: (key: string) => string | null;
171
- setItem: (key: string, value: string) => void;
172
- removeItem: (key: string) => void;
173
- }): void {
174
- this.storage = adapter;
175
- }
176
-
177
- /**
178
- * Initialize index for a user with hybrid restore strategy
179
- *
180
- * @param spaceId - User address / space identifier
181
- * @param getMemoriesFromChain - Function to fetch memories from blockchain
182
- * @param getMemoryContent - Function to fetch memory content from Walrus
183
- */
184
- async initialize(
185
- spaceId: string,
186
- getMemoriesFromChain: () => Promise<Array<{
187
- id: string;
188
- blobId: string;
189
- vectorId?: number;
190
- category?: string;
191
- importance?: number;
192
- topic?: string;
193
- createdAt?: number;
194
- }>>,
195
- getMemoryContent: (blobId: string) => Promise<{
196
- content: string;
197
- embedding?: number[];
198
- metadata?: any;
199
- }>
200
- ): Promise<{
201
- restored: boolean;
202
- method: 'cache' | 'rebuild' | 'empty';
203
- vectorCount: number;
204
- syncedCount: number;
205
- timeMs: number;
206
- }> {
207
- const startTime = Date.now();
208
- let method: 'cache' | 'rebuild' | 'empty' = 'empty';
209
- let vectorCount = 0;
210
- let syncedCount = 0;
211
-
212
- this.options.onProgress('loading', 0, 'Starting index initialization...');
213
-
214
- // Step 0: Check for on-chain MemoryIndex first (source of truth)
215
- let onChainIndex: MemoryIndex | null = null;
216
- if (this.options.getMemoryIndexFromChain) {
217
- try {
218
- this.options.onProgress('loading', 5, 'Checking on-chain MemoryIndex...');
219
- onChainIndex = await this.options.getMemoryIndexFromChain(spaceId);
220
-
221
- if (onChainIndex) {
222
- console.log(`📍 Found on-chain MemoryIndex: ${onChainIndex.id} (v${onChainIndex.version})`);
223
- console.log(` Index blob: ${onChainIndex.indexBlobId}`);
224
- console.log(` Graph blob: ${onChainIndex.graphBlobId}`);
225
- }
226
- } catch (error) {
227
- console.warn('⚠️ Failed to fetch on-chain MemoryIndex:', error);
228
- }
229
- }
230
-
231
- // Step 1: Try to load from on-chain index or localStorage cache
232
- // Priority: on-chain index > localStorage cache
233
- let cachedState = this.loadIndexState(spaceId);
234
-
235
- // If on-chain index is newer, use it instead of localStorage
236
- if (onChainIndex && onChainIndex.indexBlobId) {
237
- const onChainVersion = onChainIndex.version;
238
- const localVersion = cachedState?.version || 0;
239
-
240
- if (onChainVersion > localVersion || !cachedState) {
241
- console.log(`🔄 On-chain index (v${onChainVersion}) is newer than local (v${localVersion}), using on-chain`);
242
- cachedState = {
243
- blobId: onChainIndex.indexBlobId,
244
- graphBlobId: onChainIndex.graphBlobId,
245
- lastSyncTimestamp: 0, // Will sync all memories
246
- vectorCount: 0,
247
- version: onChainVersion,
248
- dimension: 3072,
249
- onChainIndexId: onChainIndex.id,
250
- };
251
- }
252
- }
253
-
254
- if (cachedState) {
255
- try {
256
- this.options.onProgress('loading', 10, 'Found cached index, loading from Walrus...');
257
- await this.loadFromWalrus(spaceId, cachedState.blobId);
258
-
259
- // Restore on-chain index ID if available
260
- if (onChainIndex) {
261
- const state = this.indexStates.get(spaceId);
262
- if (state) {
263
- state.onChainIndexId = onChainIndex.id;
264
- this.indexStates.set(spaceId, state);
265
- }
266
- }
267
-
268
- method = 'cache';
269
- vectorCount = this.vectorService.getIndexStats(spaceId)?.currentElements || 0;
270
-
271
- this.options.onProgress('loading', 50, `Loaded ${vectorCount} vectors from cache`);
272
-
273
- // Step 2: Sync new memories since last save (with timeout)
274
- this.options.onProgress('syncing', 60, 'Checking for new memories...');
275
- try {
276
- // Add 30 second timeout for sync operation
277
- const syncPromise = this.syncNewMemories(
278
- spaceId,
279
- cachedState.lastSyncTimestamp,
280
- getMemoriesFromChain,
281
- getMemoryContent
282
- );
283
- const timeoutPromise = new Promise<number>((_, reject) =>
284
- setTimeout(() => reject(new Error('Sync timeout after 30s')), 30000)
285
- );
286
- syncedCount = await Promise.race([syncPromise, timeoutPromise]);
287
-
288
- if (syncedCount > 0) {
289
- vectorCount += syncedCount;
290
- this.options.onProgress('syncing', 90, `Synced ${syncedCount} new memories`);
291
- }
292
- } catch (syncError: any) {
293
- console.warn('⚠️ Sync skipped:', syncError.message);
294
- // Continue without sync - index is still usable from cache
295
- }
296
-
297
- this.options.onProgress('complete', 100, 'Index ready');
298
- } catch (error) {
299
- console.warn('⚠️ Failed to load cached index, falling back to rebuild:', error);
300
- // Fall through to rebuild
301
- method = 'rebuild';
302
- }
303
- }
304
-
305
- // Step 3: Full rebuild if no cache or cache failed (Option 1 - slow but complete)
306
- if (method !== 'cache') {
307
- this.options.onProgress('rebuilding', 10, 'No cache found, rebuilding from blockchain...');
308
-
309
- try {
310
- // Add 60 second timeout for full rebuild
311
- const rebuildPromise = this.fullRebuild(
312
- spaceId,
313
- getMemoriesFromChain,
314
- getMemoryContent
315
- );
316
- const timeoutPromise = new Promise<{ memoriesCount: number }>((_, reject) =>
317
- setTimeout(() => reject(new Error('Rebuild timeout after 60s')), 60000)
318
- );
319
- const result = await Promise.race([rebuildPromise, timeoutPromise]);
320
-
321
- method = result.memoriesCount > 0 ? 'rebuild' : 'empty';
322
- vectorCount = result.memoriesCount;
323
- } catch (rebuildError: any) {
324
- console.warn('⚠️ Rebuild failed or timed out:', rebuildError.message);
325
- method = 'empty';
326
- vectorCount = 0;
327
- }
328
- }
329
-
330
- // Step 4: Mark as dirty if vectors were synced or rebuilt (save will happen when saveIndex is called)
331
- // Note: We don't auto-save here because we don't have a signer
332
- // The caller should call saveIndexWithSigner() when ready to persist
333
-
334
- // Step 5: Start auto-save if enabled
335
- if (this.options.enableAutoSave) {
336
- this.startAutoSave(spaceId);
337
- }
338
-
339
- this.options.onProgress('complete', 100, 'Index initialization complete');
340
-
341
- return {
342
- restored: method === 'cache',
343
- method,
344
- vectorCount,
345
- syncedCount,
346
- timeMs: Date.now() - startTime,
347
- };
348
- }
349
-
350
- /**
351
- * Load index from Walrus (Option 2)
352
- */
353
- private async loadFromWalrus(spaceId: string, blobId: string): Promise<void> {
354
- // Download serialized index from Walrus
355
- const result = await this.storageService.retrieveMemoryPackage(blobId);
356
-
357
- // Parse the index package - it may be in memoryPackage or need to be extracted from content
358
- let pkg: SerializedIndexPackage;
359
-
360
- console.log('📥 Loading index from Walrus, parsing format...');
361
- console.log(' memoryPackage keys:', result.memoryPackage ? Object.keys(result.memoryPackage) : 'null');
362
-
363
- if (result.memoryPackage?.formatVersion === '1.0') {
364
- // Direct format match - SerializedIndexPackage at top level
365
- console.log(' Format: Direct SerializedIndexPackage');
366
- pkg = result.memoryPackage;
367
- } else if (result.memoryPackage?.content) {
368
- // Index was wrapped in memory package format
369
- // content could be a string (JSON) or already parsed object
370
- const content = result.memoryPackage.content;
371
- console.log(' Content type:', typeof content);
372
-
373
- if (typeof content === 'string') {
374
- // Parse JSON string
375
- try {
376
- pkg = JSON.parse(content);
377
- console.log(' Parsed content from JSON string');
378
- } catch (parseErr) {
379
- console.error(' Failed to parse content string:', parseErr);
380
- throw new Error('Failed to parse index package from memory package content');
381
- }
382
- } else if (typeof content === 'object' && content !== null) {
383
- // Already parsed object
384
- pkg = content as SerializedIndexPackage;
385
- console.log(' Content is already an object');
386
- } else {
387
- throw new Error('Invalid content type in memory package');
388
- }
389
- } else {
390
- // Try to parse raw content
391
- console.log(' Trying raw content parse...');
392
- try {
393
- const contentString = new TextDecoder().decode(result.content);
394
- const parsed = JSON.parse(contentString);
395
-
396
- // Check if it's wrapped in memory package format
397
- if (parsed.content && typeof parsed.content === 'string') {
398
- pkg = JSON.parse(parsed.content);
399
- } else if (parsed.formatVersion === '1.0') {
400
- pkg = parsed;
401
- } else {
402
- throw new Error('Unknown format');
403
- }
404
- } catch (err) {
405
- console.error(' Raw parse failed:', err);
406
- throw new Error('Invalid index package format - could not parse content');
407
- }
408
- }
409
-
410
- // Validate format version
411
- if (pkg.formatVersion !== '1.0') {
412
- throw new Error(`Unsupported index format version: ${pkg.formatVersion}`);
413
- }
414
-
415
- console.log(`✅ Parsed index package: ${pkg.vectors?.length || 0} vectors, version ${pkg.version}`);
416
-
417
- // Create new index with same config
418
- await this.vectorService.createIndex(spaceId, pkg.dimension, {
419
- maxElements: pkg.hnswConfig.maxElements,
420
- m: pkg.hnswConfig.m,
421
- efConstruction: pkg.hnswConfig.efConstruction,
422
- });
423
-
424
- // Restore vectors and metadata (VectorService caches vectors internally)
425
- const metadataMap = new Map<number, any>(pkg.metadata);
426
-
427
- for (const { vectorId, vector } of pkg.vectors) {
428
- await this.vectorService.addVector(spaceId, vectorId, vector, metadataMap.get(vectorId));
429
- }
430
-
431
- // Update index state
432
- this.indexStates.set(spaceId, {
433
- blobId,
434
- lastSyncTimestamp: pkg.timestamp,
435
- vectorCount: pkg.vectors.length,
436
- version: pkg.version,
437
- dimension: pkg.dimension,
438
- });
439
-
440
- console.log(`✅ Loaded index from Walrus: ${pkg.vectors.length} vectors`);
441
- }
442
-
443
- /**
444
- * Full rebuild from blockchain + Walrus (Option 1)
445
- */
446
- private async fullRebuild(
447
- spaceId: string,
448
- getMemoriesFromChain: () => Promise<Array<{
449
- id: string;
450
- blobId: string;
451
- vectorId?: number;
452
- category?: string;
453
- importance?: number;
454
- topic?: string;
455
- createdAt?: number;
456
- }>>,
457
- getMemoryContent: (blobId: string) => Promise<{
458
- content: string;
459
- embedding?: number[];
460
- metadata?: any;
461
- }>
462
- ): Promise<{ memoriesCount: number }> {
463
- // Get all memories from blockchain
464
- this.options.onProgress('rebuilding', 15, 'Fetching memories from blockchain...');
465
- const memories = await getMemoriesFromChain();
466
-
467
- if (memories.length === 0) {
468
- console.log('No memories found on blockchain');
469
- return { memoriesCount: 0 };
470
- }
471
-
472
- this.options.onProgress('rebuilding', 20, `Found ${memories.length} memories, rebuilding index...`);
473
-
474
- // Determine dimension from first memory with embedding
475
- let dimension = 3072; // Default
476
-
477
- // Create index
478
- await this.vectorService.createIndex(spaceId, dimension, {
479
- maxElements: Math.max(10000, memories.length * 2),
480
- m: 16,
481
- efConstruction: 200,
482
- });
483
-
484
- // Process each memory (VectorService caches vectors internally)
485
- let processed = 0;
486
- const batchSize = 10;
487
-
488
- for (let i = 0; i < memories.length; i += batchSize) {
489
- const batch = memories.slice(i, i + batchSize);
490
-
491
- await Promise.all(
492
- batch.map(async (memory) => {
493
- try {
494
- // Fetch content from Walrus
495
- const content = await getMemoryContent(memory.blobId);
496
-
497
- let embedding = content.embedding;
498
-
499
- // Generate embedding if not stored
500
- if (!embedding || embedding.length === 0) {
501
- const result = await this.embeddingService.embedText({ text: content.content });
502
- embedding = result.vector;
503
- }
504
-
505
- // Update dimension if needed
506
- if (processed === 0 && embedding.length !== dimension) {
507
- dimension = embedding.length;
508
- }
509
-
510
- const vectorId = memory.vectorId ?? Date.now() % 4294967295;
511
-
512
- // Add to index (VectorService caches vector internally)
513
- await this.vectorService.addVector(spaceId, vectorId, embedding, {
514
- blobId: memory.blobId,
515
- memoryId: memory.id,
516
- category: memory.category,
517
- importance: memory.importance,
518
- topic: memory.topic,
519
- timestamp: memory.createdAt || Date.now(),
520
- });
521
-
522
- processed++;
523
- } catch (error) {
524
- console.warn(`Failed to rebuild memory ${memory.id}:`, error);
525
- }
526
- })
527
- );
528
-
529
- const progress = 20 + Math.floor((processed / memories.length) * 70);
530
- this.options.onProgress('rebuilding', progress, `Processed ${processed}/${memories.length} memories`);
531
- }
532
-
533
- console.log(`✅ Rebuilt index: ${processed} vectors`);
534
-
535
- return { memoriesCount: processed };
536
- }
537
-
538
- /**
539
- * Sync new memories since last save (incremental update)
540
- */
541
- private async syncNewMemories(
542
- spaceId: string,
543
- lastSyncTimestamp: number,
544
- getMemoriesFromChain: () => Promise<Array<{
545
- id: string;
546
- blobId: string;
547
- vectorId?: number;
548
- category?: string;
549
- importance?: number;
550
- topic?: string;
551
- createdAt?: number;
552
- }>>,
553
- getMemoryContent: (blobId: string) => Promise<{
554
- content: string;
555
- embedding?: number[];
556
- metadata?: any;
557
- }>
558
- ): Promise<number> {
559
- // Get all memories
560
- const allMemories = await getMemoriesFromChain();
561
-
562
- // Filter to only new memories
563
- const newMemories = allMemories.filter(
564
- (m) => (m.createdAt || 0) > lastSyncTimestamp
565
- );
566
-
567
- if (newMemories.length === 0) {
568
- console.log('✅ Index is up to date');
569
- return 0;
570
- }
571
-
572
- console.log(`🔄 Syncing ${newMemories.length} new memories...`);
573
-
574
- let synced = 0;
575
-
576
- for (const memory of newMemories) {
577
- try {
578
- const content = await getMemoryContent(memory.blobId);
579
-
580
- let embedding = content.embedding;
581
-
582
- if (!embedding || embedding.length === 0) {
583
- const result = await this.embeddingService.embedText({ text: content.content });
584
- embedding = result.vector;
585
- }
586
-
587
- const vectorId = memory.vectorId ?? Date.now() % 4294967295;
588
-
589
- // Add to index (VectorService caches vector internally)
590
- await this.vectorService.addVector(spaceId, vectorId, embedding, {
591
- blobId: memory.blobId,
592
- memoryId: memory.id,
593
- category: memory.category,
594
- importance: memory.importance,
595
- topic: memory.topic,
596
- timestamp: memory.createdAt || Date.now(),
597
- });
598
-
599
- synced++;
600
- } catch (error) {
601
- console.warn(`Failed to sync memory ${memory.id}:`, error);
602
- }
603
- }
604
-
605
- return synced;
606
- }
607
-
608
- /**
609
- * Save index to Walrus
610
- */
611
- async saveIndex(spaceId: string): Promise<string | null> {
612
- const stats = this.vectorService.getIndexStats(spaceId);
613
- if (!stats || stats.currentElements === 0) {
614
- console.log('No vectors to save');
615
- return null;
616
- }
617
-
618
- // Get all vectors and metadata
619
- const allVectors = this.vectorService.getAllVectors(spaceId);
620
-
621
- // Get vectors from VectorService cache (single source of truth)
622
- const vectorMap = this.vectorService.getAllCachedVectors(spaceId);
623
-
624
- if (vectorMap.size === 0) {
625
- console.warn('Vector cache is empty, cannot save index');
626
- return null;
627
- }
628
-
629
- // Build serialized package
630
- const currentState = this.indexStates.get(spaceId);
631
- const pkg: SerializedIndexPackage = {
632
- formatVersion: '1.0',
633
- spaceId,
634
- version: (currentState?.version || 0) + 1,
635
- dimension: currentState?.dimension || 3072,
636
- timestamp: Date.now(),
637
- vectors: allVectors
638
- .filter(({ vectorId }) => vectorMap.has(vectorId))
639
- .map(({ vectorId }) => ({
640
- vectorId,
641
- vector: vectorMap.get(vectorId)!,
642
- })),
643
- metadata: allVectors.map(({ vectorId, metadata }) => [vectorId, metadata]),
644
- hnswConfig: {
645
- maxElements: stats.maxElements || 10000,
646
- m: 16,
647
- efConstruction: 200,
648
- },
649
- };
650
-
651
- // Upload to Walrus as JSON package
652
- const result = await this.storageService.uploadMemoryPackage(
653
- {
654
- content: JSON.stringify(pkg),
655
- embedding: [],
656
- metadata: {
657
- category: 'vector-index',
658
- type: 'hnsw-index-package',
659
- spaceId,
660
- version: pkg.version,
661
- },
662
- identity: spaceId,
663
- },
664
- {
665
- signer: null as any, // Will be provided by caller
666
- epochs: 5, // Longer retention for index
667
- deletable: true,
668
- }
669
- );
670
-
671
- // Update state
672
- const state: IndexState = {
673
- blobId: result.blobId,
674
- lastSyncTimestamp: pkg.timestamp,
675
- vectorCount: pkg.vectors.length,
676
- version: pkg.version,
677
- dimension: pkg.dimension,
678
- };
679
-
680
- this.indexStates.set(spaceId, state);
681
- this.saveIndexState(spaceId, state);
682
-
683
- console.log(`💾 Index saved to Walrus: ${result.blobId} (${pkg.vectors.length} vectors)`);
684
-
685
- return result.blobId;
686
- }
687
-
688
- /**
689
- * Save index with signer (public API)
690
- *
691
- * This method:
692
- * 1. Uploads index to Walrus
693
- * 2. Creates or updates MemoryIndex on-chain (if transactionService is available)
694
- * 3. Updates local state
695
- *
696
- * @param spaceId - User address
697
- * @param signer - Wallet signer for transactions
698
- * @returns Blob ID of saved index
699
- */
700
- async saveIndexWithSigner(
701
- spaceId: string,
702
- signer: any
703
- ): Promise<string | null> {
704
- const stats = this.vectorService.getIndexStats(spaceId);
705
- if (!stats || stats.currentElements === 0) {
706
- console.log('No vectors to save');
707
- return null;
708
- }
709
-
710
- const allVectors = this.vectorService.getAllVectors(spaceId);
711
-
712
- // Get vectors from VectorService cache (single source of truth)
713
- const vectorMap = this.vectorService.getAllCachedVectors(spaceId);
714
-
715
- if (vectorMap.size === 0) {
716
- console.warn('Vector cache is empty, cannot save index');
717
- return null;
718
- }
719
-
720
- const currentState = this.indexStates.get(spaceId);
721
- const newVersion = (currentState?.version || 0) + 1;
722
-
723
- const pkg: SerializedIndexPackage = {
724
- formatVersion: '1.0',
725
- spaceId,
726
- version: newVersion,
727
- dimension: currentState?.dimension || 3072,
728
- timestamp: Date.now(),
729
- vectors: allVectors
730
- .filter(({ vectorId }) => vectorMap.has(vectorId))
731
- .map(({ vectorId }) => ({
732
- vectorId,
733
- vector: vectorMap.get(vectorId)!,
734
- })),
735
- metadata: allVectors.map(({ vectorId, metadata }) => [vectorId, metadata]),
736
- hnswConfig: {
737
- maxElements: stats.maxElements || 10000,
738
- m: 16,
739
- efConstruction: 200,
740
- },
741
- };
742
-
743
- // Step 1: Upload index to Walrus
744
- const result = await this.storageService.uploadMemoryPackage(
745
- {
746
- content: JSON.stringify(pkg),
747
- embedding: [],
748
- metadata: {
749
- category: 'vector-index',
750
- type: 'hnsw-index-package',
751
- spaceId,
752
- version: pkg.version,
753
- },
754
- identity: spaceId,
755
- },
756
- {
757
- signer,
758
- epochs: 5,
759
- deletable: true,
760
- }
761
- );
762
-
763
- console.log(`💾 Index uploaded to Walrus: ${result.blobId} (${pkg.vectors.length} vectors)`);
764
-
765
- // Step 2: Create or update MemoryIndex on-chain
766
- // SerialTransactionExecutor handles gas coin management and prevents equivocation
767
- let onChainIndexId = currentState?.onChainIndexId;
768
- const graphBlobId = currentState?.graphBlobId || ''; // Empty for now, could be knowledge graph
769
-
770
- if (this.options.transactionService && this.options.executeTransaction) {
771
- try {
772
- if (!onChainIndexId) {
773
- // Create new MemoryIndex on-chain
774
- console.log('📝 Creating new MemoryIndex on-chain...');
775
- const tx = this.options.transactionService.buildCreateMemoryIndex({
776
- indexBlobId: result.blobId,
777
- graphBlobId: graphBlobId,
778
- });
779
-
780
- const txResult = await this.options.executeTransaction(tx, signer);
781
-
782
- if (txResult.digest && !txResult.error) {
783
- console.log(`✅ MemoryIndex created on-chain: ${txResult.digest}`);
784
-
785
- // Extract created object ID from transaction effects
786
- if (txResult.effects?.created?.[0]?.reference?.objectId) {
787
- onChainIndexId = txResult.effects.created[0].reference.objectId;
788
- console.log(` Object ID: ${onChainIndexId}`);
789
- }
790
- } else {
791
- console.warn('⚠️ Failed to create on-chain MemoryIndex:', txResult.error);
792
- }
793
- } else {
794
- // Update existing MemoryIndex on-chain
795
- console.log(`📝 Updating MemoryIndex on-chain (v${currentState?.version} → v${newVersion})...`);
796
- const tx = this.options.transactionService.buildUpdateMemoryIndex({
797
- indexId: onChainIndexId,
798
- expectedVersion: currentState?.version || 1,
799
- newIndexBlobId: result.blobId,
800
- newGraphBlobId: graphBlobId,
801
- });
802
-
803
- const txResult = await this.options.executeTransaction(tx, signer);
804
-
805
- if (txResult.digest && !txResult.error) {
806
- console.log(`✅ MemoryIndex updated on-chain: ${txResult.digest}`);
807
- } else {
808
- console.warn('⚠️ Failed to update on-chain MemoryIndex:', txResult.error);
809
- }
810
- }
811
- } catch (error: any) {
812
- console.warn('⚠️ Failed to sync MemoryIndex on-chain:', error.message);
813
- }
814
- }
815
-
816
- // Step 3: Update local state
817
- const state: IndexState = {
818
- blobId: result.blobId,
819
- graphBlobId: graphBlobId,
820
- lastSyncTimestamp: pkg.timestamp,
821
- vectorCount: pkg.vectors.length,
822
- version: newVersion,
823
- dimension: pkg.dimension,
824
- onChainIndexId: onChainIndexId,
825
- };
826
-
827
- this.indexStates.set(spaceId, state);
828
- this.saveIndexState(spaceId, state);
829
-
830
- console.log(`💾 Index saved: ${result.blobId} (${pkg.vectors.length} vectors, v${newVersion})`);
831
-
832
- return result.blobId;
833
- }
834
-
835
- /**
836
- * Add vector and cache it for serialization
837
- * Note: VectorService now handles caching internally, this method is kept for API compatibility
838
- */
839
- async addVectorWithCache(
840
- spaceId: string,
841
- vectorId: number,
842
- vector: number[],
843
- metadata?: any
844
- ): Promise<void> {
845
- // VectorService.addVector now caches vectors internally
846
- await this.vectorService.addVector(spaceId, vectorId, vector, metadata);
847
- }
848
-
849
- /**
850
- * Start auto-save interval
851
- */
852
- private startAutoSave(spaceId: string): void {
853
- if (this.autoSaveTimer) {
854
- clearInterval(this.autoSaveTimer);
855
- }
856
-
857
- this.autoSaveTimer = setInterval(async () => {
858
- const stats = this.vectorService.getIndexStats(spaceId);
859
- if (stats?.isDirty) {
860
- try {
861
- console.log('🔄 Auto-saving index...');
862
- await this.saveIndex(spaceId);
863
- } catch (error) {
864
- console.error('Auto-save failed:', error);
865
- }
866
- }
867
- }, this.options.autoSaveInterval);
868
-
869
- console.log(`🔄 Auto-save enabled (every ${this.options.autoSaveInterval / 1000}s)`);
870
- }
871
-
872
- /**
873
- * Stop auto-save
874
- */
875
- stopAutoSave(): void {
876
- if (this.autoSaveTimer) {
877
- clearInterval(this.autoSaveTimer);
878
- this.autoSaveTimer = undefined;
879
- console.log('Auto-save disabled');
880
- }
881
- }
882
-
883
- /**
884
- * Load index state from localStorage
885
- */
886
- private loadIndexState(spaceId: string): IndexState | null {
887
- const key = `${this.options.storageKeyPrefix}${spaceId}`;
888
- const data = this.storage.getItem(key);
889
-
890
- if (!data) {
891
- return null;
892
- }
893
-
894
- try {
895
- return JSON.parse(data) as IndexState;
896
- } catch {
897
- return null;
898
- }
899
- }
900
-
901
- /**
902
- * Save index state to localStorage
903
- */
904
- private saveIndexState(spaceId: string, state: IndexState): void {
905
- const key = `${this.options.storageKeyPrefix}${spaceId}`;
906
- this.storage.setItem(key, JSON.stringify(state));
907
- }
908
-
909
- /**
910
- * Clear cached index state
911
- */
912
- clearIndexState(spaceId: string): void {
913
- const key = `${this.options.storageKeyPrefix}${spaceId}`;
914
- this.storage.removeItem(key);
915
- this.indexStates.delete(spaceId);
916
- // Note: VectorService cache is managed by HnswWasmService LRU cache
917
- console.log(`Cleared index state for ${spaceId}`);
918
- }
919
-
920
- /**
921
- * Get current index state
922
- */
923
- getIndexState(spaceId: string): IndexState | null {
924
- return this.indexStates.get(spaceId) || this.loadIndexState(spaceId);
925
- }
926
-
927
- /**
928
- * Force a full rebuild (useful for troubleshooting)
929
- */
930
- async forceRebuild(
931
- spaceId: string,
932
- getMemoriesFromChain: () => Promise<Array<{
933
- id: string;
934
- blobId: string;
935
- vectorId?: number;
936
- category?: string;
937
- importance?: number;
938
- topic?: string;
939
- createdAt?: number;
940
- }>>,
941
- getMemoryContent: (blobId: string) => Promise<{
942
- content: string;
943
- embedding?: number[];
944
- metadata?: any;
945
- }>
946
- ): Promise<{ memoriesCount: number }> {
947
- // Clear existing state
948
- this.clearIndexState(spaceId);
949
-
950
- // Full rebuild
951
- return this.fullRebuild(spaceId, getMemoriesFromChain, getMemoryContent);
952
- }
953
-
954
- /**
955
- * Get statistics
956
- */
957
- getStats(spaceId: string): {
958
- indexState: IndexState | null;
959
- vectorCacheSize: number;
960
- isAutoSaveEnabled: boolean;
961
- } {
962
- return {
963
- indexState: this.getIndexState(spaceId),
964
- vectorCacheSize: this.vectorService.getAllCachedVectors(spaceId).size,
965
- isAutoSaveEnabled: !!this.autoSaveTimer,
966
- };
967
- }
968
-
969
- /**
970
- * Cleanup resources
971
- */
972
- async cleanup(): Promise<void> {
973
- this.stopAutoSave();
974
- this.indexStates.clear();
975
- // Note: VectorService cleanup is handled by its own cleanup() method
976
- }
977
- }
1
+ /**
2
+ * IndexManager - Hybrid Index Persistence Manager
3
+ *
4
+ * Provides intelligent index management with:
5
+ * - Option 1: Full rebuild from Blockchain + Walrus (fallback)
6
+ * - Option 2: Binary serialization to/from Walrus (fast)
7
+ * - Hybrid: Combines both with incremental sync
8
+ * - On-chain MemoryIndex object for versioned index tracking
9
+ *
10
+ * Flow:
11
+ * 1. Initialize: Check for MemoryIndex on-chain → Load blob from Walrus
12
+ * 2. Save: Upload to Walrus → Create/Update MemoryIndex on-chain
13
+ *
14
+ * @module services/IndexManager
15
+ */
16
+
17
+ import type { VectorService } from './VectorService';
18
+ import type { StorageService } from './StorageService';
19
+ import type { EmbeddingService } from './EmbeddingService';
20
+ import type { TransactionService } from './TransactionService';
21
+ import type { MemoryIndex } from './ViewService';
22
+
23
+ /**
24
+ * Index state stored in localStorage/persistent storage
25
+ */
26
+ export interface IndexState {
27
+ /** Blob ID of the saved index on Walrus */
28
+ blobId: string;
29
+ /** Graph blob ID on Walrus (for knowledge graph) */
30
+ graphBlobId?: string;
31
+ /** Timestamp when index was last saved */
32
+ lastSyncTimestamp: number;
33
+ /** Number of vectors in the index at save time */
34
+ vectorCount: number;
35
+ /** Index version for optimistic locking (matches on-chain version) */
36
+ version: number;
37
+ /** Dimension of vectors in the index */
38
+ dimension: number;
39
+ /** On-chain MemoryIndex object ID (if created) */
40
+ onChainIndexId?: string;
41
+ }
42
+
43
+ /**
44
+ * Serialized index package stored on Walrus
45
+ */
46
+ export interface SerializedIndexPackage {
47
+ /** Package format version */
48
+ formatVersion: '1.0';
49
+ /** Space ID (user address) */
50
+ spaceId: string;
51
+ /** Index version */
52
+ version: number;
53
+ /** Vector dimension */
54
+ dimension: number;
55
+ /** Timestamp of serialization */
56
+ timestamp: number;
57
+ /** Serialized HNSW index as base64 (if supported) */
58
+ indexBinary?: string;
59
+ /** All vectors with their IDs for reconstruction */
60
+ vectors: Array<{
61
+ vectorId: number;
62
+ vector: number[];
63
+ }>;
64
+ /** Metadata for each vector */
65
+ metadata: Array<[number, any]>;
66
+ /** HNSW config used */
67
+ hnswConfig: {
68
+ maxElements: number;
69
+ m: number;
70
+ efConstruction: number;
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Progress callback for long operations
76
+ */
77
+ export type IndexProgressCallback = (
78
+ stage: 'loading' | 'rebuilding' | 'syncing' | 'saving' | 'complete',
79
+ progress: number,
80
+ message: string
81
+ ) => void;
82
+
83
+ /**
84
+ * Options for IndexManager initialization
85
+ */
86
+ export interface IndexManagerOptions {
87
+ /** Auto-save interval in ms (default: 5 minutes) */
88
+ autoSaveInterval?: number;
89
+ /** Enable auto-save (default: true) */
90
+ enableAutoSave?: boolean;
91
+ /** Storage key prefix for localStorage */
92
+ storageKeyPrefix?: string;
93
+ /** Progress callback */
94
+ onProgress?: IndexProgressCallback;
95
+ /** TransactionService for on-chain operations (optional) */
96
+ transactionService?: TransactionService;
97
+ /** Callback to get MemoryIndex from blockchain */
98
+ getMemoryIndexFromChain?: (userAddress: string) => Promise<MemoryIndex | null>;
99
+ /** Callback to execute transaction (for create/update MemoryIndex) */
100
+ executeTransaction?: (tx: any, signer: any) => Promise<{ digest: string; effects?: any; error?: string }>;
101
+ }
102
+
103
+ /**
104
+ * IndexManager - Manages HNSW index persistence with hybrid restore strategy
105
+ *
106
+ * Strategy:
107
+ * 1. Try to load from Walrus cache (fast, ~500ms)
108
+ * 2. If failed, rebuild from blockchain + Walrus (slow but complete)
109
+ * 3. Sync any new memories since last save
110
+ * 4. Auto-save periodically
111
+ *
112
+ * Memory Optimization:
113
+ * - Removed duplicate vectorCache - now uses VectorService.getAllCachedVectors()
114
+ * - Vectors are only stored once in VectorService's HnswWasmService LRU cache
115
+ */
116
+ export class IndexManager {
117
+ private vectorService: VectorService;
118
+ private storageService: StorageService;
119
+ private embeddingService: EmbeddingService;
120
+ private options: Required<Omit<IndexManagerOptions, 'transactionService' | 'getMemoryIndexFromChain' | 'executeTransaction'>> & {
121
+ transactionService?: TransactionService;
122
+ getMemoryIndexFromChain?: (userAddress: string) => Promise<MemoryIndex | null>;
123
+ executeTransaction?: (tx: any, signer: any) => Promise<{ digest: string; effects?: any; error?: string }>;
124
+ };
125
+ private autoSaveTimer?: ReturnType<typeof setInterval>;
126
+ private indexStates: Map<string, IndexState> = new Map();
127
+ // Note: vectorCache removed - now using VectorService.getAllCachedVectors() to avoid duplication
128
+
129
+ // Storage adapter (can be replaced for non-browser environments)
130
+ private storage: {
131
+ getItem: (key: string) => string | null;
132
+ setItem: (key: string, value: string) => void;
133
+ removeItem: (key: string) => void;
134
+ };
135
+
136
+ constructor(
137
+ vectorService: VectorService,
138
+ storageService: StorageService,
139
+ embeddingService: EmbeddingService,
140
+ options: IndexManagerOptions = {}
141
+ ) {
142
+ this.vectorService = vectorService;
143
+ this.storageService = storageService;
144
+ this.embeddingService = embeddingService;
145
+
146
+ this.options = {
147
+ autoSaveInterval: options.autoSaveInterval ?? 5 * 60 * 1000, // 5 minutes
148
+ enableAutoSave: options.enableAutoSave ?? true,
149
+ storageKeyPrefix: options.storageKeyPrefix ?? 'pdw_index_',
150
+ onProgress: options.onProgress ?? (() => {}),
151
+ transactionService: options.transactionService,
152
+ getMemoryIndexFromChain: options.getMemoryIndexFromChain,
153
+ executeTransaction: options.executeTransaction,
154
+ };
155
+
156
+ // Default to localStorage in browser, no-op in Node
157
+ this.storage = typeof localStorage !== 'undefined'
158
+ ? localStorage
159
+ : {
160
+ getItem: () => null,
161
+ setItem: () => {},
162
+ removeItem: () => {},
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Set custom storage adapter (for React Native, Node.js, etc.)
168
+ */
169
+ setStorageAdapter(adapter: {
170
+ getItem: (key: string) => string | null;
171
+ setItem: (key: string, value: string) => void;
172
+ removeItem: (key: string) => void;
173
+ }): void {
174
+ this.storage = adapter;
175
+ }
176
+
177
+ /**
178
+ * Initialize index for a user with hybrid restore strategy
179
+ *
180
+ * @param spaceId - User address / space identifier
181
+ * @param getMemoriesFromChain - Function to fetch memories from blockchain
182
+ * @param getMemoryContent - Function to fetch memory content from Walrus
183
+ */
184
+ async initialize(
185
+ spaceId: string,
186
+ getMemoriesFromChain: () => Promise<Array<{
187
+ id: string;
188
+ blobId: string;
189
+ vectorId?: number;
190
+ category?: string;
191
+ importance?: number;
192
+ topic?: string;
193
+ createdAt?: number;
194
+ }>>,
195
+ getMemoryContent: (blobId: string) => Promise<{
196
+ content: string;
197
+ embedding?: number[];
198
+ metadata?: any;
199
+ }>
200
+ ): Promise<{
201
+ restored: boolean;
202
+ method: 'cache' | 'rebuild' | 'empty';
203
+ vectorCount: number;
204
+ syncedCount: number;
205
+ timeMs: number;
206
+ }> {
207
+ const startTime = Date.now();
208
+ let method: 'cache' | 'rebuild' | 'empty' = 'empty';
209
+ let vectorCount = 0;
210
+ let syncedCount = 0;
211
+
212
+ this.options.onProgress('loading', 0, 'Starting index initialization...');
213
+
214
+ // Step 0: Check for on-chain MemoryIndex first (source of truth)
215
+ let onChainIndex: MemoryIndex | null = null;
216
+ if (this.options.getMemoryIndexFromChain) {
217
+ try {
218
+ this.options.onProgress('loading', 5, 'Checking on-chain MemoryIndex...');
219
+ onChainIndex = await this.options.getMemoryIndexFromChain(spaceId);
220
+
221
+ if (onChainIndex) {
222
+ console.log(`📍 Found on-chain MemoryIndex: ${onChainIndex.id} (v${onChainIndex.version})`);
223
+ console.log(` Index blob: ${onChainIndex.indexBlobId}`);
224
+ console.log(` Graph blob: ${onChainIndex.graphBlobId}`);
225
+ }
226
+ } catch (error) {
227
+ console.warn('⚠️ Failed to fetch on-chain MemoryIndex:', error);
228
+ }
229
+ }
230
+
231
+ // Step 1: Try to load from on-chain index or localStorage cache
232
+ // Priority: on-chain index > localStorage cache
233
+ let cachedState = this.loadIndexState(spaceId);
234
+
235
+ // If on-chain index is newer, use it instead of localStorage
236
+ if (onChainIndex && onChainIndex.indexBlobId) {
237
+ const onChainVersion = onChainIndex.version;
238
+ const localVersion = cachedState?.version || 0;
239
+
240
+ if (onChainVersion > localVersion || !cachedState) {
241
+ console.log(`🔄 On-chain index (v${onChainVersion}) is newer than local (v${localVersion}), using on-chain`);
242
+ cachedState = {
243
+ blobId: onChainIndex.indexBlobId,
244
+ graphBlobId: onChainIndex.graphBlobId,
245
+ lastSyncTimestamp: 0, // Will sync all memories
246
+ vectorCount: 0,
247
+ version: onChainVersion,
248
+ dimension: 3072,
249
+ onChainIndexId: onChainIndex.id,
250
+ };
251
+ }
252
+ }
253
+
254
+ if (cachedState) {
255
+ try {
256
+ this.options.onProgress('loading', 10, 'Found cached index, loading from Walrus...');
257
+ await this.loadFromWalrus(spaceId, cachedState.blobId);
258
+
259
+ // Restore on-chain index ID if available
260
+ if (onChainIndex) {
261
+ const state = this.indexStates.get(spaceId);
262
+ if (state) {
263
+ state.onChainIndexId = onChainIndex.id;
264
+ this.indexStates.set(spaceId, state);
265
+ }
266
+ }
267
+
268
+ method = 'cache';
269
+ vectorCount = this.vectorService.getIndexStats(spaceId)?.currentElements || 0;
270
+
271
+ this.options.onProgress('loading', 50, `Loaded ${vectorCount} vectors from cache`);
272
+
273
+ // Step 2: Sync new memories since last save (with timeout)
274
+ this.options.onProgress('syncing', 60, 'Checking for new memories...');
275
+ try {
276
+ // Add 30 second timeout for sync operation
277
+ const syncPromise = this.syncNewMemories(
278
+ spaceId,
279
+ cachedState.lastSyncTimestamp,
280
+ getMemoriesFromChain,
281
+ getMemoryContent
282
+ );
283
+ const timeoutPromise = new Promise<number>((_, reject) =>
284
+ setTimeout(() => reject(new Error('Sync timeout after 30s')), 30000)
285
+ );
286
+ syncedCount = await Promise.race([syncPromise, timeoutPromise]);
287
+
288
+ if (syncedCount > 0) {
289
+ vectorCount += syncedCount;
290
+ this.options.onProgress('syncing', 90, `Synced ${syncedCount} new memories`);
291
+ }
292
+ } catch (syncError: any) {
293
+ console.warn('⚠️ Sync skipped:', syncError.message);
294
+ // Continue without sync - index is still usable from cache
295
+ }
296
+
297
+ this.options.onProgress('complete', 100, 'Index ready');
298
+ } catch (error) {
299
+ console.warn('⚠️ Failed to load cached index, falling back to rebuild:', error);
300
+ // Fall through to rebuild
301
+ method = 'rebuild';
302
+ }
303
+ }
304
+
305
+ // Step 3: Full rebuild if no cache or cache failed (Option 1 - slow but complete)
306
+ if (method !== 'cache') {
307
+ this.options.onProgress('rebuilding', 10, 'No cache found, rebuilding from blockchain...');
308
+
309
+ try {
310
+ // Add 60 second timeout for full rebuild
311
+ const rebuildPromise = this.fullRebuild(
312
+ spaceId,
313
+ getMemoriesFromChain,
314
+ getMemoryContent
315
+ );
316
+ const timeoutPromise = new Promise<{ memoriesCount: number }>((_, reject) =>
317
+ setTimeout(() => reject(new Error('Rebuild timeout after 60s')), 60000)
318
+ );
319
+ const result = await Promise.race([rebuildPromise, timeoutPromise]);
320
+
321
+ method = result.memoriesCount > 0 ? 'rebuild' : 'empty';
322
+ vectorCount = result.memoriesCount;
323
+ } catch (rebuildError: any) {
324
+ console.warn('⚠️ Rebuild failed or timed out:', rebuildError.message);
325
+ method = 'empty';
326
+ vectorCount = 0;
327
+ }
328
+ }
329
+
330
+ // Step 4: Mark as dirty if vectors were synced or rebuilt (save will happen when saveIndex is called)
331
+ // Note: We don't auto-save here because we don't have a signer
332
+ // The caller should call saveIndexWithSigner() when ready to persist
333
+
334
+ // Step 5: Start auto-save if enabled
335
+ if (this.options.enableAutoSave) {
336
+ this.startAutoSave(spaceId);
337
+ }
338
+
339
+ this.options.onProgress('complete', 100, 'Index initialization complete');
340
+
341
+ return {
342
+ restored: method === 'cache',
343
+ method,
344
+ vectorCount,
345
+ syncedCount,
346
+ timeMs: Date.now() - startTime,
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Load index from Walrus (Option 2)
352
+ */
353
+ private async loadFromWalrus(spaceId: string, blobId: string): Promise<void> {
354
+ // Download serialized index from Walrus
355
+ const result = await this.storageService.retrieveMemoryPackage(blobId);
356
+
357
+ // Parse the index package - it may be in memoryPackage or need to be extracted from content
358
+ let pkg: SerializedIndexPackage;
359
+
360
+ console.log('📥 Loading index from Walrus, parsing format...');
361
+ console.log(' memoryPackage keys:', result.memoryPackage ? Object.keys(result.memoryPackage) : 'null');
362
+
363
+ if (result.memoryPackage?.formatVersion === '1.0') {
364
+ // Direct format match - SerializedIndexPackage at top level
365
+ console.log(' Format: Direct SerializedIndexPackage');
366
+ pkg = result.memoryPackage;
367
+ } else if (result.memoryPackage?.content) {
368
+ // Index was wrapped in memory package format
369
+ // content could be a string (JSON) or already parsed object
370
+ const content = result.memoryPackage.content;
371
+ console.log(' Content type:', typeof content);
372
+
373
+ if (typeof content === 'string') {
374
+ // Parse JSON string
375
+ try {
376
+ pkg = JSON.parse(content);
377
+ console.log(' Parsed content from JSON string');
378
+ } catch (parseErr) {
379
+ console.error(' Failed to parse content string:', parseErr);
380
+ throw new Error('Failed to parse index package from memory package content');
381
+ }
382
+ } else if (typeof content === 'object' && content !== null) {
383
+ // Already parsed object
384
+ pkg = content as SerializedIndexPackage;
385
+ console.log(' Content is already an object');
386
+ } else {
387
+ throw new Error('Invalid content type in memory package');
388
+ }
389
+ } else {
390
+ // Try to parse raw content
391
+ console.log(' Trying raw content parse...');
392
+ try {
393
+ const contentString = new TextDecoder().decode(result.content);
394
+ const parsed = JSON.parse(contentString);
395
+
396
+ // Check if it's wrapped in memory package format
397
+ if (parsed.content && typeof parsed.content === 'string') {
398
+ pkg = JSON.parse(parsed.content);
399
+ } else if (parsed.formatVersion === '1.0') {
400
+ pkg = parsed;
401
+ } else {
402
+ throw new Error('Unknown format');
403
+ }
404
+ } catch (err) {
405
+ console.error(' Raw parse failed:', err);
406
+ throw new Error('Invalid index package format - could not parse content');
407
+ }
408
+ }
409
+
410
+ // Validate format version
411
+ if (pkg.formatVersion !== '1.0') {
412
+ throw new Error(`Unsupported index format version: ${pkg.formatVersion}`);
413
+ }
414
+
415
+ console.log(`✅ Parsed index package: ${pkg.vectors?.length || 0} vectors, version ${pkg.version}`);
416
+
417
+ // Create new index with same config
418
+ await this.vectorService.createIndex(spaceId, pkg.dimension, {
419
+ maxElements: pkg.hnswConfig.maxElements,
420
+ m: pkg.hnswConfig.m,
421
+ efConstruction: pkg.hnswConfig.efConstruction,
422
+ });
423
+
424
+ // Restore vectors and metadata (VectorService caches vectors internally)
425
+ const metadataMap = new Map<number, any>(pkg.metadata);
426
+
427
+ for (const { vectorId, vector } of pkg.vectors) {
428
+ await this.vectorService.addVector(spaceId, vectorId, vector, metadataMap.get(vectorId));
429
+ }
430
+
431
+ // Update index state
432
+ this.indexStates.set(spaceId, {
433
+ blobId,
434
+ lastSyncTimestamp: pkg.timestamp,
435
+ vectorCount: pkg.vectors.length,
436
+ version: pkg.version,
437
+ dimension: pkg.dimension,
438
+ });
439
+
440
+ console.log(`✅ Loaded index from Walrus: ${pkg.vectors.length} vectors`);
441
+ }
442
+
443
+ /**
444
+ * Full rebuild from blockchain + Walrus (Option 1)
445
+ */
446
+ private async fullRebuild(
447
+ spaceId: string,
448
+ getMemoriesFromChain: () => Promise<Array<{
449
+ id: string;
450
+ blobId: string;
451
+ vectorId?: number;
452
+ category?: string;
453
+ importance?: number;
454
+ topic?: string;
455
+ createdAt?: number;
456
+ }>>,
457
+ getMemoryContent: (blobId: string) => Promise<{
458
+ content: string;
459
+ embedding?: number[];
460
+ metadata?: any;
461
+ }>
462
+ ): Promise<{ memoriesCount: number }> {
463
+ // Get all memories from blockchain
464
+ this.options.onProgress('rebuilding', 15, 'Fetching memories from blockchain...');
465
+ const memories = await getMemoriesFromChain();
466
+
467
+ if (memories.length === 0) {
468
+ console.log('No memories found on blockchain');
469
+ return { memoriesCount: 0 };
470
+ }
471
+
472
+ this.options.onProgress('rebuilding', 20, `Found ${memories.length} memories, rebuilding index...`);
473
+
474
+ // Determine dimension from first memory with embedding
475
+ let dimension = 3072; // Default
476
+
477
+ // Create index
478
+ await this.vectorService.createIndex(spaceId, dimension, {
479
+ maxElements: Math.max(10000, memories.length * 2),
480
+ m: 16,
481
+ efConstruction: 200,
482
+ });
483
+
484
+ // Process each memory (VectorService caches vectors internally)
485
+ let processed = 0;
486
+ const batchSize = 10;
487
+
488
+ for (let i = 0; i < memories.length; i += batchSize) {
489
+ const batch = memories.slice(i, i + batchSize);
490
+
491
+ await Promise.all(
492
+ batch.map(async (memory) => {
493
+ try {
494
+ // Fetch content from Walrus
495
+ const content = await getMemoryContent(memory.blobId);
496
+
497
+ let embedding = content.embedding;
498
+
499
+ // Generate embedding if not stored
500
+ if (!embedding || embedding.length === 0) {
501
+ const result = await this.embeddingService.embedText({ text: content.content });
502
+ embedding = result.vector;
503
+ }
504
+
505
+ // Update dimension if needed
506
+ if (processed === 0 && embedding.length !== dimension) {
507
+ dimension = embedding.length;
508
+ }
509
+
510
+ const vectorId = memory.vectorId ?? Date.now() % 4294967295;
511
+
512
+ // Add to index (VectorService caches vector internally)
513
+ await this.vectorService.addVector(spaceId, vectorId, embedding, {
514
+ blobId: memory.blobId,
515
+ memoryId: memory.id,
516
+ category: memory.category,
517
+ importance: memory.importance,
518
+ topic: memory.topic,
519
+ timestamp: memory.createdAt || Date.now(),
520
+ });
521
+
522
+ processed++;
523
+ } catch (error) {
524
+ console.warn(`Failed to rebuild memory ${memory.id}:`, error);
525
+ }
526
+ })
527
+ );
528
+
529
+ const progress = 20 + Math.floor((processed / memories.length) * 70);
530
+ this.options.onProgress('rebuilding', progress, `Processed ${processed}/${memories.length} memories`);
531
+ }
532
+
533
+ console.log(`✅ Rebuilt index: ${processed} vectors`);
534
+
535
+ return { memoriesCount: processed };
536
+ }
537
+
538
+ /**
539
+ * Sync new memories since last save (incremental update)
540
+ */
541
+ private async syncNewMemories(
542
+ spaceId: string,
543
+ lastSyncTimestamp: number,
544
+ getMemoriesFromChain: () => Promise<Array<{
545
+ id: string;
546
+ blobId: string;
547
+ vectorId?: number;
548
+ category?: string;
549
+ importance?: number;
550
+ topic?: string;
551
+ createdAt?: number;
552
+ }>>,
553
+ getMemoryContent: (blobId: string) => Promise<{
554
+ content: string;
555
+ embedding?: number[];
556
+ metadata?: any;
557
+ }>
558
+ ): Promise<number> {
559
+ // Get all memories
560
+ const allMemories = await getMemoriesFromChain();
561
+
562
+ // Filter to only new memories
563
+ const newMemories = allMemories.filter(
564
+ (m) => (m.createdAt || 0) > lastSyncTimestamp
565
+ );
566
+
567
+ if (newMemories.length === 0) {
568
+ console.log('✅ Index is up to date');
569
+ return 0;
570
+ }
571
+
572
+ console.log(`🔄 Syncing ${newMemories.length} new memories...`);
573
+
574
+ let synced = 0;
575
+
576
+ for (const memory of newMemories) {
577
+ try {
578
+ const content = await getMemoryContent(memory.blobId);
579
+
580
+ let embedding = content.embedding;
581
+
582
+ if (!embedding || embedding.length === 0) {
583
+ const result = await this.embeddingService.embedText({ text: content.content });
584
+ embedding = result.vector;
585
+ }
586
+
587
+ const vectorId = memory.vectorId ?? Date.now() % 4294967295;
588
+
589
+ // Add to index (VectorService caches vector internally)
590
+ await this.vectorService.addVector(spaceId, vectorId, embedding, {
591
+ blobId: memory.blobId,
592
+ memoryId: memory.id,
593
+ category: memory.category,
594
+ importance: memory.importance,
595
+ topic: memory.topic,
596
+ timestamp: memory.createdAt || Date.now(),
597
+ });
598
+
599
+ synced++;
600
+ } catch (error) {
601
+ console.warn(`Failed to sync memory ${memory.id}:`, error);
602
+ }
603
+ }
604
+
605
+ return synced;
606
+ }
607
+
608
+ /**
609
+ * Save index to Walrus
610
+ */
611
+ async saveIndex(spaceId: string): Promise<string | null> {
612
+ const stats = this.vectorService.getIndexStats(spaceId);
613
+ if (!stats || stats.currentElements === 0) {
614
+ console.log('No vectors to save');
615
+ return null;
616
+ }
617
+
618
+ // Get all vectors and metadata
619
+ const allVectors = this.vectorService.getAllVectors(spaceId);
620
+
621
+ // Get vectors from VectorService cache (single source of truth)
622
+ const vectorMap = this.vectorService.getAllCachedVectors(spaceId);
623
+
624
+ if (vectorMap.size === 0) {
625
+ console.warn('Vector cache is empty, cannot save index');
626
+ return null;
627
+ }
628
+
629
+ // Build serialized package
630
+ const currentState = this.indexStates.get(spaceId);
631
+ const pkg: SerializedIndexPackage = {
632
+ formatVersion: '1.0',
633
+ spaceId,
634
+ version: (currentState?.version || 0) + 1,
635
+ dimension: currentState?.dimension || 3072,
636
+ timestamp: Date.now(),
637
+ vectors: allVectors
638
+ .filter(({ vectorId }) => vectorMap.has(vectorId))
639
+ .map(({ vectorId }) => ({
640
+ vectorId,
641
+ vector: vectorMap.get(vectorId)!,
642
+ })),
643
+ metadata: allVectors.map(({ vectorId, metadata }) => [vectorId, metadata]),
644
+ hnswConfig: {
645
+ maxElements: stats.maxElements || 10000,
646
+ m: 16,
647
+ efConstruction: 200,
648
+ },
649
+ };
650
+
651
+ // Upload to Walrus as JSON package
652
+ const result = await this.storageService.uploadMemoryPackage(
653
+ {
654
+ content: JSON.stringify(pkg),
655
+ embedding: [],
656
+ metadata: {
657
+ category: 'vector-index',
658
+ type: 'hnsw-index-package',
659
+ spaceId,
660
+ version: pkg.version,
661
+ },
662
+ identity: spaceId,
663
+ },
664
+ {
665
+ signer: null as any, // Will be provided by caller
666
+ epochs: 5, // Longer retention for index
667
+ deletable: true,
668
+ }
669
+ );
670
+
671
+ // Update state
672
+ const state: IndexState = {
673
+ blobId: result.blobId,
674
+ lastSyncTimestamp: pkg.timestamp,
675
+ vectorCount: pkg.vectors.length,
676
+ version: pkg.version,
677
+ dimension: pkg.dimension,
678
+ };
679
+
680
+ this.indexStates.set(spaceId, state);
681
+ this.saveIndexState(spaceId, state);
682
+
683
+ console.log(`💾 Index saved to Walrus: ${result.blobId} (${pkg.vectors.length} vectors)`);
684
+
685
+ return result.blobId;
686
+ }
687
+
688
+ /**
689
+ * Save index with signer (public API)
690
+ *
691
+ * This method:
692
+ * 1. Uploads index to Walrus
693
+ * 2. Creates or updates MemoryIndex on-chain (if transactionService is available)
694
+ * 3. Updates local state
695
+ *
696
+ * @param spaceId - User address
697
+ * @param signer - Wallet signer for transactions
698
+ * @returns Blob ID of saved index
699
+ */
700
+ async saveIndexWithSigner(
701
+ spaceId: string,
702
+ signer: any
703
+ ): Promise<string | null> {
704
+ const stats = this.vectorService.getIndexStats(spaceId);
705
+ if (!stats || stats.currentElements === 0) {
706
+ console.log('No vectors to save');
707
+ return null;
708
+ }
709
+
710
+ const allVectors = this.vectorService.getAllVectors(spaceId);
711
+
712
+ // Get vectors from VectorService cache (single source of truth)
713
+ const vectorMap = this.vectorService.getAllCachedVectors(spaceId);
714
+
715
+ if (vectorMap.size === 0) {
716
+ console.warn('Vector cache is empty, cannot save index');
717
+ return null;
718
+ }
719
+
720
+ const currentState = this.indexStates.get(spaceId);
721
+ const newVersion = (currentState?.version || 0) + 1;
722
+
723
+ const pkg: SerializedIndexPackage = {
724
+ formatVersion: '1.0',
725
+ spaceId,
726
+ version: newVersion,
727
+ dimension: currentState?.dimension || 3072,
728
+ timestamp: Date.now(),
729
+ vectors: allVectors
730
+ .filter(({ vectorId }) => vectorMap.has(vectorId))
731
+ .map(({ vectorId }) => ({
732
+ vectorId,
733
+ vector: vectorMap.get(vectorId)!,
734
+ })),
735
+ metadata: allVectors.map(({ vectorId, metadata }) => [vectorId, metadata]),
736
+ hnswConfig: {
737
+ maxElements: stats.maxElements || 10000,
738
+ m: 16,
739
+ efConstruction: 200,
740
+ },
741
+ };
742
+
743
+ // Step 1: Upload index to Walrus
744
+ const result = await this.storageService.uploadMemoryPackage(
745
+ {
746
+ content: JSON.stringify(pkg),
747
+ embedding: [],
748
+ metadata: {
749
+ category: 'vector-index',
750
+ type: 'hnsw-index-package',
751
+ spaceId,
752
+ version: pkg.version,
753
+ },
754
+ identity: spaceId,
755
+ },
756
+ {
757
+ signer,
758
+ epochs: 5,
759
+ deletable: true,
760
+ }
761
+ );
762
+
763
+ console.log(`💾 Index uploaded to Walrus: ${result.blobId} (${pkg.vectors.length} vectors)`);
764
+
765
+ // Step 2: Create or update MemoryIndex on-chain
766
+ // SerialTransactionExecutor handles gas coin management and prevents equivocation
767
+ let onChainIndexId = currentState?.onChainIndexId;
768
+ const graphBlobId = currentState?.graphBlobId || ''; // Empty for now, could be knowledge graph
769
+
770
+ if (this.options.transactionService && this.options.executeTransaction) {
771
+ try {
772
+ if (!onChainIndexId) {
773
+ // Create new MemoryIndex on-chain
774
+ console.log('📝 Creating new MemoryIndex on-chain...');
775
+ const tx = this.options.transactionService.buildCreateMemoryIndex({
776
+ indexBlobId: result.blobId,
777
+ graphBlobId: graphBlobId,
778
+ });
779
+
780
+ const txResult = await this.options.executeTransaction(tx, signer);
781
+
782
+ if (txResult.digest && !txResult.error) {
783
+ console.log(`✅ MemoryIndex created on-chain: ${txResult.digest}`);
784
+
785
+ // Extract created object ID from transaction effects
786
+ if (txResult.effects?.created?.[0]?.reference?.objectId) {
787
+ onChainIndexId = txResult.effects.created[0].reference.objectId;
788
+ console.log(` Object ID: ${onChainIndexId}`);
789
+ }
790
+ } else {
791
+ console.warn('⚠️ Failed to create on-chain MemoryIndex:', txResult.error);
792
+ }
793
+ } else {
794
+ // Update existing MemoryIndex on-chain
795
+ console.log(`📝 Updating MemoryIndex on-chain (v${currentState?.version} → v${newVersion})...`);
796
+ const tx = this.options.transactionService.buildUpdateMemoryIndex({
797
+ indexId: onChainIndexId,
798
+ expectedVersion: currentState?.version || 1,
799
+ newIndexBlobId: result.blobId,
800
+ newGraphBlobId: graphBlobId,
801
+ });
802
+
803
+ const txResult = await this.options.executeTransaction(tx, signer);
804
+
805
+ if (txResult.digest && !txResult.error) {
806
+ console.log(`✅ MemoryIndex updated on-chain: ${txResult.digest}`);
807
+ } else {
808
+ console.warn('⚠️ Failed to update on-chain MemoryIndex:', txResult.error);
809
+ }
810
+ }
811
+ } catch (error: any) {
812
+ console.warn('⚠️ Failed to sync MemoryIndex on-chain:', error.message);
813
+ }
814
+ }
815
+
816
+ // Step 3: Update local state
817
+ const state: IndexState = {
818
+ blobId: result.blobId,
819
+ graphBlobId: graphBlobId,
820
+ lastSyncTimestamp: pkg.timestamp,
821
+ vectorCount: pkg.vectors.length,
822
+ version: newVersion,
823
+ dimension: pkg.dimension,
824
+ onChainIndexId: onChainIndexId,
825
+ };
826
+
827
+ this.indexStates.set(spaceId, state);
828
+ this.saveIndexState(spaceId, state);
829
+
830
+ console.log(`💾 Index saved: ${result.blobId} (${pkg.vectors.length} vectors, v${newVersion})`);
831
+
832
+ return result.blobId;
833
+ }
834
+
835
+ /**
836
+ * Add vector and cache it for serialization
837
+ * Note: VectorService now handles caching internally, this method is kept for API compatibility
838
+ */
839
+ async addVectorWithCache(
840
+ spaceId: string,
841
+ vectorId: number,
842
+ vector: number[],
843
+ metadata?: any
844
+ ): Promise<void> {
845
+ // VectorService.addVector now caches vectors internally
846
+ await this.vectorService.addVector(spaceId, vectorId, vector, metadata);
847
+ }
848
+
849
+ /**
850
+ * Start auto-save interval
851
+ */
852
+ private startAutoSave(spaceId: string): void {
853
+ if (this.autoSaveTimer) {
854
+ clearInterval(this.autoSaveTimer);
855
+ }
856
+
857
+ this.autoSaveTimer = setInterval(async () => {
858
+ const stats = this.vectorService.getIndexStats(spaceId);
859
+ if (stats?.isDirty) {
860
+ try {
861
+ console.log('🔄 Auto-saving index...');
862
+ await this.saveIndex(spaceId);
863
+ } catch (error) {
864
+ console.error('Auto-save failed:', error);
865
+ }
866
+ }
867
+ }, this.options.autoSaveInterval);
868
+
869
+ console.log(`🔄 Auto-save enabled (every ${this.options.autoSaveInterval / 1000}s)`);
870
+ }
871
+
872
+ /**
873
+ * Stop auto-save
874
+ */
875
+ stopAutoSave(): void {
876
+ if (this.autoSaveTimer) {
877
+ clearInterval(this.autoSaveTimer);
878
+ this.autoSaveTimer = undefined;
879
+ console.log('Auto-save disabled');
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Load index state from localStorage
885
+ */
886
+ private loadIndexState(spaceId: string): IndexState | null {
887
+ const key = `${this.options.storageKeyPrefix}${spaceId}`;
888
+ const data = this.storage.getItem(key);
889
+
890
+ if (!data) {
891
+ return null;
892
+ }
893
+
894
+ try {
895
+ return JSON.parse(data) as IndexState;
896
+ } catch {
897
+ return null;
898
+ }
899
+ }
900
+
901
+ /**
902
+ * Save index state to localStorage
903
+ */
904
+ private saveIndexState(spaceId: string, state: IndexState): void {
905
+ const key = `${this.options.storageKeyPrefix}${spaceId}`;
906
+ this.storage.setItem(key, JSON.stringify(state));
907
+ }
908
+
909
+ /**
910
+ * Clear cached index state
911
+ */
912
+ clearIndexState(spaceId: string): void {
913
+ const key = `${this.options.storageKeyPrefix}${spaceId}`;
914
+ this.storage.removeItem(key);
915
+ this.indexStates.delete(spaceId);
916
+ // Note: VectorService cache is managed by HnswWasmService LRU cache
917
+ console.log(`Cleared index state for ${spaceId}`);
918
+ }
919
+
920
+ /**
921
+ * Get current index state
922
+ */
923
+ getIndexState(spaceId: string): IndexState | null {
924
+ return this.indexStates.get(spaceId) || this.loadIndexState(spaceId);
925
+ }
926
+
927
+ /**
928
+ * Force a full rebuild (useful for troubleshooting)
929
+ */
930
+ async forceRebuild(
931
+ spaceId: string,
932
+ getMemoriesFromChain: () => Promise<Array<{
933
+ id: string;
934
+ blobId: string;
935
+ vectorId?: number;
936
+ category?: string;
937
+ importance?: number;
938
+ topic?: string;
939
+ createdAt?: number;
940
+ }>>,
941
+ getMemoryContent: (blobId: string) => Promise<{
942
+ content: string;
943
+ embedding?: number[];
944
+ metadata?: any;
945
+ }>
946
+ ): Promise<{ memoriesCount: number }> {
947
+ // Clear existing state
948
+ this.clearIndexState(spaceId);
949
+
950
+ // Full rebuild
951
+ return this.fullRebuild(spaceId, getMemoriesFromChain, getMemoryContent);
952
+ }
953
+
954
+ /**
955
+ * Get statistics
956
+ */
957
+ getStats(spaceId: string): {
958
+ indexState: IndexState | null;
959
+ vectorCacheSize: number;
960
+ isAutoSaveEnabled: boolean;
961
+ } {
962
+ return {
963
+ indexState: this.getIndexState(spaceId),
964
+ vectorCacheSize: this.vectorService.getAllCachedVectors(spaceId).size,
965
+ isAutoSaveEnabled: !!this.autoSaveTimer,
966
+ };
967
+ }
968
+
969
+ /**
970
+ * Cleanup resources
971
+ */
972
+ async cleanup(): Promise<void> {
973
+ this.stopAutoSave();
974
+ this.indexStates.clear();
975
+ // Note: VectorService cleanup is handled by its own cleanup() method
976
+ }
977
+ }