-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstorageService.js
More file actions
221 lines (191 loc) · 9.1 KB
/
Copy pathstorageService.js
File metadata and controls
221 lines (191 loc) · 9.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// ============================================================
// 0G Storage Service — Upload/Download Memory Blobs
// ============================================================
import { NETWORK_CONFIG } from '../config/network';
import { onNetworkChange } from '../config/network';
// UPLOAD_OPTIONS is no longer used here — uploads moved server-side (see
// server/storageUpload.js) to get around the 0G indexer's missing CORS headers.
import { PROTOCOL_VERSION } from '../config/constants';
import { EMBEDDING_DIMENSIONS } from '../../shared/embeddingConfig.js';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || '';
// Lazy-load the SDK to avoid polyfill issues at module init time
let _sdk = null;
async function getSDK() {
if (!_sdk) {
try {
_sdk = await import('@0gfoundation/0g-ts-sdk/browser');
} catch (e) {
console.warn('[StorageService] Browser SDK import failed, trying main path:', e.message);
try {
_sdk = await import('@0gfoundation/0g-ts-sdk');
} catch (e2) {
console.error('[StorageService] SDK import failed entirely:', e2.message);
throw new Error('0G SDK could not be loaded. Check polyfill configuration.');
}
}
}
return _sdk;
}
class StorageService {
constructor() {
this.indexer = null;
this.logs = [];
this.logListeners = new Set();
// Reset indexer when network changes
onNetworkChange(() => {
this.indexer = null;
this._emitLog('CONNECT', 'Network changed — indexer reset, will reconnect on next operation', 'info');
});
}
// Subscribe to log events (for the Data Terminal)
onLog(listener) {
this.logListeners.add(listener);
return () => this.logListeners.delete(listener);
}
_emitLog(type, message, status = 'info') {
const log = {
id: Date.now() + Math.random(),
type,
message,
timestamp: new Date().toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
}),
status,
};
this.logs.push(log);
this.logListeners.forEach(fn => fn(log));
return log;
}
// Initialize the Indexer client
async _getIndexer() {
if (!this.indexer) {
const sdk = await getSDK();
this.indexer = new sdk.Indexer(NETWORK_CONFIG.indexerRpc);
this._emitLog('CONNECT', `0G Storage indexer connected ❯ ${NETWORK_CONFIG.indexerRpc}`, 'success');
}
return this.indexer;
}
// ─── CORE: Store Memory Vector ───────────────────────────────
// No signer parameter: the upload is signed server-side with the backend's
// 0G key (see server/storageUpload.js), which exists because the 0G indexer
// sends no CORS headers and the browser SDK cannot reach it directly. The
// user's wallet still signs the on-chain anchor separately.
async storeMemory(memoryData) {
const startTime = performance.now();
this._emitLog('STORE', `Preparing memory blob ❯ agent: ${memoryData.agentId} ❯ dim: ${memoryData.embedding?.dim ?? EMBEDDING_DIMENSIONS} ❯ model: ${memoryData.embedding?.model?.id ?? 'none'}`, 'info');
try {
// 1. Serialize the memory object to JSON
// v2 blob schema. 0G blobs are immutable and Merkle-anchored, so this
// shape is a one-way door — every field here is permanent.
//
// The load-bearing addition is `embedding.source` + `embedding.model.id`.
// v0.1.0 blobs recorded a bare vector and a `dimensions` integer, with
// nothing distinguishing PRNG noise from a real model's output. That
// ambiguity cannot be fixed retroactively; stamping provenance now means
// this migration never has to happen a second time.
//
// The old `dimensions: embedding?.length || 1536` line is gone: with `||`
// (not `??`) a missing OR empty embedding wrote a false 1536 into a
// permanent record. A missing embedding is now simply `embedding: null`.
const memoryPayload = {
protocol: 'memoria-da',
schema: 'memoria-da/memory',
schemaVersion: 2,
version: PROTOCOL_VERSION,
type: 'memory',
timestamp: new Date().toISOString(),
agentId: memoryData.agentId,
content: memoryData.content,
// The vector is computed over exactly this `content` string, recorded
// so retrieval semantics are provable from the blob alone.
inputTransform: 'content',
embedding: memoryData.embedding ?? null,
metadata: {
...memoryData.metadata,
framework: memoryData.metadata?.framework || 'OpenClaw',
},
};
const jsonStr = JSON.stringify(memoryPayload);
const nativeBlob = new Blob([jsonStr], { type: 'application/json' });
const blobSize = nativeBlob.size;
this._emitLog('VECTOR', `Embedding serialized ❯ size: ${(blobSize / 1024).toFixed(1)} KB ❯ segments: ${Math.ceil(blobSize / 256)}`, 'info');
// 2–4. Upload via backend server (avoids browser CORS block on 0G Storage nodes)
this._emitLog('UPLOAD', `Uploading to 0G Storage via backend ❯ blob_size: ${(blobSize / 1024).toFixed(1)} KB ❯ indexer: turbo`, 'info');
const networkKey = NETWORK_CONFIG.key || 'testnet';
const uploadRes = await fetch(`${BACKEND_URL}/api/storage/upload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload: memoryPayload, network: networkKey }),
});
if (!uploadRes.ok) {
const errData = await uploadRes.json().catch(() => ({ error: `HTTP ${uploadRes.status}` }));
this._emitLog('ERROR', `Upload failed: ${errData.error}`, 'error');
throw new Error(errData.error || `Storage upload failed (${uploadRes.status})`);
}
const { rootHash, tx, blobSize: serverBlobSize } = await uploadRes.json();
this._emitLog('MERKLE', `Merkle root received ❯ root: ${rootHash.slice(0, 10)}...${rootHash.slice(-6)}`, 'success');
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
this._emitLog('CONFIRM', `✓ Memory committed to 0G Storage ❯ root: ${rootHash.slice(0, 10)}...${rootHash.slice(-6)} ❯ ${elapsed}s`, 'success');
if (tx) {
this._emitLog('TX', `Onchain tx confirmed ❯ hash: ${typeof tx === 'string' ? tx.slice(0, 14) : '0x...'}`, 'success');
}
return {
rootHash,
blobSize: serverBlobSize || blobSize,
tx,
elapsed: parseFloat(elapsed),
};
} catch (error) {
this._emitLog('ERROR', `Storage error: ${error.message}`, 'error');
throw error;
}
}
// ─── CORE: Retrieve Memory Vector ──────────────────────────────
async retrieveMemory(rootHash) {
const startTime = performance.now();
this._emitLog('FETCH', `Fetching from 0G Storage ❯ root: ${rootHash.slice(0, 10)}...${rootHash.slice(-6)}`, 'info');
try {
// Use the REST API endpoint for browser-compatible download
const apiUrl = `${NETWORK_CONFIG.indexerRpc}/file?root=${rootHash}`;
const response = await fetch(apiUrl);
if (!response.ok) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const errData = await response.json();
throw new Error(errData.message || `Download failed (${response.status})`);
}
throw new Error(`Download failed with status ${response.status}`);
}
const fileData = await response.arrayBuffer();
const decoder = new TextDecoder('utf-8');
const jsonStr = decoder.decode(fileData);
const memoryData = JSON.parse(jsonStr);
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
this._emitLog('RETRIEVE', `Memory retrieved ❯ dim: ${memoryData.dimensions || '?'} ❯ size: ${(fileData.byteLength / 1024).toFixed(1)} KB ❯ ${elapsed}s`, 'success');
return memoryData;
} catch (error) {
this._emitLog('ERROR', `Retrieval error: ${error.message}`, 'error');
throw error;
}
}
// NOTE: generateMockEmbedding() and cosineSimilarity() were removed here.
//
// The first produced 1536-dim vectors from a seeded PRNG. Because the
// vectors were statistically independent of the text, cosine similarity
// was noise and semantic recall could never fire — the retrieval filter
// required >0.3, which for random unit vectors in 1536 dims is over 11
// standard deviations out. Embeddings now come from the backend model
// (see server/embeddingService.js); there is deliberately no local
// fallback, because a random vector is indistinguishable from a real one
// once it is written into an immutable, Merkle-anchored 0G blob.
//
// The second was a duplicate cosine implementation whose null-handling
// differed from the one in memoryStore. Two copies that can drift apart
// is a latent bug; memoryStore._cosineSimilarity is now the only one.
}
export const storageService = new StorageService();
export default storageService;