Skip to content

Commit f2d906d

Browse files
phernandezclaude
andcommitted
fix: resolve reset --reindex hanging and improve sync API
- Add run_in_background parameter to run_sync() for foreground sync - Handle both background and foreground response formats - Refactor reset command to use get_sync_service() directly - Avoid ASGI transport which caused process hang on completion - Ensure proper database cleanup with db.shutdown_db() - Remove unused imports in test files 🤖 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 2950bb9 commit f2d906d

File tree

4 files changed

+72
-20
lines changed

4 files changed

+72
-20
lines changed

src/basic_memory/cli/commands/command_utils.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,45 @@ async def _with_cleanup() -> T:
4242
return asyncio.run(_with_cleanup())
4343

4444

45-
async def run_sync(project: Optional[str] = None, force_full: bool = False):
45+
async def run_sync(
46+
project: Optional[str] = None,
47+
force_full: bool = False,
48+
run_in_background: bool = True,
49+
):
4650
"""Run sync operation via API endpoint.
4751
4852
Args:
4953
project: Optional project name
5054
force_full: If True, force a full scan bypassing watermark optimization
55+
run_in_background: If True, return immediately; if False, wait for completion
5156
"""
5257

5358
try:
5459
async with get_client() as client:
5560
project_item = await get_active_project(client, project, None)
5661
url = f"{project_item.project_url}/project/sync"
62+
params = []
5763
if force_full:
58-
url += "?force_full=true"
64+
params.append("force_full=true")
65+
if not run_in_background:
66+
params.append("run_in_background=false")
67+
if params:
68+
url += "?" + "&".join(params)
5969
response = await call_post(client, url)
6070
data = response.json()
61-
console.print(f"[green]{data['message']}[/green]")
71+
# Background mode returns {"message": "..."}, foreground returns SyncReportResponse
72+
if "message" in data:
73+
console.print(f"[green]{data['message']}[/green]")
74+
else:
75+
# Foreground mode - show summary of sync results
76+
total = data.get("total", 0)
77+
new_count = len(data.get("new", []))
78+
modified_count = len(data.get("modified", []))
79+
deleted_count = len(data.get("deleted", []))
80+
console.print(
81+
f"[green]Synced {total} files[/green] "
82+
f"(new: {new_count}, modified: {modified_count}, deleted: {deleted_count})"
83+
)
6284
except (ToolError, ValueError) as e:
6385
console.print(f"[red]Sync failed: {e}[/red]")
6486
raise typer.Exit(1)

src/basic_memory/cli/commands/db.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Database management commands."""
22

33
import asyncio
4+
from pathlib import Path
45

56
import typer
67
from loguru import logger
@@ -9,21 +10,54 @@
910

1011
from basic_memory import db
1112
from basic_memory.cli.app import app
12-
from basic_memory.config import ConfigManager, BasicMemoryConfig, save_basic_memory_config
13+
from basic_memory.config import ConfigManager
14+
from basic_memory.repository import ProjectRepository
15+
from basic_memory.services.initialization import reconcile_projects_with_config
16+
from basic_memory.sync.sync_service import get_sync_service
1317

1418
console = Console()
1519

1620

21+
async def _reindex_projects(app_config):
22+
"""Reindex all projects in a single async context.
23+
24+
This ensures all database operations use the same event loop,
25+
and proper cleanup happens when the function completes.
26+
"""
27+
try:
28+
await reconcile_projects_with_config(app_config)
29+
30+
# Get database session (migrations already run if needed)
31+
_, session_maker = await db.get_or_create_db(
32+
db_path=app_config.database_path,
33+
db_type=db.DatabaseType.FILESYSTEM,
34+
)
35+
project_repository = ProjectRepository(session_maker)
36+
projects = await project_repository.get_active_projects()
37+
38+
for project in projects:
39+
console.print(f" Indexing [cyan]{project.name}[/cyan]...")
40+
logger.info(f"Starting sync for project: {project.name}")
41+
sync_service = await get_sync_service(project)
42+
sync_dir = Path(project.path)
43+
await sync_service.sync(sync_dir, project_name=project.name)
44+
logger.info(f"Sync completed for project: {project.name}")
45+
finally:
46+
# Clean up database connections before event loop closes
47+
await db.shutdown_db()
48+
49+
1750
@app.command()
1851
def reset(
1952
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
2053
): # pragma: no cover
2154
"""Reset database (drop all tables and recreate)."""
2255
console.print(
2356
"[yellow]Note:[/yellow] This only deletes the index database. "
24-
"Your markdown note files will not be affected."
57+
"Your markdown note files will not be affected.\n"
58+
"Use [green]bm reset --reindex[/green] to automatically rebuild the index afterward."
2559
)
26-
if typer.confirm("Reset the database index? (You can rebuild it with 'bm sync')"):
60+
if typer.confirm("Reset the database index?"):
2761
logger.info("Resetting database...")
2862
config_manager = ConfigManager()
2963
app_config = config_manager.config
@@ -45,12 +79,7 @@ def reset(
4579
)
4680
raise typer.Exit(1)
4781

48-
# Reset project configuration
49-
config = BasicMemoryConfig()
50-
save_basic_memory_config(config_manager.config_file, config)
51-
logger.info("Project configuration reset to default")
52-
53-
# Create a new empty database
82+
# Create a new empty database (preserves project configuration)
5483
try:
5584
asyncio.run(db.run_migrations(app_config))
5685
except OperationalError as e:
@@ -62,11 +91,13 @@ def reset(
6291
)
6392
raise typer.Exit(1)
6493
raise
65-
logger.info("Database reset complete")
94+
console.print("[green]Database reset complete[/green]")
6695

6796
if reindex:
68-
# Run database sync directly
69-
from basic_memory.cli.commands.command_utils import run_sync
70-
71-
logger.info("Rebuilding search index from filesystem...")
72-
asyncio.run(run_sync(project=None))
97+
projects = list(app_config.projects)
98+
if not projects:
99+
console.print("[yellow]No projects configured. Skipping reindex.[/yellow]")
100+
else:
101+
console.print(f"Rebuilding search index for {len(projects)} project(s)...")
102+
asyncio.run(_reindex_projects(app_config))
103+
console.print("[green]Reindex complete[/green]")

tests/mcp/clients/test_clients.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for typed API clients."""
22

33
import pytest
4-
from unittest.mock import AsyncMock, MagicMock
4+
from unittest.mock import MagicMock
55

66
from basic_memory.mcp.clients import (
77
KnowledgeClient,

tests/test_project_resolver.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Tests for ProjectResolver - unified project resolution logic."""
22

3-
import os
43
import pytest
54
from basic_memory.project_resolver import (
65
ProjectResolver,

0 commit comments

Comments
 (0)