From 2bd4031f9c8f0739a717aa5b842fa1f7bf3dd3a4 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Tue, 20 Jan 2026 17:37:27 -0800 Subject: [PATCH] fix: VFS readdir() no longer returns duplicate entries - PathResolver.getChildren() now deduplicates by entity ID (v7.4.1) This handles duplicate relationship records that can occur when multiple Brainy instances create relationships concurrently for the same storage path. - brain.clear() now invalidates GraphAdjacencyIndex (v7.4.1) Prevents stale in-memory index data after clearing storage, which could cause relate()'s duplicate check to fail. Fixes: Workshop bug where readdir('/') returned same directory 13+ times --- src/brainy.ts | 9 +++++++++ src/vfs/PathResolver.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/brainy.ts b/src/brainy.ts index 816faf9d..635d3d22 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -2636,6 +2636,15 @@ export class Brainy implements BrainyInterface { // Clear storage await this.storage.clear() + // v7.4.1: Invalidate GraphAdjacencyIndex to prevent stale in-memory data + // The index has LSMTree data and verbIdSet pointing to deleted entities. + // Without this, relate()'s duplicate check uses stale data, potentially + // allowing duplicate relationships or missing valid duplicates. + if (typeof (this.storage as any).invalidateGraphIndex === 'function') { + ;(this.storage as any).invalidateGraphIndex() + } + this.graphIndex = undefined as any + // Reset index if ('clear' in this.index && typeof this.index.clear === 'function') { await this.index.clear() diff --git a/src/vfs/PathResolver.ts b/src/vfs/PathResolver.ts index fc43c182..47db1bba 100644 --- a/src/vfs/PathResolver.ts +++ b/src/vfs/PathResolver.ts @@ -307,8 +307,20 @@ export class PathResolver { const childIds = relations.map(r => r.to) const childrenMap = await this.brain.batchGet(childIds) + // v7.4.1: Deduplicate by entity ID to handle duplicate relationship records + // This can occur when multiple Brainy instances create relationships concurrently + // for the same storage path (each instance has its own in-memory GraphAdjacencyIndex). + // The Set lookup is O(1), adding negligible overhead. + const seenEntityIds = new Set() + // Process batched results for (const relation of relations) { + // Skip if we've already processed this entity + if (seenEntityIds.has(relation.to)) { + continue + } + seenEntityIds.add(relation.to) + const entity = childrenMap.get(relation.to) if (entity && entity.metadata?.vfsType && entity.metadata?.name) { validChildren.push(entity as VFSEntity)