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)