@@ -162,6 +162,17 @@ async def _set_cached_active_workspace(
162162 await context .set_state ("active_workspace" , active_workspace .model_dump ())
163163
164164
165+ async def _clear_cached_active_workspace_for_local_route (context : Optional [Context ]) -> None :
166+ """Drop tenant workspace metadata before routing through a local project."""
167+ if not context :
168+ return
169+
170+ # Trigger: local routing follows a cloud route in the same MCP session
171+ # Why: active_workspace is tenant metadata, not part of local project identity
172+ # Outcome: memory:// resolution uses project-only local permalinks
173+ await context .set_state ("active_workspace" , None )
174+
175+
165176async def _get_cached_default_project (context : Optional [Context ]) -> Optional [str ]:
166177 """Return the cached default project name from context when available."""
167178 if not context :
@@ -446,6 +457,52 @@ def _canonical_memory_path_for_workspace(
446457 return f"{ prefix } /{ normalized_remainder } "
447458
448459
460+ def _canonical_memory_path_for_active_route (
461+ active_project : ProjectItem ,
462+ path : str ,
463+ * ,
464+ include_project : bool ,
465+ cached_workspace : WorkspaceInfo | None = None ,
466+ ) -> str :
467+ """Return the canonical permalink path for the currently routed project/workspace."""
468+ project_prefix = active_project .permalink
469+ workspace_remainder = path
470+ if include_project and (path == project_prefix or path .startswith (f"{ project_prefix } /" )):
471+ # Trigger: the memory URL already names the active project root/prefix
472+ # Why: workspace canonicalization adds the project prefix itself, so
473+ # keeping it in the remainder would produce <workspace>/<project>/<project>
474+ # Outcome: keep project-root and project-prefixed URLs canonical once
475+ workspace_remainder = (
476+ "" if path == project_prefix else path .removeprefix (f"{ project_prefix } /" )
477+ )
478+
479+ workspace_context = current_workspace_permalink_context ()
480+ if workspace_context is not None :
481+ return _canonical_memory_path_for_workspace (
482+ workspace_slug = workspace_context .workspace_slug ,
483+ workspace_type = workspace_context .workspace_type ,
484+ project_permalink = active_project .permalink ,
485+ remainder = workspace_remainder ,
486+ include_project = include_project ,
487+ )
488+
489+ if cached_workspace is not None :
490+ return _canonical_memory_path_for_workspace (
491+ workspace_slug = cached_workspace .slug ,
492+ workspace_type = cached_workspace .workspace_type ,
493+ project_permalink = active_project .permalink ,
494+ remainder = workspace_remainder ,
495+ include_project = include_project ,
496+ )
497+
498+ if not include_project :
499+ return path
500+
501+ if path == project_prefix or path .startswith (f"{ project_prefix } /" ):
502+ return path
503+ return f"{ project_prefix } /{ path } "
504+
505+
449506def _cloud_workspace_discovery_available (config : BasicMemoryConfig ) -> bool :
450507 """Return True when workspace discovery can be used without forcing local routing."""
451508 from basic_memory .mcp .async_client import (
@@ -1115,8 +1172,11 @@ async def resolve_project_and_path(
11151172 f"Project is constrained to '{ resolved_project } ', cannot use '{ project_prefix } '."
11161173 )
11171174
1118- resolved_path = (
1119- f"{ cached_project .permalink } /{ remainder } " if include_project else remainder
1175+ resolved_path = _canonical_memory_path_for_active_route (
1176+ cached_project ,
1177+ remainder ,
1178+ include_project = include_project ,
1179+ cached_workspace = cached_workspace ,
11201180 )
11211181 return cached_project , resolved_path , True
11221182
@@ -1151,25 +1211,24 @@ async def resolve_project_and_path(
11511211 )
11521212 await _set_cached_active_project (context , active_project )
11531213
1154- resolved_path = (
1155- f"{ resolved .permalink } /{ remainder } " if include_project else remainder
1214+ resolved_path = _canonical_memory_path_for_active_route (
1215+ active_project ,
1216+ remainder ,
1217+ include_project = include_project ,
1218+ cached_workspace = cached_workspace ,
11561219 )
11571220 return active_project , resolved_path , True
11581221
1159- # Trigger: no resolvable project prefix in the memory URL
1160- # Why: preserve existing memory URL behavior within the active project
1161- # Outcome: use the active project and normalize the path for lookup
1222+ # Trigger: memory URL has no resolvable project route segment
1223+ # Why: preserve active-project behavior while honoring workspace paths
1224+ # Outcome: normalize against the already-selected local/cloud route
11621225 active_project = await get_active_project (client , project , context , headers )
1163- resolved_path = normalized_path
1164- if include_project :
1165- # Trigger: project-prefixed permalinks are enabled and the path lacks a prefix
1166- # Why: ensure memory URL lookups align with canonical permalinks
1167- # Outcome: prefix the path with the active project's permalink
1168- project_prefix = active_project .permalink
1169- if resolved_path != project_prefix and not resolved_path .startswith (
1170- f"{ project_prefix } /"
1171- ):
1172- resolved_path = f"{ project_prefix } /{ resolved_path } "
1226+ resolved_path = _canonical_memory_path_for_active_route (
1227+ active_project ,
1228+ normalized_path ,
1229+ include_project = include_project ,
1230+ cached_workspace = cached_workspace ,
1231+ )
11731232 return active_project , resolved_path , True
11741233
11751234
@@ -1325,6 +1384,7 @@ async def get_project_client(
13251384 # Outcome: route strictly based on explicit flag, no workspace network calls
13261385 if _explicit_routing () and _force_local_mode ():
13271386 route_mode = "explicit_local"
1387+ await _clear_cached_active_workspace_for_local_route (context )
13281388 with logfire .span (
13291389 "routing.client_session" ,
13301390 project_name = resolved_project ,
@@ -1397,6 +1457,7 @@ async def get_project_client(
13971457
13981458 # Step 4: Local routing (default)
13991459 route_mode = "local_asgi"
1460+ await _clear_cached_active_workspace_for_local_route (context )
14001461 with logfire .span (
14011462 "routing.client_session" ,
14021463 project_name = resolved_project ,
0 commit comments