@soulcraft/brainy 5.7.0 β 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/dist/storage/baseStorage.d.ts +3 -1
- package/dist/storage/baseStorage.js +121 -46
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [5.7.2](https://github.com/soulcraftlabs/brainy/compare/v5.7.1...v5.7.2) (2025-11-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### π Bug Fixes
|
|
9
|
+
|
|
10
|
+
* resolve v5.7.x race condition with write-through cache (v5.7.2) ([732d23b](https://github.com/soulcraftlabs/brainy/commit/732d23bd2afb4ac9559a9beb7835e0f623065ff2))
|
|
11
|
+
|
|
12
|
+
### [5.7.1](https://github.com/soulcraftlabs/brainy/compare/v5.7.0...v5.7.1) (2025-11-11)
|
|
13
|
+
|
|
14
|
+
- fix: resolve v5.7.0 deadlock by restoring storage layer separation (v5.7.1) (eb9af45)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [5.7.1](https://github.com/soulcraftlabs/brainy/compare/v5.7.0...v5.7.1) (2025-11-11)
|
|
18
|
+
|
|
19
|
+
### π¨ CRITICAL BUG FIX
|
|
20
|
+
|
|
21
|
+
**v5.7.0 caused complete production failure - ALL imports hung indefinitely. This hotfix restores functionality.**
|
|
22
|
+
|
|
23
|
+
### Bug Description
|
|
24
|
+
v5.7.0 introduced a circular dependency deadlock during GraphAdjacencyIndex initialization:
|
|
25
|
+
- `GraphAdjacencyIndex.rebuild()` β `storage.getVerbs()`
|
|
26
|
+
- `storage.getVerbsBySource_internal()` β `getGraphIndex()` (NEW in v5.7.0)
|
|
27
|
+
- `getGraphIndex()` waiting for rebuild to complete
|
|
28
|
+
- **DEADLOCK**: Each component waiting for the other
|
|
29
|
+
|
|
30
|
+
### Symptoms
|
|
31
|
+
- β ALL imports hung at "Reading Data Structure" stage for 760+ seconds
|
|
32
|
+
- β `brain.add()` operations took 12+ seconds per entity (50x slower than expected)
|
|
33
|
+
- β No errors thrown - infinite wait
|
|
34
|
+
- β Zero entities imported successfully
|
|
35
|
+
- β 100% of users unable to import files
|
|
36
|
+
|
|
37
|
+
### Root Cause
|
|
38
|
+
v5.7.0 modified storage internal methods (`getVerbsBySource_internal`, `getVerbsByTarget_internal`) to use GraphAdjacencyIndex, creating tight coupling where:
|
|
39
|
+
- Storage layer depends on index
|
|
40
|
+
- Index depends on storage layer
|
|
41
|
+
- Circular dependency = deadlock during initialization
|
|
42
|
+
|
|
43
|
+
### Fix (Architectural)
|
|
44
|
+
Reverted storage internals to v5.6.3 implementation:
|
|
45
|
+
- β
Storage layer is now simple and has no index dependencies
|
|
46
|
+
- β
GraphAdjacencyIndex can safely call storage.getVerbs() to rebuild
|
|
47
|
+
- β
No circular dependency possible
|
|
48
|
+
- β
Proper separation of concerns restored
|
|
49
|
+
|
|
50
|
+
**Files changed**:
|
|
51
|
+
- `src/storage/baseStorage.ts`: Reverted lines 2320-2444 to v5.6.3 implementation
|
|
52
|
+
- `tests/regression/v5.7.0-deadlock.test.ts`: Added comprehensive regression tests
|
|
53
|
+
|
|
54
|
+
### Performance Impact
|
|
55
|
+
- Slightly slower GraphAdjacencyIndex initialization (one-time cost during rebuild)
|
|
56
|
+
- High-level query operations still use optimized index
|
|
57
|
+
- Import performance unaffected (writes don't trigger index initialization)
|
|
58
|
+
- **NO breaking changes to public API**
|
|
59
|
+
|
|
60
|
+
### Testing
|
|
61
|
+
- β
4 new regression tests verify no deadlock
|
|
62
|
+
- β
All 1146 existing tests pass
|
|
63
|
+
- β
Import + relationships complete in <1 second (not 760+ seconds)
|
|
64
|
+
- β
No 12+ second delays per entity
|
|
65
|
+
|
|
66
|
+
### Verification
|
|
67
|
+
Workshop team (production users) should upgrade immediately:
|
|
68
|
+
```bash
|
|
69
|
+
npm install @soulcraft/brainy@5.7.1
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Expected behavior after upgrade:
|
|
73
|
+
- β
Imports work again
|
|
74
|
+
- β
Fast entity creation (<100ms per entity)
|
|
75
|
+
- β
No hangs or infinite waits
|
|
76
|
+
- β
File operations responsive
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
5
80
|
### [5.7.0](https://github.com/soulcraftlabs/brainy/compare/v5.6.3...v5.7.0) (2025-11-11)
|
|
6
81
|
|
|
82
|
+
**β οΈ WARNING: This version has a critical deadlock bug. Use v5.7.1 instead.**
|
|
83
|
+
|
|
7
84
|
- test: skip flaky concurrent relationship test (race condition in duplicate detection) (a71785b)
|
|
8
85
|
- perf: optimize imports with background deduplication (12-24x speedup) (02c80a0)
|
|
9
86
|
|
|
@@ -53,6 +53,7 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
|
|
|
53
53
|
protected graphIndex?: GraphAdjacencyIndex;
|
|
54
54
|
protected graphIndexPromise?: Promise<GraphAdjacencyIndex>;
|
|
55
55
|
protected readOnly: boolean;
|
|
56
|
+
private writeCache;
|
|
56
57
|
refManager?: RefManager;
|
|
57
58
|
blobStorage?: BlobStorage;
|
|
58
59
|
commitLog?: CommitLog;
|
|
@@ -488,7 +489,8 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
|
|
|
488
489
|
protected getVerbsBySource_internal(sourceId: string): Promise<HNSWVerbWithMetadata[]>;
|
|
489
490
|
/**
|
|
490
491
|
* Get verbs by target (COW-aware implementation)
|
|
491
|
-
* v5.7.
|
|
492
|
+
* v5.7.1: Reverted to v5.6.3 implementation to fix circular dependency deadlock
|
|
493
|
+
* v5.4.0: Fixed to directly list verb files instead of directories
|
|
492
494
|
*/
|
|
493
495
|
protected getVerbsByTarget_internal(targetId: string): Promise<HNSWVerbWithMetadata[]>;
|
|
494
496
|
/**
|
|
@@ -74,6 +74,12 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
74
74
|
super(...arguments);
|
|
75
75
|
this.isInitialized = false;
|
|
76
76
|
this.readOnly = false;
|
|
77
|
+
// v5.7.2: Write-through cache for read-after-write consistency
|
|
78
|
+
// Guarantees that immediately after writeObjectToBranch(), readWithInheritance() returns the data
|
|
79
|
+
// Cache key: resolved branchPath (includes branch scope for COW isolation)
|
|
80
|
+
// Cache lifetime: write start β write completion (microseconds to milliseconds)
|
|
81
|
+
// Memory footprint: Typically <10 items (only in-flight writes), <1KB total
|
|
82
|
+
this.writeCache = new Map();
|
|
77
83
|
this.currentBranch = 'main';
|
|
78
84
|
this.cowEnabled = false;
|
|
79
85
|
// Type-first indexing support (v5.4.0)
|
|
@@ -353,7 +359,17 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
353
359
|
*/
|
|
354
360
|
async writeObjectToBranch(path, data, branch) {
|
|
355
361
|
const branchPath = this.resolveBranchPath(path, branch);
|
|
356
|
-
|
|
362
|
+
// v5.7.2: Add to write cache BEFORE async write (guarantees read-after-write consistency)
|
|
363
|
+
// This ensures readWithInheritance() returns data immediately, fixing "Source entity not found" bug
|
|
364
|
+
this.writeCache.set(branchPath, data);
|
|
365
|
+
try {
|
|
366
|
+
await this.writeObjectToPath(branchPath, data);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
// v5.7.2: Remove from cache after write completes (success or failure)
|
|
370
|
+
// Small memory footprint: cache only holds in-flight writes (typically <10 items)
|
|
371
|
+
this.writeCache.delete(branchPath);
|
|
372
|
+
}
|
|
357
373
|
}
|
|
358
374
|
/**
|
|
359
375
|
* Read object with inheritance from parent branches (COW layer)
|
|
@@ -362,12 +378,24 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
362
378
|
*/
|
|
363
379
|
async readWithInheritance(path, branch) {
|
|
364
380
|
if (!this.cowEnabled) {
|
|
365
|
-
// COW disabled, direct read
|
|
381
|
+
// COW disabled: check write cache, then direct read
|
|
382
|
+
// v5.7.2: Check cache first for read-after-write consistency
|
|
383
|
+
const cachedData = this.writeCache.get(path);
|
|
384
|
+
if (cachedData !== undefined) {
|
|
385
|
+
return cachedData;
|
|
386
|
+
}
|
|
366
387
|
return this.readObjectFromPath(path);
|
|
367
388
|
}
|
|
368
389
|
const targetBranch = branch || this.currentBranch || 'main';
|
|
369
|
-
// Try current branch first
|
|
370
390
|
const branchPath = this.resolveBranchPath(path, targetBranch);
|
|
391
|
+
// v5.7.2: Check write cache FIRST (synchronous, instant)
|
|
392
|
+
// This guarantees read-after-write consistency within the same process
|
|
393
|
+
// Fixes bug: brain.add() β brain.relate() β "Source entity not found"
|
|
394
|
+
const cachedData = this.writeCache.get(branchPath);
|
|
395
|
+
if (cachedData !== undefined) {
|
|
396
|
+
return cachedData;
|
|
397
|
+
}
|
|
398
|
+
// Try current branch first
|
|
371
399
|
let data = await this.readObjectFromPath(branchPath);
|
|
372
400
|
if (data !== null) {
|
|
373
401
|
return data; // Found in current branch
|
|
@@ -411,6 +439,9 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
411
439
|
*/
|
|
412
440
|
async deleteObjectFromBranch(path, branch) {
|
|
413
441
|
const branchPath = this.resolveBranchPath(path, branch);
|
|
442
|
+
// v5.7.2: Remove from write cache immediately (before async delete)
|
|
443
|
+
// Ensures subsequent reads don't return stale cached data
|
|
444
|
+
this.writeCache.delete(branchPath);
|
|
414
445
|
return this.deleteObjectFromPath(branchPath);
|
|
415
446
|
}
|
|
416
447
|
/**
|
|
@@ -1868,71 +1899,115 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1868
1899
|
* v5.4.0: Fixed to directly list verb files instead of directories
|
|
1869
1900
|
*/
|
|
1870
1901
|
async getVerbsBySource_internal(sourceId) {
|
|
1871
|
-
// v5.7.
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1902
|
+
// v5.7.1: Reverted to v5.6.3 implementation to fix circular dependency deadlock
|
|
1903
|
+
// v5.7.0 called getGraphIndex() here, creating deadlock during initialization:
|
|
1904
|
+
// GraphAdjacencyIndex.rebuild() β storage.getVerbs() β getVerbsBySource_internal() β getGraphIndex() β [deadlock]
|
|
1905
|
+
// v5.4.0: Type-first implementation - scan across all verb types
|
|
1906
|
+
// COW-aware: uses readWithInheritance for each verb
|
|
1874
1907
|
await this.ensureInitialized();
|
|
1875
|
-
const startTime = performance.now();
|
|
1876
|
-
// Get GraphAdjacencyIndex (lazy-initialized)
|
|
1877
|
-
const graphIndex = await this.getGraphIndex();
|
|
1878
|
-
// O(log n) lookup with bloom filter optimization
|
|
1879
|
-
const verbIds = await graphIndex.getVerbIdsBySource(sourceId);
|
|
1880
|
-
// Load each verb by ID (uses existing optimized getVerb())
|
|
1881
1908
|
const results = [];
|
|
1882
|
-
|
|
1909
|
+
// Iterate through all verb types
|
|
1910
|
+
for (let i = 0; i < VERB_TYPE_COUNT; i++) {
|
|
1911
|
+
const type = TypeUtils.getVerbFromIndex(i);
|
|
1912
|
+
const typeDir = `entities/verbs/${type}/vectors`;
|
|
1883
1913
|
try {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1914
|
+
// v5.4.0 FIX: List all verb files directly (not shard directories)
|
|
1915
|
+
// listObjectsInBranch returns full paths to .json files, not directories
|
|
1916
|
+
const verbFiles = await this.listObjectsInBranch(typeDir);
|
|
1917
|
+
for (const verbPath of verbFiles) {
|
|
1918
|
+
// Skip if not a .json file
|
|
1919
|
+
if (!verbPath.endsWith('.json'))
|
|
1920
|
+
continue;
|
|
1921
|
+
try {
|
|
1922
|
+
const verb = await this.readWithInheritance(verbPath);
|
|
1923
|
+
if (verb && verb.sourceId === sourceId) {
|
|
1924
|
+
// v5.4.0: Use proper path helper instead of string replacement
|
|
1925
|
+
const metadataPath = getVerbMetadataPath(type, verb.id);
|
|
1926
|
+
const metadata = await this.readWithInheritance(metadataPath);
|
|
1927
|
+
// v5.4.0: Extract standard fields from metadata to top-level (like nouns)
|
|
1928
|
+
results.push({
|
|
1929
|
+
...verb,
|
|
1930
|
+
weight: metadata?.weight,
|
|
1931
|
+
confidence: metadata?.confidence,
|
|
1932
|
+
createdAt: metadata?.createdAt
|
|
1933
|
+
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
|
1934
|
+
: Date.now(),
|
|
1935
|
+
updatedAt: metadata?.updatedAt
|
|
1936
|
+
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
|
1937
|
+
: Date.now(),
|
|
1938
|
+
service: metadata?.service,
|
|
1939
|
+
createdBy: metadata?.createdBy,
|
|
1940
|
+
metadata: metadata || {}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
catch (error) {
|
|
1945
|
+
// Skip verbs that fail to load
|
|
1946
|
+
}
|
|
1887
1947
|
}
|
|
1888
1948
|
}
|
|
1889
1949
|
catch (error) {
|
|
1890
|
-
// Skip
|
|
1950
|
+
// Skip types that have no data
|
|
1891
1951
|
}
|
|
1892
1952
|
}
|
|
1893
|
-
const elapsed = performance.now() - startTime;
|
|
1894
|
-
// Performance monitoring - should be 100-10,000x faster than old O(n) scan
|
|
1895
|
-
if (elapsed > 50.0) {
|
|
1896
|
-
prodLog.warn(`getVerbsBySource_internal: Slow query for ${sourceId} ` +
|
|
1897
|
-
`(${verbIds.length} verbs, ${elapsed.toFixed(2)}ms). ` +
|
|
1898
|
-
`Expected <50ms with index optimization.`);
|
|
1899
|
-
}
|
|
1900
1953
|
return results;
|
|
1901
1954
|
}
|
|
1902
1955
|
/**
|
|
1903
1956
|
* Get verbs by target (COW-aware implementation)
|
|
1904
|
-
* v5.7.
|
|
1957
|
+
* v5.7.1: Reverted to v5.6.3 implementation to fix circular dependency deadlock
|
|
1958
|
+
* v5.4.0: Fixed to directly list verb files instead of directories
|
|
1905
1959
|
*/
|
|
1906
1960
|
async getVerbsByTarget_internal(targetId) {
|
|
1907
|
-
// v5.7.
|
|
1908
|
-
//
|
|
1909
|
-
//
|
|
1961
|
+
// v5.7.1: Reverted to v5.6.3 implementation to fix circular dependency deadlock
|
|
1962
|
+
// v5.7.0 called getGraphIndex() here, creating deadlock during initialization
|
|
1963
|
+
// v5.4.0: Type-first implementation - scan across all verb types
|
|
1964
|
+
// COW-aware: uses readWithInheritance for each verb
|
|
1910
1965
|
await this.ensureInitialized();
|
|
1911
|
-
const startTime = performance.now();
|
|
1912
|
-
// Get GraphAdjacencyIndex (lazy-initialized)
|
|
1913
|
-
const graphIndex = await this.getGraphIndex();
|
|
1914
|
-
// O(log n) lookup with bloom filter optimization
|
|
1915
|
-
const verbIds = await graphIndex.getVerbIdsByTarget(targetId);
|
|
1916
|
-
// Load each verb by ID (uses existing optimized getVerb())
|
|
1917
1966
|
const results = [];
|
|
1918
|
-
|
|
1967
|
+
// Iterate through all verb types
|
|
1968
|
+
for (let i = 0; i < VERB_TYPE_COUNT; i++) {
|
|
1969
|
+
const type = TypeUtils.getVerbFromIndex(i);
|
|
1970
|
+
const typeDir = `entities/verbs/${type}/vectors`;
|
|
1919
1971
|
try {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1972
|
+
// v5.4.0 FIX: List all verb files directly (not shard directories)
|
|
1973
|
+
// listObjectsInBranch returns full paths to .json files, not directories
|
|
1974
|
+
const verbFiles = await this.listObjectsInBranch(typeDir);
|
|
1975
|
+
for (const verbPath of verbFiles) {
|
|
1976
|
+
// Skip if not a .json file
|
|
1977
|
+
if (!verbPath.endsWith('.json'))
|
|
1978
|
+
continue;
|
|
1979
|
+
try {
|
|
1980
|
+
const verb = await this.readWithInheritance(verbPath);
|
|
1981
|
+
if (verb && verb.targetId === targetId) {
|
|
1982
|
+
// v5.4.0: Use proper path helper instead of string replacement
|
|
1983
|
+
const metadataPath = getVerbMetadataPath(type, verb.id);
|
|
1984
|
+
const metadata = await this.readWithInheritance(metadataPath);
|
|
1985
|
+
// v5.4.0: Extract standard fields from metadata to top-level (like nouns)
|
|
1986
|
+
results.push({
|
|
1987
|
+
...verb,
|
|
1988
|
+
weight: metadata?.weight,
|
|
1989
|
+
confidence: metadata?.confidence,
|
|
1990
|
+
createdAt: metadata?.createdAt
|
|
1991
|
+
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
|
1992
|
+
: Date.now(),
|
|
1993
|
+
updatedAt: metadata?.updatedAt
|
|
1994
|
+
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
|
1995
|
+
: Date.now(),
|
|
1996
|
+
service: metadata?.service,
|
|
1997
|
+
createdBy: metadata?.createdBy,
|
|
1998
|
+
metadata: metadata || {}
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
catch (error) {
|
|
2003
|
+
// Skip verbs that fail to load
|
|
2004
|
+
}
|
|
1923
2005
|
}
|
|
1924
2006
|
}
|
|
1925
2007
|
catch (error) {
|
|
1926
|
-
// Skip
|
|
2008
|
+
// Skip types that have no data
|
|
1927
2009
|
}
|
|
1928
2010
|
}
|
|
1929
|
-
const elapsed = performance.now() - startTime;
|
|
1930
|
-
// Performance monitoring - should be 100-10,000x faster than old O(n) scan
|
|
1931
|
-
if (elapsed > 50.0) {
|
|
1932
|
-
prodLog.warn(`getVerbsByTarget_internal: Slow query for ${targetId} ` +
|
|
1933
|
-
`(${verbIds.length} verbs, ${elapsed.toFixed(2)}ms). ` +
|
|
1934
|
-
`Expected <50ms with index optimization.`);
|
|
1935
|
-
}
|
|
1936
2011
|
return results;
|
|
1937
2012
|
}
|
|
1938
2013
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.2",
|
|
4
4
|
"description": "Universal Knowledge Protocolβ’ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns Γ 127 verbs covering 96-97% of all human knowledge.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|