@soulcraft/brainy 4.10.0 → 4.10.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [4.10.1](https://github.com/soulcraftlabs/brainy/compare/v4.10.0...v4.10.1) (2025-10-29)
6
+
7
+ - fix: add mutex locks to FileSystemStorage for HNSW concurrency (CRITICAL) (ff86e88)
8
+
9
+
5
10
  ### [4.10.0](https://github.com/soulcraftlabs/brainy/compare/v4.9.2...v4.10.0) (2025-10-29)
6
11
 
7
12
  - perf: 48-64× faster HNSW bulk imports via concurrent neighbor updates (4038afd)
@@ -29,16 +29,10 @@ export class VFSStructureGenerator {
29
29
  async init() {
30
30
  // Get brain's cached VFS instance (creates if doesn't exist)
31
31
  this.vfs = this.brain.vfs();
32
- // Initialize if not already initialized
33
- // VFS.init() is idempotent (safe to call multiple times)
34
- try {
35
- // Check if already initialized
36
- await this.vfs.stat('/');
37
- }
38
- catch (error) {
39
- // Not initialized, initialize now
40
- await this.vfs.init();
41
- }
32
+ // CRITICAL FIX (v4.10.2): Always call vfs.init() explicitly
33
+ // The previous code tried to check if initialized via stat('/') but this was unreliable
34
+ // vfs.init() is idempotent, so calling it multiple times is safe
35
+ await this.vfs.init();
42
36
  }
43
37
  /**
44
38
  * Generate VFS structure from import result
@@ -27,6 +27,7 @@ export declare class FileSystemStorage extends BaseStorage {
27
27
  private activeLocks;
28
28
  private lockTimers;
29
29
  private allTimers;
30
+ private hnswLocks;
30
31
  private compressionEnabled;
31
32
  private compressionLevel;
32
33
  /**
@@ -392,7 +393,7 @@ export declare class FileSystemStorage extends BaseStorage {
392
393
  /**
393
394
  * Save HNSW system data (entry point, max level)
394
395
  *
395
- * CRITICAL FIX (v4.10.1): Atomic write to prevent race conditions during concurrent updates
396
+ * CRITICAL FIX (v4.10.1): Mutex lock + atomic write to prevent race conditions
396
397
  */
397
398
  saveHNSWSystem(systemData: {
398
399
  entryPointId: string | null;
@@ -53,6 +53,10 @@ export class FileSystemStorage extends BaseStorage {
53
53
  this.activeLocks = new Set();
54
54
  this.lockTimers = new Map(); // Track timers for cleanup
55
55
  this.allTimers = new Set(); // Track all timers for cleanup
56
+ // CRITICAL FIX (v4.10.1): Mutex locks for HNSW concurrency control
57
+ // Prevents read-modify-write races during concurrent neighbor updates at scale (1000+ ops)
58
+ // Matches MemoryStorage and OPFSStorage behavior (tested in production)
59
+ this.hnswLocks = new Map();
56
60
  // Compression configuration (v4.0.0)
57
61
  this.compressionEnabled = true; // Enable gzip compression by default for 60-80% disk savings
58
62
  this.compressionLevel = 6; // zlib compression level (1-9, default: 6 = balanced)
@@ -2174,51 +2178,74 @@ export class FileSystemStorage extends BaseStorage {
2174
2178
  */
2175
2179
  async saveHNSWData(nounId, hnswData) {
2176
2180
  await this.ensureInitialized();
2177
- // CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
2178
- // Previous implementation overwrote the entire file, destroying vector data
2179
- // Now we READ the existing node, UPDATE only connections/level, then WRITE back the complete node
2180
- // CRITICAL FIX (v4.10.1): Atomic write to prevent race conditions during concurrent HNSW updates
2181
- // Uses temp file + atomic rename strategy (POSIX guarantees rename() atomicity)
2182
- // Prevents data corruption when multiple entities connect to same neighbor simultaneously
2183
2181
  const filePath = this.getNodePath(nounId);
2184
- const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(2)}`;
2182
+ const lockKey = `hnsw/${nounId}`;
2183
+ // CRITICAL FIX (v4.10.1): Mutex lock to prevent read-modify-write races
2184
+ // Problem: Without mutex, concurrent operations can:
2185
+ // 1. Thread A reads file (connections: [1,2,3])
2186
+ // 2. Thread B reads file (connections: [1,2,3])
2187
+ // 3. Thread A adds connection 4, writes [1,2,3,4]
2188
+ // 4. Thread B adds connection 5, writes [1,2,3,5] ← Connection 4 LOST!
2189
+ // Solution: Mutex serializes operations per entity (like Memory/OPFS adapters)
2190
+ // Production scale: Prevents corruption at 1000+ concurrent operations
2191
+ // Wait for any pending operations on this entity
2192
+ while (this.hnswLocks.has(lockKey)) {
2193
+ await this.hnswLocks.get(lockKey);
2194
+ }
2195
+ // Acquire lock
2196
+ let releaseLock;
2197
+ const lockPromise = new Promise(resolve => { releaseLock = resolve; });
2198
+ this.hnswLocks.set(lockKey, lockPromise);
2185
2199
  try {
2186
- // Read existing node data (if exists)
2187
- let existingNode = {};
2200
+ // CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
2201
+ // Previous implementation overwrote the entire file, destroying vector data
2202
+ // Now we READ the existing node, UPDATE only connections/level, then WRITE back the complete node
2203
+ // CRITICAL FIX (v4.9.2): Atomic write to prevent torn writes during crashes
2204
+ // Uses temp file + atomic rename strategy (POSIX guarantees rename() atomicity)
2205
+ // Note: Atomic rename alone does NOT prevent concurrent read-modify-write races (needs mutex above)
2206
+ const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(2)}`;
2188
2207
  try {
2189
- const existingData = await fs.promises.readFile(filePath, 'utf-8');
2190
- existingNode = JSON.parse(existingData);
2208
+ // Read existing node data (if exists)
2209
+ let existingNode = {};
2210
+ try {
2211
+ const existingData = await fs.promises.readFile(filePath, 'utf-8');
2212
+ existingNode = JSON.parse(existingData);
2213
+ }
2214
+ catch (error) {
2215
+ // File doesn't exist yet - will create new
2216
+ if (error.code !== 'ENOENT') {
2217
+ throw error;
2218
+ }
2219
+ }
2220
+ // Preserve id and vector, update only HNSW graph metadata
2221
+ const updatedNode = {
2222
+ ...existingNode, // Preserve all existing fields (id, vector, etc.)
2223
+ level: hnswData.level,
2224
+ connections: hnswData.connections
2225
+ };
2226
+ // ATOMIC WRITE SEQUENCE:
2227
+ // 1. Write to temp file
2228
+ await this.ensureDirectoryExists(path.dirname(tempPath));
2229
+ await fs.promises.writeFile(tempPath, JSON.stringify(updatedNode, null, 2));
2230
+ // 2. Atomic rename temp → final (POSIX atomicity guarantee)
2231
+ // This operation is guaranteed atomic by POSIX - either succeeds completely or fails
2232
+ await fs.promises.rename(tempPath, filePath);
2191
2233
  }
2192
2234
  catch (error) {
2193
- // File doesn't exist yet - will create new
2194
- if (error.code !== 'ENOENT') {
2195
- throw error;
2235
+ // Clean up temp file on any error
2236
+ try {
2237
+ await fs.promises.unlink(tempPath);
2196
2238
  }
2239
+ catch (cleanupError) {
2240
+ // Ignore cleanup errors - temp file may not exist
2241
+ }
2242
+ throw error;
2197
2243
  }
2198
- // Preserve id and vector, update only HNSW graph metadata
2199
- const updatedNode = {
2200
- ...existingNode, // Preserve all existing fields (id, vector, etc.)
2201
- level: hnswData.level,
2202
- connections: hnswData.connections
2203
- };
2204
- // ATOMIC WRITE SEQUENCE:
2205
- // 1. Write to temp file
2206
- await this.ensureDirectoryExists(path.dirname(tempPath));
2207
- await fs.promises.writeFile(tempPath, JSON.stringify(updatedNode, null, 2));
2208
- // 2. Atomic rename temp → final (POSIX atomicity guarantee)
2209
- // This operation is guaranteed atomic by POSIX - either succeeds completely or fails
2210
- // Multiple concurrent renames will serialize at the kernel level
2211
- await fs.promises.rename(tempPath, filePath);
2212
2244
  }
2213
- catch (error) {
2214
- // Clean up temp file on any error
2215
- try {
2216
- await fs.promises.unlink(tempPath);
2217
- }
2218
- catch (cleanupError) {
2219
- // Ignore cleanup errors - temp file may not exist
2220
- }
2221
- throw error;
2245
+ finally {
2246
+ // Release lock (ALWAYS runs, even if error thrown)
2247
+ this.hnswLocks.delete(lockKey);
2248
+ releaseLock();
2222
2249
  }
2223
2250
  }
2224
2251
  /**
@@ -2242,28 +2269,47 @@ export class FileSystemStorage extends BaseStorage {
2242
2269
  /**
2243
2270
  * Save HNSW system data (entry point, max level)
2244
2271
  *
2245
- * CRITICAL FIX (v4.10.1): Atomic write to prevent race conditions during concurrent updates
2272
+ * CRITICAL FIX (v4.10.1): Mutex lock + atomic write to prevent race conditions
2246
2273
  */
2247
2274
  async saveHNSWSystem(systemData) {
2248
2275
  await this.ensureInitialized();
2249
- const filePath = path.join(this.systemDir, 'hnsw-system.json');
2250
- const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(2)}`;
2276
+ const lockKey = 'hnsw/system';
2277
+ // CRITICAL FIX (v4.10.1): Mutex lock to serialize system updates
2278
+ // System data (entry point, max level) updated frequently during HNSW construction
2279
+ // Without mutex, concurrent updates can lose data (same as entity-level problem)
2280
+ // Wait for any pending system updates
2281
+ while (this.hnswLocks.has(lockKey)) {
2282
+ await this.hnswLocks.get(lockKey);
2283
+ }
2284
+ // Acquire lock
2285
+ let releaseLock;
2286
+ const lockPromise = new Promise(resolve => { releaseLock = resolve; });
2287
+ this.hnswLocks.set(lockKey, lockPromise);
2251
2288
  try {
2252
- // Write to temp file
2253
- await this.ensureDirectoryExists(path.dirname(tempPath));
2254
- await fs.promises.writeFile(tempPath, JSON.stringify(systemData, null, 2));
2255
- // Atomic rename temp → final (POSIX atomicity guarantee)
2256
- await fs.promises.rename(tempPath, filePath);
2257
- }
2258
- catch (error) {
2259
- // Clean up temp file on any error
2289
+ const filePath = path.join(this.systemDir, 'hnsw-system.json');
2290
+ const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(2)}`;
2260
2291
  try {
2261
- await fs.promises.unlink(tempPath);
2292
+ // Write to temp file
2293
+ await this.ensureDirectoryExists(path.dirname(tempPath));
2294
+ await fs.promises.writeFile(tempPath, JSON.stringify(systemData, null, 2));
2295
+ // Atomic rename temp → final (POSIX atomicity guarantee)
2296
+ await fs.promises.rename(tempPath, filePath);
2262
2297
  }
2263
- catch (cleanupError) {
2264
- // Ignore cleanup errors
2298
+ catch (error) {
2299
+ // Clean up temp file on any error
2300
+ try {
2301
+ await fs.promises.unlink(tempPath);
2302
+ }
2303
+ catch (cleanupError) {
2304
+ // Ignore cleanup errors
2305
+ }
2306
+ throw error;
2265
2307
  }
2266
- throw error;
2308
+ }
2309
+ finally {
2310
+ // Release lock
2311
+ this.hnswLocks.delete(lockKey);
2312
+ releaseLock();
2267
2313
  }
2268
2314
  }
2269
2315
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "4.10.0",
3
+ "version": "4.10.2",
4
4
  "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",