3838 SearchRequest ,
3939 SearchResponse ,
4040 SearchResult ,
41+ SearchStreamResponse ,
4142 StopRequest ,
4243 StopResponse ,
4344 decode_request ,
@@ -108,15 +109,17 @@ def __init__(self, embedder: Embedder) -> None:
108109 self ._projects = {}
109110 self ._index_locks = {}
110111 self ._indexing = {}
112+ self ._load_time_done : dict [str , asyncio .Event ] = {}
111113 self ._embedder = embedder
112114
113115 async def get_project (self , project_root : str , * , suppress_auto_index : bool = False ) -> Project :
114116 """Get or create a Project for the given root. Lazy initialization.
115117
116118 When a project is newly loaded and *suppress_auto_index* is False,
117- a background indexing task is fired so the project is indexed
118- immediately. Callers that will index right away (e.g. IndexRequest,
119- SearchRequest with refresh) should pass ``suppress_auto_index=True``.
119+ a background indexing task (load-time indexing) is fired so the project
120+ is indexed immediately. Callers that will index right away (e.g.
121+ IndexRequest, SearchRequest with refresh) should pass
122+ ``suppress_auto_index=True``.
120123 """
121124 if project_root not in self ._projects :
122125 root = Path (project_root )
@@ -126,17 +129,36 @@ async def get_project(self, project_root: str, *, suppress_auto_index: bool = Fa
126129 self ._index_locks [project_root ] = asyncio .Lock ()
127130 self ._indexing [project_root ] = False
128131
129- if not suppress_auto_index :
130- asyncio .create_task (self ._auto_index (project_root ))
132+ event = asyncio .Event ()
133+ self ._load_time_done [project_root ] = event
134+ if suppress_auto_index :
135+ event .set ()
136+ else :
137+ asyncio .create_task (self ._load_time_index (project_root ))
131138 return self ._projects [project_root ]
132139
133- async def _auto_index (self , project_root : str ) -> None :
134- """Background auto-index, consuming the update_index stream."""
140+ def is_load_time_indexing (self , project_root : str ) -> bool :
141+ """Check if load-time indexing is in progress."""
142+ event = self ._load_time_done .get (project_root )
143+ return event is not None and not event .is_set ()
144+
145+ async def wait_for_load_time_indexing (self , project_root : str ) -> None :
146+ """Wait for load-time indexing to complete. Returns immediately if not in progress."""
147+ event = self ._load_time_done .get (project_root )
148+ if event is not None :
149+ await event .wait ()
150+
151+ async def _load_time_index (self , project_root : str ) -> None :
152+ """Background load-time indexing, consuming the update_index stream."""
135153 try :
136154 async for _ in self .update_index (project_root ):
137155 pass
138156 except Exception :
139- logger .exception ("Auto-index failed for %s" , project_root )
157+ logger .exception ("Load-time indexing failed for %s" , project_root )
158+ finally :
159+ event = self ._load_time_done .get (project_root )
160+ if event is not None :
161+ event .set ()
140162
141163 async def update_index (
142164 self , project_root : str , * , suppress_auto_index : bool = True
@@ -251,6 +273,7 @@ def remove_project(self, project_root: str) -> bool:
251273 project = self ._projects .pop (project_root , None )
252274 self ._index_locks .pop (project_root , None )
253275 self ._indexing .pop (project_root , None )
276+ self ._load_time_done .pop (project_root , None )
254277 if project is not None :
255278 project .close ()
256279 del project
@@ -266,6 +289,7 @@ def close_all(self) -> None:
266289 self ._projects .clear ()
267290 self ._index_locks .clear ()
268291 self ._indexing .clear ()
292+ self ._load_time_done .clear ()
269293 gc .collect ()
270294
271295 def list_projects (self ) -> list [DaemonProjectInfo ]:
@@ -357,26 +381,55 @@ def _recv() -> bytes:
357381 pass
358382
359383
384+ async def _search_with_wait (
385+ registry : ProjectRegistry , req : SearchRequest
386+ ) -> AsyncIterator [SearchStreamResponse ]:
387+ """Stream search response, waiting for load-time indexing first."""
388+ yield IndexWaitingNotice ()
389+ await registry .wait_for_load_time_indexing (req .project_root )
390+ try :
391+ results = await registry .search (
392+ project_root = req .project_root ,
393+ query = req .query ,
394+ languages = req .languages ,
395+ paths = req .paths ,
396+ limit = req .limit ,
397+ offset = req .offset ,
398+ )
399+ yield SearchResponse (
400+ success = True ,
401+ results = results ,
402+ total_returned = len (results ),
403+ offset = req .offset ,
404+ )
405+ except Exception as e :
406+ yield ErrorResponse (message = str (e ))
407+
408+
360409async def _dispatch (
361410 req : Request ,
362411 registry : ProjectRegistry ,
363412 start_time : float ,
364413 shutdown_event : asyncio .Event ,
365- ) -> Response | AsyncIterator [IndexStreamResponse ]:
414+ ) -> Response | AsyncIterator [IndexStreamResponse ] | AsyncIterator [ SearchStreamResponse ] :
366415 """Dispatch a request to the appropriate handler.
367416
368417 Returns a single Response for most requests, or an AsyncIterator for
369- streaming requests (IndexRequest).
418+ streaming requests (IndexRequest, or SearchRequest when waiting for
419+ load-time indexing).
370420 """
371421 try :
372422 if isinstance (req , IndexRequest ):
373423 return registry .update_index (req .project_root )
374424
375425 if isinstance (req , SearchRequest ):
376- if req .refresh :
377- # Consume the index stream silently for refresh
378- async for _ in registry .update_index (req .project_root ):
379- pass
426+ # Ensure the project is loaded (may trigger load-time indexing)
427+ await registry .get_project (req .project_root )
428+
429+ # If load-time indexing is in progress, return a streaming response
430+ if registry .is_load_time_indexing (req .project_root ):
431+ return _search_with_wait (registry , req )
432+
380433 results = await registry .search (
381434 project_root = req .project_root ,
382435 query = req .query ,
0 commit comments