diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea43a87..f2472a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ finalized in place with a date — no renaming/migration step needed. - **`POST /repos` adopts an index that is already on disk instead of clearing and re-embedding it.** Registering a repo (`codesearch index add`, the git-hook path, or a script adding many repos at once) unconditionally ran the destructive force reindex, so a repo that already had a valid `.codesearch.db` — for example one indexed directly before being registered — lost its index and had to re-embed every file, serialized behind the process-wide job gate. The local `codesearch index add` path already reused an existing index; the serve path now matches it. A new index (no `metadata.json`) and an explicit model change still take the force-rebuild path, because changing the embedding model invalidates every stored vector. +- **Adopting an index on `POST /repos` no longer rebuilds the HNSW graph a second time.** The adoption path's incremental refresh already rebuilds the index once under the job gate; the add-repo task still re-ran a full graph build afterwards, doubling the work (serialized behind the job gate) for an identical result. The extra build now runs only on the force-rebuild path, which builds no index internally. + ## [1.6.0] - 2026-10-18 ### Fixed diff --git a/src/serve/mod.rs b/src/serve/mod.rs index ed07b456..810c8c7b 100644 --- a/src/serve/mod.rs +++ b/src/serve/mod.rs @@ -6002,38 +6002,47 @@ async fn add_repo_handler( return; } - // Build vector index from freshly indexed data. + // Build vector index from freshly indexed data — force path only. + // On the adopt path `perform_incremental_refresh_with_stores` already + // rebuilt the HNSW once after its last batch (or left the existing + // valid graph untouched when nothing changed), so a second build here + // would redo the whole graph per adopted registration for an + // identical result. // build_index() is CPU-heavy — offload to the blocking pool, under the // same process-wide job permit as every other heavy pass. The wait is // cancellation-aware: a repo removed while queued behind another build // must not keep its stores (and a queue position) parked. - let _job_permit = match stores.acquire_job_permit_or_cancel(&token_for_task).await { - JobAcquire::Ready(permit) => permit, - JobAcquire::Cancelled => { + if !adopt_existing { + let _job_permit = match stores.acquire_job_permit_or_cancel(&token_for_task).await { + JobAcquire::Ready(permit) => permit, + JobAcquire::Cancelled => { + state_bg.end_indexing(&alias_bg, IndexingOwner::Reindex); + return; + } + }; + // Re-check liveness after the wait: a removal that landed while the + // task was parked must not resurrect the alias's vector index. + if !state_bg.is_alias_live(&alias_bg, &token_for_task) { state_bg.end_indexing(&alias_bg, IndexingOwner::Reindex); return; } - }; - // Re-check liveness after the wait: a removal that landed while the - // task was parked must not resurrect the alias's vector index. - if !state_bg.is_alias_live(&alias_bg, &token_for_task) { - state_bg.end_indexing(&alias_bg, IndexingOwner::Reindex); - return; - } - { - let vector_store = Arc::clone(&stores.vector_store); - let alias_bi = alias_bg.clone(); - match tokio::task::spawn_blocking(move || { - let mut vstore = vector_store.blocking_write(); - vstore.build_index() - }) - .await { - Ok(Ok(())) => {} - Ok(Err(e)) => { - tracing::warn!("Failed to build vector index for '{}': {}", alias_bi, e) + let vector_store = Arc::clone(&stores.vector_store); + let alias_bi = alias_bg.clone(); + match tokio::task::spawn_blocking(move || { + let mut vstore = vector_store.blocking_write(); + vstore.build_index() + }) + .await + { + Ok(Ok(())) => {} + Ok(Err(e)) => { + tracing::warn!("Failed to build vector index for '{}': {}", alias_bi, e) + } + Err(e) => { + tracing::warn!("build_index task panicked for '{}': {:?}", alias_bi, e) + } } - Err(e) => tracing::warn!("build_index task panicked for '{}': {:?}", alias_bi, e), } }