FileSystemStorage.saveBinaryBlob used a bare ${filePath}.tmp suffix for its
atomic-write temp file. Two concurrent same-key calls computed the SAME temp
path; both writeFile'd, the first rename succeeded, the second rename fired
against a missing temp and threw ENOENT. The throw propagated up through
brain.flush() and broke any downstream job that called it.
Reproduced in production (Brainy 7.30 + Cortex 2.7 + mmap-filesystem):
[job-queue] gcs-backup: failed - ENOENT: no such file or directory,
rename '/data/brainy-data/.../_column_index/owner/DELETED.bin.tmp'
-> '/data/brainy-data/.../_column_index/owner/DELETED.bin'
The race was two cron jobs (gcs-backup hourly + flush-brain every 15min) both
calling flush(), triggering column-store compaction over the same fields,
overlapping at the rename. Off-site backups had been failing continuously for
~48 hours.
PATCH
src/storage/adapters/fileSystemStorage.ts:992-999 — saveBinaryBlob now uses a
unique per-writer temp suffix matching the pattern at every other atomic-write
site in the same file (lines 336, 551, 744, 781, 1529, 2908):
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`
Adds defensive ENOENT swallow on rename: if the temp is gone, the work has
already landed (saveBinaryBlob is idempotent for a given key — all callers
persist the same logical bytes per key). Cleans up the temp on any other
rename failure to avoid orphan .tmp.* files.
SCOPE AUDIT
One bug site. Audit results:
- FileSystemStorage: six sibling atomic-write sites already used unique
suffixes (lines 336/551/744/781/1529/2908); only saveBinaryBlob was the
outlier. All six rechecked. Clean.
- OPFSStorage: WritableStream (no tmp+rename). Not affected.
- GCSStorage / R2Storage / AzureBlobStorage / S3CompatibleStorage: object-store
PUT (atomic at the API). Not affected.
- MemoryStorage: in-memory. Not affected.
- HistoricalStorageAdapter: read-only. Not affected.
- COW / versioning / snapshot / HNSW / aggregation: all delegate to storage
adapters via saveBinaryBlob / writeObjectToPath. They get the fix
automatically by using the patched primitive.
The bare-`.tmp` pattern is now gone repo-wide.
BENEFICIARIES
Beyond the reported column-store-compaction race:
- HNSW connection persistence (src/hnsw/hnswIndex.ts:252 → saveBinaryBlob)
was structurally susceptible to the same race. No production reports of
HNSW failures (probably because HNSW writes are more naturally serialized
by the index lock), but the fix removes the latent issue.
- Any future caller of saveBinaryBlob inherits the safer semantics.
TESTS
New tests/integration/savebinaryblob-concurrent-rename.test.ts (4 tests):
- 20 concurrent saveBinaryBlob(sameKey, ...) all resolve without throwing
- No orphan .tmp.* siblings remain in the blob directory afterwards
- Production-shape: two concurrent compactor passes over 10 column-index
fields (owner, path, permissions, vfsType, modified, createdAt, accessed,
updatedAt, mimeType, size). All 20 calls succeed; each field ends with
valid bytes.
- Single-writer path still produces correct bytes (no regression on common
case).
Verified the first three tests reproduce the production ENOENT error
verbatim on the pre-7.31.1 code path (stashed the fix, watched them fail
with the exact production error).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- New integration suite: 4/4
- npm run build: clean
CORTEX COMPATIBILITY
Zero changes. The Cortex mmap-filesystem adapter wraps FileSystemStorage
and inherits the patch automatically.
FORWARD-COMPAT
8.0's Db.persist() will route through the same patched primitive; no
additional work needed when the immutable Db API ships.
|
||
|---|---|---|
| .claude/skills | ||
| assets/models/all-MiniLM-L6-v2 | ||
| bin | ||
| docs | ||
| examples | ||
| integrations | ||
| models-cache/Xenova/all-MiniLM-L6-v2 | ||
| scripts | ||
| src | ||
| tests | ||
| .aiignore | ||
| .dockerignore | ||
| .gitignore | ||
| .npmignore | ||
| .nvmrc | ||
| .versionrc.json | ||
| brainy.png | ||
| bun.lock | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.js | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| RELEASES.md | ||
| tsconfig.cli.json | ||
| tsconfig.json | ||
| vitest.config.memory.ts | ||
| vitest.config.ts | ||
Brainy
Three database paradigms. One API. Zero configuration.
Built because we were tired of stitching together Pinecone + Neo4j + MongoDB and spending weeks on configuration before writing a single line of business logic. Brainy unifies vector search, graph traversal, and metadata filtering so you don't have to choose.
New here? → What is Brainy? — plain-language overview, no jargon
Install
npm install @soulcraft/brainy
Quick Start
import { Brainy, NounType, VerbType } from '@soulcraft/brainy'
const brain = new Brainy()
await brain.init()
// Add knowledge — text auto-embeds, metadata auto-indexes
const reactId = await brain.add({
data: 'React is a JavaScript library for building user interfaces',
type: NounType.Concept,
metadata: { category: 'frontend', year: 2013 }
})
const nextId = await brain.add({
data: 'Next.js framework for React with server-side rendering',
type: NounType.Concept,
metadata: { category: 'framework', year: 2016 }
})
// Create a relationship
await brain.relate({ from: nextId, to: reactId, type: VerbType.BuiltOn })
// Query all three paradigms at once
const results = await brain.find({
query: 'modern frontend frameworks', // Vector similarity
where: { year: { greaterThan: 2015 } }, // Metadata filtering
connected: { to: reactId, depth: 2 } // Graph traversal
})
Full API Reference | soulcraft.com/docs
Three Indexes, One Query
Every piece of knowledge lives in three indexes simultaneously:
data→ Vector index — Content for semantic search. Strings auto-embed into 384-dim vectors. Queried withfind({ query: '...' }).metadata→ Metadata index — Structured fields for filtering. O(1) lookups. Queried withfind({ where: { ... } }).relate()→ Graph index — Typed, directed relationships between entities. Traversed withfind({ connected: { ... } }).
// Data → vector index (semantic search)
const articleId = await brain.add({
data: 'A deep dive into transformer architectures',
type: NounType.Document,
metadata: { author: 'Dr. Chen', year: 2024, tags: ['AI'] } // → metadata index
})
// Relationships → graph index
await brain.relate({ from: authorId, to: articleId, type: VerbType.Authored })
// Query all three at once
brain.find({
query: 'attention mechanisms', // Vector similarity
where: { year: { greaterThan: 2023 } }, // Metadata filter
connected: { from: authorId, depth: 1 } // Graph traversal
})
Data Model Reference | Query Operators
Features
Triple Intelligence
Vector search + graph traversal + metadata filtering in every query. No stitching services together — one find() call combines all three.
const results = await brain.find({
query: 'machine learning',
where: { department: 'engineering', level: 'senior' },
connected: { from: teamLeadId, via: VerbType.WorksWith, depth: 2 }
})
Hybrid Search
Automatically combines keyword (text) and semantic (vector) search. No configuration needed.
await brain.find({ query: 'David Smith' }) // Auto: text + semantic
await brain.find({ query: 'AI concepts', searchMode: 'semantic' }) // Semantic only
await brain.find({ query: 'exact id', searchMode: 'text' }) // Text only
Query Operators
Filter metadata with equality, comparison, array, existence, pattern, and logical operators:
await brain.find({
where: {
status: 'active', // Exact match
score: { greaterThan: 90 }, // Comparison
tags: { contains: 'ai' }, // Array
anyOf: [{ role: 'admin' }, { role: 'owner' }] // Logical OR
}
})
Query Operators Reference — all operators with indexed/in-memory matrix
Graph Relationships
Typed, directed edges between entities. Traverse connections at any depth.
await brain.relate({ from: personId, to: projectId, type: VerbType.WorksOn })
const results = await brain.find({
connected: { from: personId, via: VerbType.WorksOn, depth: 3 }
})
Git-Style Branching
Fork your entire database in <100ms. Snowflake-style copy-on-write.
const experiment = await brain.fork('test-migration')
await experiment.add({ data: 'test data', type: NounType.Concept })
await experiment.commit({ message: 'Add test data', author: 'dev@co.com' })
await brain.checkout('test-migration')
// Time-travel: query at any past commit
const snapshot = await brain.asOf(commitId)
const pastResults = await snapshot.find({ query: 'historical data' })
await snapshot.close()
Entity Versioning
Save, restore, and compare entity snapshots.
const userId = await brain.add({ data: 'Alice', type: NounType.Person })
await brain.versions.save(userId, { tag: 'v1.0' })
await brain.update(userId, { data: 'Alice Smith' })
await brain.versions.save(userId, { tag: 'v2.0' })
const diff = await brain.versions.compare(userId, 1, 2)
await brain.versions.restore(userId, 1)
Virtual Filesystem
File operations with semantic search built in.
const vfs = brain.vfs
await vfs.writeFile('/docs/readme.md', 'Project documentation')
const content = await vfs.readFile('/docs/readme.md')
const tree = await vfs.getTreeStructure('/docs', { maxDepth: 3 })
// Semantic file search
const matches = await vfs.search('React components with hooks')
VFS Quick Start | Common Patterns
Import Anything
CSV, Excel, PDF, URLs — auto-detected format, auto-classified entities.
await brain.import('customers.csv')
await brain.import('sales-data.xlsx', { excelSheets: ['Q1', 'Q2'] })
await brain.import('research-paper.pdf', { pdfExtractTables: true })
await brain.import('https://api.example.com/data.json')
Entity Extraction
AI-powered named entity recognition with 4-signal ensemble scoring.
const entities = await brain.extractEntities('John Smith founded Acme Corp in New York')
// [
// { text: 'John Smith', type: NounType.Person, confidence: 0.95 },
// { text: 'Acme Corp', type: NounType.Organization, confidence: 0.92 },
// { text: 'New York', type: NounType.Location, confidence: 0.88 }
// ]
Plugin System
Optional native acceleration via @soulcraft/cortex — SIMD distance calculations, CRoaring bitmaps, Candle ML embeddings.
const brain = new Brainy({ plugins: ['@soulcraft/cortex'] })
await brain.init()
Plugins are opt-in. Brainy never auto-imports packages unless listed in plugins.
Type System
42 noun types and 127 verb types form a universal knowledge protocol:
42 Nouns × 127 Verbs = 5,334 base relationship combinations
Model any domain — healthcare (Patient → diagnoses → Condition), finance (Account → transfers → Transaction), education (Student → completes → Course), or your own.
Subtypes — sub-classification within a NounType or VerbType
Both noun types and verb types are intentionally coarse. Use the top-level subtype field to sub-classify entities AND relationships within a type — flat string, no hierarchy, your choice of vocabulary:
// Nouns: sub-classify entities
await brain.add({
data: 'Avery Brooks — runs the AI lab',
type: NounType.Person,
subtype: 'employee' // 'customer', 'vendor', 'contractor', …
})
// Verbs: sub-classify relationships
await brain.relate({
from: ceoId,
to: vpId,
type: VerbType.ReportsTo,
subtype: 'direct' // 'dotted-line', 'matrix', …
})
// Filter on the fast path — column-store hit, not metadata fallback:
const employees = await brain.find({ type: NounType.Person, subtype: 'employee' })
const directReports = await brain.getRelations({ from: ceoId, subtype: 'direct' })
// O(1) counts via the persisted rollups:
brain.counts.bySubtype(NounType.Person)
// → { employee: 12, customer: 847, vendor: 34 }
brain.counts.byRelationshipSubtype(VerbType.ReportsTo)
// → { direct: 12, 'dotted-line': 3 }
Enforce the pairing. Register a vocabulary per type or turn on brain-wide strict mode to ensure every entity AND relationship has both type AND subtype:
// Per-type rule with vocabulary
brain.requireSubtype(NounType.Person, { values: ['employee', 'customer'], required: true })
// Or brain-wide strict mode
const brain = new Brainy({ requireSubtype: true })
For other facets you want counted (status, source, role), register them with brain.trackField(name). Renaming an existing convention to subtype? Use brain.migrateField({from, to, entityKind: 'both'}) to walk nouns AND verbs in one pass. Full guide: Subtypes & Facets.
Noun-Verb Taxonomy | Stage 3 Canonical Reference
Storage: Memory to Cloud
The same API at every scale. Change one config line to go from prototype to production.
Development — Zero Config
const brain = new Brainy()
Production — Filesystem with Compression
const brain = new Brainy({
storage: { type: 'filesystem', path: './data', compression: true }
})
Cloud — S3, GCS, Azure, Cloudflare R2
const brain = new Brainy({
storage: {
type: 's3',
s3Storage: { bucketName: 'my-knowledge-base', region: 'us-east-1' }
}
})
Performance benchmarks and capacity planning in docs/PERFORMANCE.md.
Cloud Deployment Guide | Capacity Planning
Use Cases
- AI agents — Persistent memory with semantic recall and relationship tracking
- Knowledge bases — Auto-linking, semantic search, relationship-aware navigation
- Semantic search — Find by meaning across codebases, documents, or media
- Enterprise knowledge — CRM, product catalogs, institutional memory
- Interactive experiences — Game worlds, NPCs, and characters that remember
- Content platforms — Similarity-based discovery, intelligent tagging
Documentation
Start Here
- Brainy explained simply — Plain-language overview, no jargon, no code
Core
- API Reference — Every method with parameters, returns, and examples
- Data Model — Entity structure, data vs metadata
- Query Operators — All BFO operators with examples
- Find System — Natural language find() and hybrid search
Architecture
- Architecture Overview — System design and components
- Triple Intelligence — Vector + graph + metadata unified query
- Noun-Verb Taxonomy — Universal type system
- Data Storage Architecture — Type-aware indexing and HNSW
Virtual Filesystem
- VFS Quick Start — Build file explorers that never crash
- VFS Core — Full VFS API reference
- Semantic VFS — AI-powered file navigation
Guides
- Import Anything — CSV, Excel, PDF, URLs
- Framework Integration — React, Vue, Angular, Svelte
- Natural Language Queries — Master the find() method
Operations
- Cloud Deployment — AWS, GCS, Azure
- Capacity Planning — Memory, storage, and scaling
- Performance — Benchmarks and architecture details
- Cost Optimization: AWS S3 | GCS | Azure | R2
Requirements
Bun 1.0+ (recommended) or Node.js 22 LTS
bun install @soulcraft/brainy # Bun — best performance
npm install @soulcraft/brainy # Node.js — fully supported
Deprecation Notice: Browser support (OPFS, Web Workers, WASM embeddings) is deprecated in v7.10.0 and will be removed in v8.0.0. Brainy v8+ will be server-only.
Single-Writer Model
Brainy is single-writer, many-reader on filesystem storage. One writer holds an exclusive lock on the data directory; any number of readers can inspect it concurrently. Opening a second writer throws with the PID of the existing one.
// Live application — writer mode is the default
const brain = new Brainy({ storage: { type: 'filesystem', rootDirectory: '/data/brain' } })
await brain.init()
// Out-of-band diagnostics from a separate process — safe to run while the
// writer is live
const reader = await Brainy.openReadOnly({
storage: { type: 'filesystem', rootDirectory: '/data/brain' }
})
await reader.requestFlush({ timeoutMs: 5000 })
const stats = await reader.stats()
For incident debugging, use the brainy inspect CLI:
brainy inspect stats /data/brain
brainy inspect find /data/brain --where '{"entityType":"booking"}'
brainy inspect explain /data/brain --where '{"entityType":"booking"}'
brainy inspect health /data/brain
See the multi-process model and the inspection guide for the full story, including stale-lock detection, the cross-process flush RPC, and what's not yet enforced on cloud storage backends.
Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
License
MIT © Brainy Contributors