fix: prevent CLI commands from hanging on exit (#471) · basicmachines-co/basic-memory@916baf8 · GitHub
Skip to content

Commit 916baf8

Browse files
phernandezclaude
andauthored
fix: prevent CLI commands from hanging on exit (#471)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 95937c6 commit 916baf8

17 files changed

Lines changed: 403 additions & 273 deletions

src/basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py

Lines changed: 116 additions & 76 deletions

src/basic_memory/api/app.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""FastAPI application for basic-memory knowledge graph API."""
22

33
import asyncio
4+
import os
45
from contextlib import asynccontextmanager
56

67
from fastapi import FastAPI, HTTPException
@@ -53,12 +54,25 @@ async def lifespan(app: FastAPI): # pragma: no cover
5354
app.state.session_maker = session_maker
5455
logger.info("Database connections cached in app state")
5556

56-
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
57-
if app_config.sync_changes:
57+
# Start file sync if enabled
58+
is_test_env = (
59+
app_config.env == "test"
60+
or os.getenv("BASIC_MEMORY_ENV", "").lower() == "test"
61+
or os.getenv("PYTEST_CURRENT_TEST") is not None
62+
)
63+
if app_config.sync_changes and not is_test_env:
64+
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
65+
5866
# start file sync task in background
59-
app.state.sync_task = asyncio.create_task(initialize_file_sync(app_config))
67+
async def _file_sync_runner() -> None:
68+
await initialize_file_sync(app_config)
69+
70+
app.state.sync_task = asyncio.create_task(_file_sync_runner())
6071
else:
61-
logger.info("Sync changes disabled. Skipping file sync service.")
72+
if is_test_env:
73+
logger.info("Test environment detected. Skipping file sync service.")
74+
else:
75+
logger.info("Sync changes disabled. Skipping file sync service.")
6276
app.state.sync_task = None
6377

6478
# proceed with startup

src/basic_memory/cli/app.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,15 @@ def app_callback(
3434
# Initialize logging for CLI (file only, no stdout)
3535
init_cli_logging()
3636

37-
# Run initialization for every command unless --version was specified
38-
if not version and ctx.invoked_subcommand is not None:
37+
# Run initialization for commands that don't use the API
38+
# Skip for 'mcp' command - it has its own lifespan that handles initialization
39+
# Skip for API-using commands (status, sync, etc.) - they handle initialization via deps.py
40+
api_commands = {"mcp", "status", "sync", "project", "tool"}
41+
if (
42+
not version
43+
and ctx.invoked_subcommand is not None
44+
and ctx.invoked_subcommand not in api_commands
45+
):
3946
from basic_memory.services.initialization import ensure_initialization
4047

4148
app_config = ConfigManager().config

src/basic_memory/cli/commands/command_utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""utility functions for commands"""
22

3-
from typing import Optional
3+
import asyncio
4+
from typing import Optional, TypeVar, Coroutine, Any
45

56
from mcp.server.fastmcp.exceptions import ToolError
67
import typer
78

89
from rich.console import Console
910

11+
from basic_memory import db
1012
from basic_memory.mcp.async_client import get_client
1113

1214
from basic_memory.mcp.tools.utils import call_post, call_get
@@ -15,6 +17,30 @@
1517

1618
console = Console()
1719

20+
T = TypeVar("T")
21+
22+
23+
def run_with_cleanup(coro: Coroutine[Any, Any, T]) -> T:
24+
"""Run an async coroutine with proper database cleanup.
25+
26+
This helper ensures database connections are cleaned up before the event
27+
loop closes, preventing process hangs in CLI commands.
28+
29+
Args:
30+
coro: The coroutine to run
31+
32+
Returns:
33+
The result of the coroutine
34+
"""
35+
36+
async def _with_cleanup() -> T:
37+
try:
38+
return await coro
39+
finally:
40+
await db.shutdown_db()
41+
42+
return asyncio.run(_with_cleanup())
43+
1844

1945
async def run_sync(project: Optional[str] = None, force_full: bool = False):
2046
"""Run sync operation via API endpoint.

src/basic_memory/cli/commands/mcp.py

Lines changed: 5 additions & 25 deletions

0 commit comments

Comments
 (0)