fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
/**
|
|
|
|
|
* Rebuild Counts Utility
|
|
|
|
|
*
|
|
|
|
|
* Scans storage and rebuilds counts.json from actual data
|
2026-01-27 15:38:21 -08:00
|
|
|
* Use this to fix databases affected by the count synchronization bug
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
*
|
|
|
|
|
* NO MOCKS - Production-ready implementation
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { BaseStorage } from '../storage/baseStorage.js'
|
|
|
|
|
|
|
|
|
|
export interface RebuildCountsResult {
|
|
|
|
|
/** Total number of entities (nouns) found */
|
|
|
|
|
nounCount: number
|
|
|
|
|
|
|
|
|
|
/** Total number of relationships (verbs) found */
|
|
|
|
|
verbCount: number
|
|
|
|
|
|
|
|
|
|
/** Entity counts by type */
|
|
|
|
|
entityCounts: Map<string, number>
|
|
|
|
|
|
|
|
|
|
/** Verb counts by type */
|
|
|
|
|
verbCounts: Map<string, number>
|
|
|
|
|
|
|
|
|
|
/** Processing time in milliseconds */
|
|
|
|
|
duration: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rebuild counts.json from actual storage data
|
|
|
|
|
*
|
|
|
|
|
* This scans all entities and relationships in storage and reconstructs
|
|
|
|
|
* the counts index from scratch. Use this to fix count desynchronization.
|
|
|
|
|
*
|
|
|
|
|
* @param storage - The storage adapter to rebuild counts for
|
|
|
|
|
* @returns Promise that resolves to rebuild statistics
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```typescript
|
|
|
|
|
* const brain = new Brainy({ storage: { type: 'filesystem', path: './brainy-data' } })
|
|
|
|
|
* await brain.init()
|
|
|
|
|
*
|
|
|
|
|
* const result = await rebuildCounts(brain.storage)
|
|
|
|
|
* console.log(`Rebuilt counts: ${result.nounCount} nouns, ${result.verbCount} verbs`)
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export async function rebuildCounts(storage: BaseStorage): Promise<RebuildCountsResult> {
|
|
|
|
|
const startTime = Date.now()
|
|
|
|
|
|
|
|
|
|
console.log('🔧 Rebuilding counts from storage...')
|
|
|
|
|
|
|
|
|
|
const entityCounts = new Map<string, number>()
|
|
|
|
|
const verbCounts = new Map<string, number>()
|
|
|
|
|
let totalNouns = 0
|
|
|
|
|
let totalVerbs = 0
|
|
|
|
|
|
|
|
|
|
// Scan all nouns using pagination
|
|
|
|
|
console.log('📊 Scanning entities...')
|
|
|
|
|
|
|
|
|
|
// Check if pagination method exists
|
|
|
|
|
const storageWithPagination = storage as any
|
|
|
|
|
if (typeof storageWithPagination.getNounsWithPagination !== 'function') {
|
|
|
|
|
throw new Error('Storage adapter does not support getNounsWithPagination')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let hasMore = true
|
2026-01-27 15:38:21 -08:00
|
|
|
let offset = 0 // Use offset-based pagination instead of cursor (bug fix for infinite loop)
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
|
|
|
|
|
while (hasMore) {
|
fix: resolve critical 378x pagination infinite loop bug (v5.7.11)
CRITICAL BUG FIX: Workshop team reported 1,360,000+ entities loaded instead of 3,593
(378x multiplier), causing 15-20 minute startup times making app completely unusable.
## Root Cause
Pagination implementation had fundamental cursor/offset mismatch across codebase:
1. HNSW/Graph rebuilds passed `cursor` parameter
2. Storage methods accepted `cursor` but never used it, defaulted offset=0
3. Every pagination call returned same first N entities infinitely
4. hasMore calculation bug (>= instead of >) caused true infinite loop
## Fixes Applied (15 bugs across 5 files)
### src/storage/baseStorage.ts (5 fixes)
- Line 1086: Document cursor parameter currently ignored (offset-based for now)
- Line 1191: Fix hasMore (>= to >) in getNounsWithPagination
- Line 1221: Document cursor parameter currently ignored
- Line 1305: Fix hasMore (>= to >) in getVerbsWithPagination
- Line 1631: Fix hasMore (>= to >) in getVerbs
### src/storage/adapters/optimizedS3Search.ts (2 fixes)
- Line 110: Fix hasMore (>= to >) for nouns
- Line 193: Fix hasMore (>= to >) for verbs
### src/hnsw/typeAwareHNSWIndex.ts (2 fixes)
- Line 455: Change cursor to offset-based pagination
- Line 533: Increment offset instead of updating cursor
### src/hnsw/hnswIndex.ts (2 fixes)
- Line 1095: Change cursor to offset-based pagination
- Line 1164: Increment offset instead of updating cursor
### src/utils/rebuildCounts.ts (4 fixes)
- Line 67: Change cursor to offset for nouns
- Line 85: Increment offset for nouns
- Line 98: Change cursor to offset for verbs
- Line 115: Increment offset for verbs
## Impact
BEFORE v5.7.11:
- ❌ Loading 1,360,000+ entities (378x multiplier)
- ❌ 15-20 minute startup times
- ❌ Application completely unusable
- ❌ Workshop team blocked from using disableAutoRebuild
AFTER v5.7.11:
- ✅ Loads correct entity count (3,593 entities)
- ✅ Fast startup (< 10 seconds for 3,600 entities)
- ✅ disableAutoRebuild works correctly
- ✅ No more infinite pagination loops
## Verification
Test with 50 entities shows:
- ✅ Correct count: 50 documents + 1 collection = 51 entities
- ✅ No 378x multiplier
- ✅ No infinite loop
- ✅ Fast rebuild completion
Resolves critical production blocker for Workshop team.
## Phase 2 (Future: v5.8.0)
Implement proper cursor-based pagination for stateless billion-scale support.
Current fix uses offset-based pagination which is sufficient for datasets
up to 10M entities.
Related: BRAINY_STARTUP_PERFORMANCE_BUG.md, BRAINY_V5_7_9_HNSW_BUG.md
2025-11-13 14:20:19 -08:00
|
|
|
const result: any = await storageWithPagination.getNounsWithPagination({
|
|
|
|
|
limit: 100,
|
2026-01-27 15:38:21 -08:00
|
|
|
offset // Pass offset for proper pagination (previously passed cursor which was ignored)
|
fix: resolve critical 378x pagination infinite loop bug (v5.7.11)
CRITICAL BUG FIX: Workshop team reported 1,360,000+ entities loaded instead of 3,593
(378x multiplier), causing 15-20 minute startup times making app completely unusable.
## Root Cause
Pagination implementation had fundamental cursor/offset mismatch across codebase:
1. HNSW/Graph rebuilds passed `cursor` parameter
2. Storage methods accepted `cursor` but never used it, defaulted offset=0
3. Every pagination call returned same first N entities infinitely
4. hasMore calculation bug (>= instead of >) caused true infinite loop
## Fixes Applied (15 bugs across 5 files)
### src/storage/baseStorage.ts (5 fixes)
- Line 1086: Document cursor parameter currently ignored (offset-based for now)
- Line 1191: Fix hasMore (>= to >) in getNounsWithPagination
- Line 1221: Document cursor parameter currently ignored
- Line 1305: Fix hasMore (>= to >) in getVerbsWithPagination
- Line 1631: Fix hasMore (>= to >) in getVerbs
### src/storage/adapters/optimizedS3Search.ts (2 fixes)
- Line 110: Fix hasMore (>= to >) for nouns
- Line 193: Fix hasMore (>= to >) for verbs
### src/hnsw/typeAwareHNSWIndex.ts (2 fixes)
- Line 455: Change cursor to offset-based pagination
- Line 533: Increment offset instead of updating cursor
### src/hnsw/hnswIndex.ts (2 fixes)
- Line 1095: Change cursor to offset-based pagination
- Line 1164: Increment offset instead of updating cursor
### src/utils/rebuildCounts.ts (4 fixes)
- Line 67: Change cursor to offset for nouns
- Line 85: Increment offset for nouns
- Line 98: Change cursor to offset for verbs
- Line 115: Increment offset for verbs
## Impact
BEFORE v5.7.11:
- ❌ Loading 1,360,000+ entities (378x multiplier)
- ❌ 15-20 minute startup times
- ❌ Application completely unusable
- ❌ Workshop team blocked from using disableAutoRebuild
AFTER v5.7.11:
- ✅ Loads correct entity count (3,593 entities)
- ✅ Fast startup (< 10 seconds for 3,600 entities)
- ✅ disableAutoRebuild works correctly
- ✅ No more infinite pagination loops
## Verification
Test with 50 entities shows:
- ✅ Correct count: 50 documents + 1 collection = 51 entities
- ✅ No 378x multiplier
- ✅ No infinite loop
- ✅ Fast rebuild completion
Resolves critical production blocker for Workshop team.
## Phase 2 (Future: v5.8.0)
Implement proper cursor-based pagination for stateless billion-scale support.
Current fix uses offset-based pagination which is sufficient for datasets
up to 10M entities.
Related: BRAINY_STARTUP_PERFORMANCE_BUG.md, BRAINY_V5_7_9_HNSW_BUG.md
2025-11-13 14:20:19 -08:00
|
|
|
})
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
|
|
|
|
|
for (const noun of result.items) {
|
|
|
|
|
const metadata = await storage.getNounMetadata(noun.id)
|
|
|
|
|
if (metadata?.noun) {
|
feat: visibility tier (public/internal/system) on nouns + verbs
Adds a reserved, top-level `visibility` field (mirrors the subtype rollout):
'public' (default, surfaced) | 'internal' (a consumer's app-internal data — hidden
from default find()/getRelations()/counts/stats, opt-in via includeInternal) |
'system' (Brainy plumbing, library-set only; the add()/relate() param narrows to
'public' | 'internal').
Fixes a real leak: the VFS root entity counted in getNounCount() and appeared in
find() (a fresh brain reported 1 entity). It is now visibility:'system' → excluded
from every user-facing surface (fresh brain reports 0).
- Reserved (STANDARD_ENTITY_FIELDS / STANDARD_VERB_FIELDS) — surfaced top-level on
reads, never in the custom metadata bag; a 'public'/'internal' value smuggled
through metadata is lifted to the field, 'system' dropped with a one-shot warning.
- Threaded through add/relate/update/updateRelation + their internal mirrors;
stored only when not 'public' so the common case stays lean.
- Default exclusion in counts (baseStorage, isCountedVisibility), find()/getRelations()
(hard candidate filter via excludeVisibility — keeps top-K/limit/offset correct),
and rebuildCounts; includeInternal/includeSystem opt-ins.
- VFS root marked 'system'.
Same on-disk shape as the 8.0 line, so it carries forward unchanged. Backward-compatible:
absent === 'public', so all existing data stays counted + returned.
Tests: tests/unit/brainy/visibility.test.ts 17/17 (fresh-brain getNounCount()===0,
internal hidden + opt-in, verb symmetry, top-level surfacing, metadata-spoof rejection).
1522 unit green; count-synchronization integration green.
2026-06-19 16:40:35 -07:00
|
|
|
// 8.0 visibility: the user-facing counts only include public entities.
|
|
|
|
|
// Internal/system entities (e.g. the VFS root) are excluded — keeping this
|
|
|
|
|
// rebuild consistent with the incremental gating in baseStorage.
|
|
|
|
|
const visibility = metadata.visibility
|
|
|
|
|
if (visibility === 'internal' || visibility === 'system') continue
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
const entityType = metadata.noun
|
|
|
|
|
entityCounts.set(entityType, (entityCounts.get(entityType) || 0) + 1)
|
|
|
|
|
totalNouns++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasMore = result.hasMore
|
2026-01-27 15:38:21 -08:00
|
|
|
offset += 100 // Increment offset for next page
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(` Found ${totalNouns} entities across ${entityCounts.size} types`)
|
|
|
|
|
|
|
|
|
|
// Scan all verbs using pagination
|
|
|
|
|
console.log('🔗 Scanning relationships...')
|
|
|
|
|
|
|
|
|
|
if (typeof storageWithPagination.getVerbsWithPagination !== 'function') {
|
|
|
|
|
throw new Error('Storage adapter does not support getVerbsWithPagination')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasMore = true
|
2026-01-27 15:38:21 -08:00
|
|
|
offset = 0 // Reset offset for verbs pagination
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
|
|
|
|
|
while (hasMore) {
|
fix: resolve critical 378x pagination infinite loop bug (v5.7.11)
CRITICAL BUG FIX: Workshop team reported 1,360,000+ entities loaded instead of 3,593
(378x multiplier), causing 15-20 minute startup times making app completely unusable.
## Root Cause
Pagination implementation had fundamental cursor/offset mismatch across codebase:
1. HNSW/Graph rebuilds passed `cursor` parameter
2. Storage methods accepted `cursor` but never used it, defaulted offset=0
3. Every pagination call returned same first N entities infinitely
4. hasMore calculation bug (>= instead of >) caused true infinite loop
## Fixes Applied (15 bugs across 5 files)
### src/storage/baseStorage.ts (5 fixes)
- Line 1086: Document cursor parameter currently ignored (offset-based for now)
- Line 1191: Fix hasMore (>= to >) in getNounsWithPagination
- Line 1221: Document cursor parameter currently ignored
- Line 1305: Fix hasMore (>= to >) in getVerbsWithPagination
- Line 1631: Fix hasMore (>= to >) in getVerbs
### src/storage/adapters/optimizedS3Search.ts (2 fixes)
- Line 110: Fix hasMore (>= to >) for nouns
- Line 193: Fix hasMore (>= to >) for verbs
### src/hnsw/typeAwareHNSWIndex.ts (2 fixes)
- Line 455: Change cursor to offset-based pagination
- Line 533: Increment offset instead of updating cursor
### src/hnsw/hnswIndex.ts (2 fixes)
- Line 1095: Change cursor to offset-based pagination
- Line 1164: Increment offset instead of updating cursor
### src/utils/rebuildCounts.ts (4 fixes)
- Line 67: Change cursor to offset for nouns
- Line 85: Increment offset for nouns
- Line 98: Change cursor to offset for verbs
- Line 115: Increment offset for verbs
## Impact
BEFORE v5.7.11:
- ❌ Loading 1,360,000+ entities (378x multiplier)
- ❌ 15-20 minute startup times
- ❌ Application completely unusable
- ❌ Workshop team blocked from using disableAutoRebuild
AFTER v5.7.11:
- ✅ Loads correct entity count (3,593 entities)
- ✅ Fast startup (< 10 seconds for 3,600 entities)
- ✅ disableAutoRebuild works correctly
- ✅ No more infinite pagination loops
## Verification
Test with 50 entities shows:
- ✅ Correct count: 50 documents + 1 collection = 51 entities
- ✅ No 378x multiplier
- ✅ No infinite loop
- ✅ Fast rebuild completion
Resolves critical production blocker for Workshop team.
## Phase 2 (Future: v5.8.0)
Implement proper cursor-based pagination for stateless billion-scale support.
Current fix uses offset-based pagination which is sufficient for datasets
up to 10M entities.
Related: BRAINY_STARTUP_PERFORMANCE_BUG.md, BRAINY_V5_7_9_HNSW_BUG.md
2025-11-13 14:20:19 -08:00
|
|
|
const result: any = await storageWithPagination.getVerbsWithPagination({
|
|
|
|
|
limit: 100,
|
2026-01-27 15:38:21 -08:00
|
|
|
offset // Pass offset for proper pagination (previously passed cursor which was ignored)
|
fix: resolve critical 378x pagination infinite loop bug (v5.7.11)
CRITICAL BUG FIX: Workshop team reported 1,360,000+ entities loaded instead of 3,593
(378x multiplier), causing 15-20 minute startup times making app completely unusable.
## Root Cause
Pagination implementation had fundamental cursor/offset mismatch across codebase:
1. HNSW/Graph rebuilds passed `cursor` parameter
2. Storage methods accepted `cursor` but never used it, defaulted offset=0
3. Every pagination call returned same first N entities infinitely
4. hasMore calculation bug (>= instead of >) caused true infinite loop
## Fixes Applied (15 bugs across 5 files)
### src/storage/baseStorage.ts (5 fixes)
- Line 1086: Document cursor parameter currently ignored (offset-based for now)
- Line 1191: Fix hasMore (>= to >) in getNounsWithPagination
- Line 1221: Document cursor parameter currently ignored
- Line 1305: Fix hasMore (>= to >) in getVerbsWithPagination
- Line 1631: Fix hasMore (>= to >) in getVerbs
### src/storage/adapters/optimizedS3Search.ts (2 fixes)
- Line 110: Fix hasMore (>= to >) for nouns
- Line 193: Fix hasMore (>= to >) for verbs
### src/hnsw/typeAwareHNSWIndex.ts (2 fixes)
- Line 455: Change cursor to offset-based pagination
- Line 533: Increment offset instead of updating cursor
### src/hnsw/hnswIndex.ts (2 fixes)
- Line 1095: Change cursor to offset-based pagination
- Line 1164: Increment offset instead of updating cursor
### src/utils/rebuildCounts.ts (4 fixes)
- Line 67: Change cursor to offset for nouns
- Line 85: Increment offset for nouns
- Line 98: Change cursor to offset for verbs
- Line 115: Increment offset for verbs
## Impact
BEFORE v5.7.11:
- ❌ Loading 1,360,000+ entities (378x multiplier)
- ❌ 15-20 minute startup times
- ❌ Application completely unusable
- ❌ Workshop team blocked from using disableAutoRebuild
AFTER v5.7.11:
- ✅ Loads correct entity count (3,593 entities)
- ✅ Fast startup (< 10 seconds for 3,600 entities)
- ✅ disableAutoRebuild works correctly
- ✅ No more infinite pagination loops
## Verification
Test with 50 entities shows:
- ✅ Correct count: 50 documents + 1 collection = 51 entities
- ✅ No 378x multiplier
- ✅ No infinite loop
- ✅ Fast rebuild completion
Resolves critical production blocker for Workshop team.
## Phase 2 (Future: v5.8.0)
Implement proper cursor-based pagination for stateless billion-scale support.
Current fix uses offset-based pagination which is sufficient for datasets
up to 10M entities.
Related: BRAINY_STARTUP_PERFORMANCE_BUG.md, BRAINY_V5_7_9_HNSW_BUG.md
2025-11-13 14:20:19 -08:00
|
|
|
})
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
|
|
|
|
|
for (const verb of result.items) {
|
|
|
|
|
if (verb.verb) {
|
feat: visibility tier (public/internal/system) on nouns + verbs
Adds a reserved, top-level `visibility` field (mirrors the subtype rollout):
'public' (default, surfaced) | 'internal' (a consumer's app-internal data — hidden
from default find()/getRelations()/counts/stats, opt-in via includeInternal) |
'system' (Brainy plumbing, library-set only; the add()/relate() param narrows to
'public' | 'internal').
Fixes a real leak: the VFS root entity counted in getNounCount() and appeared in
find() (a fresh brain reported 1 entity). It is now visibility:'system' → excluded
from every user-facing surface (fresh brain reports 0).
- Reserved (STANDARD_ENTITY_FIELDS / STANDARD_VERB_FIELDS) — surfaced top-level on
reads, never in the custom metadata bag; a 'public'/'internal' value smuggled
through metadata is lifted to the field, 'system' dropped with a one-shot warning.
- Threaded through add/relate/update/updateRelation + their internal mirrors;
stored only when not 'public' so the common case stays lean.
- Default exclusion in counts (baseStorage, isCountedVisibility), find()/getRelations()
(hard candidate filter via excludeVisibility — keeps top-K/limit/offset correct),
and rebuildCounts; includeInternal/includeSystem opt-ins.
- VFS root marked 'system'.
Same on-disk shape as the 8.0 line, so it carries forward unchanged. Backward-compatible:
absent === 'public', so all existing data stays counted + returned.
Tests: tests/unit/brainy/visibility.test.ts 17/17 (fresh-brain getNounCount()===0,
internal hidden + opt-in, verb symmetry, top-level surfacing, metadata-spoof rejection).
1522 unit green; count-synchronization integration green.
2026-06-19 16:40:35 -07:00
|
|
|
// 8.0 visibility: exclude internal/system edges from the user-facing counts.
|
|
|
|
|
if (verb.visibility === 'internal' || verb.visibility === 'system') continue
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
const verbType = verb.verb
|
|
|
|
|
verbCounts.set(verbType, (verbCounts.get(verbType) || 0) + 1)
|
|
|
|
|
totalVerbs++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasMore = result.hasMore
|
2026-01-27 15:38:21 -08:00
|
|
|
offset += 100 // Increment offset for next page
|
fix(storage): resolve count synchronization race condition across all storage adapters
Fixed critical bug where entity and relationship counts were not being tracked correctly
during add(), relate(), and import() operations. The root cause was a race condition where
count increment code tried to read metadata before it was saved to storage.
Core Fixes:
- Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved
- Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved
- Added verb type to VerbMetadata to avoid circular dependency during count tracking
- Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper)
Storage Adapter Cleanup:
- Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage
- Updated MemoryStorage comments to reflect centralized fix
- All count tracking now centralized in baseStorage (fixes ALL adapters automatically)
New Utilities:
- Added rebuildCounts utility to repair corrupted counts.json from actual storage data
- Added comprehensive integration tests for count synchronization across all operations
Verification:
- All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware)
- All code paths verified (add, relate, import, batch, update, delete)
- 599 tests passing (no regressions)
- No deadlocks (tests complete in 6s vs 150s+)
Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(` Found ${totalVerbs} relationships across ${verbCounts.size} types`)
|
|
|
|
|
|
|
|
|
|
// Update storage adapter's in-memory counts FIRST
|
|
|
|
|
storageWithPagination.totalNounCount = totalNouns
|
|
|
|
|
storageWithPagination.totalVerbCount = totalVerbs
|
|
|
|
|
storageWithPagination.entityCounts = entityCounts
|
|
|
|
|
storageWithPagination.verbCounts = verbCounts
|
|
|
|
|
|
|
|
|
|
// Mark counts as pending persist (required for flushCounts to actually persist)
|
|
|
|
|
storageWithPagination.pendingCountPersist = true
|
|
|
|
|
storageWithPagination.pendingCountOperations = 1
|
|
|
|
|
|
|
|
|
|
// Persist counts using storage adapter's own persist method
|
|
|
|
|
// This ensures counts.json is written correctly (compressed or uncompressed)
|
|
|
|
|
await storageWithPagination.flushCounts()
|
|
|
|
|
|
|
|
|
|
const duration = Date.now() - startTime
|
|
|
|
|
|
|
|
|
|
console.log(`✅ Counts rebuilt successfully in ${duration}ms`)
|
|
|
|
|
console.log(` Entities: ${totalNouns}`)
|
|
|
|
|
console.log(` Relationships: ${totalVerbs}`)
|
|
|
|
|
console.log('')
|
|
|
|
|
console.log('Entity breakdown:')
|
|
|
|
|
entityCounts.forEach((count, entityType) => {
|
|
|
|
|
console.log(` ${entityType}: ${count}`)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (verbCounts.size > 0) {
|
|
|
|
|
console.log('')
|
|
|
|
|
console.log('Relationship breakdown:')
|
|
|
|
|
verbCounts.forEach((count, verbType) => {
|
|
|
|
|
console.log(` ${verbType}: ${count}`)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
nounCount: totalNouns,
|
|
|
|
|
verbCount: totalVerbs,
|
|
|
|
|
entityCounts,
|
|
|
|
|
verbCounts,
|
|
|
|
|
duration
|
|
|
|
|
}
|
|
|
|
|
}
|