Bug: Background file sync fails with "cannot schedule new futures after shutdown" in MCP server
Description
When running the Basic Memory MCP server (via basic-memory mcp), background file synchronization tasks fail with the error:
Error in background sync for project main: cannot schedule new futures after shutdown
This prevents files from being indexed in the database, leaving the knowledge base incomplete.
Symptoms
watch-status.json shows running: true but synced_files: 0 and last_scan: null
- MCP logs show recurring error:
Error in background sync for project main: cannot schedule new futures after shutdown
- Files exist on disk (e.g., through Nov 14) but database only has files indexed through an earlier date (e.g., Nov 4)
- No way to manually trigger sync to recover (the
sync command was removed)
Root Cause
The MCP server runs file synchronization in a daemon thread with its own event loop (src/basic_memory/cli/commands/mcp.py:62-72):
def run_file_sync():
"""Run file sync in a separate thread with its own event loop."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(initialize_file_sync(app_config))
except Exception as e:
logger.error(f"File sync error: {e}", err=True)
finally:
loop.close() # ❌ BUG: This shuts down the ThreadPoolExecutor!
The initialization function (src/basic_memory/services/initialization.py:107-132) creates background tasks that use aiofiles.os.scandir():
# Create background tasks for all project syncs (non-blocking)
sync_tasks = [
asyncio.create_task(sync_project_background(project)) for project in active_projects
]
# Don't await the tasks - let them run in background while we continue
These background tasks call aiofiles.os.scandir() which requires the event loop's ThreadPoolExecutor. However, the daemon thread's finally block calls loop.close() which shuts down the executor before the background tasks complete.
Timeline of the bug:
- Daemon thread starts, creates new event loop
initialize_file_sync() creates background tasks (these start running immediately)
- Background tasks begin calling
aiofiles.os.scandir() which needs the executor
- Watch service completes or encounters an exception
finally block calls loop.close() → shuts down ThreadPoolExecutor
- Background tasks try to use executor → "cannot schedule new futures after shutdown"
Impact
- High: Files created/modified after MCP server starts are not indexed
- Knowledge graph is incomplete and stale
- Recent activity and search miss newly created content
- No workaround available (sync command removed)
Fix
Remove the loop.close() call from the daemon thread. Since this is a daemon thread meant to run for the lifetime of the MCP server process, the event loop should never be closed. The OS will clean up resources when the process exits.
src/basic_memory/cli/commands/mcp.py:
def run_file_sync():
"""Run file sync in a separate thread with its own event loop."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(initialize_file_sync(app_config))
except Exception as e:
logger.error(f"File sync error: {e}", err=True)
# Note: Do NOT close the loop here! This is a daemon thread that should
# run until process exit. Closing the loop shuts down the ThreadPoolExecutor
# which breaks aiofiles operations in background sync tasks.
Testing
Two test scripts are available to verify the fix:
-
Unit test (test_executor_fix.py): Reproduces the bug in isolation
python test_executor_fix.py
-
Integration test (test_mcp_startup.sh): Full end-to-end test
Expected Behavior After Fix
- Background sync tasks complete successfully
watch-status.json shows synced_files > 0 and populated last_scan
- All files on disk are indexed in the database
- No "cannot schedule new futures after shutdown" errors in logs
Environment
- Basic Memory version: v0.16.x (any version with MCP server)
- Python: 3.12+
- OS: macOS Sequoia 15.7.1 (reproduced), likely affects all platforms
- MCP transport: stdio (Claude Desktop)
Related Code
src/basic_memory/cli/commands/mcp.py:62-72 - Daemon thread with buggy finally block
src/basic_memory/services/initialization.py:107-132 - Background task creation
src/basic_memory/sync/sync_service.py:1165 - Use of aiofiles.os.scandir()
Additional Notes
This is a classic async lifecycle bug where:
- A resource (ThreadPoolExecutor) is shared between a parent scope and child tasks
- The parent scope closes the resource before child tasks complete
- Child tasks fail when trying to use the closed resource
The fix is simple but critical for MCP server reliability.
Bug: Background file sync fails with "cannot schedule new futures after shutdown" in MCP server
Description
When running the Basic Memory MCP server (via
basic-memory mcp), background file synchronization tasks fail with the error:This prevents files from being indexed in the database, leaving the knowledge base incomplete.
Symptoms
watch-status.jsonshowsrunning: truebutsynced_files: 0andlast_scan: nullError in background sync for project main: cannot schedule new futures after shutdownsynccommand was removed)Root Cause
The MCP server runs file synchronization in a daemon thread with its own event loop (
src/basic_memory/cli/commands/mcp.py:62-72):The initialization function (
src/basic_memory/services/initialization.py:107-132) creates background tasks that useaiofiles.os.scandir():These background tasks call
aiofiles.os.scandir()which requires the event loop's ThreadPoolExecutor. However, the daemon thread'sfinallyblock callsloop.close()which shuts down the executor before the background tasks complete.Timeline of the bug:
initialize_file_sync()creates background tasks (these start running immediately)aiofiles.os.scandir()which needs the executorfinallyblock callsloop.close()→ shuts down ThreadPoolExecutorImpact
Fix
Remove the
loop.close()call from the daemon thread. Since this is a daemon thread meant to run for the lifetime of the MCP server process, the event loop should never be closed. The OS will clean up resources when the process exits.src/basic_memory/cli/commands/mcp.py:
Testing
Two test scripts are available to verify the fix:
Unit test (
test_executor_fix.py): Reproduces the bug in isolationIntegration test (
test_mcp_startup.sh): Full end-to-end testExpected Behavior After Fix
watch-status.jsonshowssynced_files > 0and populatedlast_scanEnvironment
Related Code
src/basic_memory/cli/commands/mcp.py:62-72- Daemon thread with buggy finally blocksrc/basic_memory/services/initialization.py:107-132- Background task creationsrc/basic_memory/sync/sync_service.py:1165- Use ofaiofiles.os.scandir()Additional Notes
This is a classic async lifecycle bug where:
The fix is simple but critical for MCP server reliability.