@cmdoss/memwal-sdk 0.6.2 → 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 (247) hide show
  1. package/ARCHITECTURE.md +547 -547
  2. package/BENCHMARKS.md +238 -238
  3. package/README.md +310 -181
  4. package/dist/ai-sdk/tools.d.ts +2 -2
  5. package/dist/ai-sdk/tools.js +2 -2
  6. package/dist/client/ClientMemoryManager.js +2 -2
  7. package/dist/client/ClientMemoryManager.js.map +1 -1
  8. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  9. package/dist/client/SimplePDWClient.d.ts +29 -1
  10. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  11. package/dist/client/SimplePDWClient.js +45 -13
  12. package/dist/client/SimplePDWClient.js.map +1 -1
  13. package/dist/client/namespaces/EmbeddingsNamespace.d.ts +1 -1
  14. package/dist/client/namespaces/EmbeddingsNamespace.js +1 -1
  15. package/dist/client/namespaces/MemoryNamespace.d.ts +31 -0
  16. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  17. package/dist/client/namespaces/MemoryNamespace.js +272 -39
  18. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  19. package/dist/client/namespaces/consolidated/AINamespace.d.ts +2 -2
  20. package/dist/client/namespaces/consolidated/AINamespace.js +2 -2
  21. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts +12 -2
  22. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
  23. package/dist/client/namespaces/consolidated/BlockchainNamespace.js +62 -4
  24. package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
  25. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +67 -2
  26. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  27. package/dist/client/namespaces/consolidated/StorageNamespace.js +549 -16
  28. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  29. package/dist/config/ConfigurationHelper.js +61 -61
  30. package/dist/config/defaults.js +2 -2
  31. package/dist/config/defaults.js.map +1 -1
  32. package/dist/graph/GraphService.js +21 -21
  33. package/dist/graph/GraphService.js.map +1 -1
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/infrastructure/seal/EncryptionService.d.ts +9 -5
  39. package/dist/infrastructure/seal/EncryptionService.d.ts.map +1 -1
  40. package/dist/infrastructure/seal/EncryptionService.js +37 -15
  41. package/dist/infrastructure/seal/EncryptionService.js.map +1 -1
  42. package/dist/infrastructure/seal/SealService.d.ts +13 -5
  43. package/dist/infrastructure/seal/SealService.d.ts.map +1 -1
  44. package/dist/infrastructure/seal/SealService.js +36 -34
  45. package/dist/infrastructure/seal/SealService.js.map +1 -1
  46. package/dist/langchain/createPDWRAG.js +30 -30
  47. package/dist/retrieval/MemoryDecryptionPipeline.d.ts.map +1 -1
  48. package/dist/retrieval/MemoryDecryptionPipeline.js +2 -1
  49. package/dist/retrieval/MemoryDecryptionPipeline.js.map +1 -1
  50. package/dist/retrieval/MemoryRetrievalService.d.ts +31 -0
  51. package/dist/retrieval/MemoryRetrievalService.d.ts.map +1 -1
  52. package/dist/retrieval/MemoryRetrievalService.js +44 -4
  53. package/dist/retrieval/MemoryRetrievalService.js.map +1 -1
  54. package/dist/services/CapabilityService.d.ts.map +1 -1
  55. package/dist/services/CapabilityService.js +30 -14
  56. package/dist/services/CapabilityService.js.map +1 -1
  57. package/dist/services/CrossContextPermissionService.d.ts.map +1 -1
  58. package/dist/services/CrossContextPermissionService.js +9 -7
  59. package/dist/services/CrossContextPermissionService.js.map +1 -1
  60. package/dist/services/EmbeddingService.d.ts +28 -1
  61. package/dist/services/EmbeddingService.d.ts.map +1 -1
  62. package/dist/services/EmbeddingService.js +54 -0
  63. package/dist/services/EmbeddingService.js.map +1 -1
  64. package/dist/services/EncryptionService.d.ts.map +1 -1
  65. package/dist/services/EncryptionService.js +6 -5
  66. package/dist/services/EncryptionService.js.map +1 -1
  67. package/dist/services/GeminiAIService.js +309 -309
  68. package/dist/services/IndexManager.d.ts +5 -1
  69. package/dist/services/IndexManager.d.ts.map +1 -1
  70. package/dist/services/IndexManager.js +17 -40
  71. package/dist/services/IndexManager.js.map +1 -1
  72. package/dist/services/QueryService.js +1 -1
  73. package/dist/services/QueryService.js.map +1 -1
  74. package/dist/services/StorageService.d.ts +11 -0
  75. package/dist/services/StorageService.d.ts.map +1 -1
  76. package/dist/services/StorageService.js +73 -10
  77. package/dist/services/StorageService.js.map +1 -1
  78. package/dist/services/TransactionService.d.ts +20 -0
  79. package/dist/services/TransactionService.d.ts.map +1 -1
  80. package/dist/services/TransactionService.js +43 -0
  81. package/dist/services/TransactionService.js.map +1 -1
  82. package/dist/services/ViewService.js +2 -2
  83. package/dist/services/ViewService.js.map +1 -1
  84. package/dist/services/storage/QuiltBatchManager.d.ts +101 -1
  85. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  86. package/dist/services/storage/QuiltBatchManager.js +410 -20
  87. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  88. package/dist/services/storage/index.d.ts +1 -1
  89. package/dist/services/storage/index.d.ts.map +1 -1
  90. package/dist/services/storage/index.js.map +1 -1
  91. package/dist/utils/LRUCache.d.ts +106 -0
  92. package/dist/utils/LRUCache.d.ts.map +1 -0
  93. package/dist/utils/LRUCache.js +281 -0
  94. package/dist/utils/LRUCache.js.map +1 -0
  95. package/dist/utils/index.d.ts +1 -0
  96. package/dist/utils/index.d.ts.map +1 -1
  97. package/dist/utils/index.js +2 -0
  98. package/dist/utils/index.js.map +1 -1
  99. package/dist/utils/memoryIndexOnChain.d.ts +212 -0
  100. package/dist/utils/memoryIndexOnChain.d.ts.map +1 -0
  101. package/dist/utils/memoryIndexOnChain.js +312 -0
  102. package/dist/utils/memoryIndexOnChain.js.map +1 -0
  103. package/dist/utils/rebuildIndexNode.d.ts +29 -0
  104. package/dist/utils/rebuildIndexNode.d.ts.map +1 -1
  105. package/dist/utils/rebuildIndexNode.js +366 -98
  106. package/dist/utils/rebuildIndexNode.js.map +1 -1
  107. package/dist/vector/HnswWasmService.d.ts +20 -5
  108. package/dist/vector/HnswWasmService.d.ts.map +1 -1
  109. package/dist/vector/HnswWasmService.js +73 -40
  110. package/dist/vector/HnswWasmService.js.map +1 -1
  111. package/dist/vector/IHnswService.d.ts +10 -1
  112. package/dist/vector/IHnswService.d.ts.map +1 -1
  113. package/dist/vector/IHnswService.js.map +1 -1
  114. package/dist/vector/NodeHnswService.d.ts +16 -0
  115. package/dist/vector/NodeHnswService.d.ts.map +1 -1
  116. package/dist/vector/NodeHnswService.js +84 -5
  117. package/dist/vector/NodeHnswService.js.map +1 -1
  118. package/dist/vector/createHnswService.d.ts +1 -1
  119. package/dist/vector/createHnswService.js +1 -1
  120. package/dist/vector/index.d.ts +1 -1
  121. package/dist/vector/index.js +1 -1
  122. package/package.json +157 -157
  123. package/src/access/PermissionService.ts +635 -635
  124. package/src/aggregation/AggregationService.ts +389 -389
  125. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  126. package/src/ai-sdk/index.ts +65 -65
  127. package/src/ai-sdk/tools.ts +460 -460
  128. package/src/ai-sdk/types.ts +404 -404
  129. package/src/batch/BatchManager.ts +597 -597
  130. package/src/batch/BatchingService.ts +429 -429
  131. package/src/batch/MemoryProcessingCache.ts +492 -492
  132. package/src/batch/index.ts +30 -30
  133. package/src/browser.ts +200 -200
  134. package/src/client/ClientMemoryManager.ts +987 -987
  135. package/src/client/PersonalDataWallet.ts +345 -345
  136. package/src/client/SimplePDWClient.ts +1289 -1222
  137. package/src/client/factory.ts +154 -154
  138. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  139. package/src/client/namespaces/BatchNamespace.ts +356 -356
  140. package/src/client/namespaces/CacheNamespace.ts +123 -123
  141. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  142. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  143. package/src/client/namespaces/ContextNamespace.ts +297 -297
  144. package/src/client/namespaces/EmbeddingsNamespace.ts +99 -99
  145. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  146. package/src/client/namespaces/GraphNamespace.ts +468 -468
  147. package/src/client/namespaces/IndexNamespace.ts +361 -361
  148. package/src/client/namespaces/MemoryNamespace.ts +1422 -1135
  149. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  150. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  151. package/src/client/namespaces/SearchNamespace.ts +1049 -1049
  152. package/src/client/namespaces/StorageNamespace.ts +458 -458
  153. package/src/client/namespaces/TxNamespace.ts +260 -260
  154. package/src/client/namespaces/WalletNamespace.ts +243 -243
  155. package/src/client/namespaces/consolidated/AINamespace.ts +449 -449
  156. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -546
  157. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  158. package/src/client/namespaces/consolidated/StorageNamespace.ts +1141 -497
  159. package/src/client/namespaces/consolidated/index.ts +39 -39
  160. package/src/client/signers/KeypairSigner.ts +108 -108
  161. package/src/client/signers/UnifiedSigner.ts +110 -110
  162. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  163. package/src/client/signers/index.ts +26 -26
  164. package/src/config/ConfigurationHelper.ts +412 -412
  165. package/src/config/defaults.ts +51 -51
  166. package/src/config/index.ts +8 -8
  167. package/src/config/validation.ts +70 -70
  168. package/src/core/index.ts +14 -14
  169. package/src/core/interfaces/IService.ts +307 -307
  170. package/src/core/interfaces/index.ts +8 -8
  171. package/src/core/types/capability.ts +297 -297
  172. package/src/core/types/index.ts +870 -870
  173. package/src/core/types/wallet.ts +270 -270
  174. package/src/core/types.ts +9 -9
  175. package/src/core/wallet.ts +222 -222
  176. package/src/embedding/index.ts +19 -19
  177. package/src/embedding/types.ts +357 -357
  178. package/src/errors/index.ts +602 -602
  179. package/src/errors/recovery.ts +461 -461
  180. package/src/errors/validation.ts +567 -567
  181. package/src/generated/pdw/capability.ts +319 -319
  182. package/src/graph/GraphService.ts +887 -887
  183. package/src/graph/KnowledgeGraphManager.ts +728 -728
  184. package/src/graph/index.ts +25 -25
  185. package/src/index.ts +498 -474
  186. package/src/infrastructure/index.ts +22 -22
  187. package/src/infrastructure/seal/EncryptionService.ts +628 -603
  188. package/src/infrastructure/seal/SealService.ts +613 -615
  189. package/src/infrastructure/seal/index.ts +9 -9
  190. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  191. package/src/infrastructure/sui/SuiService.ts +888 -888
  192. package/src/infrastructure/sui/index.ts +9 -9
  193. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  194. package/src/infrastructure/walrus/WalrusStorageService.ts +612 -612
  195. package/src/infrastructure/walrus/index.ts +9 -9
  196. package/src/langchain/PDWEmbeddings.ts +145 -145
  197. package/src/langchain/PDWVectorStore.ts +456 -456
  198. package/src/langchain/createPDWRAG.ts +303 -303
  199. package/src/langchain/index.ts +47 -47
  200. package/src/permissions/ConsentRepository.browser.ts +249 -249
  201. package/src/permissions/ConsentRepository.ts +364 -364
  202. package/src/pipeline/MemoryPipeline.ts +862 -862
  203. package/src/pipeline/PipelineManager.ts +683 -683
  204. package/src/pipeline/index.ts +26 -26
  205. package/src/retrieval/AdvancedSearchService.ts +629 -629
  206. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  207. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -824
  208. package/src/retrieval/MemoryRetrievalService.ts +904 -830
  209. package/src/retrieval/index.ts +42 -42
  210. package/src/services/BatchService.ts +352 -352
  211. package/src/services/CapabilityService.ts +464 -448
  212. package/src/services/ClassifierService.ts +465 -465
  213. package/src/services/CrossContextPermissionService.ts +486 -484
  214. package/src/services/EmbeddingService.ts +771 -706
  215. package/src/services/EncryptionService.ts +712 -711
  216. package/src/services/GeminiAIService.ts +753 -753
  217. package/src/services/IndexManager.ts +977 -1004
  218. package/src/services/MemoryIndexService.ts +1003 -1003
  219. package/src/services/MemoryService.ts +369 -369
  220. package/src/services/QueryService.ts +890 -890
  221. package/src/services/StorageService.ts +1182 -1111
  222. package/src/services/TransactionService.ts +838 -790
  223. package/src/services/VectorService.ts +462 -462
  224. package/src/services/ViewService.ts +484 -484
  225. package/src/services/index.ts +25 -25
  226. package/src/services/storage/BlobAttributesManager.ts +333 -333
  227. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  228. package/src/services/storage/MemorySearchManager.ts +387 -387
  229. package/src/services/storage/QuiltBatchManager.ts +1130 -660
  230. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  231. package/src/services/storage/WalrusStorageManager.ts +287 -287
  232. package/src/services/storage/index.ts +57 -52
  233. package/src/types/index.ts +13 -13
  234. package/src/utils/LRUCache.ts +378 -0
  235. package/src/utils/index.ts +76 -68
  236. package/src/utils/memoryIndexOnChain.ts +507 -0
  237. package/src/utils/rebuildIndex.ts +290 -290
  238. package/src/utils/rebuildIndexNode.ts +771 -424
  239. package/src/vector/BrowserHnswIndexService.ts +758 -758
  240. package/src/vector/HnswWasmService.ts +731 -679
  241. package/src/vector/IHnswService.ts +233 -224
  242. package/src/vector/NodeHnswService.ts +833 -735
  243. package/src/vector/VectorManager.ts +478 -478
  244. package/src/vector/createHnswService.ts +135 -135
  245. package/src/vector/index.ts +56 -56
  246. package/src/wallet/ContextWalletService.ts +656 -656
  247. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,987 +1,987 @@
1
- /**
2
- * ClientMemoryManager - Client-side Memory Operations for React dApps
3
- *
4
- * Provides a simplified API for creating and retrieving memories in React dApps
5
- * using @mysten/dapp-kit. Handles the complete flow:
6
- * - Memory creation: embedding → encryption → Walrus upload → on-chain registration
7
- * - Memory retrieval: Walrus fetch → SEAL decryption → content extraction
8
- *
9
- * Usage:
10
- * ```typescript
11
- * const manager = new ClientMemoryManager({
12
- * packageId: '0x...',
13
- * accessRegistryId: '0x...',
14
- * walrusAggregator: 'https://...',
15
- * geminiApiKey: 'your-key'
16
- * });
17
- *
18
- * // Create memory
19
- * const blobId = await manager.createMemory({
20
- * content: 'My memory',
21
- * account,
22
- * signAndExecute,
23
- * client,
24
- * onProgress: (status) => console.log(status)
25
- * });
26
- *
27
- * // Retrieve memory
28
- * const content = await manager.retrieveMemory({
29
- * blobId: '...',
30
- * account,
31
- * signPersonalMessage,
32
- * client
33
- * });
34
- * ```
35
- */
36
-
37
- import { SealClient, SessionKey } from '@mysten/seal';
38
- import { WalrusClient } from '@mysten/walrus';
39
- import { Transaction } from '@mysten/sui/transactions';
40
- import { fromHex } from '@mysten/sui/utils';
41
- import type { SuiClient } from '@mysten/sui/client';
42
- // Import environment detection from browser-safe file (no Node.js deps)
43
- import { isBrowser, isNode } from '../vector/IHnswService';
44
- import type { IHnswService } from '../vector/IHnswService';
45
- import { EmbeddingService } from '../services/EmbeddingService';
46
- import { GeminiAIService } from '../services/GeminiAIService';
47
-
48
- export interface ClientMemoryManagerConfig {
49
- packageId: string;
50
- accessRegistryId: string;
51
- walrusAggregator: string;
52
- geminiApiKey: string;
53
- sealServerObjectIds?: string[];
54
- walrusNetwork?: 'testnet' | 'mainnet';
55
- categories?: string[];
56
- /** Enable local browser indexing for vector search (default: true) */
57
- enableLocalIndexing?: boolean;
58
- /** Pre-initialized HNSW service instance (shared singleton) */
59
- hnswService?: IHnswService;
60
- /** Enable SEAL encryption for memory content (default: true) */
61
- enableEncryption?: boolean;
62
- }
63
-
64
- export interface CreateMemoryOptions {
65
- content: string;
66
- category?: string;
67
- account: { address: string };
68
- signAndExecute: (params: { transaction: Transaction }, callbacks: {
69
- onSuccess: (result: any) => void;
70
- onError: (error: Error) => void;
71
- }) => void;
72
- client: SuiClient;
73
- onProgress?: (status: string) => void;
74
- }
75
-
76
- export interface RetrieveMemoryOptions {
77
- blobId: string;
78
- account: { address: string };
79
- signPersonalMessage: (params: { message: Uint8Array }) => Promise<{ signature: string }>;
80
- client: SuiClient;
81
- onProgress?: (status: string) => void;
82
- }
83
-
84
- export interface BatchRetrieveMemoriesOptions {
85
- blobIds: string[];
86
- account: { address: string };
87
- signPersonalMessage: (params: { message: Uint8Array }) => Promise<{ signature: string }>;
88
- client: SuiClient;
89
- onProgress?: (status: string, current: number, total: number) => void;
90
- }
91
-
92
- export interface BatchRetrieveResult {
93
- blobId: string;
94
- content?: string;
95
- error?: string;
96
- }
97
-
98
- interface DecryptionSession {
99
- sealClient: SealClient;
100
- sessionKey: SessionKey;
101
- txBytes: Uint8Array;
102
- }
103
-
104
- export interface ClientMemoryMetadata {
105
- content: string;
106
- embedding: number[];
107
- timestamp: number;
108
- }
109
-
110
- /**
111
- * Rich metadata structure aligned with on-chain MemoryMetadata
112
- * This is extracted during AI analysis and used for:
113
- * 1. Metadata-based vector embeddings
114
- * 2. On-chain registration
115
- * 3. Display in UI
116
- */
117
- export interface RichMetadataAnalysis {
118
- category: string; // e.g., "work"
119
- topic: string; // e.g., "Q4 project deadline meeting"
120
- importance: number; // 1-10 scale
121
- summary: string; // e.g., "Team discussion about Q4 deadlines..."
122
- }
123
-
124
- /**
125
- * Client-side memory manager for React dApps
126
- */
127
- export class ClientMemoryManager {
128
- private readonly config: Omit<Required<ClientMemoryManagerConfig>, 'hnswService'> & { enableLocalIndexing: boolean; enableEncryption: boolean; hnswService?: IHnswService };
129
- private readonly defaultCategories = [
130
- 'personal', 'work', 'education', 'health', 'finance',
131
- 'travel', 'family', 'hobbies', 'goals', 'ideas'
132
- ];
133
-
134
- // Local indexing services (optional)
135
- private embeddingService?: EmbeddingService;
136
- private geminiAIService?: GeminiAIService;
137
- private hnswService: IHnswService | null = null;
138
- private hnswServicePromise: Promise<IHnswService> | null = null;
139
-
140
- constructor(config: ClientMemoryManagerConfig) {
141
- this.config = {
142
- ...config,
143
- sealServerObjectIds: config.sealServerObjectIds || [
144
- '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75',
145
- '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8'
146
- ],
147
- walrusNetwork: config.walrusNetwork || 'testnet',
148
- categories: config.categories || this.defaultCategories,
149
- enableLocalIndexing: config.enableLocalIndexing !== false, // Default: true
150
- enableEncryption: config.enableEncryption !== false // Default: true (SEAL encryption)
151
- };
152
-
153
- // Initialize AI services if Gemini API key is provided
154
- if (this.config.geminiApiKey) {
155
- this.embeddingService = new EmbeddingService({
156
- apiKey: this.config.geminiApiKey,
157
- model: 'text-embedding-004',
158
- dimensions: 3072
159
- });
160
-
161
- this.geminiAIService = new GeminiAIService({
162
- apiKey: this.config.geminiApiKey,
163
- model: process.env.AI_CHAT_MODEL || 'google/gemini-2.5-flash',
164
- temperature: 0.1
165
- });
166
-
167
- console.log('✅ AI services initialized (Embedding + Metadata Extraction)');
168
- }
169
-
170
- // Initialize local indexing service if enabled
171
- if (this.config.enableLocalIndexing) {
172
- // Use pre-initialized HNSW service if provided (shared singleton pattern)
173
- if (config.hnswService) {
174
- this.hnswService = config.hnswService;
175
- console.log('✅ ClientMemoryManager using shared HNSW service instance');
176
- } else {
177
- // Create own HNSW service (async via factory)
178
- const envType = isBrowser() ? 'browser (hnswlib-wasm)' : isNode() ? 'Node.js (hnswlib-node)' : 'unknown';
179
- console.log(`✅ ClientMemoryManager initializing local indexing (${envType})`);
180
- this.hnswServicePromise = this.initializeHnswService();
181
- }
182
- }
183
- }
184
-
185
- /**
186
- * Initialize HNSW service using factory (async)
187
- * Uses dynamic import to avoid bundling Node.js modules in browser
188
- */
189
- private async initializeHnswService(): Promise<IHnswService> {
190
- try {
191
- // Dynamic import to avoid webpack bundling Node.js modules at build time
192
- const { createHnswService } = await import('../vector/createHnswService');
193
-
194
- const service = await createHnswService({
195
- indexConfig: {
196
- dimension: 3072,
197
- maxElements: 10000,
198
- m: 16,
199
- efConstruction: 200
200
- },
201
- batchConfig: {
202
- maxBatchSize: 50,
203
- batchDelayMs: 5000
204
- }
205
- });
206
-
207
- this.hnswService = service;
208
- console.log('✅ ClientMemoryManager HNSW service initialized');
209
- return service;
210
- } catch (error) {
211
- console.error('❌ ClientMemoryManager failed to initialize HNSW service:', error);
212
- throw error;
213
- }
214
- }
215
-
216
- /**
217
- * Get HNSW service (waits for initialization if needed)
218
- */
219
- private async getHnswService(): Promise<IHnswService | null> {
220
- if (this.hnswService) {
221
- return this.hnswService;
222
- }
223
- if (this.hnswServicePromise) {
224
- try {
225
- return await this.hnswServicePromise;
226
- } catch (error) {
227
- console.warn('HNSW service initialization failed:', error);
228
- return null;
229
- }
230
- }
231
- return null;
232
- }
233
-
234
- // In-memory counter for Node.js environment
235
- private vectorIdCounters = new Map<string, number>();
236
-
237
- /**
238
- * Get next sequential vector ID for a user (fits in 32-bit unsigned int)
239
- * Uses IndexedDB in browser, in-memory counter in Node.js
240
- */
241
- private async getNextVectorId(userAddress: string): Promise<number> {
242
- // Check if IndexedDB is available (browser environment)
243
- if (typeof indexedDB !== 'undefined') {
244
- try {
245
- // Use separate database for counters to avoid version conflicts
246
- const db = await new Promise<IDBDatabase>((resolve, reject) => {
247
- const request = indexedDB.open('PDW_VectorCounters', 1);
248
- request.onsuccess = () => resolve(request.result);
249
- request.onerror = () => reject(request.error);
250
- request.onupgradeneeded = (event) => {
251
- const db = (event.target as IDBOpenDBRequest).result;
252
- if (!db.objectStoreNames.contains('counters')) {
253
- db.createObjectStore('counters', { keyPath: 'userAddress' });
254
- }
255
- };
256
- });
257
-
258
- const transaction = db.transaction(['counters'], 'readwrite');
259
- const store = transaction.objectStore('counters');
260
-
261
- // Get current counter
262
- const getRequest = store.get(userAddress);
263
- const currentData = await new Promise<{ userAddress: string; counter: number } | undefined>((resolve) => {
264
- getRequest.onsuccess = () => resolve(getRequest.result);
265
- getRequest.onerror = () => resolve(undefined);
266
- });
267
-
268
- const nextId = (currentData?.counter || 0) + 1;
269
-
270
- // Update counter
271
- store.put({ userAddress, counter: nextId });
272
-
273
- await new Promise<void>((resolve, reject) => {
274
- transaction.oncomplete = () => resolve();
275
- transaction.onerror = () => reject(transaction.error);
276
- });
277
-
278
- db.close();
279
- console.log(`✅ Sequential vector ID generated: ${nextId} (from IndexedDB)`);
280
- return nextId;
281
- } catch (error) {
282
- console.warn('⚠️ IndexedDB failed, using in-memory counter:', error);
283
- }
284
- }
285
-
286
- // Node.js or IndexedDB failure: use in-memory counter
287
- const current = this.vectorIdCounters.get(userAddress) || 0;
288
- const nextId = current + 1;
289
- this.vectorIdCounters.set(userAddress, nextId);
290
- console.log(`✅ Sequential vector ID generated: ${nextId} (in-memory)`);
291
- return nextId;
292
- }
293
-
294
- /**
295
- * Create a new memory (3 signatures: Walrus register, certify, on-chain)
296
- */
297
- async createMemory(options: CreateMemoryOptions): Promise<string> {
298
- const { content, category, account, signAndExecute, client, onProgress } = options;
299
-
300
- console.log('🚀 Starting memory creation...');
301
- onProgress?.('Starting memory creation...');
302
-
303
- try {
304
- // Step 1: Analyze content (category + importance)
305
- console.log('🏷️ Step 1: Analyzing content...');
306
- onProgress?.('Analyzing content with AI...');
307
- const analysis = await this.analyzeContent(content, category);
308
- console.log('✅ Analysis:', analysis);
309
-
310
- // Step 2: Build metadata text and generate embedding
311
- console.log('🔮 Step 2: Building metadata and generating embedding...');
312
- onProgress?.('Generating metadata embedding...');
313
-
314
- // Build embeddable metadata text
315
- const metadataText = this.buildMetadataText(analysis);
316
- console.log('📝 Metadata text for embedding:');
317
- console.log(metadataText);
318
- console.log('📊 Metadata fields:', {
319
- category: analysis.category,
320
- topic: analysis.topic,
321
- importance: analysis.importance,
322
- summaryLength: analysis.summary.length
323
- });
324
-
325
- // Generate embedding from METADATA (not content)
326
- const embedding = await this.generateEmbedding(metadataText);
327
- console.log('✅ Metadata embedding generated:', embedding.length, 'dimensions');
328
- console.log(' Source: metadata text (not full content)');
329
- console.log(' Privacy: Only metadata semantics embedded, content stays encrypted');
330
-
331
- // Step 3: Prepare combined data (content + embedding)
332
- console.log('📦 Step 3: Preparing data...');
333
- const memoryData: ClientMemoryMetadata = {
334
- content,
335
- embedding,
336
- timestamp: Date.now(),
337
- };
338
- const dataBytes = new TextEncoder().encode(JSON.stringify(memoryData));
339
- console.log('✅ Data prepared:', dataBytes.length, 'bytes');
340
-
341
- // Step 4: Conditionally encrypt with SEAL
342
- let uploadData: Uint8Array;
343
- const isEncrypted = this.config.enableEncryption;
344
-
345
- if (isEncrypted) {
346
- console.log('🔒 Step 4: Encrypting with SEAL...');
347
- onProgress?.('Encrypting with SEAL...');
348
- uploadData = await this.encryptWithSEAL(dataBytes, account.address, client);
349
- console.log('✅ Encrypted:', uploadData.length, 'bytes');
350
- } else {
351
- console.log('📝 Step 4: Skipping encryption (enableEncryption=false)');
352
- onProgress?.('Preparing data (no encryption)...');
353
- uploadData = dataBytes;
354
- console.log('✅ Data ready (unencrypted):', uploadData.length, 'bytes');
355
- }
356
-
357
- // Step 5: Upload to Walrus (2 signatures)
358
- console.log('🐳 Step 5: Uploading to Walrus...');
359
- onProgress?.('Uploading to Walrus (2 signatures)...');
360
- const blobId = await this.uploadToWalrus(uploadData, account, signAndExecute, client);
361
- console.log('✅ Uploaded to Walrus:', blobId);
362
-
363
- // Generate sequential vector ID (fits in 32-bit for WASM)
364
- const vectorId = await this.getNextVectorId(account.address);
365
- console.log('🔢 Generated sequential vector ID:', vectorId);
366
-
367
- // Step 6: Register on-chain with rich metadata (1 signature)
368
- console.log('⛓️ Step 6: Registering on-chain with rich metadata...');
369
- onProgress?.('Registering on-chain (1 signature)...');
370
- await this.registerOnChain({
371
- blobId,
372
- category: analysis.category,
373
- topic: analysis.topic, // ✅ Pass AI-extracted topic to blockchain
374
- importance: analysis.importance,
375
- contentLength: content.length,
376
- vectorId, // ✅ Use sequential ID
377
- account,
378
- signAndExecute,
379
- client
380
- });
381
- console.log('✅ Memory registered on-chain with metadata:', {
382
- category: analysis.category,
383
- topic: analysis.topic,
384
- importance: analysis.importance,
385
- vectorId
386
- });
387
- onProgress?.('Memory created successfully!');
388
-
389
- // Step 7: Index locally for search (if enabled)
390
- console.log('\n📊 === Step 7: Local Vector Indexing ===');
391
- console.log(' enableLocalIndexing:', this.config.enableLocalIndexing);
392
- console.log(' account address:', account.address);
393
- console.log(' embedding length:', embedding?.length || 0);
394
-
395
- if (this.config.enableLocalIndexing && account.address) {
396
- try {
397
- // Get HNSW service (wait for async initialization)
398
- const hnswService = await this.getHnswService();
399
- console.log(' hnswService available:', !!hnswService);
400
-
401
- if (hnswService) {
402
- console.log('✅ Local indexing conditions met - proceeding...');
403
- onProgress?.('Indexing for local search...');
404
- console.log(' Using vector ID:', vectorId);
405
-
406
- // Prepare rich metadata (aligned with on-chain structure)
407
- // Option A+: Content storage is controlled by isEncrypted flag.
408
- // When encryption is OFF, content is stored in local index for fast retrieval.
409
- // When encryption is ON, only metadata is stored (security).
410
- const metadata = {
411
- blobId,
412
- category: analysis.category,
413
- topic: analysis.topic, // ✅ Rich metadata field
414
- importance: analysis.importance,
415
- summary: analysis.summary, // ✅ Rich metadata field
416
- createdTimestamp: Date.now(),
417
- contentType: 'text/plain',
418
- contentSize: content.length,
419
- source: 'client_memory_manager',
420
- embeddingType: 'metadata', // ✅ Mark as metadata-based embedding
421
- isEncrypted, // ✅ Dynamic based on config.enableEncryption
422
- // Option A+: Store content in index when NOT encrypted (for fast local search)
423
- ...(isEncrypted ? {} : { content })
424
- };
425
- console.log(' Rich metadata prepared:', metadata);
426
-
427
- // Add to local index using IHnswService interface
428
- console.log('📝 Adding vector to index...');
429
- await hnswService.addVector(
430
- account.address,
431
- vectorId,
432
- embedding, // ← Metadata embedding from Step 2 (not content embedding!)
433
- metadata
434
- );
435
- console.log('✅ Vector added to index');
436
- console.log(' Embedding represents: metadata semantics (category, topic, summary)');
437
- console.log(' NOT content semantics - privacy preserved!');
438
-
439
- // Flush to make searchable
440
- console.log('🔄 Flushing index to make memory immediately searchable...');
441
- await hnswService.flushBatch(account.address);
442
- console.log('✅ Index flushed - vectors now searchable');
443
-
444
- // Save index for persistence
445
- console.log('💾 Saving index for persistence...');
446
- await hnswService.saveIndex(account.address);
447
- console.log('✅ Index saved - will persist after refresh');
448
-
449
- console.log('🎉 Memory indexed, flushed, and persisted!');
450
- console.log(' Vector ID:', vectorId);
451
- console.log(' User:', account.address.substring(0, 10) + '...');
452
- console.log(' Embedding dimensions:', embedding.length);
453
- onProgress?.('Memory indexed and saved!');
454
- } else {
455
- console.warn('⚠️ HNSW service not available - skipping local indexing');
456
- }
457
- } catch (indexError: any) {
458
- // Non-fatal: memory is still created on-chain
459
- console.error('❌ Local indexing failed:', indexError);
460
- console.error(' Error details:', indexError.message);
461
- console.error(' Stack:', indexError.stack);
462
- console.warn('⚠️ Memory still created on-chain, but not indexed locally');
463
- }
464
- } else {
465
- console.warn('⚠️ Local indexing skipped - conditions not met:');
466
- if (!this.config.enableLocalIndexing) console.warn(' - Local indexing disabled');
467
- if (!account.address) console.warn(' - No account address');
468
- if (!embedding || embedding.length === 0) console.warn(' - No embedding generated');
469
- }
470
-
471
- console.log('🎉 Memory creation complete!');
472
- return blobId;
473
- } catch (error: any) {
474
- console.error('❌ Memory creation failed:', error);
475
- throw new Error(`Failed to create memory: ${error.message}`);
476
- }
477
- }
478
-
479
- /**
480
- * Retrieve and decrypt a memory
481
- */
482
- async retrieveMemory(options: RetrieveMemoryOptions): Promise<ClientMemoryMetadata> {
483
- const { blobId, account, signPersonalMessage, client, onProgress } = options;
484
-
485
- console.log('🔍 Starting memory retrieval...');
486
- onProgress?.('Starting retrieval...');
487
-
488
- try {
489
- // Step 1: Fetch from Walrus
490
- console.log('🐳 Step 1: Fetching from Walrus...');
491
- onProgress?.('Fetching from Walrus...');
492
- const encryptedData = await this.fetchFromWalrus(blobId, client);
493
- console.log('✅ Retrieved:', encryptedData.length, 'bytes');
494
-
495
- // Step 2: Decrypt with SEAL
496
- console.log('🔓 Step 2: Decrypting with SEAL...');
497
- onProgress?.('Decrypting with SEAL...');
498
- const decryptedData = await this.decryptWithSEAL({
499
- encryptedData,
500
- account,
501
- signPersonalMessage,
502
- client
503
- });
504
- console.log('✅ Decrypted:', decryptedData.length, 'bytes');
505
-
506
- // Step 3: Parse JSON
507
- const decryptedString = new TextDecoder().decode(decryptedData);
508
- const parsed: ClientMemoryMetadata = JSON.parse(decryptedString);
509
-
510
- console.log('🎉 Memory retrieval complete!');
511
- onProgress?.('Memory retrieved successfully!');
512
-
513
- return parsed;
514
- } catch (error: any) {
515
- console.error('❌ Memory retrieval failed:', error);
516
- throw new Error(`Failed to retrieve memory: ${error.message}`);
517
- }
518
- }
519
-
520
- /**
521
- * Batch retrieve and decrypt multiple memories with a single signature
522
- * This is much more efficient than calling retrieveMemory multiple times
523
- */
524
- async batchRetrieveMemories(options: BatchRetrieveMemoriesOptions): Promise<BatchRetrieveResult[]> {
525
- const { blobIds, account, signPersonalMessage, client, onProgress } = options;
526
-
527
- console.log('🔍 Starting batch memory retrieval for', blobIds.length, 'memories...');
528
- onProgress?.('Initializing decryption session...', 0, blobIds.length);
529
-
530
- try {
531
- // Step 1: Create reusable decryption session (SINGLE SIGNATURE!)
532
- const session = await this.createDecryptionSession({
533
- account,
534
- signPersonalMessage,
535
- client
536
- });
537
- console.log('✅ Decryption session created - will decrypt all memories without additional signatures');
538
-
539
- const results: BatchRetrieveResult[] = [];
540
-
541
- // Step 2: Decrypt all memories using the same session
542
- for (let i = 0; i < blobIds.length; i++) {
543
- const blobId = blobIds[i];
544
- console.log(`🔓 Decrypting memory ${i + 1}/${blobIds.length}: ${blobId}`);
545
- onProgress?.(`Decrypting memory ${i + 1}/${blobIds.length}...`, i + 1, blobIds.length);
546
-
547
- try {
548
- // Fetch from Walrus
549
- const encryptedData = await this.fetchFromWalrus(blobId, client);
550
-
551
- // Decrypt using shared session (NO SIGNING!)
552
- const decryptedData = await session.sealClient.decrypt({
553
- data: encryptedData,
554
- sessionKey: session.sessionKey,
555
- txBytes: session.txBytes,
556
- });
557
-
558
- // Parse JSON
559
- const decryptedString = new TextDecoder().decode(decryptedData);
560
- const parsed: ClientMemoryMetadata = JSON.parse(decryptedString);
561
-
562
- results.push({
563
- blobId,
564
- content: parsed.content
565
- });
566
-
567
- console.log(`✅ Memory ${i + 1} decrypted successfully`);
568
- } catch (error: any) {
569
- console.error(`❌ Failed to decrypt memory ${blobId}:`, error);
570
-
571
- // Handle old format (binary embedding data)
572
- if (error.message?.includes('not valid JSON') || error.message?.includes('Unexpected token')) {
573
- results.push({
574
- blobId,
575
- content: '[Old format - cannot display content]'
576
- });
577
- } else {
578
- results.push({
579
- blobId,
580
- error: error.message || 'Decryption failed'
581
- });
582
- }
583
- }
584
- }
585
-
586
- console.log('🎉 Batch retrieval complete!');
587
- onProgress?.('All memories decrypted!', blobIds.length, blobIds.length);
588
-
589
- return results;
590
- } catch (error: any) {
591
- console.error('❌ Batch retrieval failed:', error);
592
- throw new Error(`Failed to batch retrieve memories: ${error.message}`);
593
- }
594
- }
595
-
596
- // ==================== PRIVATE METHODS ====================
597
-
598
- private async analyzeContent(text: string, categoryOverride?: string): Promise<RichMetadataAnalysis> {
599
- // Use client-side Gemini AI for rich metadata extraction
600
- try {
601
- if (this.geminiAIService) {
602
- console.log('🤖 Using Gemini AI for metadata extraction...');
603
- const metadata = await this.geminiAIService.extractRichMetadata(text, categoryOverride);
604
- console.log('✅ AI metadata extracted:', metadata);
605
- return metadata;
606
- } else {
607
- console.warn('⚠️ Gemini AI service not initialized, using fallback extraction');
608
- return this.getFallbackAnalysis(text, categoryOverride);
609
- }
610
- } catch (error) {
611
- console.warn('⚠️ AI analysis failed, using fallback:', error);
612
- return this.getFallbackAnalysis(text, categoryOverride);
613
- }
614
- }
615
-
616
- private getFallbackAnalysis(text: string, categoryOverride?: string): RichMetadataAnalysis {
617
- return {
618
- category: categoryOverride || 'personal',
619
- topic: this.extractTopicFromContent(text),
620
- importance: 5,
621
- summary: text.substring(0, 200) + (text.length > 200 ? '...' : '')
622
- };
623
- }
624
-
625
- /**
626
- * Extract topic from content as fallback (when AI fails)
627
- * Tries to get first sentence, or first 50 characters
628
- *
629
- * @param text - Content text to extract topic from
630
- * @returns Topic string (max 100 characters)
631
- */
632
- private extractTopicFromContent(text: string): string {
633
- // Try to get first sentence
634
- const firstSentence = text.match(/^[^.!?]+[.!?]/);
635
- if (firstSentence) {
636
- const topic = firstSentence[0].trim();
637
- return topic.length > 100 ? topic.substring(0, 97) + '...' : topic;
638
- }
639
-
640
- // Fallback: first 50 characters
641
- return text.substring(0, 50) + (text.length > 50 ? '...' : '');
642
- }
643
-
644
- /**
645
- * Build embeddable metadata text from rich metadata
646
- * Format optimized for semantic embedding and alignment with on-chain metadata
647
- *
648
- * @param metadata - Rich metadata extracted from AI analysis
649
- * @returns Formatted text string ready for embedding
650
- *
651
- * @example
652
- * ```typescript
653
- * const metadataText = buildMetadataText({
654
- * category: 'work',
655
- * topic: 'Q4 project deadline meeting',
656
- * importance: 7,
657
- * summary: 'Team discussion about Q4 deadlines'
658
- * });
659
- * // Returns:
660
- * // "category: work
661
- * // topic: Q4 project deadline meeting
662
- * // importance: 7
663
- * // summary: Team discussion about Q4 deadlines"
664
- * ```
665
- */
666
- private buildMetadataText(metadata: RichMetadataAnalysis): string {
667
- const parts = [
668
- `category: ${metadata.category}`,
669
- `topic: ${metadata.topic}`,
670
- `importance: ${metadata.importance}`
671
- ];
672
-
673
- if (metadata.summary && metadata.summary.trim().length > 0) {
674
- parts.push(`summary: ${metadata.summary}`);
675
- }
676
-
677
- return parts.join('\n');
678
- }
679
-
680
- private async generateEmbedding(text: string): Promise<number[]> {
681
- if (!this.embeddingService) {
682
- throw new Error(
683
- 'EmbeddingService not configured. Please provide an API key for embedding generation ' +
684
- '(e.g., GEMINI_API_KEY or OPENROUTER_API_KEY in environment variables).'
685
- );
686
- }
687
-
688
- try {
689
- const result = await this.embeddingService.embedText({
690
- text,
691
- type: 'content'
692
- });
693
- return result.vector;
694
- } catch (error) {
695
- throw new Error(`Failed to generate embedding: ${error instanceof Error ? error.message : 'Unknown error'}`);
696
- }
697
- }
698
-
699
- private async encryptWithSEAL(
700
- data: Uint8Array,
701
- ownerAddress: string,
702
- client: SuiClient
703
- ): Promise<Uint8Array> {
704
- const sealClient = new SealClient({
705
- suiClient: client as any,
706
- serverConfigs: this.config.sealServerObjectIds.map((id) => ({
707
- objectId: id,
708
- weight: 1,
709
- })),
710
- verifyKeyServers: false,
711
- });
712
-
713
- const { encryptedObject: encryptedBytes } = await sealClient.encrypt({
714
- threshold: 1,
715
- packageId: this.config.packageId,
716
- id: ownerAddress, // Use owner's address as ID for simple access control
717
- data,
718
- });
719
-
720
- return encryptedBytes;
721
- }
722
-
723
- private async uploadToWalrus(
724
- data: Uint8Array,
725
- account: { address: string },
726
- signAndExecute: CreateMemoryOptions['signAndExecute'],
727
- client: SuiClient
728
- ): Promise<string> {
729
- const extendedClient = (client as any).$extend(
730
- WalrusClient.experimental_asClientExtension({
731
- network: this.config.walrusNetwork,
732
- uploadRelay: {
733
- host: `https://upload-relay.${this.config.walrusNetwork}.walrus.space`,
734
- sendTip: { max: 1_000 },
735
- timeout: 60_000,
736
- },
737
- storageNodeClientOptions: {
738
- timeout: 60_000,
739
- },
740
- })
741
- );
742
-
743
- const walrusClient = extendedClient.walrus as any;
744
- const flow = walrusClient.writeBlobFlow({ blob: data });
745
-
746
- // Encode
747
- await flow.encode();
748
-
749
- // Register (signature 1)
750
- const registerTx = flow.register({
751
- epochs: 5,
752
- deletable: true,
753
- owner: account.address,
754
- });
755
- registerTx.setSender(account.address);
756
-
757
- const registerDigest = await new Promise<string>((resolve, reject) => {
758
- signAndExecute(
759
- { transaction: registerTx },
760
- {
761
- onSuccess: (result) => resolve(result.digest),
762
- onError: (error) => reject(error),
763
- }
764
- );
765
- });
766
-
767
- // Upload
768
- await flow.upload({ digest: registerDigest });
769
-
770
- // Certify (signature 2)
771
- const certifyTx = flow.certify();
772
- certifyTx.setSender(account.address);
773
-
774
- await new Promise<void>((resolve, reject) => {
775
- signAndExecute(
776
- { transaction: certifyTx },
777
- {
778
- onSuccess: () => resolve(),
779
- onError: (error) => reject(error),
780
- }
781
- );
782
- });
783
-
784
- const blob = await flow.getBlob();
785
- return blob.blobId;
786
- }
787
-
788
- private async registerOnChain(params: {
789
- blobId: string;
790
- category: string;
791
- topic: string; // ✅ NEW: Real topic from AI analysis
792
- importance: number;
793
- contentLength: number;
794
- vectorId: number; // ✅ Sequential ID (fits in 32-bit unsigned int)
795
- account: { address: string };
796
- signAndExecute: CreateMemoryOptions['signAndExecute'];
797
- client: SuiClient;
798
- }): Promise<void> {
799
- const { blobId, category, topic, importance, contentLength, vectorId, account, signAndExecute, client } = params;
800
-
801
- const tx = new Transaction();
802
- const packageId = this.config.packageId.replace(/^0x/, '');
803
-
804
- console.log('📝 Registering on-chain with rich metadata:', {
805
- category,
806
- topic,
807
- importance,
808
- vectorId,
809
- blobId: blobId.substring(0, 20) + '...'
810
- });
811
-
812
- tx.moveCall({
813
- target: `${packageId}::memory::create_memory_record`,
814
- arguments: [
815
- tx.pure.string(category),
816
- tx.pure.u64(vectorId),
817
- tx.pure.string(blobId),
818
- tx.pure.string('text/plain'),
819
- tx.pure.u64(contentLength),
820
- tx.pure.string(blobId), // ✅ FIX: content_hash = blobId (content-addressed)
821
- tx.pure.string(topic), // ✅ FIX: Real topic from AI (not hardcoded "memory")
822
- tx.pure.u8(importance),
823
- tx.pure.string(blobId), // embedding_blob_id (same as content for now)
824
- ],
825
- });
826
-
827
- return new Promise((resolve, reject) => {
828
- signAndExecute(
829
- { transaction: tx },
830
- {
831
- onSuccess: (result) => {
832
- console.log('✅ Transaction successful:', result.digest);
833
- console.log('✅ On-chain Memory created with rich metadata:', {
834
- category,
835
- topic,
836
- importance
837
- });
838
- resolve();
839
- },
840
- onError: (error) => {
841
- console.error('❌ Transaction failed:', error);
842
- reject(error);
843
- },
844
- }
845
- );
846
- });
847
- }
848
-
849
- private async fetchFromWalrus(blobId: string, client: SuiClient): Promise<Uint8Array> {
850
- // Use Walrus SDK for reading blobs (consistent with uploadToWalrus)
851
- const extendedClient = (client as any).$extend(
852
- WalrusClient.experimental_asClientExtension({
853
- network: this.config.walrusNetwork,
854
- })
855
- );
856
-
857
- const walrusClient = extendedClient.walrus as any;
858
- const blob = await walrusClient.readBlob({ blobId });
859
- return blob;
860
- }
861
-
862
- /**
863
- * Create a reusable decryption session (requires one signature)
864
- * This session can be used to decrypt multiple memories without additional signatures
865
- */
866
- private async createDecryptionSession(params: {
867
- account: { address: string };
868
- signPersonalMessage: (params: { message: Uint8Array }) => Promise<{ signature: string }>;
869
- client: SuiClient;
870
- }): Promise<DecryptionSession> {
871
- const { account, signPersonalMessage, client } = params;
872
-
873
- console.log('🔑 Creating decryption session...');
874
-
875
- // Create SEAL client (reusable)
876
- const sealClient = new SealClient({
877
- suiClient: client as any,
878
- serverConfigs: this.config.sealServerObjectIds.map((id) => ({
879
- objectId: id,
880
- weight: 1,
881
- })),
882
- verifyKeyServers: false,
883
- });
884
-
885
- // Create session key (reusable)
886
- const sessionKey = await SessionKey.create({
887
- address: account.address,
888
- packageId: this.config.packageId,
889
- ttlMin: 10,
890
- suiClient: client as any,
891
- });
892
-
893
- // Sign personal message ONCE
894
- const personalMessage = sessionKey.getPersonalMessage();
895
- const signatureResult = await signPersonalMessage({ message: personalMessage });
896
- await sessionKey.setPersonalMessageSignature(signatureResult.signature);
897
- console.log('✅ Personal message signed');
898
-
899
- // Build seal_approve transaction ONCE
900
- const tx = new Transaction();
901
- const addressHex = account.address.startsWith('0x')
902
- ? account.address.slice(2)
903
- : account.address;
904
- const idBytes = fromHex(addressHex);
905
-
906
- tx.moveCall({
907
- target: `${this.config.packageId}::seal_access_control::seal_approve`,
908
- arguments: [
909
- tx.pure.vector('u8', Array.from(idBytes)),
910
- tx.pure.address(account.address),
911
- tx.object(this.config.accessRegistryId),
912
- tx.object('0x6'),
913
- ],
914
- });
915
-
916
- const txBytes = await tx.build({ client, onlyTransactionKind: true });
917
- console.log('✅ Session created - can now decrypt multiple memories');
918
-
919
- return {
920
- sealClient,
921
- sessionKey,
922
- txBytes
923
- };
924
- }
925
-
926
- private async decryptWithSEAL(params: {
927
- encryptedData: Uint8Array;
928
- account: { address: string };
929
- signPersonalMessage: RetrieveMemoryOptions['signPersonalMessage'];
930
- client: SuiClient;
931
- }): Promise<Uint8Array> {
932
- const { encryptedData, account, signPersonalMessage, client } = params;
933
-
934
- // Create SEAL client
935
- const sealClient = new SealClient({
936
- suiClient: client as any,
937
- serverConfigs: this.config.sealServerObjectIds.map((id) => ({
938
- objectId: id,
939
- weight: 1,
940
- })),
941
- verifyKeyServers: false,
942
- });
943
-
944
- // Create session key
945
- const sessionKey = await SessionKey.create({
946
- address: account.address,
947
- packageId: this.config.packageId,
948
- ttlMin: 10,
949
- suiClient: client as any,
950
- });
951
-
952
- // Sign personal message
953
- const personalMessage = sessionKey.getPersonalMessage();
954
- const signatureResult = await signPersonalMessage({ message: personalMessage });
955
- await sessionKey.setPersonalMessageSignature(signatureResult.signature);
956
-
957
- // Build seal_approve transaction
958
- const tx = new Transaction();
959
- const addressHex = account.address.startsWith('0x')
960
- ? account.address.slice(2)
961
- : account.address;
962
- const idBytes = fromHex(addressHex);
963
-
964
- tx.moveCall({
965
- target: `${this.config.packageId}::seal_access_control::seal_approve`,
966
- arguments: [
967
- tx.pure.vector('u8', Array.from(idBytes)),
968
- tx.pure.address(account.address),
969
- tx.object(this.config.accessRegistryId),
970
- tx.object('0x6'),
971
- ],
972
- });
973
-
974
- const txBytes = await tx.build({ client, onlyTransactionKind: true });
975
-
976
- // Decrypt
977
- const decryptedData = await sealClient.decrypt({
978
- data: encryptedData,
979
- sessionKey,
980
- txBytes,
981
- });
982
-
983
- return decryptedData;
984
- }
985
- }
986
-
987
- export default ClientMemoryManager;
1
+ /**
2
+ * ClientMemoryManager - Client-side Memory Operations for React dApps
3
+ *
4
+ * Provides a simplified API for creating and retrieving memories in React dApps
5
+ * using @mysten/dapp-kit. Handles the complete flow:
6
+ * - Memory creation: embedding → encryption → Walrus upload → on-chain registration
7
+ * - Memory retrieval: Walrus fetch → SEAL decryption → content extraction
8
+ *
9
+ * Usage:
10
+ * ```typescript
11
+ * const manager = new ClientMemoryManager({
12
+ * packageId: '0x...',
13
+ * accessRegistryId: '0x...',
14
+ * walrusAggregator: 'https://...',
15
+ * geminiApiKey: 'your-key'
16
+ * });
17
+ *
18
+ * // Create memory
19
+ * const blobId = await manager.createMemory({
20
+ * content: 'My memory',
21
+ * account,
22
+ * signAndExecute,
23
+ * client,
24
+ * onProgress: (status) => console.log(status)
25
+ * });
26
+ *
27
+ * // Retrieve memory
28
+ * const content = await manager.retrieveMemory({
29
+ * blobId: '...',
30
+ * account,
31
+ * signPersonalMessage,
32
+ * client
33
+ * });
34
+ * ```
35
+ */
36
+
37
+ import { SealClient, SessionKey } from '@mysten/seal';
38
+ import { WalrusClient } from '@mysten/walrus';
39
+ import { Transaction } from '@mysten/sui/transactions';
40
+ import { fromHex } from '@mysten/sui/utils';
41
+ import type { SuiClient } from '@mysten/sui/client';
42
+ // Import environment detection from browser-safe file (no Node.js deps)
43
+ import { isBrowser, isNode } from '../vector/IHnswService';
44
+ import type { IHnswService } from '../vector/IHnswService';
45
+ import { EmbeddingService } from '../services/EmbeddingService';
46
+ import { GeminiAIService } from '../services/GeminiAIService';
47
+
48
+ export interface ClientMemoryManagerConfig {
49
+ packageId: string;
50
+ accessRegistryId: string;
51
+ walrusAggregator: string;
52
+ geminiApiKey: string;
53
+ sealServerObjectIds?: string[];
54
+ walrusNetwork?: 'testnet' | 'mainnet';
55
+ categories?: string[];
56
+ /** Enable local browser indexing for vector search (default: true) */
57
+ enableLocalIndexing?: boolean;
58
+ /** Pre-initialized HNSW service instance (shared singleton) */
59
+ hnswService?: IHnswService;
60
+ /** Enable SEAL encryption for memory content (default: true) */
61
+ enableEncryption?: boolean;
62
+ }
63
+
64
+ export interface CreateMemoryOptions {
65
+ content: string;
66
+ category?: string;
67
+ account: { address: string };
68
+ signAndExecute: (params: { transaction: Transaction }, callbacks: {
69
+ onSuccess: (result: any) => void;
70
+ onError: (error: Error) => void;
71
+ }) => void;
72
+ client: SuiClient;
73
+ onProgress?: (status: string) => void;
74
+ }
75
+
76
+ export interface RetrieveMemoryOptions {
77
+ blobId: string;
78
+ account: { address: string };
79
+ signPersonalMessage: (params: { message: Uint8Array }) => Promise<{ signature: string }>;
80
+ client: SuiClient;
81
+ onProgress?: (status: string) => void;
82
+ }
83
+
84
+ export interface BatchRetrieveMemoriesOptions {
85
+ blobIds: string[];
86
+ account: { address: string };
87
+ signPersonalMessage: (params: { message: Uint8Array }) => Promise<{ signature: string }>;
88
+ client: SuiClient;
89
+ onProgress?: (status: string, current: number, total: number) => void;
90
+ }
91
+
92
+ export interface BatchRetrieveResult {
93
+ blobId: string;
94
+ content?: string;
95
+ error?: string;
96
+ }
97
+
98
+ interface DecryptionSession {
99
+ sealClient: SealClient;
100
+ sessionKey: SessionKey;
101
+ txBytes: Uint8Array;
102
+ }
103
+
104
+ export interface ClientMemoryMetadata {
105
+ content: string;
106
+ embedding: number[];
107
+ timestamp: number;
108
+ }
109
+
110
+ /**
111
+ * Rich metadata structure aligned with on-chain MemoryMetadata
112
+ * This is extracted during AI analysis and used for:
113
+ * 1. Metadata-based vector embeddings
114
+ * 2. On-chain registration
115
+ * 3. Display in UI
116
+ */
117
+ export interface RichMetadataAnalysis {
118
+ category: string; // e.g., "work"
119
+ topic: string; // e.g., "Q4 project deadline meeting"
120
+ importance: number; // 1-10 scale
121
+ summary: string; // e.g., "Team discussion about Q4 deadlines..."
122
+ }
123
+
124
+ /**
125
+ * Client-side memory manager for React dApps
126
+ */
127
+ export class ClientMemoryManager {
128
+ private readonly config: Omit<Required<ClientMemoryManagerConfig>, 'hnswService'> & { enableLocalIndexing: boolean; enableEncryption: boolean; hnswService?: IHnswService };
129
+ private readonly defaultCategories = [
130
+ 'personal', 'work', 'education', 'health', 'finance',
131
+ 'travel', 'family', 'hobbies', 'goals', 'ideas'
132
+ ];
133
+
134
+ // Local indexing services (optional)
135
+ private embeddingService?: EmbeddingService;
136
+ private geminiAIService?: GeminiAIService;
137
+ private hnswService: IHnswService | null = null;
138
+ private hnswServicePromise: Promise<IHnswService> | null = null;
139
+
140
+ constructor(config: ClientMemoryManagerConfig) {
141
+ this.config = {
142
+ ...config,
143
+ sealServerObjectIds: config.sealServerObjectIds || [
144
+ '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75',
145
+ '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8'
146
+ ],
147
+ walrusNetwork: config.walrusNetwork || 'testnet',
148
+ categories: config.categories || this.defaultCategories,
149
+ enableLocalIndexing: config.enableLocalIndexing !== false, // Default: true
150
+ enableEncryption: config.enableEncryption !== false // Default: true (SEAL encryption)
151
+ };
152
+
153
+ // Initialize AI services if Gemini API key is provided
154
+ if (this.config.geminiApiKey) {
155
+ this.embeddingService = new EmbeddingService({
156
+ apiKey: this.config.geminiApiKey,
157
+ model: 'text-embedding-004',
158
+ dimensions: 3072
159
+ });
160
+
161
+ this.geminiAIService = new GeminiAIService({
162
+ apiKey: this.config.geminiApiKey,
163
+ model: process.env.AI_CHAT_MODEL || 'google/gemini-2.5-flash',
164
+ temperature: 0.1
165
+ });
166
+
167
+ console.log('✅ AI services initialized (Embedding + Metadata Extraction)');
168
+ }
169
+
170
+ // Initialize local indexing service if enabled
171
+ if (this.config.enableLocalIndexing) {
172
+ // Use pre-initialized HNSW service if provided (shared singleton pattern)
173
+ if (config.hnswService) {
174
+ this.hnswService = config.hnswService;
175
+ console.log('✅ ClientMemoryManager using shared HNSW service instance');
176
+ } else {
177
+ // Create own HNSW service (async via factory)
178
+ const envType = isBrowser() ? 'browser (hnswlib-wasm)' : isNode() ? 'Node.js (hnswlib-node)' : 'unknown';
179
+ console.log(`✅ ClientMemoryManager initializing local indexing (${envType})`);
180
+ this.hnswServicePromise = this.initializeHnswService();
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Initialize HNSW service using factory (async)
187
+ * Uses dynamic import to avoid bundling Node.js modules in browser
188
+ */
189
+ private async initializeHnswService(): Promise<IHnswService> {
190
+ try {
191
+ // Dynamic import to avoid webpack bundling Node.js modules at build time
192
+ const { createHnswService } = await import('../vector/createHnswService');
193
+
194
+ const service = await createHnswService({
195
+ indexConfig: {
196
+ dimension: 3072,
197
+ maxElements: 10000,
198
+ m: 16,
199
+ efConstruction: 200
200
+ },
201
+ batchConfig: {
202
+ maxBatchSize: 50,
203
+ batchDelayMs: 5000
204
+ }
205
+ });
206
+
207
+ this.hnswService = service;
208
+ console.log('✅ ClientMemoryManager HNSW service initialized');
209
+ return service;
210
+ } catch (error) {
211
+ console.error('❌ ClientMemoryManager failed to initialize HNSW service:', error);
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get HNSW service (waits for initialization if needed)
218
+ */
219
+ private async getHnswService(): Promise<IHnswService | null> {
220
+ if (this.hnswService) {
221
+ return this.hnswService;
222
+ }
223
+ if (this.hnswServicePromise) {
224
+ try {
225
+ return await this.hnswServicePromise;
226
+ } catch (error) {
227
+ console.warn('HNSW service initialization failed:', error);
228
+ return null;
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+
234
+ // In-memory counter for Node.js environment
235
+ private vectorIdCounters = new Map<string, number>();
236
+
237
+ /**
238
+ * Get next sequential vector ID for a user (fits in 32-bit unsigned int)
239
+ * Uses IndexedDB in browser, in-memory counter in Node.js
240
+ */
241
+ private async getNextVectorId(userAddress: string): Promise<number> {
242
+ // Check if IndexedDB is available (browser environment)
243
+ if (typeof indexedDB !== 'undefined') {
244
+ try {
245
+ // Use separate database for counters to avoid version conflicts
246
+ const db = await new Promise<IDBDatabase>((resolve, reject) => {
247
+ const request = indexedDB.open('PDW_VectorCounters', 1);
248
+ request.onsuccess = () => resolve(request.result);
249
+ request.onerror = () => reject(request.error);
250
+ request.onupgradeneeded = (event) => {
251
+ const db = (event.target as IDBOpenDBRequest).result;
252
+ if (!db.objectStoreNames.contains('counters')) {
253
+ db.createObjectStore('counters', { keyPath: 'userAddress' });
254
+ }
255
+ };
256
+ });
257
+
258
+ const transaction = db.transaction(['counters'], 'readwrite');
259
+ const store = transaction.objectStore('counters');
260
+
261
+ // Get current counter
262
+ const getRequest = store.get(userAddress);
263
+ const currentData = await new Promise<{ userAddress: string; counter: number } | undefined>((resolve) => {
264
+ getRequest.onsuccess = () => resolve(getRequest.result);
265
+ getRequest.onerror = () => resolve(undefined);
266
+ });
267
+
268
+ const nextId = (currentData?.counter || 0) + 1;
269
+
270
+ // Update counter
271
+ store.put({ userAddress, counter: nextId });
272
+
273
+ await new Promise<void>((resolve, reject) => {
274
+ transaction.oncomplete = () => resolve();
275
+ transaction.onerror = () => reject(transaction.error);
276
+ });
277
+
278
+ db.close();
279
+ console.log(`✅ Sequential vector ID generated: ${nextId} (from IndexedDB)`);
280
+ return nextId;
281
+ } catch (error) {
282
+ console.warn('⚠️ IndexedDB failed, using in-memory counter:', error);
283
+ }
284
+ }
285
+
286
+ // Node.js or IndexedDB failure: use in-memory counter
287
+ const current = this.vectorIdCounters.get(userAddress) || 0;
288
+ const nextId = current + 1;
289
+ this.vectorIdCounters.set(userAddress, nextId);
290
+ console.log(`✅ Sequential vector ID generated: ${nextId} (in-memory)`);
291
+ return nextId;
292
+ }
293
+
294
+ /**
295
+ * Create a new memory (3 signatures: Walrus register, certify, on-chain)
296
+ */
297
+ async createMemory(options: CreateMemoryOptions): Promise<string> {
298
+ const { content, category, account, signAndExecute, client, onProgress } = options;
299
+
300
+ console.log('🚀 Starting memory creation...');
301
+ onProgress?.('Starting memory creation...');
302
+
303
+ try {
304
+ // Step 1: Analyze content (category + importance)
305
+ console.log('🏷️ Step 1: Analyzing content...');
306
+ onProgress?.('Analyzing content with AI...');
307
+ const analysis = await this.analyzeContent(content, category);
308
+ console.log('✅ Analysis:', analysis);
309
+
310
+ // Step 2: Build metadata text and generate embedding
311
+ console.log('🔮 Step 2: Building metadata and generating embedding...');
312
+ onProgress?.('Generating metadata embedding...');
313
+
314
+ // Build embeddable metadata text
315
+ const metadataText = this.buildMetadataText(analysis);
316
+ console.log('📝 Metadata text for embedding:');
317
+ console.log(metadataText);
318
+ console.log('📊 Metadata fields:', {
319
+ category: analysis.category,
320
+ topic: analysis.topic,
321
+ importance: analysis.importance,
322
+ summaryLength: analysis.summary.length
323
+ });
324
+
325
+ // Generate embedding from METADATA (not content)
326
+ const embedding = await this.generateEmbedding(metadataText);
327
+ console.log('✅ Metadata embedding generated:', embedding.length, 'dimensions');
328
+ console.log(' Source: metadata text (not full content)');
329
+ console.log(' Privacy: Only metadata semantics embedded, content stays encrypted');
330
+
331
+ // Step 3: Prepare combined data (content + embedding)
332
+ console.log('📦 Step 3: Preparing data...');
333
+ const memoryData: ClientMemoryMetadata = {
334
+ content,
335
+ embedding,
336
+ timestamp: Date.now(),
337
+ };
338
+ const dataBytes = new TextEncoder().encode(JSON.stringify(memoryData));
339
+ console.log('✅ Data prepared:', dataBytes.length, 'bytes');
340
+
341
+ // Step 4: Conditionally encrypt with SEAL
342
+ let uploadData: Uint8Array;
343
+ const isEncrypted = this.config.enableEncryption;
344
+
345
+ if (isEncrypted) {
346
+ console.log('🔒 Step 4: Encrypting with SEAL...');
347
+ onProgress?.('Encrypting with SEAL...');
348
+ uploadData = await this.encryptWithSEAL(dataBytes, account.address, client);
349
+ console.log('✅ Encrypted:', uploadData.length, 'bytes');
350
+ } else {
351
+ console.log('📝 Step 4: Skipping encryption (enableEncryption=false)');
352
+ onProgress?.('Preparing data (no encryption)...');
353
+ uploadData = dataBytes;
354
+ console.log('✅ Data ready (unencrypted):', uploadData.length, 'bytes');
355
+ }
356
+
357
+ // Step 5: Upload to Walrus (2 signatures)
358
+ console.log('🐳 Step 5: Uploading to Walrus...');
359
+ onProgress?.('Uploading to Walrus (2 signatures)...');
360
+ const blobId = await this.uploadToWalrus(uploadData, account, signAndExecute, client);
361
+ console.log('✅ Uploaded to Walrus:', blobId);
362
+
363
+ // Generate sequential vector ID (fits in 32-bit for WASM)
364
+ const vectorId = await this.getNextVectorId(account.address);
365
+ console.log('🔢 Generated sequential vector ID:', vectorId);
366
+
367
+ // Step 6: Register on-chain with rich metadata (1 signature)
368
+ console.log('⛓️ Step 6: Registering on-chain with rich metadata...');
369
+ onProgress?.('Registering on-chain (1 signature)...');
370
+ await this.registerOnChain({
371
+ blobId,
372
+ category: analysis.category,
373
+ topic: analysis.topic, // ✅ Pass AI-extracted topic to blockchain
374
+ importance: analysis.importance,
375
+ contentLength: content.length,
376
+ vectorId, // ✅ Use sequential ID
377
+ account,
378
+ signAndExecute,
379
+ client
380
+ });
381
+ console.log('✅ Memory registered on-chain with metadata:', {
382
+ category: analysis.category,
383
+ topic: analysis.topic,
384
+ importance: analysis.importance,
385
+ vectorId
386
+ });
387
+ onProgress?.('Memory created successfully!');
388
+
389
+ // Step 7: Index locally for search (if enabled)
390
+ console.log('\n📊 === Step 7: Local Vector Indexing ===');
391
+ console.log(' enableLocalIndexing:', this.config.enableLocalIndexing);
392
+ console.log(' account address:', account.address);
393
+ console.log(' embedding length:', embedding?.length || 0);
394
+
395
+ if (this.config.enableLocalIndexing && account.address) {
396
+ try {
397
+ // Get HNSW service (wait for async initialization)
398
+ const hnswService = await this.getHnswService();
399
+ console.log(' hnswService available:', !!hnswService);
400
+
401
+ if (hnswService) {
402
+ console.log('✅ Local indexing conditions met - proceeding...');
403
+ onProgress?.('Indexing for local search...');
404
+ console.log(' Using vector ID:', vectorId);
405
+
406
+ // Prepare rich metadata (aligned with on-chain structure)
407
+ // Option A+: Content storage is controlled by isEncrypted flag.
408
+ // When encryption is OFF, content is stored in local index for fast retrieval.
409
+ // When encryption is ON, only metadata is stored (security).
410
+ const metadata = {
411
+ blobId,
412
+ category: analysis.category,
413
+ topic: analysis.topic, // ✅ Rich metadata field
414
+ importance: analysis.importance,
415
+ summary: analysis.summary, // ✅ Rich metadata field
416
+ createdTimestamp: Date.now(),
417
+ contentType: 'text/plain',
418
+ contentSize: content.length,
419
+ source: 'client_memory_manager',
420
+ embeddingType: 'metadata', // ✅ Mark as metadata-based embedding
421
+ isEncrypted, // ✅ Dynamic based on config.enableEncryption
422
+ // Option A+: Store content in index when NOT encrypted (for fast local search)
423
+ ...(isEncrypted ? {} : { content })
424
+ };
425
+ console.log(' Rich metadata prepared:', metadata);
426
+
427
+ // Add to local index using IHnswService interface
428
+ console.log('📝 Adding vector to index...');
429
+ await hnswService.addVector(
430
+ account.address,
431
+ vectorId,
432
+ embedding, // ← Metadata embedding from Step 2 (not content embedding!)
433
+ metadata
434
+ );
435
+ console.log('✅ Vector added to index');
436
+ console.log(' Embedding represents: metadata semantics (category, topic, summary)');
437
+ console.log(' NOT content semantics - privacy preserved!');
438
+
439
+ // Flush to make searchable
440
+ console.log('🔄 Flushing index to make memory immediately searchable...');
441
+ await hnswService.flushBatch(account.address);
442
+ console.log('✅ Index flushed - vectors now searchable');
443
+
444
+ // Save index for persistence
445
+ console.log('💾 Saving index for persistence...');
446
+ await hnswService.saveIndex(account.address);
447
+ console.log('✅ Index saved - will persist after refresh');
448
+
449
+ console.log('🎉 Memory indexed, flushed, and persisted!');
450
+ console.log(' Vector ID:', vectorId);
451
+ console.log(' User:', account.address.substring(0, 10) + '...');
452
+ console.log(' Embedding dimensions:', embedding.length);
453
+ onProgress?.('Memory indexed and saved!');
454
+ } else {
455
+ console.warn('⚠️ HNSW service not available - skipping local indexing');
456
+ }
457
+ } catch (indexError: any) {
458
+ // Non-fatal: memory is still created on-chain
459
+ console.error('❌ Local indexing failed:', indexError);
460
+ console.error(' Error details:', indexError.message);
461
+ console.error(' Stack:', indexError.stack);
462
+ console.warn('⚠️ Memory still created on-chain, but not indexed locally');
463
+ }
464
+ } else {
465
+ console.warn('⚠️ Local indexing skipped - conditions not met:');
466
+ if (!this.config.enableLocalIndexing) console.warn(' - Local indexing disabled');
467
+ if (!account.address) console.warn(' - No account address');
468
+ if (!embedding || embedding.length === 0) console.warn(' - No embedding generated');
469
+ }
470
+
471
+ console.log('🎉 Memory creation complete!');
472
+ return blobId;
473
+ } catch (error: any) {
474
+ console.error('❌ Memory creation failed:', error);
475
+ throw new Error(`Failed to create memory: ${error.message}`);
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Retrieve and decrypt a memory
481
+ */
482
+ async retrieveMemory(options: RetrieveMemoryOptions): Promise<ClientMemoryMetadata> {
483
+ const { blobId, account, signPersonalMessage, client, onProgress } = options;
484
+
485
+ console.log('🔍 Starting memory retrieval...');
486
+ onProgress?.('Starting retrieval...');
487
+
488
+ try {
489
+ // Step 1: Fetch from Walrus
490
+ console.log('🐳 Step 1: Fetching from Walrus...');
491
+ onProgress?.('Fetching from Walrus...');
492
+ const encryptedData = await this.fetchFromWalrus(blobId, client);
493
+ console.log('✅ Retrieved:', encryptedData.length, 'bytes');
494
+
495
+ // Step 2: Decrypt with SEAL
496
+ console.log('🔓 Step 2: Decrypting with SEAL...');
497
+ onProgress?.('Decrypting with SEAL...');
498
+ const decryptedData = await this.decryptWithSEAL({
499
+ encryptedData,
500
+ account,
501
+ signPersonalMessage,
502
+ client
503
+ });
504
+ console.log('✅ Decrypted:', decryptedData.length, 'bytes');
505
+
506
+ // Step 3: Parse JSON
507
+ const decryptedString = new TextDecoder().decode(decryptedData);
508
+ const parsed: ClientMemoryMetadata = JSON.parse(decryptedString);
509
+
510
+ console.log('🎉 Memory retrieval complete!');
511
+ onProgress?.('Memory retrieved successfully!');
512
+
513
+ return parsed;
514
+ } catch (error: any) {
515
+ console.error('❌ Memory retrieval failed:', error);
516
+ throw new Error(`Failed to retrieve memory: ${error.message}`);
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Batch retrieve and decrypt multiple memories with a single signature
522
+ * This is much more efficient than calling retrieveMemory multiple times
523
+ */
524
+ async batchRetrieveMemories(options: BatchRetrieveMemoriesOptions): Promise<BatchRetrieveResult[]> {
525
+ const { blobIds, account, signPersonalMessage, client, onProgress } = options;
526
+
527
+ console.log('🔍 Starting batch memory retrieval for', blobIds.length, 'memories...');
528
+ onProgress?.('Initializing decryption session...', 0, blobIds.length);
529
+
530
+ try {
531
+ // Step 1: Create reusable decryption session (SINGLE SIGNATURE!)
532
+ const session = await this.createDecryptionSession({
533
+ account,
534
+ signPersonalMessage,
535
+ client
536
+ });
537
+ console.log('✅ Decryption session created - will decrypt all memories without additional signatures');
538
+
539
+ const results: BatchRetrieveResult[] = [];
540
+
541
+ // Step 2: Decrypt all memories using the same session
542
+ for (let i = 0; i < blobIds.length; i++) {
543
+ const blobId = blobIds[i];
544
+ console.log(`🔓 Decrypting memory ${i + 1}/${blobIds.length}: ${blobId}`);
545
+ onProgress?.(`Decrypting memory ${i + 1}/${blobIds.length}...`, i + 1, blobIds.length);
546
+
547
+ try {
548
+ // Fetch from Walrus
549
+ const encryptedData = await this.fetchFromWalrus(blobId, client);
550
+
551
+ // Decrypt using shared session (NO SIGNING!)
552
+ const decryptedData = await session.sealClient.decrypt({
553
+ data: encryptedData,
554
+ sessionKey: session.sessionKey,
555
+ txBytes: session.txBytes,
556
+ });
557
+
558
+ // Parse JSON
559
+ const decryptedString = new TextDecoder().decode(decryptedData);
560
+ const parsed: ClientMemoryMetadata = JSON.parse(decryptedString);
561
+
562
+ results.push({
563
+ blobId,
564
+ content: parsed.content
565
+ });
566
+
567
+ console.log(`✅ Memory ${i + 1} decrypted successfully`);
568
+ } catch (error: any) {
569
+ console.error(`❌ Failed to decrypt memory ${blobId}:`, error);
570
+
571
+ // Handle old format (binary embedding data)
572
+ if (error.message?.includes('not valid JSON') || error.message?.includes('Unexpected token')) {
573
+ results.push({
574
+ blobId,
575
+ content: '[Old format - cannot display content]'
576
+ });
577
+ } else {
578
+ results.push({
579
+ blobId,
580
+ error: error.message || 'Decryption failed'
581
+ });
582
+ }
583
+ }
584
+ }
585
+
586
+ console.log('🎉 Batch retrieval complete!');
587
+ onProgress?.('All memories decrypted!', blobIds.length, blobIds.length);
588
+
589
+ return results;
590
+ } catch (error: any) {
591
+ console.error('❌ Batch retrieval failed:', error);
592
+ throw new Error(`Failed to batch retrieve memories: ${error.message}`);
593
+ }
594
+ }
595
+
596
+ // ==================== PRIVATE METHODS ====================
597
+
598
+ private async analyzeContent(text: string, categoryOverride?: string): Promise<RichMetadataAnalysis> {
599
+ // Use client-side Gemini AI for rich metadata extraction
600
+ try {
601
+ if (this.geminiAIService) {
602
+ console.log('🤖 Using Gemini AI for metadata extraction...');
603
+ const metadata = await this.geminiAIService.extractRichMetadata(text, categoryOverride);
604
+ console.log('✅ AI metadata extracted:', metadata);
605
+ return metadata;
606
+ } else {
607
+ console.warn('⚠️ Gemini AI service not initialized, using fallback extraction');
608
+ return this.getFallbackAnalysis(text, categoryOverride);
609
+ }
610
+ } catch (error) {
611
+ console.warn('⚠️ AI analysis failed, using fallback:', error);
612
+ return this.getFallbackAnalysis(text, categoryOverride);
613
+ }
614
+ }
615
+
616
+ private getFallbackAnalysis(text: string, categoryOverride?: string): RichMetadataAnalysis {
617
+ return {
618
+ category: categoryOverride || 'personal',
619
+ topic: this.extractTopicFromContent(text),
620
+ importance: 5,
621
+ summary: text.substring(0, 200) + (text.length > 200 ? '...' : '')
622
+ };
623
+ }
624
+
625
+ /**
626
+ * Extract topic from content as fallback (when AI fails)
627
+ * Tries to get first sentence, or first 50 characters
628
+ *
629
+ * @param text - Content text to extract topic from
630
+ * @returns Topic string (max 100 characters)
631
+ */
632
+ private extractTopicFromContent(text: string): string {
633
+ // Try to get first sentence
634
+ const firstSentence = text.match(/^[^.!?]+[.!?]/);
635
+ if (firstSentence) {
636
+ const topic = firstSentence[0].trim();
637
+ return topic.length > 100 ? topic.substring(0, 97) + '...' : topic;
638
+ }
639
+
640
+ // Fallback: first 50 characters
641
+ return text.substring(0, 50) + (text.length > 50 ? '...' : '');
642
+ }
643
+
644
+ /**
645
+ * Build embeddable metadata text from rich metadata
646
+ * Format optimized for semantic embedding and alignment with on-chain metadata
647
+ *
648
+ * @param metadata - Rich metadata extracted from AI analysis
649
+ * @returns Formatted text string ready for embedding
650
+ *
651
+ * @example
652
+ * ```typescript
653
+ * const metadataText = buildMetadataText({
654
+ * category: 'work',
655
+ * topic: 'Q4 project deadline meeting',
656
+ * importance: 7,
657
+ * summary: 'Team discussion about Q4 deadlines'
658
+ * });
659
+ * // Returns:
660
+ * // "category: work
661
+ * // topic: Q4 project deadline meeting
662
+ * // importance: 7
663
+ * // summary: Team discussion about Q4 deadlines"
664
+ * ```
665
+ */
666
+ private buildMetadataText(metadata: RichMetadataAnalysis): string {
667
+ const parts = [
668
+ `category: ${metadata.category}`,
669
+ `topic: ${metadata.topic}`,
670
+ `importance: ${metadata.importance}`
671
+ ];
672
+
673
+ if (metadata.summary && metadata.summary.trim().length > 0) {
674
+ parts.push(`summary: ${metadata.summary}`);
675
+ }
676
+
677
+ return parts.join('\n');
678
+ }
679
+
680
+ private async generateEmbedding(text: string): Promise<number[]> {
681
+ if (!this.embeddingService) {
682
+ throw new Error(
683
+ 'EmbeddingService not configured. Please provide an API key for embedding generation ' +
684
+ '(e.g., GEMINI_API_KEY or OPENROUTER_API_KEY in environment variables).'
685
+ );
686
+ }
687
+
688
+ try {
689
+ const result = await this.embeddingService.embedText({
690
+ text,
691
+ type: 'content'
692
+ });
693
+ return result.vector;
694
+ } catch (error) {
695
+ throw new Error(`Failed to generate embedding: ${error instanceof Error ? error.message : 'Unknown error'}`);
696
+ }
697
+ }
698
+
699
+ private async encryptWithSEAL(
700
+ data: Uint8Array,
701
+ ownerAddress: string,
702
+ client: SuiClient
703
+ ): Promise<Uint8Array> {
704
+ const sealClient = new SealClient({
705
+ suiClient: client as any,
706
+ serverConfigs: this.config.sealServerObjectIds.map((id) => ({
707
+ objectId: id,
708
+ weight: 1,
709
+ })),
710
+ verifyKeyServers: false,
711
+ });
712
+
713
+ const { encryptedObject: encryptedBytes } = await sealClient.encrypt({
714
+ threshold: 1,
715
+ packageId: this.config.packageId,
716
+ id: ownerAddress, // Use owner's address as ID for simple access control
717
+ data,
718
+ });
719
+
720
+ return encryptedBytes;
721
+ }
722
+
723
+ private async uploadToWalrus(
724
+ data: Uint8Array,
725
+ account: { address: string },
726
+ signAndExecute: CreateMemoryOptions['signAndExecute'],
727
+ client: SuiClient
728
+ ): Promise<string> {
729
+ const extendedClient = (client as any).$extend(
730
+ WalrusClient.experimental_asClientExtension({
731
+ network: this.config.walrusNetwork,
732
+ uploadRelay: {
733
+ host: `https://upload-relay.${this.config.walrusNetwork}.walrus.space`,
734
+ sendTip: { max: 1_000 },
735
+ timeout: 60_000,
736
+ },
737
+ storageNodeClientOptions: {
738
+ timeout: 60_000,
739
+ },
740
+ })
741
+ );
742
+
743
+ const walrusClient = extendedClient.walrus as any;
744
+ const flow = walrusClient.writeBlobFlow({ blob: data });
745
+
746
+ // Encode
747
+ await flow.encode();
748
+
749
+ // Register (signature 1)
750
+ const registerTx = flow.register({
751
+ epochs: 5,
752
+ deletable: true,
753
+ owner: account.address,
754
+ });
755
+ registerTx.setSender(account.address);
756
+
757
+ const registerDigest = await new Promise<string>((resolve, reject) => {
758
+ signAndExecute(
759
+ { transaction: registerTx },
760
+ {
761
+ onSuccess: (result) => resolve(result.digest),
762
+ onError: (error) => reject(error),
763
+ }
764
+ );
765
+ });
766
+
767
+ // Upload
768
+ await flow.upload({ digest: registerDigest });
769
+
770
+ // Certify (signature 2)
771
+ const certifyTx = flow.certify();
772
+ certifyTx.setSender(account.address);
773
+
774
+ await new Promise<void>((resolve, reject) => {
775
+ signAndExecute(
776
+ { transaction: certifyTx },
777
+ {
778
+ onSuccess: () => resolve(),
779
+ onError: (error) => reject(error),
780
+ }
781
+ );
782
+ });
783
+
784
+ const blob = await flow.getBlob();
785
+ return blob.blobId;
786
+ }
787
+
788
+ private async registerOnChain(params: {
789
+ blobId: string;
790
+ category: string;
791
+ topic: string; // ✅ NEW: Real topic from AI analysis
792
+ importance: number;
793
+ contentLength: number;
794
+ vectorId: number; // ✅ Sequential ID (fits in 32-bit unsigned int)
795
+ account: { address: string };
796
+ signAndExecute: CreateMemoryOptions['signAndExecute'];
797
+ client: SuiClient;
798
+ }): Promise<void> {
799
+ const { blobId, category, topic, importance, contentLength, vectorId, account, signAndExecute, client } = params;
800
+
801
+ const tx = new Transaction();
802
+ const packageId = this.config.packageId.replace(/^0x/, '');
803
+
804
+ console.log('📝 Registering on-chain with rich metadata:', {
805
+ category,
806
+ topic,
807
+ importance,
808
+ vectorId,
809
+ blobId: blobId.substring(0, 20) + '...'
810
+ });
811
+
812
+ tx.moveCall({
813
+ target: `${packageId}::memory::create_memory_record`,
814
+ arguments: [
815
+ tx.pure.string(category),
816
+ tx.pure.u64(vectorId),
817
+ tx.pure.string(blobId),
818
+ tx.pure.string('text/plain'),
819
+ tx.pure.u64(contentLength),
820
+ tx.pure.string(blobId), // ✅ FIX: content_hash = blobId (content-addressed)
821
+ tx.pure.string(topic), // ✅ FIX: Real topic from AI (not hardcoded "memory")
822
+ tx.pure.u8(importance),
823
+ tx.pure.string(blobId), // embedding_blob_id (same as content for now)
824
+ ],
825
+ });
826
+
827
+ return new Promise((resolve, reject) => {
828
+ signAndExecute(
829
+ { transaction: tx },
830
+ {
831
+ onSuccess: (result) => {
832
+ console.log('✅ Transaction successful:', result.digest);
833
+ console.log('✅ On-chain Memory created with rich metadata:', {
834
+ category,
835
+ topic,
836
+ importance
837
+ });
838
+ resolve();
839
+ },
840
+ onError: (error) => {
841
+ console.error('❌ Transaction failed:', error);
842
+ reject(error);
843
+ },
844
+ }
845
+ );
846
+ });
847
+ }
848
+
849
+ private async fetchFromWalrus(blobId: string, client: SuiClient): Promise<Uint8Array> {
850
+ // Use Walrus SDK for reading blobs (consistent with uploadToWalrus)
851
+ const extendedClient = (client as any).$extend(
852
+ WalrusClient.experimental_asClientExtension({
853
+ network: this.config.walrusNetwork,
854
+ })
855
+ );
856
+
857
+ const walrusClient = extendedClient.walrus as any;
858
+ const blob = await walrusClient.readBlob({ blobId });
859
+ return blob;
860
+ }
861
+
862
+ /**
863
+ * Create a reusable decryption session (requires one signature)
864
+ * This session can be used to decrypt multiple memories without additional signatures
865
+ */
866
+ private async createDecryptionSession(params: {
867
+ account: { address: string };
868
+ signPersonalMessage: (params: { message: Uint8Array }) => Promise<{ signature: string }>;
869
+ client: SuiClient;
870
+ }): Promise<DecryptionSession> {
871
+ const { account, signPersonalMessage, client } = params;
872
+
873
+ console.log('🔑 Creating decryption session...');
874
+
875
+ // Create SEAL client (reusable)
876
+ const sealClient = new SealClient({
877
+ suiClient: client as any,
878
+ serverConfigs: this.config.sealServerObjectIds.map((id) => ({
879
+ objectId: id,
880
+ weight: 1,
881
+ })),
882
+ verifyKeyServers: false,
883
+ });
884
+
885
+ // Create session key (reusable)
886
+ const sessionKey = await SessionKey.create({
887
+ address: account.address,
888
+ packageId: this.config.packageId,
889
+ ttlMin: 10,
890
+ suiClient: client as any,
891
+ });
892
+
893
+ // Sign personal message ONCE
894
+ const personalMessage = sessionKey.getPersonalMessage();
895
+ const signatureResult = await signPersonalMessage({ message: personalMessage });
896
+ await sessionKey.setPersonalMessageSignature(signatureResult.signature);
897
+ console.log('✅ Personal message signed');
898
+
899
+ // Build seal_approve transaction ONCE
900
+ const tx = new Transaction();
901
+ const addressHex = account.address.startsWith('0x')
902
+ ? account.address.slice(2)
903
+ : account.address;
904
+ const idBytes = fromHex(addressHex);
905
+
906
+ tx.moveCall({
907
+ target: `${this.config.packageId}::capability::seal_approve`,
908
+ arguments: [
909
+ tx.pure.vector('u8', Array.from(idBytes)),
910
+ tx.pure.address(account.address),
911
+ tx.object(this.config.accessRegistryId),
912
+ tx.object('0x6'),
913
+ ],
914
+ });
915
+
916
+ const txBytes = await tx.build({ client, onlyTransactionKind: true });
917
+ console.log('✅ Session created - can now decrypt multiple memories');
918
+
919
+ return {
920
+ sealClient,
921
+ sessionKey,
922
+ txBytes
923
+ };
924
+ }
925
+
926
+ private async decryptWithSEAL(params: {
927
+ encryptedData: Uint8Array;
928
+ account: { address: string };
929
+ signPersonalMessage: RetrieveMemoryOptions['signPersonalMessage'];
930
+ client: SuiClient;
931
+ }): Promise<Uint8Array> {
932
+ const { encryptedData, account, signPersonalMessage, client } = params;
933
+
934
+ // Create SEAL client
935
+ const sealClient = new SealClient({
936
+ suiClient: client as any,
937
+ serverConfigs: this.config.sealServerObjectIds.map((id) => ({
938
+ objectId: id,
939
+ weight: 1,
940
+ })),
941
+ verifyKeyServers: false,
942
+ });
943
+
944
+ // Create session key
945
+ const sessionKey = await SessionKey.create({
946
+ address: account.address,
947
+ packageId: this.config.packageId,
948
+ ttlMin: 10,
949
+ suiClient: client as any,
950
+ });
951
+
952
+ // Sign personal message
953
+ const personalMessage = sessionKey.getPersonalMessage();
954
+ const signatureResult = await signPersonalMessage({ message: personalMessage });
955
+ await sessionKey.setPersonalMessageSignature(signatureResult.signature);
956
+
957
+ // Build seal_approve transaction
958
+ const tx = new Transaction();
959
+ const addressHex = account.address.startsWith('0x')
960
+ ? account.address.slice(2)
961
+ : account.address;
962
+ const idBytes = fromHex(addressHex);
963
+
964
+ tx.moveCall({
965
+ target: `${this.config.packageId}::capability::seal_approve`,
966
+ arguments: [
967
+ tx.pure.vector('u8', Array.from(idBytes)),
968
+ tx.pure.address(account.address),
969
+ tx.object(this.config.accessRegistryId),
970
+ tx.object('0x6'),
971
+ ],
972
+ });
973
+
974
+ const txBytes = await tx.build({ client, onlyTransactionKind: true });
975
+
976
+ // Decrypt
977
+ const decryptedData = await sealClient.decrypt({
978
+ data: encryptedData,
979
+ sessionKey,
980
+ txBytes,
981
+ });
982
+
983
+ return decryptedData;
984
+ }
985
+ }
986
+
987
+ export default ClientMemoryManager;