fix(vfs): resolve two critical VFS bugs causing directory listing corruption

Bug 1: verbCountsByType optimization skipping requested verb types
- After restart, stale statistics could cause VerbType.Contains to be skipped
- readdir() would return empty/incomplete results
- Fixed by never skipping verb types explicitly requested in filter
- Added fast path for sourceId + verbType combo (common VFS pattern)
- Save statistics on first entity of each type (not just every 100th)

Bug 2: UnifiedCache not invalidated on path deletion
- rmdir() cleared local caches but NOT the global UnifiedCache
- When folder recreated, resolve() returned stale entity ID
- Caused "Source entity not found" errors
- Fixed by adding deleteByPrefix() to UnifiedCache
- Fixed invalidatePath() to also clear UnifiedCache entries

Files modified:
- src/storage/baseStorage.ts (verbCountsByType fix + fast path)
- src/utils/unifiedCache.ts (deleteByPrefix method)
- src/vfs/PathResolver.ts (cache invalidation fix)

Tests added:
- tests/unit/storage/vfs-mkdir-bug.test.ts (7 tests)

Reported by: Soulcraft Workshop team

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-12-04 11:22:30 -08:00
parent 1da6048245
commit 2ba69eccdc
4 changed files with 375 additions and 13 deletions

View file

@ -345,30 +345,44 @@ export class PathResolver {
/**
* Invalidate cache entries for a path and its children
* v6.2.9 FIX: Also invalidates UnifiedCache to prevent stale entity IDs
* This fixes the "Source entity not found" bug after delete+recreate operations
*/
invalidatePath(path: string, recursive = false): void {
const normalizedPath = this.normalizePath(path)
// Remove from all caches
// v6.2.9 FIX: Clear parent cache BEFORE deleting from pathCache
// (we need the entityId from the cache entry)
const cached = this.pathCache.get(normalizedPath)
if (cached) {
this.parentCache.delete(cached.entityId)
}
// Remove from local caches
this.pathCache.delete(normalizedPath)
this.hotPaths.delete(normalizedPath)
// v6.2.9 CRITICAL FIX: Also invalidate UnifiedCache (global LRU cache)
// This was missing before, causing stale entity IDs to be returned after delete
const cacheKey = `vfs:path:${normalizedPath}`
getGlobalCache().delete(cacheKey)
if (recursive) {
// Remove all paths that start with this path
const prefix = normalizedPath.endsWith('/') ? normalizedPath : normalizedPath + '/'
for (const [cachedPath] of this.pathCache) {
for (const [cachedPath, entry] of this.pathCache) {
if (cachedPath.startsWith(prefix)) {
this.pathCache.delete(cachedPath)
this.hotPaths.delete(cachedPath)
// v6.2.9: Also clear parent cache for this entry
this.parentCache.delete(entry.entityId)
}
}
}
// Clear parent cache for the entity
const cached = this.pathCache.get(normalizedPath)
if (cached) {
this.parentCache.delete(cached.entityId)
// v6.2.9 CRITICAL FIX: Also invalidate UnifiedCache entries with this prefix
const globalCachePrefix = `vfs:path:${prefix}`
getGlobalCache().deleteByPrefix(globalCachePrefix)
}
}