Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] fce96b7083 fix: disable file sync for short-lived CLI commands to prevent hang
CLI commands like 'project list' use AsyncClient with ASGITransport to make
in-process API calls. The ASGITransport triggers the FastAPI lifespan, which
starts background file-watching tasks. These background tasks can cause
asyncio.run() to hang during shutdown, especially on certain platforms or
Python versions (e.g., Python 3.14).

This fix disables file sync when the API is accessed via ASGI transport from
CLI commands by setting BASIC_MEMORY_DISABLE_FILE_SYNC=1 environment variable.
File sync is only needed for long-running processes (MCP server, API server),
not for short-lived CLI queries.

Changes:
- async_client.py: Set/unset BASIC_MEMORY_DISABLE_FILE_SYNC around ASGI client
- container.py: Check env var in should_sync_files property

Fixes #504

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2026-01-08 23:57:36 +00:00
2 changed files with 21 additions and 6 deletions
+7
View File
@@ -10,6 +10,7 @@ Design principles:
- Factories for services are provided, not singletons
"""
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -61,7 +62,11 @@ class ApiContainer:
Sync is enabled when:
- sync_changes is True in config
- Not in test mode (tests manage their own sync)
- Not disabled via BASIC_MEMORY_DISABLE_FILE_SYNC env var (CLI ASGI context)
"""
# Check if file sync is explicitly disabled (e.g., for short-lived CLI commands)
if os.environ.get("BASIC_MEMORY_DISABLE_FILE_SYNC") == "1":
return False
return self.config.sync_changes and not self.mode.is_test
@property
@@ -70,6 +75,8 @@ class ApiContainer:
Useful for logging why sync was disabled.
"""
if os.environ.get("BASIC_MEMORY_DISABLE_FILE_SYNC") == "1":
return "File sync disabled for short-lived CLI command"
if self.mode.is_test:
return "Test environment detected"
if not self.config.sync_changes:
+14 -6
View File
@@ -1,3 +1,4 @@
import os
from contextlib import asynccontextmanager, AbstractAsyncContextManager
from typing import AsyncIterator, Callable, Optional
@@ -95,12 +96,19 @@ async def get_client() -> AsyncIterator[AsyncClient]:
yield client
else:
# Local mode: ASGI transport for in-process calls
# Note: ASGI transport does NOT trigger FastAPI lifespan, so no special handling needed
logger.info("Creating ASGI client for local Basic Memory API")
async with AsyncClient(
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
) as client:
yield client
# Set environment variable to disable file sync for short-lived CLI commands
# File sync is designed for long-running processes (MCP server, API server)
# not for quick CLI queries that trigger lifespan via ASGITransport
os.environ["BASIC_MEMORY_DISABLE_FILE_SYNC"] = "1"
try:
logger.info("Creating ASGI client for local Basic Memory API")
async with AsyncClient(
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
) as client:
yield client
finally:
# Clean up environment variable
os.environ.pop("BASIC_MEMORY_DISABLE_FILE_SYNC", None)
def create_client() -> AsyncClient: