Skip to content

Commit 1652f86

Browse files
phernandezclaude
andcommitted
fix: handle FileNotFoundError gracefully during sync
When a file exists in the database but is missing from the filesystem, the sync worker now treats this as a deletion instead of crashing. The sync_file() method catches FileNotFoundError specifically and calls handle_delete() to clean up the orphaned database record. This prevents the sync from failing on database/filesystem inconsistencies that can occur due to race conditions, manual file deletions, or cloud storage caching issues. Includes a test to verify the graceful handling behavior. Fixes #386 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> Signed-off-by: phernandez <[email protected]>
1 parent c23927d commit 1652f86

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed

src/basic_memory/sync/sync_service.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,16 @@ async def sync_file(
610610
)
611611
return entity, checksum
612612

613+
except FileNotFoundError:
614+
# File exists in database but not on filesystem
615+
# This indicates a database/filesystem inconsistency - treat as deletion
616+
logger.warning(
617+
f"File not found during sync, treating as deletion: path={path}. "
618+
"This may indicate a race condition or manual file deletion."
619+
)
620+
await self.handle_delete(path)
621+
return None, None
622+
613623
except Exception as e:
614624
# Check if this is a fatal error (or caused by one)
615625
# Fatal errors like project deletion should terminate sync immediately

tests/sync/test_sync_service.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,3 +2055,63 @@ async def test_file_service_checksum_correctness(
20552055
expected = hashlib.sha256(small_content.encode("utf-8")).hexdigest()
20562056
assert checksum == expected
20572057
assert len(checksum) == 64 # SHA256 hex digest length
2058+
2059+
2060+
@pytest.mark.asyncio
2061+
async def test_sync_handles_file_not_found_gracefully(
2062+
sync_service: SyncService, project_config: ProjectConfig
2063+
):
2064+
"""Test that FileNotFoundError during sync is handled gracefully.
2065+
2066+
This tests the fix for issue #386 where files existing in the database
2067+
but missing from the filesystem would crash the sync worker.
2068+
"""
2069+
from unittest.mock import patch
2070+
2071+
project_dir = project_config.home
2072+
2073+
# Create a test file
2074+
test_file = project_dir / "missing_file.md"
2075+
await create_test_file(
2076+
test_file,
2077+
dedent(
2078+
"""
2079+
---
2080+
type: knowledge
2081+
permalink: missing-file
2082+
---
2083+
# Missing File
2084+
Content that will disappear
2085+
"""
2086+
),
2087+
)
2088+
2089+
# Sync to add entity to database
2090+
await sync_service.sync(project_dir)
2091+
2092+
# Verify entity was created
2093+
entity = await sync_service.entity_repository.get_by_file_path("missing_file.md")
2094+
assert entity is not None
2095+
assert entity.permalink == "missing-file"
2096+
2097+
# Delete the file but leave the entity in database (simulating inconsistency)
2098+
test_file.unlink()
2099+
2100+
# Mock file_service methods to raise FileNotFoundError
2101+
# (since the file doesn't exist, read operations will fail)
2102+
async def mock_read_that_fails(*args, **kwargs):
2103+
raise FileNotFoundError("Simulated file not found")
2104+
2105+
with patch.object(sync_service.file_service, "read_file_content", side_effect=mock_read_that_fails):
2106+
# Force full scan to detect the file
2107+
await force_full_scan(sync_service)
2108+
2109+
# Sync should handle the error gracefully and delete the orphaned entity
2110+
report = await sync_service.sync(project_dir)
2111+
2112+
# Should not crash and should not have errors (FileNotFoundError is handled specially)
2113+
# The file should be treated as deleted
2114+
2115+
# Entity should be deleted from database
2116+
entity = await sync_service.entity_repository.get_by_file_path("missing_file.md")
2117+
assert entity is None, "Orphaned entity should be deleted when file is not found"

0 commit comments

Comments
 (0)