@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
|
-
//
|
|
340
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
//
|
|
424
|
-
|
|
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.
|
|
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",
|