@soulcraft/brainy 3.37.6 → 3.37.8

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.
@@ -146,6 +146,12 @@ export class GcsStorage extends BaseStorage {
146
146
  });
147
147
  // Initialize counts from storage
148
148
  await this.initializeCounts();
149
+ // CRITICAL FIX (v3.37.7): Clear any stale cache entries from previous runs
150
+ // This prevents cache poisoning from causing silent failures on container restart
151
+ prodLog.info('🧹 Clearing cache from previous run to prevent cache poisoning');
152
+ this.nounCacheManager.clear();
153
+ this.verbCacheManager.clear();
154
+ prodLog.info('✅ Cache cleared - starting fresh');
149
155
  this.isInitialized = true;
150
156
  }
151
157
  catch (error) {
@@ -336,8 +342,14 @@ export class GcsStorage extends BaseStorage {
336
342
  contentType: 'application/json',
337
343
  resumable: false // For small objects, non-resumable is faster
338
344
  });
339
- // Update cache
340
- this.nounCacheManager.set(node.id, node);
345
+ // CRITICAL FIX (v3.37.8): Only cache nodes with non-empty vectors
346
+ // This prevents cache pollution from HNSW's lazy-loading nodes (vector: [])
347
+ if (node.vector && Array.isArray(node.vector) && node.vector.length > 0) {
348
+ this.nounCacheManager.set(node.id, node);
349
+ }
350
+ else {
351
+ prodLog.warn(`[saveNode] Not caching node ${node.id.substring(0, 8)}... with empty vector (HNSW lazy mode)`);
352
+ }
341
353
  // Increment noun count
342
354
  const metadata = await this.getNounMetadata(node.id);
343
355
  if (metadata && metadata.type) {
@@ -380,11 +392,53 @@ export class GcsStorage extends BaseStorage {
380
392
  */
381
393
  async getNode(id) {
382
394
  await this.ensureInitialized();
383
- // Check cache first
384
- const cached = this.nounCacheManager.get(id);
385
- if (cached) {
386
- this.logger.trace(`Cache hit for noun ${id}`);
387
- return cached;
395
+ // Check cache first WITH LOGGING
396
+ const cached = await this.nounCacheManager.get(id);
397
+ // DIAGNOSTIC LOGGING: Reveal cache poisoning
398
+ prodLog.info(`[getNode] 🔍 Cache check for ${id.substring(0, 8)}...:`, {
399
+ hasCached: cached !== undefined,
400
+ isNull: cached === null,
401
+ isObject: cached !== null && typeof cached === 'object',
402
+ type: typeof cached
403
+ });
404
+ // CRITICAL FIX (v3.37.8): Validate cached object before returning
405
+ if (cached !== undefined && cached !== null) {
406
+ // Log cached object structure to diagnose incomplete objects
407
+ prodLog.info(`[getNode] Cached object structure:`, {
408
+ hasId: !!cached.id,
409
+ idMatches: cached.id === id,
410
+ hasVector: !!cached.vector,
411
+ vectorLength: cached.vector?.length,
412
+ hasConnections: !!cached.connections,
413
+ connectionsType: typeof cached.connections,
414
+ hasLevel: cached.level !== undefined,
415
+ level: cached.level,
416
+ objectKeys: Object.keys(cached || {})
417
+ });
418
+ // Validate cached object has required fields (including non-empty vector!)
419
+ if (!cached.id || !cached.vector || !Array.isArray(cached.vector) || cached.vector.length === 0) {
420
+ prodLog.error(`[getNode] ❌ INVALID cached object for ${id.substring(0, 8)}...:`, {
421
+ reason: !cached.id ? 'missing id' :
422
+ !cached.vector ? 'missing vector' :
423
+ !Array.isArray(cached.vector) ? 'vector not array' :
424
+ cached.vector.length === 0 ? 'vector is empty array' :
425
+ 'unknown'
426
+ });
427
+ prodLog.error(`[getNode] Removing invalid object from cache and loading from GCS`);
428
+ this.nounCacheManager.delete(id);
429
+ // Fall through to load from GCS
430
+ }
431
+ else {
432
+ prodLog.info(`[getNode] ✅ Valid cached object - returning`);
433
+ this.logger.trace(`Cache hit for noun ${id}`);
434
+ return cached;
435
+ }
436
+ }
437
+ else if (cached === null) {
438
+ prodLog.warn(`[getNode] ⚠️ Cache contains NULL for ${id.substring(0, 8)}... - ignoring and loading from GCS`);
439
+ }
440
+ else {
441
+ prodLog.info(`[getNode] ❌ Cache MISS - loading from GCS for ${id.substring(0, 8)}...`);
388
442
  }
389
443
  // Apply backpressure
390
444
  const requestId = await this.applyBackpressure();
@@ -420,8 +474,14 @@ export class GcsStorage extends BaseStorage {
420
474
  level: data.level || 0
421
475
  // NO metadata field - retrieved separately for scalability
422
476
  };
423
- // Update cache
424
- this.nounCacheManager.set(id, node);
477
+ // CRITICAL FIX: Only cache valid nodes with non-empty vectors (never cache null or empty)
478
+ if (node && node.id && node.vector && Array.isArray(node.vector) && node.vector.length > 0) {
479
+ this.nounCacheManager.set(id, node);
480
+ prodLog.info(`[getNode] 💾 Cached node ${id.substring(0, 8)}... successfully`);
481
+ }
482
+ else {
483
+ prodLog.warn(`[getNode] ⚠️ NOT caching invalid node for ${id.substring(0, 8)}... (missing id/vector or empty vector)`);
484
+ }
425
485
  this.logger.trace(`Successfully retrieved node ${id}`);
426
486
  this.releaseBackpressure(true, requestId);
427
487
  return node;
@@ -440,7 +500,8 @@ export class GcsStorage extends BaseStorage {
440
500
  prodLog.error(`[getNode] Error object:`, JSON.stringify(error, null, 2));
441
501
  // Check if this is a "not found" error
442
502
  if (error.code === 404) {
443
- prodLog.warn(`[getNode] Identified as 404 error - returning null`);
503
+ prodLog.warn(`[getNode] Identified as 404 error - returning null WITHOUT caching`);
504
+ // CRITICAL FIX: Do NOT cache null values
444
505
  return null;
445
506
  }
446
507
  // Handle throttling
@@ -237,6 +237,16 @@ export class S3CompatibleStorage extends BaseStorage {
237
237
  await this.cleanupLegacyIndexFolder();
238
238
  // Initialize counts from storage
239
239
  await this.initializeCounts();
240
+ // CRITICAL FIX (v3.37.7): Clear any stale cache entries from previous runs
241
+ // This prevents cache poisoning from causing silent failures on container restart
242
+ const nodeCacheSize = this.nodeCache?.size || 0;
243
+ if (nodeCacheSize > 0) {
244
+ prodLog.info(`🧹 Clearing ${nodeCacheSize} cached node entries from previous run`);
245
+ this.nodeCache.clear();
246
+ }
247
+ else {
248
+ prodLog.info('🧹 Node cache is empty - starting fresh');
249
+ }
240
250
  this.isInitialized = true;
241
251
  this.logger.info(`Initialized ${this.serviceType} storage with bucket ${this.bucketName}`);
242
252
  }
@@ -808,6 +818,52 @@ export class S3CompatibleStorage extends BaseStorage {
808
818
  */
809
819
  async getNode(id) {
810
820
  await this.ensureInitialized();
821
+ // Check cache first WITH LOGGING
822
+ const cached = this.nodeCache.get(id);
823
+ // DIAGNOSTIC LOGGING: Reveal cache poisoning
824
+ prodLog.info(`[getNode] 🔍 Cache check for ${id.substring(0, 8)}...:`, {
825
+ hasCached: cached !== undefined,
826
+ isNull: cached === null,
827
+ isObject: cached !== null && typeof cached === 'object',
828
+ type: typeof cached
829
+ });
830
+ // CRITICAL FIX (v3.37.8): Validate cached object before returning
831
+ if (cached !== undefined && cached !== null) {
832
+ // Log cached object structure to diagnose incomplete objects
833
+ prodLog.info(`[getNode] Cached object structure:`, {
834
+ hasId: !!cached.id,
835
+ idMatches: cached.id === id,
836
+ hasVector: !!cached.vector,
837
+ vectorLength: cached.vector?.length,
838
+ hasConnections: !!cached.connections,
839
+ connectionsType: typeof cached.connections,
840
+ objectKeys: Object.keys(cached || {})
841
+ });
842
+ // Validate cached object has required fields (including non-empty vector!)
843
+ if (!cached.id || !cached.vector || !Array.isArray(cached.vector) || cached.vector.length === 0) {
844
+ prodLog.error(`[getNode] ❌ INVALID cached object for ${id.substring(0, 8)}...:`, {
845
+ reason: !cached.id ? 'missing id' :
846
+ !cached.vector ? 'missing vector' :
847
+ !Array.isArray(cached.vector) ? 'vector not array' :
848
+ cached.vector.length === 0 ? 'vector is empty array' :
849
+ 'unknown'
850
+ });
851
+ prodLog.error(`[getNode] Removing invalid object from cache and loading from S3`);
852
+ this.nodeCache.delete(id);
853
+ // Fall through to load from S3
854
+ }
855
+ else {
856
+ prodLog.info(`[getNode] ✅ Valid cached object - returning`);
857
+ this.logger.trace(`Cache hit for node ${id}`);
858
+ return cached;
859
+ }
860
+ }
861
+ else if (cached === null) {
862
+ prodLog.warn(`[getNode] ⚠️ Cache contains NULL for ${id.substring(0, 8)}... - ignoring and loading from S3`);
863
+ }
864
+ else {
865
+ prodLog.info(`[getNode] ❌ Cache MISS - loading from S3 for ${id.substring(0, 8)}...`);
866
+ }
811
867
  try {
812
868
  // Import the GetObjectCommand only when needed
813
869
  const { GetObjectCommand } = await import('@aws-sdk/client-s3');
@@ -858,6 +914,14 @@ export class S3CompatibleStorage extends BaseStorage {
858
914
  connections,
859
915
  level: parsedNode.level || 0
860
916
  };
917
+ // CRITICAL FIX: Only cache valid nodes with non-empty vectors (never cache null or empty)
918
+ if (node && node.id && node.vector && Array.isArray(node.vector) && node.vector.length > 0) {
919
+ this.nodeCache.set(id, node);
920
+ prodLog.info(`[getNode] 💾 Cached node ${id.substring(0, 8)}... successfully`);
921
+ }
922
+ else {
923
+ prodLog.warn(`[getNode] ⚠️ NOT caching invalid node for ${id.substring(0, 8)}... (missing id/vector or empty vector)`);
924
+ }
861
925
  this.logger.trace(`Successfully retrieved node ${id}`);
862
926
  return node;
863
927
  }
@@ -876,7 +940,8 @@ export class S3CompatibleStorage extends BaseStorage {
876
940
  prodLog.error(`[getNode] Error object:`, JSON.stringify(error, null, 2));
877
941
  // Check if this is a "not found" error (S3 uses "NoSuchKey")
878
942
  if (error?.name === 'NoSuchKey' || error?.Code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
879
- prodLog.warn(`[getNode] Identified as 404/NoSuchKey error - returning null`);
943
+ prodLog.warn(`[getNode] Identified as 404/NoSuchKey error - returning null WITHOUT caching`);
944
+ // CRITICAL FIX: Do NOT cache null values
880
945
  return null;
881
946
  }
882
947
  // Handle throttling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.37.6",
3
+ "version": "3.37.8",
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",