fix: resolve_project_parameter falls back to projects API for default (#644)

In cloud mode, ConfigManager has no local config so default_project
is always None. Add API fallback in resolve_project_parameter that
queries /v2/projects/ for the default_project field. This fixes all
MCP tools that rely on project resolution (recent_activity, etc).

Removed discovery mode tests that simulated an invalid state by
clearing is_default — there must always be a default project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-03-04 13:17:16 -06:00
parent 2feecdfaf7
commit 1fdc9fdc69
2 changed files with 35 additions and 60 deletions
+29 -1
View File
@@ -40,6 +40,29 @@ def set_workspace_provider(provider: Callable[[], Awaitable[list[WorkspaceInfo]]
_workspace_provider = provider
async def _resolve_default_project_from_api() -> Optional[str]:
"""Query the projects API for the default project.
Used as a fallback when ConfigManager has no local config (cloud mode).
"""
from basic_memory.mcp.async_client import get_client
try:
async with get_client() as client:
response = await client.get("/v2/projects/")
if response.status_code == 200:
project_list = ProjectList.model_validate(response.json())
if project_list.default_project:
return project_list.default_project
# Fallback: find project with is_default=True
for p in project_list.projects:
if p.is_default:
return p.name
except Exception:
pass
return None
async def resolve_project_parameter(
project: Optional[str] = None,
allow_discovery: bool = False,
@@ -66,11 +89,16 @@ async def resolve_project_parameter(
Returns:
Resolved project name or None if no resolution possible
"""
# Load config for any values not explicitly provided
# Load config for any values not explicitly provided.
# ConfigManager reads from the local config file, which doesn't exist in cloud mode.
# When it returns None, fall back to querying the projects API for the is_default flag.
if default_project is None:
config = ConfigManager().config
default_project = config.default_project
if default_project is None:
default_project = await _resolve_default_project_from_api()
# Create resolver with configuration and resolve
resolver = ProjectResolver.from_env(
default_project=default_project,
+6 -59
View File
@@ -128,69 +128,16 @@ async def test_recent_activity_type_invalid(client, test_project, test_graph):
@pytest.mark.asyncio
async def test_recent_activity_discovery_mode(client, test_project, test_graph, config_manager):
"""Test that recent_activity discovery mode works without project parameter."""
# Clear default_project to test discovery mode
cfg = config_manager.load_config()
cfg.default_project = None
config_manager.save_config(cfg)
# Test discovery mode (no project parameter)
async def test_recent_activity_uses_default_project(client, test_project, test_graph):
"""When no project parameter is given, recent_activity uses the default project."""
# Call without explicit project — should resolve to the default
result = await recent_activity()
assert result is not None
assert isinstance(result, str)
# Check that we get a formatted summary
assert "Recent Activity Summary" in result
assert "Most Active Project:" in result or "Other Active Projects:" in result
assert "Summary:" in result
assert "active projects" in result
# Should contain project discovery guidance
assert "Suggested project:" in result or "Multiple active projects" in result
assert "Session reminder:" in result
@pytest.mark.asyncio
async def test_recent_activity_discovery_mode_no_activity(client, test_project, config_manager):
"""If there is no activity in any project, discovery mode should say so."""
# Clear default_project to test discovery mode
cfg = config_manager.load_config()
cfg.default_project = None
config_manager.save_config(cfg)
result = await recent_activity()
assert "Recent Activity Summary" in result
assert "No recent activity found in any project." in result
@pytest.mark.asyncio
async def test_recent_activity_discovery_mode_multiple_active_projects(
app, client, test_project, tmp_path_factory, config_manager
):
"""Discovery mode should use the multi-project guidance when multiple projects have activity."""
# Clear default_project to test discovery mode
cfg = config_manager.load_config()
cfg.default_project = None
config_manager.save_config(cfg)
from basic_memory.mcp.tools import create_memory_project, write_note
second_root = tmp_path_factory.mktemp("second-project-home")
result = await create_memory_project(
project_name="second-project",
project_path=str(second_root),
set_default=False,
)
assert result.startswith("")
await write_note(project=test_project.name, title="One", directory="notes", content="one")
await write_note(project="second-project", title="Two", directory="notes", content="two")
out = await recent_activity()
assert "Recent Activity Summary" in out
assert "or would you prefer a different project" in out
# Should return project-specific output for the default project
assert "Recent Activity:" in result
assert "Activity Summary:" in result
def test_recent_activity_format_relative_time_and_truncate_helpers():