From 03d5beaf49ab549513fdfa55e005a2a051fce5e2 Mon Sep 17 00:00:00 2001 From: flupkede Date: Tue, 20 Oct 2026 12:51:42 +0200 Subject: [PATCH 1/2] fix(serve): skip the redundant tail build_index on the adopt path POST /repos adoption runs perform_incremental_refresh_with_stores, which already rebuilds the HNSW once after its last batch under the job gate (manager.rs), and leaves the existing valid graph untouched when nothing changed. The add-repo task tail then re-acquired a permit and called build_index() again - a second full graph build per adopted registration, serialized behind the process-wide job gate, for an identical result. That tail is load-bearing only on the force path (force_reindex builds no HNSW internally), so it now runs only there. Closes the Design finding from the PR #357 audit ledger (audit/audit.md). No dedicated test: the observable difference is work done, not resulting state - the adopt e2e (chunk/file survival) passes unchanged, which is exactly the equivalence this fix must preserve. --- src/serve/mod.rs | 55 ++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) 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), } } From b47fa6f65266b132641e948f925339925c5393cb Mon Sep 17 00:00:00 2001 From: flupkede Date: Tue, 20 Oct 2026 13:05:23 +0200 Subject: [PATCH 2/2] docs: changelog entry for the adopt-path double-build fix --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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