@soulcraft/brainy 3.20.0 → 3.20.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
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
### [3.20.2](https://github.com/soulcraftlabs/brainy/compare/v3.20.1...v3.20.2) (2025-09-30)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **vfs**: resolve VFS race conditions and decompression errors ([1a2661f](https://github.com/soulcraftlabs/brainy/commit/1a2661f))
|
|
10
|
+
- Fixes duplicate directory nodes caused by concurrent writes
|
|
11
|
+
- Fixes file read decompression errors caused by rawData compression state mismatch
|
|
12
|
+
- Adds mutex-based concurrency control for mkdir operations
|
|
13
|
+
- Adds explicit compression tracking for file reads
|
|
14
|
+
|
|
5
15
|
### [3.19.1](https://github.com/soulcraftlabs/brainy/compare/v3.19.0...v3.19.1) (2025-09-29)
|
|
6
16
|
|
|
7
17
|
## [3.19.0](https://github.com/soulcraftlabs/brainy/compare/v3.18.0...v3.19.0) (2025-09-29)
|
|
@@ -250,6 +250,8 @@ main()
|
|
|
250
250
|
});
|
|
251
251
|
await brain.init();
|
|
252
252
|
spinner.succeed('Initialized Brainy database');
|
|
253
|
+
// Shutdown Brainy to release resources
|
|
254
|
+
await brain.close();
|
|
253
255
|
// Success!
|
|
254
256
|
console.log(chalk.bold.green('\n✅ Setup complete!\n'));
|
|
255
257
|
console.log(chalk.cyan('📁 Memory storage:'), brainyDir);
|
|
@@ -367,6 +369,7 @@ async function handleSearch(argv) {
|
|
|
367
369
|
spinner.succeed(`Found ${results.length} messages`);
|
|
368
370
|
if (results.length === 0) {
|
|
369
371
|
console.log(chalk.yellow('No messages found'));
|
|
372
|
+
await brain.close();
|
|
370
373
|
return;
|
|
371
374
|
}
|
|
372
375
|
// Display results
|
|
@@ -376,6 +379,7 @@ async function handleSearch(argv) {
|
|
|
376
379
|
console.log(chalk.dim(` Score: ${result.score.toFixed(3)} | Conv: ${result.conversationId}`));
|
|
377
380
|
console.log();
|
|
378
381
|
}
|
|
382
|
+
await brain.close();
|
|
379
383
|
}
|
|
380
384
|
/**
|
|
381
385
|
* Handle context command - Get relevant context
|
|
@@ -398,6 +402,7 @@ async function handleContext(argv) {
|
|
|
398
402
|
spinner.succeed(`Retrieved ${context.messages.length} relevant messages`);
|
|
399
403
|
if (context.messages.length === 0) {
|
|
400
404
|
console.log(chalk.yellow('No relevant context found'));
|
|
405
|
+
await brain.close();
|
|
401
406
|
return;
|
|
402
407
|
}
|
|
403
408
|
// Display context
|
|
@@ -420,6 +425,7 @@ async function handleContext(argv) {
|
|
|
420
425
|
console.log(chalk.dim(` - ${conv.title || conv.id} (${conv.relevance.toFixed(2)})`));
|
|
421
426
|
}
|
|
422
427
|
}
|
|
428
|
+
await brain.close();
|
|
423
429
|
}
|
|
424
430
|
/**
|
|
425
431
|
* Handle thread command - Get conversation thread
|
|
@@ -452,6 +458,7 @@ async function handleThread(argv) {
|
|
|
452
458
|
console.log(chalk.cyan(`${msg.role}:`), msg.content);
|
|
453
459
|
console.log(chalk.dim(` ${new Date(msg.createdAt).toLocaleString()}`));
|
|
454
460
|
}
|
|
461
|
+
await brain.close();
|
|
455
462
|
}
|
|
456
463
|
/**
|
|
457
464
|
* Handle stats command - Show statistics
|
|
@@ -487,6 +494,7 @@ async function handleStats(argv) {
|
|
|
487
494
|
console.log(chalk.dim(` ${phase}: ${count}`));
|
|
488
495
|
}
|
|
489
496
|
}
|
|
497
|
+
await brain.close();
|
|
490
498
|
}
|
|
491
499
|
/**
|
|
492
500
|
* Handle export command - Export conversation
|
|
@@ -505,6 +513,7 @@ async function handleExport(argv) {
|
|
|
505
513
|
const output = argv.output || `conversation_${argv.conversationId}.json`;
|
|
506
514
|
await fs.writeFile(output, JSON.stringify(exported, null, 2), 'utf8');
|
|
507
515
|
spinner.succeed(`Exported to ${output}`);
|
|
516
|
+
await brain.close();
|
|
508
517
|
}
|
|
509
518
|
/**
|
|
510
519
|
* Handle import command - Import conversation
|
|
@@ -523,6 +532,7 @@ async function handleImport(argv) {
|
|
|
523
532
|
const data = JSON.parse(await fs.readFile(inputFile, 'utf8'));
|
|
524
533
|
const conversationId = await conv.importConversation(data);
|
|
525
534
|
spinner.succeed(`Imported as conversation ${conversationId}`);
|
|
535
|
+
await brain.close();
|
|
526
536
|
}
|
|
527
537
|
export default conversationCommand;
|
|
528
538
|
//# sourceMappingURL=conversation.js.map
|
|
@@ -25,6 +25,8 @@ export class VirtualFileSystem {
|
|
|
25
25
|
this.currentUser = 'system'; // Track current user for collaboration
|
|
26
26
|
// Background task timer
|
|
27
27
|
this.backgroundTimer = null;
|
|
28
|
+
// Mutex for preventing race conditions in directory creation
|
|
29
|
+
this.mkdirLocks = new Map();
|
|
28
30
|
this.brain = brain || new Brainy();
|
|
29
31
|
this.contentCache = new Map();
|
|
30
32
|
this.statCache = new Map();
|
|
@@ -159,16 +161,20 @@ export class VirtualFileSystem {
|
|
|
159
161
|
}
|
|
160
162
|
// Get content based on storage type
|
|
161
163
|
let content;
|
|
164
|
+
let isCompressed = false;
|
|
162
165
|
if (!entity.metadata.storage || entity.metadata.storage.type === 'inline') {
|
|
163
166
|
// Content stored in metadata for new files, or try entity data for compatibility
|
|
164
167
|
if (entity.metadata.rawData) {
|
|
168
|
+
// rawData is ALWAYS stored uncompressed as base64
|
|
165
169
|
content = Buffer.from(entity.metadata.rawData, 'base64');
|
|
170
|
+
isCompressed = false; // rawData is never compressed
|
|
166
171
|
}
|
|
167
172
|
else if (!entity.data) {
|
|
168
173
|
content = Buffer.alloc(0);
|
|
169
174
|
}
|
|
170
175
|
else if (Buffer.isBuffer(entity.data)) {
|
|
171
176
|
content = entity.data;
|
|
177
|
+
isCompressed = entity.metadata.storage?.compressed || false;
|
|
172
178
|
}
|
|
173
179
|
else if (typeof entity.data === 'string') {
|
|
174
180
|
content = Buffer.from(entity.data);
|
|
@@ -180,16 +186,18 @@ export class VirtualFileSystem {
|
|
|
180
186
|
else if (entity.metadata.storage.type === 'reference') {
|
|
181
187
|
// Content stored in external storage
|
|
182
188
|
content = await this.readExternalContent(entity.metadata.storage.key);
|
|
189
|
+
isCompressed = entity.metadata.storage.compressed || false;
|
|
183
190
|
}
|
|
184
191
|
else if (entity.metadata.storage.type === 'chunked') {
|
|
185
192
|
// Content stored in chunks
|
|
186
193
|
content = await this.readChunkedContent(entity.metadata.storage.chunks);
|
|
194
|
+
isCompressed = entity.metadata.storage.compressed || false;
|
|
187
195
|
}
|
|
188
196
|
else {
|
|
189
197
|
throw new VFSError(VFSErrorCode.EIO, `Unknown storage type: ${entity.metadata.storage.type}`, path, 'readFile');
|
|
190
198
|
}
|
|
191
|
-
// Decompress if needed
|
|
192
|
-
if (
|
|
199
|
+
// Decompress if needed (but NOT for rawData which is never compressed)
|
|
200
|
+
if (isCompressed && options?.decompress !== false) {
|
|
193
201
|
content = await this.decompress(content);
|
|
194
202
|
}
|
|
195
203
|
// Update access time
|
|
@@ -317,7 +325,7 @@ export class VirtualFileSystem {
|
|
|
317
325
|
type: this.getFileNounType(mimeType),
|
|
318
326
|
metadata
|
|
319
327
|
});
|
|
320
|
-
// Create parent-child relationship
|
|
328
|
+
// Create parent-child relationship (no need to check for duplicates on new entities)
|
|
321
329
|
await this.brain.relate({
|
|
322
330
|
from: parentId,
|
|
323
331
|
to: entity,
|
|
@@ -498,78 +506,106 @@ export class VirtualFileSystem {
|
|
|
498
506
|
*/
|
|
499
507
|
async mkdir(path, options) {
|
|
500
508
|
await this.ensureInitialized();
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
509
|
+
// Use mutex to prevent race conditions when creating the same directory concurrently
|
|
510
|
+
// If another call is already creating this directory, wait for it to complete
|
|
511
|
+
const existingLock = this.mkdirLocks.get(path);
|
|
512
|
+
if (existingLock) {
|
|
513
|
+
await existingLock;
|
|
514
|
+
// After waiting, check if directory now exists
|
|
515
|
+
try {
|
|
516
|
+
const existing = await this.pathResolver.resolve(path);
|
|
517
|
+
const entity = await this.getEntityById(existing);
|
|
518
|
+
if (entity.metadata.vfsType === 'directory') {
|
|
519
|
+
return; // Directory was created by the other call
|
|
508
520
|
}
|
|
509
|
-
return; // Already exists and recursive is true
|
|
510
521
|
}
|
|
511
|
-
|
|
512
|
-
//
|
|
513
|
-
throw new VFSError(VFSErrorCode.EEXIST, `File exists: ${path}`, path, 'mkdir');
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
catch (err) {
|
|
517
|
-
// Only proceed if it's a ENOENT error (path doesn't exist)
|
|
518
|
-
if (err instanceof VFSError && err.code !== VFSErrorCode.ENOENT) {
|
|
519
|
-
throw err; // Re-throw non-ENOENT errors
|
|
522
|
+
catch (err) {
|
|
523
|
+
// Still doesn't exist, proceed to create
|
|
520
524
|
}
|
|
521
|
-
// Doesn't exist, proceed to create
|
|
522
525
|
}
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
parentId = this.rootEntityId;
|
|
530
|
-
}
|
|
531
|
-
else if (options?.recursive) {
|
|
532
|
-
parentId = await this.ensureDirectory(parentPath);
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
526
|
+
// Create a lock promise for this path
|
|
527
|
+
let resolveLock;
|
|
528
|
+
const lockPromise = new Promise(resolve => { resolveLock = resolve; });
|
|
529
|
+
this.mkdirLocks.set(path, lockPromise);
|
|
530
|
+
try {
|
|
531
|
+
// Check if already exists
|
|
535
532
|
try {
|
|
536
|
-
|
|
533
|
+
const existing = await this.pathResolver.resolve(path);
|
|
534
|
+
const entity = await this.getEntityById(existing);
|
|
535
|
+
if (entity.metadata.vfsType === 'directory') {
|
|
536
|
+
if (!options?.recursive) {
|
|
537
|
+
throw new VFSError(VFSErrorCode.EEXIST, `Directory exists: ${path}`, path, 'mkdir');
|
|
538
|
+
}
|
|
539
|
+
return; // Already exists and recursive is true
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
// Path exists but it's not a directory
|
|
543
|
+
throw new VFSError(VFSErrorCode.EEXIST, `File exists: ${path}`, path, 'mkdir');
|
|
544
|
+
}
|
|
537
545
|
}
|
|
538
546
|
catch (err) {
|
|
539
|
-
|
|
547
|
+
// Only proceed if it's a ENOENT error (path doesn't exist)
|
|
548
|
+
if (err instanceof VFSError && err.code !== VFSErrorCode.ENOENT) {
|
|
549
|
+
throw err; // Re-throw non-ENOENT errors
|
|
550
|
+
}
|
|
551
|
+
// Doesn't exist, proceed to create
|
|
540
552
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
553
|
+
// Parse path
|
|
554
|
+
const parentPath = this.getParentPath(path);
|
|
555
|
+
const name = this.getBasename(path);
|
|
556
|
+
// Ensure parent exists (recursive mkdir if needed)
|
|
557
|
+
let parentId;
|
|
558
|
+
if (parentPath === '/' || parentPath === null) {
|
|
559
|
+
parentId = this.rootEntityId;
|
|
560
|
+
}
|
|
561
|
+
else if (options?.recursive) {
|
|
562
|
+
parentId = await this.ensureDirectory(parentPath);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
try {
|
|
566
|
+
parentId = await this.pathResolver.resolve(parentPath);
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
throw new VFSError(VFSErrorCode.ENOENT, `Parent directory not found: ${parentPath}`, path, 'mkdir');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Create directory entity
|
|
573
|
+
const metadata = {
|
|
574
|
+
path,
|
|
575
|
+
name,
|
|
576
|
+
parent: parentId,
|
|
577
|
+
vfsType: 'directory',
|
|
578
|
+
size: 0,
|
|
579
|
+
permissions: options?.mode || this.config.permissions?.defaultDirectory || 0o755,
|
|
580
|
+
owner: 'user',
|
|
581
|
+
group: 'users',
|
|
582
|
+
accessed: Date.now(),
|
|
583
|
+
modified: Date.now(),
|
|
584
|
+
...options?.metadata
|
|
585
|
+
};
|
|
586
|
+
const entity = await this.brain.add({
|
|
587
|
+
data: path, // Directory path as string content
|
|
588
|
+
type: NounType.Collection,
|
|
589
|
+
metadata
|
|
567
590
|
});
|
|
591
|
+
// Create parent-child relationship (no need to check for duplicates on new entities)
|
|
592
|
+
if (parentId !== entity) { // Don't relate to self (root)
|
|
593
|
+
await this.brain.relate({
|
|
594
|
+
from: parentId,
|
|
595
|
+
to: entity,
|
|
596
|
+
type: VerbType.Contains
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// Update path resolver cache
|
|
600
|
+
await this.pathResolver.createPath(path, entity);
|
|
601
|
+
// Trigger watchers
|
|
602
|
+
this.triggerWatchers(path, 'rename');
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
// Release the lock
|
|
606
|
+
resolveLock();
|
|
607
|
+
this.mkdirLocks.delete(path);
|
|
568
608
|
}
|
|
569
|
-
// Update path resolver cache
|
|
570
|
-
await this.pathResolver.createPath(path, entity);
|
|
571
|
-
// Trigger watchers
|
|
572
|
-
this.triggerWatchers(path, 'rename');
|
|
573
609
|
}
|
|
574
610
|
/**
|
|
575
611
|
* Remove a directory
|
|
@@ -1964,17 +2000,20 @@ export class VirtualFileSystem {
|
|
|
1964
2000
|
};
|
|
1965
2001
|
let earliestModified = null;
|
|
1966
2002
|
let latestModified = null;
|
|
1967
|
-
const traverse = async (currentPath) => {
|
|
2003
|
+
const traverse = async (currentPath, isRoot = false) => {
|
|
1968
2004
|
try {
|
|
1969
2005
|
const entityId = await this.pathResolver.resolve(currentPath);
|
|
1970
2006
|
const entity = await this.getEntityById(entityId);
|
|
1971
2007
|
if (entity.metadata.vfsType === 'directory') {
|
|
1972
|
-
|
|
2008
|
+
// Don't count the root/starting directory itself
|
|
2009
|
+
if (!isRoot) {
|
|
2010
|
+
stats.directoryCount++;
|
|
2011
|
+
}
|
|
1973
2012
|
// Traverse children
|
|
1974
2013
|
const children = await this.readdir(currentPath);
|
|
1975
2014
|
for (const child of children) {
|
|
1976
2015
|
const childPath = currentPath === '/' ? `/${child}` : `${currentPath}/${child}`;
|
|
1977
|
-
await traverse(childPath);
|
|
2016
|
+
await traverse(childPath, false);
|
|
1978
2017
|
}
|
|
1979
2018
|
}
|
|
1980
2019
|
else if (entity.metadata.vfsType === 'file') {
|
|
@@ -2005,7 +2044,7 @@ export class VirtualFileSystem {
|
|
|
2005
2044
|
// Skip if path doesn't exist
|
|
2006
2045
|
}
|
|
2007
2046
|
};
|
|
2008
|
-
await traverse(path);
|
|
2047
|
+
await traverse(path, true);
|
|
2009
2048
|
// Calculate averages
|
|
2010
2049
|
if (stats.fileCount > 0) {
|
|
2011
2050
|
stats.averageFileSize = Math.round(stats.totalSize / stats.fileCount);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "3.20.
|
|
3
|
+
"version": "3.20.2",
|
|
4
4
|
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|