Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.
Three concurrent fixes:
A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
(reservedQueryMemory / containerMemory / freeMemory) all divided by
100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
unchanged in behavior.
B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
call site (dedup keyed on caller stack frame + limit value), query
proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
safety-cap limits keeps working; the warning teaches the recipe so consumers
can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
format. Real OOM territory; the cap stops being a recommendation and becomes
a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
against a 9 K-cap box) without disabling OOM protection. Real OOM territory
on a JS in-memory brain is hundreds of thousands of results, not 10x the
safety cap.
C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
name the three escape valves (maxQueryLimit / reservedQueryMemory /
pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
the limit enforcement (7.30.2) share one implementation without circular
imports.
DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
exists, the four memory sources the auto-config considers, the three escape
valves with when-to-use-which guidance, and an explicit "pagination is the
future-proof pattern" callout (8.0 may tighten the cap further; pagination
keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
to the new guide.
- RELEASES.md v7.30.2 entry.
TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
soft-tier warns once per call site (dedup verified by exercising same vs.
different source lines via wrapper closures); soft-tier message format
(names all three escape valves + docs link); soft-tier message includes
caller location; hard-tier throws; hard-tier message format same as
soft-tier; consumer maxQueryLimit override raises the cap and shifts both
tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.
CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
runs in ValidationConfig.constructor(), two-tier enforcement runs in
validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
per-call limits to keep snapshot semantics cheap; pagination remains the
pattern that's guaranteed to keep working.
REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.
Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
/**
|
|
* VFS bulkWrite Race Condition Fix Tests (v6.5.0)
|
|
*
|
|
* Tests for the race condition where parallel mkdir and write operations
|
|
* could create duplicate directory entities, causing files to become invisible.
|
|
*
|
|
* Bug: When mkdir and write for related paths are in the same parallel batch,
|
|
* the mkdir mutex race window can create duplicate entities. Files created
|
|
* with the "wrong" parent entity become invisible in tree traversal.
|
|
*
|
|
* Fix: Sort operations so mkdirs run first (sequentially, by depth), then
|
|
* other operations in parallel batches.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { Brainy } from '../../src/brainy.js'
|
|
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
|
|
|
describe('VFS bulkWrite Race Condition Fix', () => {
|
|
let brain: Brainy
|
|
let vfs: VirtualFileSystem
|
|
|
|
beforeEach(async () => {
|
|
brain = new Brainy({
|
|
storage: { type: 'memory' },
|
|
embeddingModel: 'Q8'
|
|
})
|
|
await brain.init()
|
|
vfs = brain.vfs
|
|
await vfs.init()
|
|
})
|
|
|
|
describe('operation ordering', () => {
|
|
it('should create directories before files when mixed in same batch', async () => {
|
|
// This is the exact scenario that triggered the race condition:
|
|
// mkdir and write for related paths in the same batch
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'write', path: '/data/config.json', data: '{}' },
|
|
{ type: 'mkdir', path: '/data' },
|
|
{ type: 'write', path: '/data/users.json', data: '[]' },
|
|
{ type: 'mkdir', path: '/logs' },
|
|
{ type: 'write', path: '/logs/app.log', data: 'log entry' }
|
|
])
|
|
|
|
expect(result.successful).toBe(5)
|
|
expect(result.failed.length).toBe(0)
|
|
|
|
// Verify all files are visible
|
|
const dataFiles = await vfs.readdir('/data')
|
|
expect(dataFiles).toContain('config.json')
|
|
expect(dataFiles).toContain('users.json')
|
|
|
|
const logFiles = await vfs.readdir('/logs')
|
|
expect(logFiles).toContain('app.log')
|
|
})
|
|
|
|
it('should handle nested directory creation in correct order', async () => {
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'write', path: '/a/b/c/file.txt', data: 'content' },
|
|
{ type: 'mkdir', path: '/a/b/c' }, // deepest
|
|
{ type: 'mkdir', path: '/a' }, // shallowest
|
|
{ type: 'mkdir', path: '/a/b' }, // middle
|
|
])
|
|
|
|
expect(result.successful).toBe(4)
|
|
|
|
// Verify tree structure is correct
|
|
const rootDirs = await vfs.readdir('/')
|
|
expect(rootDirs).toContain('a')
|
|
|
|
const aContent = await vfs.readdir('/a')
|
|
expect(aContent).toContain('b')
|
|
|
|
const bContent = await vfs.readdir('/a/b')
|
|
expect(bContent).toContain('c')
|
|
|
|
const cContent = await vfs.readdir('/a/b/c')
|
|
expect(cContent).toContain('file.txt')
|
|
})
|
|
|
|
it('should not create duplicate directory entities under concurrent load', async () => {
|
|
// Simulate the exact race condition scenario with many operations
|
|
const operations: Array<{
|
|
type: 'write' | 'mkdir'
|
|
path: string
|
|
data?: string
|
|
options?: { recursive?: boolean }
|
|
}> = []
|
|
|
|
// Mix mkdir and write operations that would trigger race condition
|
|
// Using recursive: true makes mkdir idempotent (no error if exists)
|
|
for (let i = 0; i < 20; i++) {
|
|
operations.push({ type: 'mkdir', path: `/concurrent-test-${i % 5}`, options: { recursive: true } })
|
|
operations.push({
|
|
type: 'write',
|
|
path: `/concurrent-test-${i % 5}/file${i}.txt`,
|
|
data: `content ${i}`
|
|
})
|
|
}
|
|
|
|
const result = await vfs.bulkWrite(operations)
|
|
|
|
// All operations should succeed (mkdirs are idempotent with recursive: true)
|
|
expect(result.failed.length).toBe(0)
|
|
|
|
// Verify no duplicate directories
|
|
const rootChildren = await vfs.getDirectChildren('/')
|
|
const dirNames = rootChildren
|
|
.filter(c => c.metadata.vfsType === 'directory')
|
|
.map(c => c.metadata.name)
|
|
|
|
// Each directory name should appear exactly once
|
|
for (let i = 0; i < 5; i++) {
|
|
const count = dirNames.filter(n => n === `concurrent-test-${i}`).length
|
|
expect(count).toBe(1)
|
|
}
|
|
|
|
// Verify all files are visible in their directories
|
|
for (let i = 0; i < 5; i++) {
|
|
const files = await vfs.readdir(`/concurrent-test-${i}`)
|
|
expect(files.length).toBe(4) // 4 files per directory (indices 0,5,10,15 for dir 0, etc.)
|
|
}
|
|
})
|
|
|
|
it('should handle the Consumer template creation scenario', async () => {
|
|
// Exact scenario from bug report: template creation with mixed ops
|
|
const operations = [
|
|
{ type: 'mkdir' as const, path: '/project/src' },
|
|
{ type: 'mkdir' as const, path: '/project/src/components' },
|
|
{ type: 'write' as const, path: '/project/src/index.ts', data: '// index' },
|
|
{ type: 'write' as const, path: '/project/src/components/App.tsx', data: '// app' },
|
|
{ type: 'mkdir' as const, path: '/project/public' },
|
|
{ type: 'write' as const, path: '/project/public/index.html', data: '<html>' },
|
|
{ type: 'write' as const, path: '/project/package.json', data: '{}' },
|
|
{ type: 'write' as const, path: '/project/README.md', data: '# Project' },
|
|
{ type: 'mkdir' as const, path: '/project' }, // Parent after children - should work
|
|
{ type: 'write' as const, path: '/project/tsconfig.json', data: '{}' },
|
|
]
|
|
|
|
const result = await vfs.bulkWrite(operations)
|
|
|
|
expect(result.successful).toBe(10)
|
|
expect(result.failed.length).toBe(0)
|
|
|
|
// Verify tree structure via getTreeStructure (this was failing before fix)
|
|
const tree = await vfs.getTreeStructure('/', { maxDepth: 4 })
|
|
|
|
// Find project directory
|
|
const projectDir = tree.children?.find(c => c.name === 'project')
|
|
expect(projectDir).toBeDefined()
|
|
expect(projectDir?.type).toBe('directory')
|
|
|
|
// Verify all files are visible
|
|
const projectFiles = await vfs.readdir('/project')
|
|
expect(projectFiles).toContain('src')
|
|
expect(projectFiles).toContain('public')
|
|
expect(projectFiles).toContain('package.json')
|
|
expect(projectFiles).toContain('README.md')
|
|
expect(projectFiles).toContain('tsconfig.json')
|
|
|
|
const srcFiles = await vfs.readdir('/project/src')
|
|
expect(srcFiles).toContain('index.ts')
|
|
expect(srcFiles).toContain('components')
|
|
|
|
const componentFiles = await vfs.readdir('/project/src/components')
|
|
expect(componentFiles).toContain('App.tsx')
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty operations array', async () => {
|
|
const result = await vfs.bulkWrite([])
|
|
expect(result.successful).toBe(0)
|
|
expect(result.failed.length).toBe(0)
|
|
})
|
|
|
|
it('should handle only mkdir operations', async () => {
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'mkdir', path: '/only-dirs/a' },
|
|
{ type: 'mkdir', path: '/only-dirs/b' },
|
|
{ type: 'mkdir', path: '/only-dirs' }
|
|
])
|
|
|
|
expect(result.successful).toBe(3)
|
|
expect(await vfs.exists('/only-dirs/a')).toBe(true)
|
|
expect(await vfs.exists('/only-dirs/b')).toBe(true)
|
|
})
|
|
|
|
it('should handle only write operations', async () => {
|
|
// Pre-create directory
|
|
await vfs.mkdir('/files-only', { recursive: true })
|
|
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'write', path: '/files-only/a.txt', data: 'a' },
|
|
{ type: 'write', path: '/files-only/b.txt', data: 'b' },
|
|
{ type: 'write', path: '/files-only/c.txt', data: 'c' }
|
|
])
|
|
|
|
expect(result.successful).toBe(3)
|
|
})
|
|
|
|
it('should handle mkdir for already existing directories gracefully', async () => {
|
|
// Pre-create directory
|
|
await vfs.mkdir('/existing', { recursive: true })
|
|
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'mkdir', path: '/existing', options: { recursive: true } },
|
|
{ type: 'write', path: '/existing/new-file.txt', data: 'content' }
|
|
])
|
|
|
|
// mkdir should succeed (no-op for existing dir with recursive: true)
|
|
expect(result.successful).toBe(2)
|
|
expect(await vfs.exists('/existing/new-file.txt')).toBe(true)
|
|
})
|
|
|
|
it('should handle delete operations after writes', async () => {
|
|
// First create some files
|
|
await vfs.mkdir('/temp', { recursive: true })
|
|
await vfs.writeFile('/temp/to-delete.txt', 'delete me')
|
|
await vfs.writeFile('/temp/to-keep.txt', 'keep me')
|
|
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'write', path: '/temp/new-file.txt', data: 'new' },
|
|
{ type: 'delete', path: '/temp/to-delete.txt' }
|
|
])
|
|
|
|
expect(result.successful).toBe(2)
|
|
expect(await vfs.exists('/temp/new-file.txt')).toBe(true)
|
|
expect(await vfs.exists('/temp/to-delete.txt')).toBe(false)
|
|
expect(await vfs.exists('/temp/to-keep.txt')).toBe(true)
|
|
})
|
|
|
|
it('should handle update operations', async () => {
|
|
await vfs.writeFile('/doc.txt', 'original content')
|
|
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'update', path: '/doc.txt', options: { metadata: { custom: 'value' } } }
|
|
])
|
|
|
|
expect(result.successful).toBe(1)
|
|
|
|
const entity = await vfs.getEntity('/doc.txt')
|
|
expect(entity.metadata.custom).toBe('value')
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
it('should continue processing after mkdir failure', async () => {
|
|
// Create a file where we'll try to mkdir
|
|
await vfs.writeFile('/not-a-dir', 'content')
|
|
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'mkdir', path: '/not-a-dir' }, // Will fail - file exists
|
|
{ type: 'mkdir', path: '/good-dir' },
|
|
{ type: 'write', path: '/good-dir/file.txt', data: 'content' }
|
|
])
|
|
|
|
expect(result.successful).toBe(2) // good-dir and file.txt
|
|
expect(result.failed.length).toBe(1) // not-a-dir
|
|
expect(await vfs.exists('/good-dir/file.txt')).toBe(true)
|
|
})
|
|
|
|
it('should continue processing after write failure', async () => {
|
|
const result = await vfs.bulkWrite([
|
|
{ type: 'mkdir', path: '/test-dir' },
|
|
{ type: 'write', path: '/test-dir/good.txt', data: 'content' },
|
|
{ type: 'delete', path: '/nonexistent/file.txt' } // Will fail
|
|
])
|
|
|
|
expect(result.successful).toBe(2) // mkdir and write
|
|
expect(result.failed.length).toBe(1) // delete
|
|
expect(await vfs.exists('/test-dir/good.txt')).toBe(true)
|
|
})
|
|
})
|
|
})
|