|
18 | 18 | from basic_memory.mcp.server import mcp |
19 | 19 | from basic_memory.mcp.tools.search import search_notes |
20 | 20 | from basic_memory.schemas.memory import memory_url_path |
21 | | -from basic_memory.utils import validate_project_path |
| 21 | +from basic_memory.utils import generate_permalink, validate_project_path |
| 22 | +from basic_memory.workspace_context import current_workspace_permalink_context |
22 | 23 |
|
23 | 24 |
|
24 | 25 | def _is_exact_title_match(identifier: str, title: str) -> bool: |
@@ -277,24 +278,51 @@ def _result_file_path(item: dict[str, object]) -> Optional[str]: |
277 | 278 | value = item.get("file_path") |
278 | 279 | return str(value) if value else None |
279 | 280 |
|
280 | | - try: |
281 | | - # Try to resolve identifier to entity ID |
282 | | - entity_id = await knowledge_client.resolve_entity(entity_path, strict=True) |
| 281 | + def _legacy_workspace_unqualified_path(path: str) -> str | None: |
| 282 | + workspace_context = current_workspace_permalink_context() |
| 283 | + if workspace_context is None: |
| 284 | + return None |
| 285 | + |
| 286 | + workspace_prefix = generate_permalink(workspace_context.workspace_slug) |
| 287 | + project_prefix = active_project.permalink |
| 288 | + qualified_prefix = f"{workspace_prefix}/{project_prefix}" |
| 289 | + normalized_path = path.strip("/") |
| 290 | + if normalized_path == qualified_prefix: |
| 291 | + return project_prefix |
| 292 | + if normalized_path.startswith(f"{qualified_prefix}/"): |
| 293 | + return f"{project_prefix}/{normalized_path.removeprefix(f'{qualified_prefix}/')}" |
| 294 | + return None |
| 295 | + |
| 296 | + direct_lookup_paths = [entity_path] |
| 297 | + legacy_path = _legacy_workspace_unqualified_path(entity_path) |
| 298 | + if legacy_path and legacy_path not in direct_lookup_paths: |
| 299 | + # Trigger: existing cloud rows may still use project-prefixed permalinks. |
| 300 | + # Why: new workspace-qualified IDs should read old notes without a re-sync. |
| 301 | + # Outcome: try the legacy path after the canonical workspace path misses. |
| 302 | + direct_lookup_paths.append(legacy_path) |
| 303 | + |
| 304 | + for direct_lookup_path in direct_lookup_paths: |
| 305 | + try: |
| 306 | + # Try to resolve identifier to entity ID |
| 307 | + entity_id = await knowledge_client.resolve_entity( |
| 308 | + direct_lookup_path, strict=True |
| 309 | + ) |
283 | 310 |
|
284 | | - # Fetch content using entity ID |
285 | | - response = await resource_client.read(entity_id) |
| 311 | + # Fetch content using entity ID |
| 312 | + response = await resource_client.read(entity_id) |
286 | 313 |
|
287 | | - # If successful, return the content |
288 | | - if response.status_code == 200: |
289 | | - logger.info( |
290 | | - "Returning read_note result from resource: {path}", path=entity_path |
291 | | - ) |
292 | | - if output_format == "json": |
293 | | - return await _read_json_payload(entity_id) |
294 | | - return response.text |
295 | | - except Exception as e: # pragma: no cover |
296 | | - logger.info(f"Direct lookup failed for '{entity_path}': {e}") |
297 | | - # Continue to fallback methods |
| 314 | + # If successful, return the content |
| 315 | + if response.status_code == 200: |
| 316 | + logger.info( |
| 317 | + "Returning read_note result from resource: {path}", |
| 318 | + path=direct_lookup_path, |
| 319 | + ) |
| 320 | + if output_format == "json": |
| 321 | + return await _read_json_payload(entity_id) |
| 322 | + return response.text |
| 323 | + except Exception as e: # pragma: no cover |
| 324 | + logger.info(f"Direct lookup failed for '{direct_lookup_path}': {e}") |
| 325 | + # Continue to alternate direct lookup paths, then fallback methods |
298 | 326 |
|
299 | 327 | # Fallback 1: Try title search via API |
300 | 328 | logger.info(f"Search title for: {identifier}") |
|
0 commit comments