2025-08-26 12:32:21 -07:00
/ * *
* Memory Storage Adapter
* In - memory storage adapter for environments where persistent storage is not available or needed
* /
2025-10-17 12:29:27 -07:00
import {
GraphVerb ,
HNSWNoun ,
HNSWVerb ,
NounMetadata ,
VerbMetadata ,
HNSWNounWithMetadata ,
HNSWVerbWithMetadata ,
StatisticsData
} from '../../coreTypes.js'
2025-08-26 12:32:21 -07:00
import { BaseStorage , STATISTICS_KEY } from '../baseStorage.js'
import { PaginatedResult } from '../../types/paginationTypes.js'
// No type aliases needed - using the original types directly
/ * *
* In - memory storage adapter
* Uses Maps to store data in memory
* /
export class MemoryStorage extends BaseStorage {
// Single map of noun ID to noun
private nouns : Map < string , HNSWNoun > = new Map ( )
private verbs : Map < string , HNSWVerb > = new Map ( )
private statistics : StatisticsData | null = null
2025-10-09 13:10:06 -07:00
// Unified object store for primitive operations (replaces metadata, nounMetadata, verbMetadata)
private objectStore : Map < string , any > = new Map ( )
// Backward compatibility aliases
private get metadata ( ) : Map < string , any > {
return this . objectStore
}
private get nounMetadata ( ) : Map < string , any > {
return this . objectStore
}
private get verbMetadata ( ) : Map < string , any > {
return this . objectStore
}
2025-08-26 12:32:21 -07:00
constructor ( ) {
super ( )
}
/ * *
* Initialize the storage adapter
* Nothing to initialize for in - memory storage
* /
public async init ( ) : Promise < void > {
this . isInitialized = true
}
/ * *
2025-10-17 12:29:27 -07:00
* Save a noun to storage ( v4.0.0 : pure vector only , no metadata )
2025-08-26 12:32:21 -07:00
* /
protected async saveNoun_internal ( noun : HNSWNoun ) : Promise < void > {
2025-09-22 15:45:35 -07:00
const isNew = ! this . nouns . has ( noun . id )
2025-08-26 12:32:21 -07:00
// Create a deep copy to avoid reference issues
2025-10-17 12:29:27 -07:00
// v4.0.0: Store ONLY vector data (no metadata field)
// Metadata is saved separately via saveNounMetadata() by base class
2025-08-26 12:32:21 -07:00
const nounCopy : HNSWNoun = {
id : noun.id ,
vector : [ . . . noun . vector ] ,
connections : new Map ( ) ,
2025-10-10 16:25:51 -07:00
level : noun.level || 0
2025-10-17 12:29:27 -07:00
// ✅ NO metadata field in v4.0.0
2025-08-26 12:32:21 -07:00
}
// Copy connections
for ( const [ level , connections ] of noun . connections . entries ( ) ) {
nounCopy . connections . set ( level , new Set ( connections ) )
}
// Save the noun directly in the nouns map
this . nouns . set ( noun . id , nounCopy )
2025-09-22 15:45:35 -07:00
2025-10-17 12:29:27 -07:00
// Note: Count tracking happens in saveNounMetadata since type info is in metadata now
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-17 12:29:27 -07:00
* Get a noun from storage ( v4.0.0 : returns pure vector only )
* Base class handles combining with metadata
2025-08-26 12:32:21 -07:00
* /
protected async getNoun_internal ( id : string ) : Promise < HNSWNoun | null > {
// Get the noun directly from the nouns map
const noun = this . nouns . get ( id )
// If not found, return null
if ( ! noun ) {
return null
}
// Return a deep copy to avoid reference issues
2025-10-17 12:29:27 -07:00
// v4.0.0: Return ONLY vector data (no metadata field)
2025-08-26 12:32:21 -07:00
const nounCopy : HNSWNoun = {
id : noun.id ,
vector : [ . . . noun . vector ] ,
connections : new Map ( ) ,
2025-10-10 16:25:51 -07:00
level : noun.level || 0
2025-10-17 12:29:27 -07:00
// ✅ NO metadata field in v4.0.0
2025-08-26 12:32:21 -07:00
}
// Copy connections
for ( const [ level , connections ] of noun . connections . entries ( ) ) {
nounCopy . connections . set ( level , new Set ( connections ) )
}
2025-10-17 12:29:27 -07:00
return nounCopy
2025-08-26 12:32:21 -07:00
}
/ * *
* Get nouns with pagination and filtering
2025-10-17 12:29:27 -07:00
* v4.0.0 : Returns HNSWNounWithMetadata [ ] ( includes metadata field )
2025-08-26 12:32:21 -07:00
* @param options Pagination and filtering options
2025-10-17 12:29:27 -07:00
* @returns Promise that resolves to a paginated result of nouns with metadata
2025-08-26 12:32:21 -07:00
* /
public async getNouns ( options : {
pagination ? : {
offset? : number
limit? : number
cursor? : string
}
filter ? : {
nounType? : string | string [ ]
service? : string | string [ ]
metadata? : Record < string , any >
}
2025-10-17 12:29:27 -07:00
} = { } ) : Promise < { items : HNSWNounWithMetadata [ ] ; totalCount? : number ; hasMore : boolean ; nextCursor? : string } > {
2025-08-26 12:32:21 -07:00
const pagination = options . pagination || { }
const filter = options . filter || { }
// Default values
const offset = pagination . offset || 0
const limit = pagination . limit || 100
// Convert string types to arrays for consistent handling
const nounTypes = filter . nounType
? Array . isArray ( filter . nounType ) ? filter . nounType : [ filter . nounType ]
: undefined
const services = filter . service
? Array . isArray ( filter . service ) ? filter . service : [ filter . service ]
: undefined
// First, collect all noun IDs that match the filter criteria
const matchingIds : string [ ] = [ ]
// Iterate through all nouns to find matches
2025-10-17 12:29:27 -07:00
// v4.0.0: Load metadata from separate storage (no embedded metadata field)
2025-08-26 12:32:21 -07:00
for ( const [ nounId , noun ] of this . nouns . entries ( ) ) {
2025-10-17 12:29:27 -07:00
// Get metadata from separate storage
const metadata = await this . getNounMetadata ( nounId )
// Skip if no metadata (shouldn't happen in v4.0.0 but be defensive)
if ( ! metadata ) {
continue
}
2025-08-26 12:32:21 -07:00
// Filter by noun type if specified
2025-09-11 16:23:32 -07:00
if ( nounTypes && metadata . noun && ! nounTypes . includes ( metadata . noun ) ) {
2025-08-26 12:32:21 -07:00
continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by service if specified
if ( services && metadata . service && ! services . includes ( metadata . service ) ) {
continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by metadata fields if specified
if ( filter . metadata ) {
let metadataMatch = true
for ( const [ key , value ] of Object . entries ( filter . metadata ) ) {
if ( metadata [ key ] !== value ) {
metadataMatch = false
break
}
}
if ( ! metadataMatch ) continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// If we got here, the noun matches all filters
matchingIds . push ( nounId )
}
// Calculate pagination
const totalCount = matchingIds . length
const paginatedIds = matchingIds . slice ( offset , offset + limit )
const hasMore = offset + limit < totalCount
// Create cursor for next page if there are more results
const nextCursor = hasMore ? ` ${ offset + limit } ` : undefined
// Fetch the actual nouns for the current page
2025-10-17 12:29:27 -07:00
// v4.0.0: Return HNSWNounWithMetadata (includes metadata field)
const items : HNSWNounWithMetadata [ ] = [ ]
2025-08-26 12:32:21 -07:00
for ( const id of paginatedIds ) {
const noun = this . nouns . get ( id )
if ( ! noun ) continue
2025-10-17 12:29:27 -07:00
// Get metadata from separate storage
const metadata = await this . getNounMetadata ( id )
if ( ! metadata ) continue // Skip if no metadata
// v4.0.0: Create HNSWNounWithMetadata with metadata field
const nounWithMetadata : HNSWNounWithMetadata = {
id : noun.id ,
vector : [ . . . noun . vector ] ,
connections : new Map ( ) ,
level : noun.level || 0 ,
metadata : metadata // Include metadata field
}
2025-08-26 12:32:21 -07:00
// Copy connections
for ( const [ level , connections ] of noun . connections . entries ( ) ) {
2025-10-17 12:29:27 -07:00
nounWithMetadata . connections . set ( level , new Set ( connections ) )
2025-08-26 12:32:21 -07:00
}
2025-10-17 12:29:27 -07:00
items . push ( nounWithMetadata )
2025-08-26 12:32:21 -07:00
}
return {
items ,
totalCount ,
hasMore ,
nextCursor
}
}
/ * *
* Get nouns with pagination - simplified interface for compatibility
2025-10-17 12:29:27 -07:00
* v4.0.0 : Returns HNSWNounWithMetadata [ ] ( includes metadata field )
2025-08-26 12:32:21 -07:00
* /
public async getNounsWithPagination ( options : {
limit? : number
cursor? : string
filter? : any
} = { } ) : Promise < {
2025-10-17 12:29:27 -07:00
items : HNSWNounWithMetadata [ ]
2025-08-26 12:32:21 -07:00
totalCount : number
hasMore : boolean
nextCursor? : string
} > {
// Convert to the getNouns format
const result = await this . getNouns ( {
pagination : {
offset : options.cursor ? parseInt ( options . cursor ) : 0 ,
limit : options.limit || 100
} ,
filter : options.filter
} )
return {
items : result.items ,
totalCount : result.totalCount || 0 ,
hasMore : result.hasMore ,
nextCursor : result.nextCursor
}
}
/ * *
* Get nouns by noun type
* @param nounType The noun type to filter by
* @returns Promise that resolves to an array of nouns of the specified noun type
* @deprecated Use getNouns ( ) with filter . nounType instead
* /
protected async getNounsByNounType_internal ( nounType : string ) : Promise < HNSWNoun [ ] > {
const result = await this . getNouns ( {
filter : {
nounType
}
} )
return result . items
}
/ * *
2025-10-17 12:29:27 -07:00
* Delete a noun from storage ( v4 . 0.0 )
2025-08-26 12:32:21 -07:00
* /
protected async deleteNoun_internal ( id : string ) : Promise < void > {
2025-10-17 12:29:27 -07:00
// v4.0.0: Get type from separate metadata storage
const metadata = await this . getNounMetadata ( id )
if ( metadata ) {
const type = metadata . noun || 'default'
2025-09-22 15:45:35 -07:00
this . decrementEntityCount ( type )
}
2025-08-26 12:32:21 -07:00
this . nouns . delete ( id )
}
/ * *
2025-10-17 12:29:27 -07:00
* Save a verb to storage ( v4.0.0 : pure vector + core fields , no metadata )
2025-08-26 12:32:21 -07:00
* /
protected async saveVerb_internal ( verb : HNSWVerb ) : Promise < void > {
2025-09-22 15:45:35 -07:00
const isNew = ! this . verbs . has ( verb . id )
2025-08-26 12:32:21 -07:00
// Create a deep copy to avoid reference issues
2025-10-17 12:29:27 -07:00
// v4.0.0: Include core relational fields but NO metadata field
2025-08-26 12:32:21 -07:00
const verbCopy : HNSWVerb = {
id : verb.id ,
vector : [ . . . verb . vector ] ,
fix: metadata explosion bug - 69K files reduced to ~1K
Critical fix for metadata indexing that was creating 60+ chunk files per entity.
Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).
Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)
Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading
Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing
Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:10:31 -07:00
connections : new Map ( ) ,
2025-10-17 12:29:27 -07:00
// CORE RELATIONAL DATA (part of HNSWVerb in v4.0.0)
fix: metadata explosion bug - 69K files reduced to ~1K
Critical fix for metadata indexing that was creating 60+ chunk files per entity.
Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).
Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)
Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading
Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing
Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:10:31 -07:00
verb : verb.verb ,
sourceId : verb.sourceId ,
2025-10-17 12:29:27 -07:00
targetId : verb.targetId
// ✅ NO metadata field in v4.0.0
2025-08-26 12:32:21 -07:00
}
// Copy connections
for ( const [ level , connections ] of verb . connections . entries ( ) ) {
verbCopy . connections . set ( level , new Set ( connections ) )
}
// Save the verb directly in the verbs map
this . verbs . set ( verb . id , verbCopy )
2025-09-22 15:45:35 -07:00
2025-10-17 12:29:27 -07:00
// Note: Count tracking happens in saveVerbMetadata since metadata is separate
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-17 12:29:27 -07:00
* Get a verb from storage ( v4.0.0 : returns pure vector + core fields )
* Base class handles combining with metadata
2025-08-26 12:32:21 -07:00
* /
protected async getVerb_internal ( id : string ) : Promise < HNSWVerb | null > {
// Get the verb directly from the verbs map
const verb = this . verbs . get ( id )
// If not found, return null
if ( ! verb ) {
return null
}
// Return a deep copy of the HNSWVerb
2025-10-17 12:29:27 -07:00
// v4.0.0: Include core relational fields but NO metadata field
2025-08-26 12:32:21 -07:00
const verbCopy : HNSWVerb = {
id : verb.id ,
vector : [ . . . verb . vector ] ,
fix: metadata explosion bug - 69K files reduced to ~1K
Critical fix for metadata indexing that was creating 60+ chunk files per entity.
Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).
Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)
Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading
Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing
Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:10:31 -07:00
connections : new Map ( ) ,
2025-10-17 12:29:27 -07:00
// CORE RELATIONAL DATA (part of HNSWVerb in v4.0.0)
fix: metadata explosion bug - 69K files reduced to ~1K
Critical fix for metadata indexing that was creating 60+ chunk files per entity.
Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).
Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)
Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading
Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing
Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:10:31 -07:00
verb : verb.verb ,
sourceId : verb.sourceId ,
2025-10-17 12:29:27 -07:00
targetId : verb.targetId
// ✅ NO metadata field in v4.0.0
2025-08-26 12:32:21 -07:00
}
// Copy connections
for ( const [ level , connections ] of verb . connections . entries ( ) ) {
verbCopy . connections . set ( level , new Set ( connections ) )
}
fix: metadata explosion bug - 69K files reduced to ~1K
Critical fix for metadata indexing that was creating 60+ chunk files per entity.
Root cause: Vector embeddings (384-dimensional arrays) were being indexed in
metadata, causing each dimension to create a separate chunk file with numeric
field names ("0", "1", "2", etc.).
Changes:
- Modified extractIndexableFields() to exclude vector/embedding fields
- Added NEVER_INDEX set: ['vector', 'embedding', 'embeddings', 'connections']
- Added safety check to skip arrays > 10 elements
- Preserves small array indexing (tags, categories, roles)
Impact:
- Reduces metadata files from 69,429 → ~1,200 (58x reduction)
- Fixes server initialization hangs
- Fixes metadata batch loading stalling at batch 23
- Fixes VFS getDescendants() hanging with large datasets
- Fixes Graph View UI not loading
Test Results:
- 7/7 integration tests passing
- Verified: 6 chunk files for 10 entities (was 7,210 before fix)
- 611/622 unit tests passing
Files Modified:
- src/utils/metadataIndex.ts - Core fix
- src/coreTypes.ts - HNSWVerb type enforcement with VerbType enum
- src/storage/adapters/* - Include core relational fields in HNSWVerb
- src/storage/adapters/baseStorageAdapter.ts - Type enforcement (HNSWNoun, GraphVerb)
- tests/integration/metadata-vector-exclusion.test.ts - Comprehensive test coverage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:10:31 -07:00
return verbCopy
2025-08-26 12:32:21 -07:00
}
/ * *
* Get verbs with pagination and filtering
2025-10-17 12:29:27 -07:00
* v4.0.0 : Returns HNSWVerbWithMetadata [ ] ( includes metadata field )
2025-08-26 12:32:21 -07:00
* @param options Pagination and filtering options
2025-10-17 12:29:27 -07:00
* @returns Promise that resolves to a paginated result of verbs with metadata
2025-08-26 12:32:21 -07:00
* /
public async getVerbs ( options : {
pagination ? : {
offset? : number
limit? : number
cursor? : string
}
filter ? : {
verbType? : string | string [ ]
sourceId? : string | string [ ]
targetId? : string | string [ ]
service? : string | string [ ]
metadata? : Record < string , any >
}
2025-10-17 12:29:27 -07:00
} = { } ) : Promise < { items : HNSWVerbWithMetadata [ ] ; totalCount? : number ; hasMore : boolean ; nextCursor? : string } > {
2025-08-26 12:32:21 -07:00
const pagination = options . pagination || { }
const filter = options . filter || { }
// Default values
const offset = pagination . offset || 0
const limit = pagination . limit || 100
// Convert string types to arrays for consistent handling
const verbTypes = filter . verbType
? Array . isArray ( filter . verbType ) ? filter . verbType : [ filter . verbType ]
: undefined
const sourceIds = filter . sourceId
? Array . isArray ( filter . sourceId ) ? filter . sourceId : [ filter . sourceId ]
: undefined
const targetIds = filter . targetId
? Array . isArray ( filter . targetId ) ? filter . targetId : [ filter . targetId ]
: undefined
const services = filter . service
? Array . isArray ( filter . service ) ? filter . service : [ filter . service ]
: undefined
// First, collect all verb IDs that match the filter criteria
const matchingIds : string [ ] = [ ]
// Iterate through all verbs to find matches
2025-10-17 12:29:27 -07:00
// v4.0.0: Core fields (verb, sourceId, targetId) are in HNSWVerb, not metadata
2025-08-26 12:32:21 -07:00
for ( const [ verbId , hnswVerb ] of this . verbs . entries ( ) ) {
2025-10-17 12:29:27 -07:00
// Get the metadata for service/data filtering
2025-10-09 16:33:08 -07:00
const metadata = await this . getVerbMetadata ( verbId )
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by verb type if specified
2025-10-17 12:29:27 -07:00
// v4.0.0: verb type is in HNSWVerb.verb
if ( verbTypes && ! verbTypes . includes ( hnswVerb . verb || '' ) ) {
2025-08-26 12:32:21 -07:00
continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by source ID if specified
2025-10-17 12:29:27 -07:00
// v4.0.0: sourceId is in HNSWVerb.sourceId
if ( sourceIds && ! sourceIds . includes ( hnswVerb . sourceId || '' ) ) {
2025-08-26 12:32:21 -07:00
continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by target ID if specified
2025-10-17 12:29:27 -07:00
// v4.0.0: targetId is in HNSWVerb.targetId
if ( targetIds && ! targetIds . includes ( hnswVerb . targetId || '' ) ) {
2025-08-26 12:32:21 -07:00
continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by metadata fields if specified
2025-10-17 12:29:27 -07:00
if ( filter . metadata && metadata ) {
2025-08-26 12:32:21 -07:00
let metadataMatch = true
for ( const [ key , value ] of Object . entries ( filter . metadata ) ) {
2025-10-17 12:29:27 -07:00
const metadataValue = ( metadata as any ) [ key ]
if ( metadataValue !== value ) {
2025-08-26 12:32:21 -07:00
metadataMatch = false
break
}
}
if ( ! metadataMatch ) continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// Filter by service if specified
2025-10-17 12:29:27 -07:00
if ( services && metadata && metadata . service && ! services . includes ( metadata . service ) ) {
2025-08-26 12:32:21 -07:00
continue
}
2025-10-17 12:29:27 -07:00
2025-08-26 12:32:21 -07:00
// If we got here, the verb matches all filters
matchingIds . push ( verbId )
}
// Calculate pagination
const totalCount = matchingIds . length
const paginatedIds = matchingIds . slice ( offset , offset + limit )
const hasMore = offset + limit < totalCount
// Create cursor for next page if there are more results
const nextCursor = hasMore ? ` ${ offset + limit } ` : undefined
// Fetch the actual verbs for the current page
2025-10-17 12:29:27 -07:00
// v4.0.0: Return HNSWVerbWithMetadata (includes metadata field)
const items : HNSWVerbWithMetadata [ ] = [ ]
2025-08-26 12:32:21 -07:00
for ( const id of paginatedIds ) {
const hnswVerb = this . verbs . get ( id )
if ( ! hnswVerb ) continue
2025-10-17 12:29:27 -07:00
// Get metadata from separate storage
const metadata = await this . getVerbMetadata ( id )
if ( ! metadata ) continue // Skip if no metadata
// v4.0.0: Create HNSWVerbWithMetadata with metadata field
const verbWithMetadata : HNSWVerbWithMetadata = {
2025-08-26 12:32:21 -07:00
id : hnswVerb.id ,
vector : [ . . . hnswVerb . vector ] ,
2025-10-17 12:29:27 -07:00
connections : new Map ( ) ,
// Core relational fields (part of HNSWVerb)
verb : hnswVerb.verb ,
sourceId : hnswVerb.sourceId ,
targetId : hnswVerb.targetId ,
// Metadata field
metadata : metadata
2025-08-26 12:32:21 -07:00
}
2025-10-17 12:29:27 -07:00
// Copy connections
for ( const [ level , connections ] of hnswVerb . connections . entries ( ) ) {
verbWithMetadata . connections . set ( level , new Set ( connections ) )
}
items . push ( verbWithMetadata )
2025-08-26 12:32:21 -07:00
}
return {
items ,
totalCount ,
hasMore ,
nextCursor
}
}
/ * *
* Get verbs by source
* @deprecated Use getVerbs ( ) with filter . sourceId instead
* /
2025-10-17 12:29:27 -07:00
protected async getVerbsBySource_internal ( sourceId : string ) : Promise < HNSWVerbWithMetadata [ ] > {
2025-08-26 12:32:21 -07:00
const result = await this . getVerbs ( {
filter : {
sourceId
}
} )
return result . items
}
/ * *
* Get verbs by target
* @deprecated Use getVerbs ( ) with filter . targetId instead
* /
2025-10-17 12:29:27 -07:00
protected async getVerbsByTarget_internal ( targetId : string ) : Promise < HNSWVerbWithMetadata [ ] > {
2025-08-26 12:32:21 -07:00
const result = await this . getVerbs ( {
filter : {
targetId
}
} )
return result . items
}
/ * *
* Get verbs by type
* @deprecated Use getVerbs ( ) with filter . verbType instead
* /
2025-10-17 12:29:27 -07:00
protected async getVerbsByType_internal ( type : string ) : Promise < HNSWVerbWithMetadata [ ] > {
2025-08-26 12:32:21 -07:00
const result = await this . getVerbs ( {
filter : {
verbType : type
}
} )
return result . items
}
/ * *
* Delete a verb from storage
* /
protected async deleteVerb_internal ( id : string ) : Promise < void > {
2025-10-09 16:33:08 -07:00
// Delete the HNSWVerb from the verbs map
2025-08-26 12:32:21 -07:00
this . verbs . delete ( id )
2025-10-09 16:33:08 -07:00
// CRITICAL: Also delete verb metadata - this is what getVerbs() uses to find verbs
// Without this, getVerbsBySource() will still find "deleted" verbs via their metadata
const metadata = await this . getVerbMetadata ( id )
if ( metadata ) {
const verbType = metadata . verb || metadata . type || 'default'
2025-10-17 12:29:27 -07:00
this . decrementVerbCount ( verbType as string )
2025-10-09 16:33:08 -07:00
// Delete the metadata using the base storage method
await this . deleteVerbMetadata ( id )
}
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-09 13:10:06 -07:00
* Primitive operation : Write object to path
* All metadata operations use this internally via base class routing
2025-08-26 12:32:21 -07:00
* /
2025-10-09 13:10:06 -07:00
protected async writeObjectToPath ( path : string , data : any ) : Promise < void > {
// Store in unified object store using path as key
this . objectStore . set ( path , JSON . parse ( JSON . stringify ( data ) ) )
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-09 13:10:06 -07:00
* Primitive operation : Read object from path
* All metadata operations use this internally via base class routing
2025-08-26 12:32:21 -07:00
* /
2025-10-09 13:10:06 -07:00
protected async readObjectFromPath ( path : string ) : Promise < any | null > {
const data = this . objectStore . get ( path )
if ( ! data ) {
2025-08-26 12:32:21 -07:00
return null
}
2025-10-09 13:10:06 -07:00
return JSON . parse ( JSON . stringify ( data ) )
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-09 13:10:06 -07:00
* Primitive operation : Delete object from path
* All metadata operations use this internally via base class routing
2025-08-26 12:32:21 -07:00
* /
2025-10-09 13:10:06 -07:00
protected async deleteObjectFromPath ( path : string ) : Promise < void > {
this . objectStore . delete ( path )
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-09 13:10:06 -07:00
* Primitive operation : List objects under path prefix
* All metadata operations use this internally via base class routing
2025-08-26 12:32:21 -07:00
* /
2025-10-09 13:10:06 -07:00
protected async listObjectsUnderPath ( prefix : string ) : Promise < string [ ] > {
const paths : string [ ] = [ ]
for ( const key of this . objectStore . keys ( ) ) {
if ( key . startsWith ( prefix ) ) {
paths . push ( key )
}
2025-08-26 12:32:21 -07:00
}
2025-10-09 13:10:06 -07:00
return paths . sort ( )
2025-08-26 12:32:21 -07:00
}
/ * *
2025-10-09 13:10:06 -07:00
* Get multiple metadata objects in batches ( CRITICAL : Prevents socket exhaustion )
* Memory storage implementation is simple since all data is already in memory
2025-08-26 12:32:21 -07:00
* /
2025-10-09 13:10:06 -07:00
public async getMetadataBatch ( ids : string [ ] ) : Promise < Map < string , any > > {
const results = new Map < string , any > ( )
2025-08-26 12:32:21 -07:00
2025-10-09 13:10:06 -07:00
// Memory storage can handle all IDs at once since it's in-memory
for ( const id of ids ) {
2025-10-10 16:25:51 -07:00
// CRITICAL: Use getNounMetadata() instead of deprecated getMetadata()
// This ensures we fetch from the correct noun metadata store (2-file system)
const metadata = await this . getNounMetadata ( id )
2025-10-09 13:10:06 -07:00
if ( metadata ) {
results . set ( id , metadata )
}
2025-08-26 12:32:21 -07:00
}
2025-10-09 13:10:06 -07:00
return results
2025-08-26 12:32:21 -07:00
}
/ * *
* Clear all data from storage
* /
public async clear ( ) : Promise < void > {
this . nouns . clear ( )
this . verbs . clear ( )
2025-10-09 13:10:06 -07:00
this . objectStore . clear ( )
2025-08-26 12:32:21 -07:00
this . statistics = null
2025-10-09 13:10:06 -07:00
2025-08-26 12:32:21 -07:00
// Clear the statistics cache
this . statisticsCache = null
this . statisticsModified = false
}
/ * *
* Get information about storage usage and capacity
* /
public async getStorageStatus ( ) : Promise < {
type : string
used : number
quota : number | null
details? : Record < string , any >
} > {
return {
type : 'memory' ,
used : 0 , // In-memory storage doesn't have a meaningful size
quota : null , // In-memory storage doesn't have a quota
details : {
nodeCount : this.nouns.size ,
edgeCount : this.verbs.size ,
2025-10-09 13:10:06 -07:00
metadataCount : this.objectStore.size
2025-08-26 12:32:21 -07:00
}
}
}
/ * *
* Save statistics data to storage
* @param statistics The statistics data to save
* /
protected async saveStatisticsData ( statistics : StatisticsData ) : Promise < void > {
// For memory storage, we just need to store the statistics in memory
// Create a deep copy to avoid reference issues
this . statistics = {
nounCount : { . . . statistics . nounCount } ,
verbCount : { . . . statistics . verbCount } ,
metadataCount : { . . . statistics . metadataCount } ,
hnswIndexSize : statistics.hnswIndexSize ,
lastUpdated : statistics.lastUpdated ,
// Include serviceActivity if present
. . . ( statistics . serviceActivity && {
serviceActivity : Object.fromEntries (
Object . entries ( statistics . serviceActivity ) . map ( ( [ k , v ] ) = > [ k , { . . . v } ] )
)
} ) ,
// Include services if present
. . . ( statistics . services && {
services : statistics.services.map ( s = > ( { . . . s } ) )
} ) ,
// Include distributedConfig if present
. . . ( statistics . distributedConfig && {
distributedConfig : JSON.parse ( JSON . stringify ( statistics . distributedConfig ) )
} )
}
// Since this is in-memory, there's no need for time-based partitioning
// or legacy file handling
}
/ * *
* Get statistics data from storage
* @returns Promise that resolves to the statistics data or null if not found
* /
protected async getStatisticsData ( ) : Promise < StatisticsData | null > {
if ( ! this . statistics ) {
2025-10-11 09:05:16 -07:00
// CRITICAL FIX (v3.37.4): Statistics don't exist yet (first init)
// Return minimal stats with counts instead of null
// This prevents HNSW from seeing entityCount=0 during index rebuild
return {
nounCount : { } ,
verbCount : { } ,
metadataCount : { } ,
hnswIndexSize : 0 ,
totalNodes : this.totalNounCount ,
totalEdges : this.totalVerbCount ,
totalMetadata : 0 ,
lastUpdated : new Date ( ) . toISOString ( )
}
2025-08-26 12:32:21 -07:00
}
// Return a deep copy to avoid reference issues
return {
nounCount : { . . . this . statistics . nounCount } ,
verbCount : { . . . this . statistics . verbCount } ,
metadataCount : { . . . this . statistics . metadataCount } ,
hnswIndexSize : this.statistics.hnswIndexSize ,
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)
2025-10-10 17:48:40 -07:00
// 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 ,
2025-08-26 12:32:21 -07:00
lastUpdated : this.statistics.lastUpdated ,
// Include serviceActivity if present
. . . ( this . statistics . serviceActivity && {
serviceActivity : Object.fromEntries (
Object . entries ( this . statistics . serviceActivity ) . map ( ( [ k , v ] ) = > [ k , { . . . v } ] )
)
} ) ,
// Include services if present
. . . ( this . statistics . services && {
services : this.statistics.services.map ( s = > ( { . . . s } ) )
} ) ,
// Include distributedConfig if present
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)
2025-10-10 17:48:40 -07:00
. . . ( this . statistics . distributedConfig && {
distributedConfig : JSON.parse ( JSON . stringify ( this . statistics . distributedConfig ) )
2025-08-26 12:32:21 -07:00
} )
}
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)
2025-10-10 17:48:40 -07:00
2025-08-26 12:32:21 -07:00
// Since this is in-memory, there's no need for fallback mechanisms
// to check multiple storage locations
}
2025-09-22 15:45:35 -07:00
/ * *
2025-10-17 12:29:27 -07:00
* Initialize counts from in - memory storage - O ( 1 ) operation ( v4 . 0.0 )
2025-09-22 15:45:35 -07:00
* /
protected async initializeCounts ( ) : Promise < void > {
// For memory storage, initialize counts from current in-memory state
this . totalNounCount = this . nouns . size
2025-10-17 12:29:27 -07:00
this . totalVerbCount = this . verbs . size
2025-09-22 15:45:35 -07:00
2025-10-17 12:29:27 -07:00
// Initialize type-based counts by scanning metadata storage (v4.0.0)
2025-09-22 15:45:35 -07:00
this . entityCounts . clear ( )
this . verbCounts . clear ( )
2025-10-17 12:29:27 -07:00
// Count nouns by loading metadata for each
for ( const [ nounId , noun ] of this . nouns . entries ( ) ) {
const metadata = await this . getNounMetadata ( nounId )
if ( metadata ) {
const type = metadata . noun || 'default'
this . entityCounts . set ( type , ( this . entityCounts . get ( type ) || 0 ) + 1 )
}
2025-09-22 15:45:35 -07:00
}
2025-10-17 12:29:27 -07:00
// Count verbs by loading metadata for each
for ( const [ verbId , verb ] of this . verbs . entries ( ) ) {
const metadata = await this . getVerbMetadata ( verbId )
if ( metadata ) {
// VerbMetadata doesn't have verb type - that's in HNSWVerb now
// Use the verb's type from the HNSWVerb itself
const type = verb . verb || 'default'
this . verbCounts . set ( type , ( this . verbCounts . get ( type ) || 0 ) + 1 )
}
2025-09-22 15:45:35 -07:00
}
}
/ * *
* Persist counts to storage - no - op for memory storage
* /
protected async persistCounts ( ) : Promise < void > {
// No persistence needed for in-memory storage
// Counts are always accurate from the live data structures
}
2025-10-10 11:15:17 -07:00
// =============================================
// HNSW Index Persistence (v3.35.0+)
// =============================================
/ * *
* Get vector for a noun
* /
public async getNounVector ( id : string ) : Promise < number [ ] | null > {
const noun = this . nouns . get ( id )
return noun ? [ . . . noun . vector ] : null
}
/ * *
* Save HNSW graph data for a noun
* /
public async saveHNSWData ( nounId : string , hnswData : {
level : number
connections : Record < string , string [ ] >
} ) : Promise < void > {
// For memory storage, HNSW data is already in the noun object
// This method is a no-op since saveNoun already stores the full graph
// But we store it separately for consistency with other adapters
const path = ` hnsw/ ${ nounId } .json `
await this . writeObjectToPath ( path , hnswData )
}
/ * *
* Get HNSW graph data for a noun
* /
public async getHNSWData ( nounId : string ) : Promise < {
level : number
connections : Record < string , string [ ] >
} | null > {
const path = ` hnsw/ ${ nounId } .json `
const data = await this . readObjectFromPath ( path )
return data || null
}
/ * *
* Save HNSW system data ( entry point , max level )
* /
public async saveHNSWSystem ( systemData : {
entryPointId : string | null
maxLevel : number
} ) : Promise < void > {
const path = 'system/hnsw-system.json'
await this . writeObjectToPath ( path , systemData )
}
/ * *
* Get HNSW system data
* /
public async getHNSWSystem ( ) : Promise < {
entryPointId : string | null
maxLevel : number
} | null > {
const path = 'system/hnsw-system.json'
const data = await this . readObjectFromPath ( path )
return data || null
}
2025-08-26 12:32:21 -07:00
}