fix: populate totalNodes/totalEdges in ALL storage adapters for HNSW rebuild

Critical fix for v3.37.1 GCS persistence bug where HNSW rebuild reported
"Preloading 0 vectors" and hung indefinitely during container restarts.

Root Cause:
HNSW rebuild (hnswIndex.ts:835) calls storage.getStatistics() and expects
stats.totalNodes to determine entity count for adaptive caching. When
totalNodes was undefined, entityCount evaluated to 0, causing HNSW to
report "0 vectors" and hang waiting for entities that it couldn't find.

Fixes Applied:
- GcsStorage: Populate totalNodes/totalEdges from this.totalNounCount/totalVerbCount
- MemoryStorage: Populate totalNodes/totalEdges from in-memory counts
- OPFSStorage: Populate totalNodes/totalEdges in ALL return paths (cache, current day, yesterday, legacy)
- S3CompatibleStorage: Populate totalNodes/totalEdges in ALL return paths (cache, fresh stats, error fallback)

Impact:
-  GCS container restarts now work correctly
-  HNSW rebuild correctly detects entity count
-  Adaptive caching strategy works as designed
-  All storage adapters now consistent

Files exist in GCS, counts load correctly, but HNSW couldn't see them
because getStatisticsData() didn't populate the totalNodes field that
HNSW depends on.

Closes: BRAINY_V3_37_1_INDEX_REBUILD_BUG.md
Related: v3.37.0 (metadata reconstruction), v3.37.1 (getNoun_internal fix)
This commit is contained in:
David Snelling 2025-10-10 17:48:40 -07:00
parent 9fe790e0a7
commit a21a84545a
4 changed files with 58 additions and 10 deletions

View file

@ -1532,7 +1532,15 @@ export class GcsStorage extends BaseStorage {
const statistics = JSON.parse(contents.toString())
this.logger.trace('Statistics retrieved successfully')
return statistics
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
return {
...statistics,
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: new Date().toISOString()
}
} catch (error: any) {
if (error.code === 404) {
this.logger.trace('Statistics not found (creating new)')

View file

@ -691,6 +691,10 @@ export class MemoryStorage extends BaseStorage {
verbCount: {...this.statistics.verbCount},
metadataCount: {...this.statistics.metadataCount},
hnswIndexSize: this.statistics.hnswIndexSize,
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: this.statistics.lastUpdated,
// Include serviceActivity if present
...(this.statistics.serviceActivity && {

View file

@ -1346,6 +1346,10 @@ export class OPFSStorage extends BaseStorage {
verbCount: { ...this.statistics.verbCount },
metadataCount: { ...this.statistics.metadataCount },
hnswIndexSize: this.statistics.hnswIndexSize,
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: this.statistics.lastUpdated
}
}
@ -1374,6 +1378,10 @@ export class OPFSStorage extends BaseStorage {
verbCount: { ...this.statistics.verbCount },
metadataCount: { ...this.statistics.metadataCount },
hnswIndexSize: this.statistics.hnswIndexSize,
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: this.statistics.lastUpdated
}
}
@ -1397,6 +1405,10 @@ export class OPFSStorage extends BaseStorage {
verbCount: { ...this.statistics.verbCount },
metadataCount: { ...this.statistics.metadataCount },
hnswIndexSize: this.statistics.hnswIndexSize,
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: this.statistics.lastUpdated
}
}
@ -1418,6 +1430,10 @@ export class OPFSStorage extends BaseStorage {
verbCount: { ...this.statistics.verbCount },
metadataCount: { ...this.statistics.metadataCount },
hnswIndexSize: this.statistics.hnswIndexSize,
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: this.statistics.lastUpdated
}
}

View file

@ -2742,6 +2742,10 @@ export class S3CompatibleStorage extends BaseStorage {
verbCount: { ...this.statisticsCache.verbCount },
metadataCount: { ...this.statisticsCache.metadataCount },
hnswIndexSize: this.statisticsCache.hnswIndexSize,
// CRITICAL FIX: Populate totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount,
lastUpdated: this.statisticsCache.lastUpdated
}
}
@ -2789,6 +2793,14 @@ export class S3CompatibleStorage extends BaseStorage {
hnswIndexSize: statistics.hnswIndexSize,
lastUpdated: statistics.lastUpdated
}
// CRITICAL FIX: Add totalNodes and totalEdges from in-memory counts
// HNSW rebuild depends on these fields to determine entity count
return {
...statistics,
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount
}
}
// Successfully loaded statistics from storage
@ -2797,7 +2809,15 @@ export class S3CompatibleStorage extends BaseStorage {
} catch (error: any) {
this.logger.warn('Error getting statistics data, returning cached or null:', error)
// Return cached data if available, even if stale, rather than throwing
return this.statisticsCache || null
// CRITICAL FIX: Add totalNodes and totalEdges when returning cached data
if (this.statisticsCache) {
return {
...this.statisticsCache,
totalNodes: this.totalNounCount,
totalEdges: this.totalVerbCount
}
}
return null
}
}