feat(8.0): zero-config finalize + cut JS quantization (config.vector = recall + persistMode)

This commit is contained in:
David Snelling 2026-06-15 10:08:51 -07:00
parent f4c5d9749f
commit f8e0079d3f
12 changed files with 348 additions and 1739 deletions

View file

@ -116,7 +116,7 @@ import type { MigrationPreview, MigrationResult, MigrateOptions } from './migrat
import { AggregationIndex } from './aggregation/AggregationIndex.js'
import { AggregateMaterializer } from './aggregation/materializer.js'
import type { AggregateDefinition, AggregateQueryParams, AggregateResult } from './types/brainy.types.js'
import { resolveJsHnswConfig } from './utils/recallPreset.js'
import { resolveJsHnswConfig, DEFAULT_RECALL } from './utils/recallPreset.js'
import * as fs from 'node:fs'
import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js'
import { GenerationStore } from './db/generationStore.js'
@ -205,6 +205,8 @@ type ResolvedBrainyConfig = Required<
| 'vector'
| 'plugins'
| 'integrations'
| 'history'
| 'eagerEmbeddings'
>
> &
Pick<
@ -215,6 +217,8 @@ type ResolvedBrainyConfig = Required<
| 'vector'
| 'plugins'
| 'integrations'
| 'history'
| 'eagerEmbeddings'
>
/**
@ -387,6 +391,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
private _readyResolve: (() => void) | null = null
private _readyReject: ((error: Error) => void) | null = null
// In-flight init() promise. Guards against concurrent initialization when
// multiple operations race to lazily initialize the same instance — all
// racers await the single in-flight run. Reset on failure so init() can
// be retried after the underlying cause (e.g. a held writer lock) clears.
private initPromise: Promise<void> | null = null
// Set once close() has torn the instance down. close() is terminal: a closed
// instance must NOT silently lazy-re-initialize on the next operation (that
// would resurrect released indexes and timers). The lazy-init convenience
// applies only to instances that were never closed.
private closed = false
// Lazy rebuild state (Production-scale lazy loading)
// Prevents race conditions when multiple queries trigger rebuild simultaneously
private lazyRebuildInProgress = false
@ -542,13 +558,42 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
/**
* Initialize Brainy - MUST be called before use
* Initialize Brainy.
*
* **Calling this explicitly is optional.** Every public operation lazily
* initializes the instance on first use (`new Brainy()` followed directly
* by `add()` / `find()` / `transact()` just works). Call `init()` yourself
* when you want to control *when* the startup cost is paid (e.g. during
* server boot rather than on the first request) or to pass init-time
* configuration overrides.
*
* Idempotent and concurrency-safe: repeat calls after success return
* immediately; calls racing an in-flight initialization await that single
* run (overrides are only applied by the run that starts initialization).
*
* @param overrides Optional configuration overrides for init
*/
async init(overrides?: Partial<BrainyConfig & { dimensions?: number }>): Promise<void> {
if (this.initialized) {
return
}
if (this.initPromise) {
return this.initPromise
}
const run = this.performInit(overrides)
this.initPromise = run
try {
await run
} finally {
this.initPromise = null
}
}
/**
* The single initialization run guarded by `init()`. Never call directly
* always go through `init()` (or any public method, which lazily inits).
*/
private async performInit(overrides?: Partial<BrainyConfig & { dimensions?: number }>): Promise<void> {
// Apply any init-time configuration overrides
if (overrides) {
@ -566,7 +611,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
...this.config,
...configOverrides,
storage: mergedStorage,
index: { ...this.config.index, ...configOverrides.index },
verbose: configOverrides.verbose ?? this.config.verbose,
silent: configOverrides.silent ?? this.config.silent
}
@ -702,32 +746,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
setMsgpackImplementation(msgpackProvider)
}
// Provider: native SQ8 approximate distance (e.g. cortex's Rust SIMD) — swaps
// the JS quantized-distance used in HNSW SQ8 reranking. Signature is
// byte-compatible with the JS distanceSQ8; falls back to JS when absent.
const sq8DistanceProvider = this.pluginRegistry.getProvider<
(a: Uint8Array, aMin: number, aMax: number, b: Uint8Array, bMin: number, bMax: number) => number
>('distance:sq8')
if (sq8DistanceProvider) {
const { setSQ8DistanceImplementation } = await import('./utils/vectorQuantization.js')
setSQ8DistanceImplementation(sq8DistanceProvider)
}
// Provider: native SQ4 approximate distance (cortex's Rust). Same swap
// pattern as SQ8; signature is byte-compatible with the JS distanceSQ4
// (4-bit quantization range, packed nibbles). Used in HNSW SQ4 reranking
// when config.hnsw.quantization.bits === 4. Falls back to JS when absent.
const sq4DistanceProvider = this.pluginRegistry.getProvider<
(
a: Uint8Array, aMin: number, aMax: number, aDim: number,
b: Uint8Array, bMin: number, bMax: number, bDim: number
) => number
>('distance:sq4')
if (sq4DistanceProvider) {
const { setSQ4DistanceImplementation } = await import('./utils/vectorQuantization.js')
setSQ4DistanceImplementation(sq4DistanceProvider)
}
// Provider: sort:topK (e.g. cortex's native partial-sort / heap-select) — swaps the
// JS result-ranking used by find() to pick the top `offset + limit` rows. The provider
// returns indices into a scores array, ordered descending with stable ties, identical
@ -831,11 +849,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// Connect distributed components to storage
await this.connectDistributedStorage()
// Warm up if configured
if (this.config.warmup) {
await this.warmup()
}
// Register shutdown hooks for graceful count flushing (once globally)
if (!Brainy.shutdownHooksRegisteredGlobally) {
this.registerShutdownHooks()
@ -875,12 +888,34 @@ export class Brainy<T = any> implements BrainyInterface<T> {
await this._vfs.init()
this._vfsInitialized = true // Mark VFS as fully initialized
// Eager embedding initialization for cloud deployments
// When eagerEmbeddings is true, initialize the WASM embedding engine now
// instead of lazily on first embed() call. This moves the 90-140 second
// WASM compilation to container startup rather than first request.
// Recommended for: Cloud Run, Lambda, Fargate, Kubernetes
if (this.config.eagerEmbeddings && !this.pluginRegistry.hasProvider('embeddings')) {
// Eager embedding initialization.
//
// Adaptive default (8.0): the WASM embedding engine eagerly initializes
// during init() WHENEVER it is the active embedder — i.e. no native
// 'embeddings' provider has taken over — and the instance is a writer
// (not reader-mode) outside of unit tests. The WASM module (≈93MB with
// the embedded model) takes 90-140s to compile on throttled CPUs; paying
// that during boot rather than on the first embed()-driven call is the
// right default for the overwhelmingly common single-process server.
//
// Skipped automatically when:
// - a native 'embeddings' provider is registered (it owns embeddings;
// the WASM engine is dead weight),
// - reader-mode (readers don't embed — they query existing vectors),
// - unit-test mode (tests must stay fast and use the mock embedder).
//
// `eagerEmbeddings: false` is the explicit override to force lazy init
// (first-embed) even when this instance is the active embedder.
const isUnitTestMode =
process.env.BRAINY_UNIT_TEST === 'true' ||
(globalThis as { __BRAINY_UNIT_TEST__?: boolean }).__BRAINY_UNIT_TEST__ === true
const eager = this.config.eagerEmbeddings ?? true
if (
eager &&
!this.pluginRegistry.hasProvider('embeddings') &&
this.config.mode !== 'reader' &&
!isUnitTestMode
) {
console.log('Eager embedding initialization enabled...')
await embeddingManager.init()
console.log('Embedding engine ready')
@ -1026,11 +1061,24 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
/**
* Ensure Brainy is initialized
* Ensure Brainy is initialized, lazily initializing on first use.
*
* Called at the top of every public operation, so `new Brainy()` followed
* directly by `add()` / `find()` / any other call just works forgetting
* `init()` is not an error. Concurrent first operations share a single
* initialization run (see `init()`); if initialization fails, the error
* propagates to the operation that triggered it.
*
* `close()` is terminal: a closed instance throws here instead of silently
* re-initializing using a closed Brainy is a consumer bug, not a lazy-init
* opportunity.
*/
private async ensureInitialized(): Promise<void> {
if (this.closed) {
throw new Error('Brainy instance is not initialized: it was closed via close(). Create a new instance.')
}
if (!this.initialized) {
throw new Error('Brainy not initialized. Call init() first.')
await this.init()
}
}
@ -5143,6 +5191,68 @@ export class Brainy<T = any> implements BrainyInterface<T> {
return this.generationStore.compact(options)
}
/**
* @description Resolve the effective generational-history retention policy
* from `config.history`, filling per-field defaults so a partial object
* inherits the rest. This is the single read site for the policy referenced
* by {@link autoCompactHistory}.
*
* Defaults (zero-config): keep the 100 most recent committed generations
* AND everything committed within the last 7 days; auto-compact on
* `flush()` / `close()`. A generation is reclaimed only when it is older
* than BOTH bounds and no live `Db` pin protects it.
*
* @returns The resolved policy: retention bounds plus the `autoCompact` flag.
*/
private resolveHistoryPolicy(): {
retainGenerations: number
retainMs: number
autoCompact: boolean
} {
const history = this.config.history
return {
retainGenerations: history?.retainGenerations ?? 100,
retainMs: history?.retainMs ?? 604_800_000, // 7 days
autoCompact: history?.autoCompact ?? true
}
}
/**
* @description Run history compaction with the resolved retention policy if
* `config.history.autoCompact` is on (the default). Invoked from `flush()`
* and `close()` so generational record-sets cannot accumulate unbounded
* across a long-lived writer's lifetime.
*
* Read-only instances and an explicit `autoCompact: false` skip silently.
* Pinned generations (live `Db` values) are never reclaimed the safety
* invariant lives in {@link GenerationStore.compact}. Failures are logged
* and swallowed: compaction is housekeeping and must never fail a flush or
* a clean shutdown.
*/
private async autoCompactHistory(): Promise<void> {
// Nothing to compact on a read-only instance or before init wired up the
// generation store (close() can run defensively on an uninitialized brain).
if (this.isReadOnly || !this.generationStore) {
return
}
const policy = this.resolveHistoryPolicy()
if (!policy.autoCompact) {
return
}
try {
await this.generationStore.compact({
retainGenerations: policy.retainGenerations,
retainMs: policy.retainMs
})
} catch (error) {
console.warn(
`Auto-compaction of generational history failed (non-fatal): ${
error instanceof Error ? error.message : String(error)
}`
)
}
}
/**
* @description Replace this store's ENTIRE state from a snapshot directory
* previously produced by `db.persist(path)`. Destructive: current
@ -6858,6 +6968,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
this.generationStore.persistCounterNow()
])
// 6. Auto-compact generational history per config.history (default on).
// Runs after the flush so durable state is in place; respects live
// Db pins and an explicit autoCompact: false.
await this.autoCompactHistory()
const elapsed = Date.now() - startTime
console.log(`All indexes flushed to disk in ${elapsed}ms`)
@ -10167,14 +10282,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
return this.embedder(textToEmbed)
}
/**
* Warm up the system
*/
private async warmup(): Promise<void> {
// Warm up embedder
await this.embed('warmup')
}
/**
* Explicitly warm up the embedding engine
*
@ -10300,26 +10407,22 @@ export class Brainy<T = any> implements BrainyInterface<T> {
/**
* Setup index single unified vector graph.
*
* Brainy 8.0 ships filesystem-only storage (no cloud adapters), so persistence
* mode defaults to `'immediate'` everywhere. Operators who want bulk-ingest
* speed can set `config.vector.persistMode = 'deferred'`.
* Persistence mode is auto-selected from the storage adapter (see
* {@link resolveHNSWPersistMode}): `'immediate'` on filesystem storage,
* `'deferred'` on memory storage. Operators can override with an explicit
* `config.vector.persistMode` (e.g. `'deferred'` for bulk-ingest speed on
* filesystem storage).
*/
private setupIndex(): JsHnswVectorIndex {
// 8.0 config surface: config.vector.{recall, quantization, persistMode}.
// 8.0 config surface: config.vector.{recall, persistMode}.
// The recall preset translates to HNSW knobs (M / efConstruction / efSearch)
// via resolveJsHnswConfig. No algorithm-internal knobs are exposed at the
// public surface.
// public surface. The JS index computes exact float32 distances throughout —
// there is no quantization path.
const recallKnobs = resolveJsHnswConfig(this.config.vector)
const vectorCfg = this.config.vector
const indexConfig = {
...this.config.index,
...recallKnobs,
distanceFunction: this.distance,
quantization: vectorCfg?.quantization ? {
enabled: vectorCfg.quantization.enabled ?? false,
bits: vectorCfg.quantization.bits ?? 8,
rerankMultiplier: 3
} : undefined
distanceFunction: this.distance
}
const persistMode = this.resolveHNSWPersistMode()
@ -10346,8 +10449,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
const persistMode = this.resolveHNSWPersistMode()
const vectorFactory = this.pluginRegistry.getProvider<(config: any, distance: DistanceFunction, options: any) => any>('vector')
if (vectorFactory) {
// Native providers receive the algorithm-neutral surface: the resolved
// recall preset name (per the 8.0 provider contract they translate it
// to their own internal knobs), plus the JS HNSW knob tuple for
// providers that mirror the open-core implementation. Quantization at
// scale is the native provider's own concern (e.g. DiskANN PQ) — Brainy
// exposes no quantization knob.
return vectorFactory(
{ ...this.config.index, distanceFunction: this.distance },
{
...resolveJsHnswConfig(this.config.vector),
recall: this.config.vector?.recall ?? DEFAULT_RECALL,
distanceFunction: this.distance
},
this.distance,
{ storage: this.storage, persistMode }
)
@ -10356,15 +10469,23 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
/**
* Resolve HNSW persistence mode.
* Extracted so both setupIndex() and the HNSW plugin factory path can use it.
* Resolve the vector persistence mode from the storage adapter.
*
* Brainy 8.0 ships filesystem-only; the cloud-storage smart-default
* (deferred for cloud, immediate for local) collapses to "always
* immediate unless the user overrides via config.vector.persistMode".
* Auto-selected when `config.vector.persistMode` is omitted:
* - **filesystem** (and any persistent adapter): `'immediate'` per-add
* durability is the point of a persistent backend.
* - **memory**: `'deferred'` nothing survives the process anyway, so
* per-add persistence writes are pure overhead; deferred batches them
* into `flush()` / `close()`.
*
* An explicit `config.vector.persistMode` always wins (the operator
* escape hatch for bulk-ingest pipelines on filesystem storage).
*/
private resolveHNSWPersistMode(): 'immediate' | 'deferred' {
return this.config.vector?.persistMode ?? 'immediate'
if (this.config.vector?.persistMode) {
return this.config.vector.persistMode
}
return this.getStorageType() === 'memory' ? 'deferred' : 'immediate'
}
/**
@ -10390,59 +10511,40 @@ export class Brainy<T = any> implements BrainyInterface<T> {
)
}
// Validate numeric configurations
if (config?.index?.m && (config.index.m < 1 || config.index.m > 128)) {
throw new Error(`Invalid index m parameter: ${config.index.m}. Must be between 1 and 128`)
}
if (config?.index?.efConstruction && (config.index.efConstruction < 1 || config.index.efConstruction > 1000)) {
throw new Error(`Invalid index efConstruction: ${config.index.efConstruction}. Must be between 1 and 1000`)
}
if (config?.index?.efSearch && (config.index.efSearch < 1 || config.index.efSearch > 1000)) {
throw new Error(`Invalid index efSearch: ${config.index.efSearch}. Must be between 1 and 1000`)
}
// Auto-detect distributed mode based on environment and configuration
const distributedConfig = this.autoDetectDistributed(config?.distributed)
// Distributed mode is explicit opt-in only (config or BRAINY_DISTRIBUTED
// env var) — never inferred from deployment heuristics.
const distributedConfig = this.resolveDistributedConfig(config?.distributed)
return {
storage: config?.storage || { type: 'auto' },
index: config?.index || {},
cache: config?.cache ?? true,
distributed: distributedConfig,
warmup: config?.warmup ?? false,
realtime: config?.realtime ?? false,
multiTenancy: config?.multiTenancy ?? false,
telemetry: config?.telemetry ?? false,
verbose: config?.verbose ?? false,
silent: config?.silent ?? false,
// New performance options with smart defaults
disableAutoRebuild: config?.disableAutoRebuild ?? false, // false = auto-decide based on size
disableMetrics: config?.disableMetrics ?? false,
disableAutoOptimize: config?.disableAutoOptimize ?? false,
batchWrites: config?.batchWrites ?? true,
maxConcurrentOperations: config?.maxConcurrentOperations ?? 10,
// Memory management options
// false = auto-decide based on dataset size (inline vs lazy rebuild)
disableAutoRebuild: config?.disableAutoRebuild ?? false,
// Memory management escape hatches over the RAM-derived auto limits
maxQueryLimit: config?.maxQueryLimit ?? undefined,
reservedQueryMemory: config?.reservedQueryMemory ?? undefined,
// Vector index configuration (8.0) — algorithm-neutral surface with
// `recall` preset + `persistMode` (folded in from 7.x's hnswPersistMode).
// See BRAINY-8.0-RENAME-COORDINATION § A.2 + § G.2.
vector: config?.vector ?? undefined,
// Embedding initialization - false = lazy init on first embed()
eagerEmbeddings: config?.eagerEmbeddings ?? false,
// Generational-history retention — defaults applied at the read site
// (resolveHistoryPolicy) so a partial object inherits per-field defaults.
history: config?.history ?? undefined,
// Embedding initialization — left undefined when omitted so the adaptive
// default resolves at the init() read site (eager when the WASM embedder
// is the active one on a non-reader writer outside unit tests). Explicit
// true/false always wins. See the eagerEmbeddings JSDoc in brainy.types.ts.
eagerEmbeddings: config?.eagerEmbeddings ?? undefined,
// Plugin configuration - undefined = auto-detect
plugins: config?.plugins ?? undefined,
// Integration Hub - undefined/false = disabled
integrations: config?.integrations ?? undefined,
// Migration — disabled by default, opt-in for automatic migration
autoMigrate: config?.autoMigrate ?? false,
// Subtype pairing enforcement (7.30.0) — opt-in.
// false: only per-type rules registered via brain.requireSubtype() apply.
// true: every public write path requires subtype on every type.
// { except: [...] }: strict, but listed types may omit subtype.
// Becomes the default in 8.0.0.
// Migration — auto-run by default. Pending data migrations apply inline
// during init() for small datasets (<10K entities); larger datasets
// defer with a notice so the operator can schedule brain.migrate().
autoMigrate: config?.autoMigrate ?? true,
// Subtype required-by-default (8.0 — per BRAINY-8.0-SUBTYPE-CONTRACT § C-1).
// Every write path now requires `subtype` on every type unless the consumer
// opts out explicitly. Opt-out: `requireSubtype: false` (last-resort migration
@ -10926,6 +11028,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* This ensures deferred persistence mode data is saved
*/
async close(): Promise<void> {
// Phase 0: Auto-compact generational history per config.history (default
// on) BEFORE the generation store closes below. Respects live Db pins and
// an explicit autoCompact: false; no-op on read-only instances.
await this.autoCompactHistory()
// Phase 1: Flush ALL components in parallel to persist buffered data
// This is critical when cortex native providers buffer data in Rust memory
await Promise.all([
@ -11033,26 +11140,34 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
this.initialized = false
// close() is terminal: block lazy re-initialization on any subsequent
// operation (ensureInitialized() throws once this is set).
this.closed = true
}
/**
* Intelligently auto-detect distributed configuration
* Zero-config: Automatically determines best distributed settings
*/
private autoDetectDistributed(config?: BrainyConfig['distributed']): BrainyConfig['distributed'] {
// If explicitly disabled, respect that
if (config?.enabled === false) {
return config
}
// Auto-detect based on environment variables (common in production)
const envEnabled = process.env.BRAINY_DISTRIBUTED === 'true' ||
process.env.NODE_ENV === 'production' ||
process.env.CLUSTER_SIZE ||
process.env.KUBERNETES_SERVICE_HOST // Running in K8s
// If not explicitly configured but environment suggests distributed
if (!config && envEnabled) {
/**
* Resolve distributed-cluster configuration. **Explicit opt-in only**:
* either `config.distributed.enabled === true` or the
* `BRAINY_DISTRIBUTED=true` environment variable. Once opted in, the
* remaining fields default sensibly (64 shards, 3 replicas, raft, http,
* hostname-derived nodeId).
*
* Earlier versions auto-enabled cluster mode from deployment heuristics
* (`NODE_ENV=production`, a Kubernetes service host, `CLUSTER_SIZE`).
* That violated zero-config safety: a single-process production
* deployment the overwhelmingly common case would silently start a
* coordinator, bind a port, and run raft against itself. Deployment
* environment does not imply a multi-node Brainy cluster, so the
* heuristics are gone.
*/
private resolveDistributedConfig(config?: BrainyConfig['distributed']): BrainyConfig['distributed'] {
// Env-var opt-in for deployments that can't touch code (the ONLY
// environment signal honoured — it names brainy explicitly).
if (!config && process.env.BRAINY_DISTRIBUTED === 'true') {
return {
enabled: true,
nodeId: process.env.HOSTNAME || process.env.NODE_ID || `node-${Date.now()}`,
@ -11065,7 +11180,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
}
// Merge with provided config, applying intelligent defaults
// Explicit config: fill the per-field defaults.
return config ? {
...config,
nodeId: config.nodeId || process.env.HOSTNAME || `node-${Date.now()}`,