2025-08-26 12:32:21 -07:00
|
|
|
/**
|
|
|
|
|
* UnifiedCache - Single cache for both HNSW and MetadataIndex
|
|
|
|
|
* Prevents resource competition with cost-aware eviction
|
2025-10-10 14:09:30 -07:00
|
|
|
*
|
2026-01-27 15:38:21 -08:00
|
|
|
* Features:
|
2025-10-10 14:09:30 -07:00
|
|
|
* - Adaptive sizing: Automatically scales from 2GB to 128GB+ based on available memory
|
|
|
|
|
* - Container-aware: Detects Docker/K8s limits (cgroups v1/v2)
|
|
|
|
|
* - Environment detection: Production vs development allocation strategies
|
|
|
|
|
* - Memory pressure monitoring: Warns when approaching limits
|
2025-08-26 12:32:21 -07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { prodLog } from './logger.js'
|
2025-10-10 14:09:30 -07:00
|
|
|
import {
|
|
|
|
|
getRecommendedCacheConfig,
|
|
|
|
|
formatBytes,
|
|
|
|
|
checkMemoryPressure,
|
|
|
|
|
type MemoryInfo,
|
|
|
|
|
type CacheAllocationStrategy
|
|
|
|
|
} from './memoryDetection.js'
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
import type { CacheProvider } from '../plugin.js'
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
export interface CacheItem {
|
|
|
|
|
key: string
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
type: 'vectors' | 'metadata' | 'embedding' | 'other'
|
2025-08-26 12:32:21 -07:00
|
|
|
data: any
|
|
|
|
|
size: number
|
|
|
|
|
rebuildCost: number // milliseconds to rebuild
|
|
|
|
|
lastAccess: number
|
|
|
|
|
accessCount: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UnifiedCacheConfig {
|
2025-10-10 14:09:30 -07:00
|
|
|
/** Maximum cache size in bytes (auto-detected if not specified) */
|
|
|
|
|
maxSize?: number
|
|
|
|
|
|
|
|
|
|
/** Minimum cache size in bytes (default 256MB) */
|
|
|
|
|
minSize?: number
|
|
|
|
|
|
|
|
|
|
/** Force development mode allocation (25% instead of 40-50%) */
|
|
|
|
|
developmentMode?: boolean
|
|
|
|
|
|
|
|
|
|
/** Enable request coalescing to prevent duplicate loads */
|
2025-08-26 12:32:21 -07:00
|
|
|
enableRequestCoalescing?: boolean
|
2025-10-10 14:09:30 -07:00
|
|
|
|
|
|
|
|
/** Enable fairness monitoring to prevent cache starvation */
|
2025-08-26 12:32:21 -07:00
|
|
|
enableFairnessCheck?: boolean
|
2025-10-10 14:09:30 -07:00
|
|
|
|
|
|
|
|
/** Fairness check interval in milliseconds */
|
|
|
|
|
fairnessCheckInterval?: number
|
|
|
|
|
|
|
|
|
|
/** Enable access pattern persistence for warm starts */
|
2025-08-26 12:32:21 -07:00
|
|
|
persistPatterns?: boolean
|
2025-10-10 14:09:30 -07:00
|
|
|
|
|
|
|
|
/** Enable memory pressure monitoring (default true) */
|
|
|
|
|
enableMemoryMonitoring?: boolean
|
|
|
|
|
|
|
|
|
|
/** Memory pressure check interval in milliseconds (default 30s) */
|
|
|
|
|
memoryCheckInterval?: number
|
2025-08-26 12:32:21 -07:00
|
|
|
}
|
|
|
|
|
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
/**
|
|
|
|
|
* Single cost-aware cache for HNSW and MetadataIndex.
|
|
|
|
|
*
|
|
|
|
|
* Implements {@link CacheProvider}: the surface Brainy calls on whatever cache
|
|
|
|
|
* is installed as the global cache (its own instance, or a registered
|
2026-07-02 15:11:41 -07:00
|
|
|
* `'cache'` provider such as Cor's native eviction engine).
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
*/
|
|
|
|
|
export class UnifiedCache implements CacheProvider {
|
2025-08-26 12:32:21 -07:00
|
|
|
private cache = new Map<string, CacheItem>()
|
|
|
|
|
private access = new Map<string, number>() // Access counts
|
|
|
|
|
private loadingPromises = new Map<string, Promise<any>>()
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
private typeAccessCounts = { vectors: 0, metadata: 0, embedding: 0, other: 0 }
|
2025-08-26 12:32:21 -07:00
|
|
|
private totalAccessCount = 0
|
|
|
|
|
private currentSize = 0
|
2026-03-22 16:52:02 -07:00
|
|
|
private maxSize: number
|
2025-08-26 12:32:21 -07:00
|
|
|
private readonly config: UnifiedCacheConfig
|
|
|
|
|
|
2026-01-27 15:38:21 -08:00
|
|
|
// Memory management
|
2025-10-10 14:09:30 -07:00
|
|
|
private readonly memoryInfo: MemoryInfo
|
|
|
|
|
private readonly allocationStrategy: CacheAllocationStrategy
|
|
|
|
|
private memoryPressureCheckTimer: NodeJS.Timeout | null = null
|
|
|
|
|
private lastMemoryWarning = 0
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
constructor(config: UnifiedCacheConfig = {}) {
|
2026-01-27 15:38:21 -08:00
|
|
|
// Adaptive cache sizing
|
2025-10-10 14:09:30 -07:00
|
|
|
const recommendation = getRecommendedCacheConfig({
|
|
|
|
|
manualSize: config.maxSize,
|
|
|
|
|
minSize: config.minSize,
|
|
|
|
|
developmentMode: config.developmentMode
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.memoryInfo = recommendation.memoryInfo
|
|
|
|
|
this.allocationStrategy = recommendation.allocation
|
|
|
|
|
this.maxSize = recommendation.allocation.cacheSize
|
|
|
|
|
|
2026-01-27 15:38:21 -08:00
|
|
|
// Log allocation decision (includes model memory)
|
2025-10-10 14:09:30 -07:00
|
|
|
prodLog.info(
|
|
|
|
|
`UnifiedCache initialized: ${formatBytes(this.maxSize)} ` +
|
|
|
|
|
`(${this.allocationStrategy.environment} mode, ` +
|
|
|
|
|
`${(this.allocationStrategy.ratio * 100).toFixed(0)}% of ${formatBytes(this.allocationStrategy.availableForCache)} ` +
|
|
|
|
|
`after ${formatBytes(this.allocationStrategy.modelMemory)} ${this.allocationStrategy.modelPrecision.toUpperCase()} model)`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Log memory detection details
|
|
|
|
|
prodLog.debug(
|
|
|
|
|
`Memory detection: source=${this.memoryInfo.source}, ` +
|
|
|
|
|
`container=${this.memoryInfo.isContainer}, ` +
|
|
|
|
|
`system=${formatBytes(this.memoryInfo.systemTotal)}, ` +
|
|
|
|
|
`free=${formatBytes(this.memoryInfo.free)}, ` +
|
|
|
|
|
`totalAvailable=${formatBytes(this.memoryInfo.available)}, ` +
|
|
|
|
|
`modelReserved=${formatBytes(this.allocationStrategy.modelMemory)}, ` +
|
|
|
|
|
`availableForCache=${formatBytes(this.allocationStrategy.availableForCache)}`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Log warnings if any
|
|
|
|
|
for (const warning of recommendation.warnings) {
|
|
|
|
|
prodLog.warn(`UnifiedCache: ${warning}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finalize configuration
|
2025-08-26 12:32:21 -07:00
|
|
|
this.config = {
|
|
|
|
|
enableRequestCoalescing: true,
|
|
|
|
|
enableFairnessCheck: true,
|
2026-01-27 15:38:21 -08:00
|
|
|
fairnessCheckInterval: 30000, // Check fairness every 30 seconds (was 60s)
|
2025-08-26 12:32:21 -07:00
|
|
|
persistPatterns: true,
|
2025-10-10 14:09:30 -07:00
|
|
|
enableMemoryMonitoring: true,
|
|
|
|
|
memoryCheckInterval: 30000, // Check memory every 30s
|
2025-08-26 12:32:21 -07:00
|
|
|
...config
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 14:09:30 -07:00
|
|
|
// Start monitoring
|
2025-08-26 12:32:21 -07:00
|
|
|
if (this.config.enableFairnessCheck) {
|
|
|
|
|
this.startFairnessMonitor()
|
|
|
|
|
}
|
2025-10-10 14:09:30 -07:00
|
|
|
|
|
|
|
|
if (this.config.enableMemoryMonitoring) {
|
|
|
|
|
this.startMemoryPressureMonitor()
|
|
|
|
|
}
|
2025-08-26 12:32:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get item from cache with request coalescing
|
|
|
|
|
*/
|
|
|
|
|
async get(key: string, loadFn?: () => Promise<any>): Promise<any> {
|
|
|
|
|
// Update access tracking
|
|
|
|
|
this.access.set(key, (this.access.get(key) || 0) + 1)
|
|
|
|
|
this.totalAccessCount++
|
|
|
|
|
|
|
|
|
|
// Check if in cache
|
|
|
|
|
const item = this.cache.get(key)
|
|
|
|
|
if (item) {
|
|
|
|
|
item.lastAccess = Date.now()
|
|
|
|
|
item.accessCount++
|
|
|
|
|
this.typeAccessCounts[item.type]++
|
|
|
|
|
return item.data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no load function, return undefined
|
|
|
|
|
if (!loadFn) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Request coalescing - prevent stampede
|
|
|
|
|
if (this.config.enableRequestCoalescing && this.loadingPromises.has(key)) {
|
|
|
|
|
prodLog.debug('Request coalescing for key:', key)
|
|
|
|
|
return this.loadingPromises.get(key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load data
|
|
|
|
|
const loadPromise = loadFn()
|
|
|
|
|
if (this.config.enableRequestCoalescing) {
|
|
|
|
|
this.loadingPromises.set(key, loadPromise)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const data = await loadPromise
|
|
|
|
|
return data
|
|
|
|
|
} finally {
|
|
|
|
|
if (this.config.enableRequestCoalescing) {
|
|
|
|
|
this.loadingPromises.delete(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 14:09:30 -07:00
|
|
|
/**
|
2026-01-27 15:38:21 -08:00
|
|
|
* Synchronous cache lookup
|
2025-10-10 14:09:30 -07:00
|
|
|
* Returns cached data immediately or undefined if not cached
|
|
|
|
|
* Use for sync fast path optimization - zero async overhead
|
|
|
|
|
*/
|
|
|
|
|
getSync(key: string): any | undefined {
|
|
|
|
|
// Check if in cache
|
|
|
|
|
const item = this.cache.get(key)
|
|
|
|
|
if (item) {
|
|
|
|
|
// Update access tracking synchronously
|
|
|
|
|
this.access.set(key, (this.access.get(key) || 0) + 1)
|
|
|
|
|
this.totalAccessCount++
|
|
|
|
|
item.lastAccess = Date.now()
|
|
|
|
|
item.accessCount++
|
|
|
|
|
this.typeAccessCounts[item.type]++
|
|
|
|
|
return item.data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
/**
|
|
|
|
|
* Set item in cache with cost-aware eviction
|
|
|
|
|
*/
|
|
|
|
|
set(
|
|
|
|
|
key: string,
|
|
|
|
|
data: any,
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
type: 'vectors' | 'metadata' | 'embedding' | 'other',
|
2025-08-26 12:32:21 -07:00
|
|
|
size: number,
|
|
|
|
|
rebuildCost: number = 1
|
|
|
|
|
): void {
|
|
|
|
|
// Make room if needed
|
|
|
|
|
while (this.currentSize + size > this.maxSize && this.cache.size > 0) {
|
|
|
|
|
this.evictLowestValue()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add to cache
|
|
|
|
|
const item: CacheItem = {
|
|
|
|
|
key,
|
|
|
|
|
type,
|
|
|
|
|
data,
|
|
|
|
|
size,
|
|
|
|
|
rebuildCost,
|
|
|
|
|
lastAccess: Date.now(),
|
|
|
|
|
accessCount: 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update or add
|
|
|
|
|
const existing = this.cache.get(key)
|
|
|
|
|
if (existing) {
|
|
|
|
|
this.currentSize -= existing.size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.cache.set(key, item)
|
|
|
|
|
this.currentSize += size
|
|
|
|
|
this.typeAccessCounts[type]++
|
|
|
|
|
this.totalAccessCount++
|
2025-10-13 11:50:53 -07:00
|
|
|
|
2026-01-27 15:38:21 -08:00
|
|
|
// Proactive fairness check: Check immediately if adding to a dominant type
|
2025-10-13 11:50:53 -07:00
|
|
|
// This prevents imbalance formation instead of reacting to it
|
|
|
|
|
if (this.config.enableFairnessCheck && this.cache.size > 10) {
|
|
|
|
|
this.checkProactiveFairness(type)
|
|
|
|
|
}
|
2025-08-26 12:32:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Evict item with lowest value (access count / rebuild cost)
|
|
|
|
|
*/
|
|
|
|
|
private evictLowestValue(): void {
|
|
|
|
|
let victim: string | null = null
|
|
|
|
|
let lowestScore = Infinity
|
|
|
|
|
|
|
|
|
|
for (const [key, item] of this.cache) {
|
2025-10-13 11:21:19 -07:00
|
|
|
// Calculate value score: access frequency * rebuild cost (higher is better)
|
2025-08-26 12:32:21 -07:00
|
|
|
const accessScore = (this.access.get(key) || 1)
|
2025-10-13 11:21:19 -07:00
|
|
|
const score = accessScore * item.rebuildCost
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
if (score < lowestScore) {
|
|
|
|
|
lowestScore = score
|
|
|
|
|
victim = key
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (victim) {
|
|
|
|
|
const item = this.cache.get(victim)!
|
|
|
|
|
prodLog.debug(`Evicting ${victim} (type: ${item.type}, score: ${lowestScore})`)
|
|
|
|
|
|
|
|
|
|
this.currentSize -= item.size
|
|
|
|
|
this.cache.delete(victim)
|
|
|
|
|
// Keep access count for a while to prevent re-caching cold items
|
|
|
|
|
// this.access.delete(victim) // Don't delete immediately
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Size-aware eviction - try to match needed size
|
|
|
|
|
*/
|
|
|
|
|
evictForSize(bytesNeeded: number): boolean {
|
|
|
|
|
const candidates: Array<[string, number, CacheItem]> = []
|
2025-10-13 11:21:19 -07:00
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
for (const [key, item] of this.cache) {
|
2025-10-13 11:21:19 -07:00
|
|
|
const score = (this.access.get(key) || 1) * item.rebuildCost
|
2025-08-26 12:32:21 -07:00
|
|
|
candidates.push([key, score, item])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by score (lower is worse)
|
|
|
|
|
candidates.sort((a, b) => a[1] - b[1])
|
|
|
|
|
|
|
|
|
|
let freedBytes = 0
|
|
|
|
|
const toEvict: string[] = []
|
|
|
|
|
|
|
|
|
|
// Try to free exactly what we need
|
|
|
|
|
for (const [key, , item] of candidates) {
|
|
|
|
|
toEvict.push(key)
|
|
|
|
|
freedBytes += item.size
|
|
|
|
|
if (freedBytes >= bytesNeeded) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Evict selected items
|
|
|
|
|
for (const key of toEvict) {
|
|
|
|
|
const item = this.cache.get(key)!
|
|
|
|
|
this.currentSize -= item.size
|
|
|
|
|
this.cache.delete(key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return freedBytes >= bytesNeeded
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fairness monitoring - prevent one type from hogging cache
|
|
|
|
|
*/
|
|
|
|
|
private startFairnessMonitor(): void {
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
this.checkFairness()
|
|
|
|
|
}, this.config.fairnessCheckInterval!)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private checkFairness(): void {
|
|
|
|
|
// Calculate type ratios in cache
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
const typeSizes = { vectors: 0, metadata: 0, embedding: 0, other: 0 }
|
|
|
|
|
const typeCounts = { vectors: 0, metadata: 0, embedding: 0, other: 0 }
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
for (const item of this.cache.values()) {
|
|
|
|
|
typeSizes[item.type] += item.size
|
|
|
|
|
typeCounts[item.type]++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate access ratios
|
|
|
|
|
const totalAccess = this.totalAccessCount || 1
|
|
|
|
|
const accessRatios = {
|
refactor(8.0): add config.vector path + 'vectors' cache category (scaffold step 2)
Step 2 of the brainy 8.0 rename scaffolding. Adds the new algorithm-neutral
config surface alongside the legacy config.hnsw path (which becomes a compat
shim removed in the final cleanup commit).
CHANGES
src/utils/recallPreset.ts (NEW)
- DEFAULT_RECALL = 'balanced'
- HNSW_PRESETS table — fast/balanced/accurate → (M, efConstruction, efSearch).
'balanced' equals brainy 7.x DEFAULT_CONFIG byte-for-byte so a 7.x → 8.0
upgrade with no explicit recall value is a no-op.
- resolveRecallHnswKnobs(preset) — preset → knobs.
- resolveJsHnswConfig(vectorConfig) — preset first, then `advanced.hnsw` overrides
win. Explicit knobs always beat the preset.
src/types/brainy.types.ts
- New optional `vector` field on BrainyConfig:
vector?: {
recall?: 'fast' | 'balanced' | 'accurate'
quantization?: { enabled?, bits?: 8 | 4, rerankMultiplier? }
vectorStorage?: 'memory' | 'lazy'
advanced?: {
hnsw?: { M?, efConstruction?, efSearch?, ml? }
diskann?: { pqM?, pqKsub?, maxDegree?, searchListSize?, alpha?, ... }
}
}
- `hnsw` field marked @deprecated with note that it stays as a compat shim
during the 8.0 rename and is removed in the final cleanup commit.
src/utils/unifiedCache.ts
- Cache-category union widened from
'hnsw' | 'metadata' | 'embedding' | 'other'
to
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
- 5 union sites + 4 typed-counter maps (typeAccessCounts, checkFairness
typeSizes/typeCounts/accessRatios/sizeRatios, getStats typeSizes/typeCounts,
evictType loop) all gained the 'vectors' key with initial value 0.
- The hard-coded fairness-check loop now iterates both keys.
- Final 8.0 cleanup will narrow back to 'vectors' | 'metadata' | 'embedding' | 'other'.
src/brainy.ts (normalizeConfig)
- Adds the new `vector` field to the Required<BrainyConfig> return shape so
TS compiles. Reads config?.vector ?? undefined as any (same pattern as
the other optional config fields).
NO-OP scope
No behavioural change. The new config.vector path is silently ignored at
runtime in this commit — wired up in step 4 (storage + index resolution).
Pure surface plumbing. Tests + build green.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
2026-06-09 13:06:10 -07:00
|
|
|
vectors: this.typeAccessCounts.vectors / totalAccess,
|
2025-08-26 12:32:21 -07:00
|
|
|
metadata: this.typeAccessCounts.metadata / totalAccess,
|
|
|
|
|
embedding: this.typeAccessCounts.embedding / totalAccess,
|
|
|
|
|
other: this.typeAccessCounts.other / totalAccess
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate size ratios
|
|
|
|
|
const totalSize = this.currentSize || 1
|
|
|
|
|
const sizeRatios = {
|
refactor(8.0): add config.vector path + 'vectors' cache category (scaffold step 2)
Step 2 of the brainy 8.0 rename scaffolding. Adds the new algorithm-neutral
config surface alongside the legacy config.hnsw path (which becomes a compat
shim removed in the final cleanup commit).
CHANGES
src/utils/recallPreset.ts (NEW)
- DEFAULT_RECALL = 'balanced'
- HNSW_PRESETS table — fast/balanced/accurate → (M, efConstruction, efSearch).
'balanced' equals brainy 7.x DEFAULT_CONFIG byte-for-byte so a 7.x → 8.0
upgrade with no explicit recall value is a no-op.
- resolveRecallHnswKnobs(preset) — preset → knobs.
- resolveJsHnswConfig(vectorConfig) — preset first, then `advanced.hnsw` overrides
win. Explicit knobs always beat the preset.
src/types/brainy.types.ts
- New optional `vector` field on BrainyConfig:
vector?: {
recall?: 'fast' | 'balanced' | 'accurate'
quantization?: { enabled?, bits?: 8 | 4, rerankMultiplier? }
vectorStorage?: 'memory' | 'lazy'
advanced?: {
hnsw?: { M?, efConstruction?, efSearch?, ml? }
diskann?: { pqM?, pqKsub?, maxDegree?, searchListSize?, alpha?, ... }
}
}
- `hnsw` field marked @deprecated with note that it stays as a compat shim
during the 8.0 rename and is removed in the final cleanup commit.
src/utils/unifiedCache.ts
- Cache-category union widened from
'hnsw' | 'metadata' | 'embedding' | 'other'
to
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
- 5 union sites + 4 typed-counter maps (typeAccessCounts, checkFairness
typeSizes/typeCounts/accessRatios/sizeRatios, getStats typeSizes/typeCounts,
evictType loop) all gained the 'vectors' key with initial value 0.
- The hard-coded fairness-check loop now iterates both keys.
- Final 8.0 cleanup will narrow back to 'vectors' | 'metadata' | 'embedding' | 'other'.
src/brainy.ts (normalizeConfig)
- Adds the new `vector` field to the Required<BrainyConfig> return shape so
TS compiles. Reads config?.vector ?? undefined as any (same pattern as
the other optional config fields).
NO-OP scope
No behavioural change. The new config.vector path is silently ignored at
runtime in this commit — wired up in step 4 (storage + index resolution).
Pure surface plumbing. Tests + build green.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
2026-06-09 13:06:10 -07:00
|
|
|
vectors: typeSizes.vectors / totalSize,
|
2025-08-26 12:32:21 -07:00
|
|
|
metadata: typeSizes.metadata / totalSize,
|
|
|
|
|
embedding: typeSizes.embedding / totalSize,
|
|
|
|
|
other: typeSizes.other / totalSize
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 15:38:21 -08:00
|
|
|
// Check for starvation (more aggressive - 70% cache with <15% accesses)
|
2025-10-13 11:50:53 -07:00
|
|
|
// Previous: 90% cache, <10% access (too lenient, caused thrashing)
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
for (const type of ['vectors', 'metadata', 'embedding', 'other'] as const) {
|
2025-10-13 11:50:53 -07:00
|
|
|
if (sizeRatios[type] > 0.7 && accessRatios[type] < 0.15) {
|
2025-08-26 12:32:21 -07:00
|
|
|
prodLog.warn(`Type ${type} is hogging cache (${(sizeRatios[type] * 100).toFixed(1)}% size, ${(accessRatios[type] * 100).toFixed(1)}% access)`)
|
|
|
|
|
this.evictType(type)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 11:50:53 -07:00
|
|
|
/**
|
2026-01-27 15:38:21 -08:00
|
|
|
* Proactive fairness check
|
2025-10-13 11:50:53 -07:00
|
|
|
* Called immediately when adding items to prevent imbalance formation
|
|
|
|
|
* Uses same thresholds as periodic check but runs on-demand
|
|
|
|
|
*/
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
private checkProactiveFairness(addedType: 'vectors' | 'metadata' | 'embedding' | 'other'): void {
|
2025-10-13 11:50:53 -07:00
|
|
|
// Quick check: only evaluate the type being added
|
|
|
|
|
let typeSize = 0
|
|
|
|
|
for (const item of this.cache.values()) {
|
|
|
|
|
if (item.type === addedType) {
|
|
|
|
|
typeSize += item.size
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sizeRatio = typeSize / (this.currentSize || 1)
|
|
|
|
|
const accessRatio = this.typeAccessCounts[addedType] / (this.totalAccessCount || 1)
|
|
|
|
|
|
|
|
|
|
// Same threshold as periodic check: 70% size, <15% access
|
|
|
|
|
if (sizeRatio > 0.7 && accessRatio < 0.15) {
|
|
|
|
|
prodLog.debug(`Proactive fairness: ${addedType} reaching dominance (${(sizeRatio * 100).toFixed(1)}% size, ${(accessRatio * 100).toFixed(1)}% access)`)
|
|
|
|
|
this.evictType(addedType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
/**
|
|
|
|
|
* Force evict items of a specific type
|
|
|
|
|
*/
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
private evictType(type: 'vectors' | 'metadata' | 'embedding' | 'other'): void {
|
2025-08-26 12:32:21 -07:00
|
|
|
const candidates: Array<[string, number, CacheItem]> = []
|
2025-10-13 11:21:19 -07:00
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
for (const [key, item] of this.cache) {
|
|
|
|
|
if (item.type === type) {
|
2025-10-13 11:21:19 -07:00
|
|
|
const score = (this.access.get(key) || 1) * item.rebuildCost
|
2025-08-26 12:32:21 -07:00
|
|
|
candidates.push([key, score, item])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by score (lower is worse)
|
|
|
|
|
candidates.sort((a, b) => a[1] - b[1])
|
|
|
|
|
|
2026-01-27 15:38:21 -08:00
|
|
|
// Evict bottom 50% of this type (was 20%, too slow to prevent thrashing)
|
2025-10-13 11:50:53 -07:00
|
|
|
const evictCount = Math.max(1, Math.floor(candidates.length * 0.5))
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
for (let i = 0; i < evictCount && i < candidates.length; i++) {
|
|
|
|
|
const [key, , item] = candidates[i]
|
|
|
|
|
this.currentSize -= item.size
|
|
|
|
|
this.cache.delete(key)
|
|
|
|
|
prodLog.debug(`Fairness eviction: ${key} (type: ${type})`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete specific item from cache
|
|
|
|
|
*/
|
|
|
|
|
delete(key: string): boolean {
|
|
|
|
|
const item = this.cache.get(key)
|
|
|
|
|
if (item) {
|
|
|
|
|
this.currentSize -= item.size
|
|
|
|
|
this.cache.delete(key)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 11:22:30 -08:00
|
|
|
/**
|
|
|
|
|
* Delete all items with keys starting with the given prefix
|
2026-01-27 15:38:21 -08:00
|
|
|
* Added for VFS cache invalidation (fixes stale parent ID bug)
|
2025-12-04 11:22:30 -08:00
|
|
|
* @param prefix - The key prefix to match
|
|
|
|
|
* @returns Number of items deleted
|
|
|
|
|
*/
|
|
|
|
|
deleteByPrefix(prefix: string): number {
|
|
|
|
|
let deleted = 0
|
|
|
|
|
for (const [key, item] of this.cache) {
|
|
|
|
|
if (key.startsWith(prefix)) {
|
|
|
|
|
this.currentSize -= item.size
|
|
|
|
|
this.cache.delete(key)
|
|
|
|
|
deleted++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return deleted
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 16:52:02 -07:00
|
|
|
/**
|
|
|
|
|
* Dynamically resize the cache maximum. If the new size is smaller than
|
|
|
|
|
* the current usage, evicts lowest-value items until the cache fits within
|
2026-07-02 15:11:41 -07:00
|
|
|
* the new budget. Used by Cor's ResourceManager to rebalance cache vs
|
2026-03-22 16:52:02 -07:00
|
|
|
* instance memory as brainy instances are created and evicted.
|
|
|
|
|
*
|
|
|
|
|
* @param newMaxSize - New maximum cache size in bytes
|
|
|
|
|
*/
|
|
|
|
|
setMaxSize(newMaxSize: number): void {
|
|
|
|
|
this.maxSize = Math.max(newMaxSize, 64 * 1024 * 1024) // Floor at 64MB
|
|
|
|
|
while (this.currentSize > this.maxSize && this.cache.size > 0) {
|
|
|
|
|
this.evictLowestValue()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the current maximum cache size in bytes.
|
|
|
|
|
*/
|
|
|
|
|
getMaxSize(): number {
|
|
|
|
|
return this.maxSize
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
/**
|
|
|
|
|
* Clear cache or specific type
|
|
|
|
|
*/
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
clear(type?: 'vectors' | 'metadata' | 'embedding' | 'other'): void {
|
2025-08-26 12:32:21 -07:00
|
|
|
if (!type) {
|
|
|
|
|
this.cache.clear()
|
|
|
|
|
this.currentSize = 0
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [key, item] of this.cache) {
|
|
|
|
|
if (item.type === type) {
|
|
|
|
|
this.currentSize -= item.size
|
|
|
|
|
this.cache.delete(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-10 14:09:30 -07:00
|
|
|
* Start memory pressure monitoring
|
|
|
|
|
* Periodically checks if we're approaching memory limits
|
|
|
|
|
*/
|
|
|
|
|
private startMemoryPressureMonitor(): void {
|
|
|
|
|
const checkInterval = this.config.memoryCheckInterval || 30000
|
|
|
|
|
|
|
|
|
|
this.memoryPressureCheckTimer = setInterval(() => {
|
|
|
|
|
this.checkMemoryPressure()
|
|
|
|
|
}, checkInterval)
|
|
|
|
|
|
|
|
|
|
// Unref so it doesn't keep process alive
|
|
|
|
|
if (this.memoryPressureCheckTimer.unref) {
|
|
|
|
|
this.memoryPressureCheckTimer.unref()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check current memory pressure and warn if needed
|
|
|
|
|
*/
|
|
|
|
|
private checkMemoryPressure(): void {
|
|
|
|
|
const pressure = checkMemoryPressure(this.currentSize, this.memoryInfo)
|
|
|
|
|
|
|
|
|
|
// Only log warnings every 5 minutes to avoid spam
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
const fiveMinutes = 5 * 60 * 1000
|
|
|
|
|
|
|
|
|
|
if (pressure.warnings.length > 0 && now - this.lastMemoryWarning > fiveMinutes) {
|
|
|
|
|
for (const warning of pressure.warnings) {
|
|
|
|
|
prodLog.warn(`UnifiedCache: ${warning}`)
|
|
|
|
|
}
|
|
|
|
|
this.lastMemoryWarning = now
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If critical, force aggressive eviction
|
|
|
|
|
if (pressure.pressure === 'critical') {
|
|
|
|
|
const targetSize = Math.floor(this.maxSize * 0.7) // Evict to 70%
|
|
|
|
|
const bytesToFree = this.currentSize - targetSize
|
|
|
|
|
|
|
|
|
|
if (bytesToFree > 0) {
|
|
|
|
|
prodLog.warn(
|
|
|
|
|
`UnifiedCache: Critical memory pressure - forcing eviction of ${formatBytes(bytesToFree)}`
|
|
|
|
|
)
|
|
|
|
|
this.evictForSize(bytesToFree)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cache statistics with memory information
|
2025-08-26 12:32:21 -07:00
|
|
|
*/
|
|
|
|
|
getStats() {
|
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.
REMOVED — PHASE A (type aliases)
src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.
src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.
src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
carries the same boolean).
src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.
REMOVED — PHASE B (storage adapter rename, 8 adapters)
Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:
- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)
The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).
REMOVED — PHASE C (cache category 'hnsw')
src/utils/unifiedCache.ts
- Cache-category union narrowed from
'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
to
'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
category. Cache is rebuildable; entries naturally don't exist after
a restart, so no migration path is needed.
src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
`.hnsw` → `.vectors`.
tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.
REMOVED — PHASE D (config.hnsw)
src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
`BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.
src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
- Reads from `this.config.vector` (not `this.config.hnsw`).
- Calls `resolveJsHnswConfig(this.config.vector)` to translate the
`recall` preset into M / efConstruction / efSearch knobs (with
`advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.
NOT IN THIS COMMIT (deliberately)
- Persisted file path migration `_system/hnsw-*.json` →
`_system/vector-index-*.json`. Requires dual-read logic on boot
across 8 storage adapters. Per integration doc lines 531-539:
"Reader accepts either spelling on load; writer emits the new spelling
only; brains self-migrate on the next persist after upgrade." This is
a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
the rename inventory; a future commit can fold it into
`config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
normalizeConfig() with default 'warn'; the actual warning emission at
knob-mismatch sites lands when the config-resolution layer is touched
for the persisted-path migration.
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
category name; assertion still passes after fixture rename)
- npm run build: clean
The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
|
|
|
const typeSizes = { vectors: 0, metadata: 0, embedding: 0, other: 0 }
|
|
|
|
|
const typeCounts = { vectors: 0, metadata: 0, embedding: 0, other: 0 }
|
2025-08-26 12:32:21 -07:00
|
|
|
|
|
|
|
|
for (const item of this.cache.values()) {
|
|
|
|
|
typeSizes[item.type] += item.size
|
|
|
|
|
typeCounts[item.type]++
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 14:09:30 -07:00
|
|
|
const hitRate = this.cache.size > 0 ?
|
|
|
|
|
Array.from(this.cache.values()).reduce((sum, item) => sum + item.accessCount, 0) / this.totalAccessCount : 0
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
return {
|
2025-10-10 14:09:30 -07:00
|
|
|
// Cache statistics
|
2025-08-26 12:32:21 -07:00
|
|
|
totalSize: this.currentSize,
|
|
|
|
|
maxSize: this.maxSize,
|
|
|
|
|
utilization: this.currentSize / this.maxSize,
|
|
|
|
|
itemCount: this.cache.size,
|
|
|
|
|
typeSizes,
|
|
|
|
|
typeCounts,
|
|
|
|
|
typeAccessCounts: this.typeAccessCounts,
|
|
|
|
|
totalAccessCount: this.totalAccessCount,
|
2025-10-10 14:09:30 -07:00
|
|
|
hitRate,
|
|
|
|
|
|
2026-01-27 15:38:21 -08:00
|
|
|
// Memory management
|
2025-10-10 14:09:30 -07:00
|
|
|
memory: {
|
|
|
|
|
available: this.memoryInfo.available,
|
|
|
|
|
source: this.memoryInfo.source,
|
|
|
|
|
isContainer: this.memoryInfo.isContainer,
|
|
|
|
|
systemTotal: this.memoryInfo.systemTotal,
|
|
|
|
|
allocationRatio: this.allocationStrategy.ratio,
|
|
|
|
|
environment: this.allocationStrategy.environment
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get detailed memory information
|
|
|
|
|
*/
|
|
|
|
|
getMemoryInfo() {
|
|
|
|
|
return {
|
|
|
|
|
memoryInfo: { ...this.memoryInfo },
|
|
|
|
|
allocationStrategy: { ...this.allocationStrategy },
|
|
|
|
|
currentPressure: checkMemoryPressure(this.currentSize, this.memoryInfo)
|
2025-08-26 12:32:21 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save access patterns for cold start optimization
|
|
|
|
|
*/
|
|
|
|
|
async saveAccessPatterns(): Promise<any> {
|
|
|
|
|
if (!this.config.persistPatterns) return
|
|
|
|
|
|
|
|
|
|
const patterns = Array.from(this.cache.entries())
|
|
|
|
|
.map(([key, item]) => ({
|
|
|
|
|
key,
|
|
|
|
|
type: item.type,
|
|
|
|
|
accessCount: this.access.get(key) || 0,
|
|
|
|
|
size: item.size,
|
|
|
|
|
rebuildCost: item.rebuildCost
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => b.accessCount - a.accessCount)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
patterns,
|
|
|
|
|
typeAccessCounts: this.typeAccessCounts,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load access patterns for warm start
|
|
|
|
|
*/
|
|
|
|
|
async loadAccessPatterns(patterns: any): Promise<void> {
|
|
|
|
|
if (!patterns?.patterns) return
|
|
|
|
|
|
|
|
|
|
// Pre-populate access counts
|
|
|
|
|
for (const pattern of patterns.patterns) {
|
|
|
|
|
this.access.set(pattern.key, pattern.accessCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore type access counts
|
|
|
|
|
if (patterns.typeAccessCounts) {
|
|
|
|
|
this.typeAccessCounts = patterns.typeAccessCounts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prodLog.debug('Loaded access patterns:', patterns.patterns.length, 'items')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Export singleton for global coordination
|
|
|
|
|
let globalCache: UnifiedCache | null = null
|
|
|
|
|
|
|
|
|
|
export function getGlobalCache(config?: UnifiedCacheConfig): UnifiedCache {
|
|
|
|
|
if (!globalCache) {
|
|
|
|
|
globalCache = new UnifiedCache(config)
|
|
|
|
|
}
|
|
|
|
|
return globalCache
|
|
|
|
|
}
|
|
|
|
|
|
feat: wire plugin system with provider resolution, storage factories, and browser deprecation
- Wire PluginRegistry into Brainy init() with provider resolution for distance,
metadataIndex, graphIndex, embeddings, roaring, msgpack, and storage adapters
- Add setupStorage() factory that resolves storage:* providers from plugins before
falling back to built-in createStorage()
- Export internals API (setGlobalCache, UnifiedCache, EntityIdMapper, etc.) for
cortex plugin consumption
- Add plugin.test.ts verifying registration, activation, and provider resolution
- Deprecate browser support (OPFS, Web Workers, WASM embeddings) with warnings
in preparation for v8.0 server-only release
- FileSystemStorage: fix setupStorage resolution for mmap-filesystem provider
2026-01-31 12:02:13 -08:00
|
|
|
export function setGlobalCache(cache: UnifiedCache): void {
|
|
|
|
|
globalCache = cache
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 12:32:21 -07:00
|
|
|
export function clearGlobalCache(): void {
|
|
|
|
|
if (globalCache) {
|
|
|
|
|
globalCache.clear()
|
|
|
|
|
globalCache = null
|
|
|
|
|
}
|
|
|
|
|
}
|