@@ -142,6 +142,15 @@ async def _set_cached_active_project(
142142 await context .set_state ("default_project_name" , active_project .name )
143143
144144
145+ async def _clear_cached_active_project (context : Optional [Context ]) -> None :
146+ """Clear cached project metadata that may no longer match the active route."""
147+ if not context :
148+ return
149+
150+ await context .set_state ("active_project" , None )
151+ await context .set_state ("default_project_name" , None )
152+
153+
145154async def _get_cached_active_workspace (context : Optional [Context ]) -> Optional [WorkspaceInfo ]:
146155 """Return the cached active workspace from context when available."""
147156 if not context :
@@ -167,8 +176,7 @@ async def _set_cached_active_workspace(
167176 # Why: project names are only unique inside one workspace, so a cached
168177 # ProjectItem from the previous tenant can point at the wrong project
169178 # Outcome: force the next validation call to resolve within the new tenant
170- await context .set_state ("active_project" , None )
171- await context .set_state ("default_project_name" , None )
179+ await _clear_cached_active_project (context )
172180
173181 await context .set_state ("active_workspace" , active_workspace .model_dump ())
174182
@@ -525,6 +533,28 @@ def _cloud_workspace_discovery_available(config: BasicMemoryConfig) -> bool:
525533 )
526534
527535
536+ def _workspace_identifier_discovery_available (
537+ identifier : str ,
538+ config : BasicMemoryConfig ,
539+ ) -> bool :
540+ """Return True when an identifier is allowed to consult workspace discovery."""
541+ if _cloud_workspace_discovery_available (config ):
542+ return True
543+
544+ from basic_memory .mcp .async_client import (
545+ _explicit_routing ,
546+ _force_local_mode ,
547+ )
548+
549+ if _explicit_routing () and _force_local_mode ():
550+ return False
551+
552+ return (
553+ has_cloud_credentials (config )
554+ and _split_workspace_identifier_segments (identifier ) is not None
555+ )
556+
557+
528558async def resolve_workspace_qualified_memory_url (
529559 identifier : str ,
530560 context : Optional [Context ] = None ,
@@ -1326,7 +1356,7 @@ async def detect_project_from_identifier_prefix(
13261356 # Outcome: keep unqualified search/title input on the active/default project route.
13271357 return None
13281358
1329- if _cloud_workspace_discovery_available ( config ):
1359+ if _workspace_identifier_discovery_available ( identifier , config ):
13301360 workspace_resolution = await resolve_workspace_qualified_identifier (
13311361 identifier ,
13321362 context = context ,
@@ -1462,9 +1492,26 @@ async def get_project_client(
14621492 if project_id and not cloud_available :
14631493 project_mode = ProjectMode .LOCAL
14641494
1495+ if (
1496+ project_id
1497+ and config .projects
1498+ and not factory_mode
1499+ and not explicit_cloud_routing
1500+ and project_mode == ProjectMode .CLOUD
1501+ ):
1502+ try :
1503+ canonical_project_id = str (UUID (project_id ))
1504+ except ValueError :
1505+ pass
1506+ else :
1507+ workspace_index = await _ensure_workspace_project_index (context = context )
1508+ if canonical_project_id not in workspace_index .entries_by_external_id :
1509+ project_mode = ProjectMode .LOCAL
1510+
14651511 if factory_mode or project_mode == ProjectMode .CLOUD or explicit_cloud_routing :
14661512 route_mode = "factory" if factory_mode else "cloud_proxy"
14671513 active_ws : WorkspaceInfo | None = None
1514+ resolved_entry : WorkspaceProjectEntry | None = None
14681515 workspace_id : str
14691516 project_for_api = _unqualified_project_identifier (resolved_project )
14701517
@@ -1487,6 +1534,13 @@ async def get_project_client(
14871534
14881535 if active_ws is not None :
14891536 await _set_cached_active_workspace (context , active_ws )
1537+ if resolved_entry is not None :
1538+ cached_project = await _get_cached_active_project (context )
1539+ if (
1540+ cached_project is not None
1541+ and cached_project .external_id != resolved_entry .project .external_id
1542+ ):
1543+ await _clear_cached_active_project (context )
14901544 with logfire .span (
14911545 "routing.client_session" ,
14921546 project_name = project_for_api ,
0 commit comments