@@ -93,6 +93,7 @@ def __init__(
9393 self .status_path = app_config .data_dir_path / WATCH_STATUS_JSON
9494 self .status_path .parent .mkdir (parents = True , exist_ok = True )
9595 self ._ignore_patterns_cache : dict [Path , Set [str ]] = {}
96+ self ._watch_filter_roots : tuple [Path , ...] | None = None
9697 self ._sync_service_factory = sync_service_factory
9798 # When set (typically from BASIC_MEMORY_MCP_PROJECT), the watch cycle
9899 # only observes this project. Without it, each `basic-memory mcp --project X`
@@ -126,41 +127,48 @@ def _get_ignore_patterns(self, project_path: Path) -> Set[str]:
126127 async def _watch_projects_cycle (self , projects : Sequence [Project ], stop_event : asyncio .Event ):
127128 """Run one cycle of watching the given projects until stop_event is set."""
128129 project_paths = [project .path for project in projects ]
130+ previous_filter_roots = self ._watch_filter_roots
131+ self ._watch_filter_roots = tuple (
132+ Path (project .path ).expanduser ().resolve () for project in projects
133+ )
129134
130- async for changes in awatch (
131- * project_paths ,
132- debounce = self .app_config .sync_delay ,
133- watch_filter = self .filter_changes ,
134- recursive = True ,
135- stop_event = stop_event ,
136- ):
137- # group changes by project and filter using ignore patterns
138- project_changes = defaultdict (list )
139- for change , path in changes :
140- for project in projects :
141- if self .is_project_path (project , path ):
142- # Check if the file should be ignored based on gitignore patterns
143- project_path = Path (project .path )
144- file_path = Path (path )
145- ignore_patterns = self ._get_ignore_patterns (project_path )
146-
147- if should_ignore_path (file_path , project_path , ignore_patterns ):
148- logger .trace (
149- f"Ignoring watched file change: { file_path .relative_to (project_path )} "
150- )
151- continue
152-
153- project_changes [project ].append ((change , path ))
154- break
135+ try :
136+ async for changes in awatch (
137+ * project_paths ,
138+ debounce = self .app_config .sync_delay ,
139+ watch_filter = self .filter_changes ,
140+ recursive = True ,
141+ stop_event = stop_event ,
142+ ):
143+ # group changes by project and filter using ignore patterns
144+ project_changes = defaultdict (list )
145+ for change , path in changes :
146+ for project in projects :
147+ if self .is_project_path (project , path ):
148+ # Check if the file should be ignored based on gitignore patterns
149+ project_path = Path (project .path )
150+ file_path = Path (path )
151+ ignore_patterns = self ._get_ignore_patterns (project_path )
152+
153+ if should_ignore_path (file_path , project_path , ignore_patterns ):
154+ logger .trace (
155+ f"Ignoring watched file change: { file_path .relative_to (project_path )} "
156+ )
157+ continue
158+
159+ project_changes [project ].append ((change , path ))
160+ break
155161
156- # create coroutines to handle changes
157- change_handlers = [
158- self .handle_changes (project , set (changes ))
159- for project , changes in project_changes .items ()
160- ]
162+ # create coroutines to handle changes
163+ change_handlers = [
164+ self .handle_changes (project , set (changes ))
165+ for project , changes in project_changes .items ()
166+ ]
161167
162- # process changes
163- await asyncio .gather (* change_handlers )
168+ # process changes
169+ await asyncio .gather (* change_handlers )
170+ finally :
171+ self ._watch_filter_roots = previous_filter_roots
164172
165173 async def _select_projects_to_watch (self ) -> list [Project ]:
166174 """Return the set of projects this watch cycle should observe.
@@ -276,14 +284,20 @@ def filter_changes(self, change: Change, path: str) -> bool: # pragma: no cover
276284
277285 path_obj = Path (path ).expanduser ().resolve ()
278286
279- project_paths = sorted (
280- (
287+ project_roots = self ._watch_filter_roots
288+ if project_roots is None :
289+ project_roots = tuple (
281290 Path (entry .path ).expanduser ().resolve ()
282291 for entry in self .app_config .projects .values ()
283292 if entry .path
284- ),
293+ )
294+
295+ project_paths = sorted (
296+ project_roots ,
297+ # Trigger: configured project roots can overlap.
298+ # Why: an enclosing project's hidden directory should still hide descendants.
299+ # Outcome: choose the outermost matching root when checking hidden path parts.
285300 key = lambda project_path : len (project_path .parts ),
286- reverse = True ,
287301 )
288302
289303 relative_path = None
0 commit comments