index_codebase(force: true) doesn't cancel the previous in-flight indexing task, causing concurrent duplicate runs
Problem
handleIndexCodebase in packages/mcp/src/handlers.ts, when called with force: true on a codebase that's already being indexed, only clears tracking metadata — it does not cancel the still-running background task:
if (this.snapshotManager.getIndexingCodebases().includes(absolutePath)) {
if (forceReindex) {
console.log(`[FORCE-REINDEX] Clearing stale indexing state for '${absolutePath}'`);
this.snapshotManager.removeCodebaseCompletely(absolutePath);
this.snapshotManager.saveCodebaseSnapshot();
}
else {
return { ...already being indexed error... };
}
}The actual in-flight promise is tracked separately in this.indexingTasks (a Map<path, {controller, promise}>), specifically so that clear_index can cancel it via the AbortController before tearing anything down (per the comment above that map's declaration). The force: true branch above does not touch this.indexingTasks or call .abort() — it just clears the snapshot's bookkeeping and then proceeds to call startBackgroundIndexing(...) again, which overwrites the map entry for that path:
const promise = this.startBackgroundIndexing(...).finally(() => {
const current = this.indexingTasks.get(absolutePath);
if (current && current.controller === controller) {
this.indexingTasks.delete(absolutePath);
}
});
this.indexingTasks.set(absolutePath, { controller, promise }); // clobbers the old entryThe original task's promise is now orphaned but still executing — nothing ever awaited or aborted it. Two independent Context.indexCodebase() runs now execute concurrently against the same Milvus collection.
Impact
- Calling
index_codebase(force: true)while a previous run for the same path is still genuinely in progress starts a second, fully independent indexing run on top of the first, rather than replacing it. - Both runs read/chunk/embed/insert concurrently, and both write progress to the same snapshot entry, producing non-monotonic progress percentages (observed oscillating between two independent progress tracks, e.g.
22% → 14% → 22% → 15% → 23%...). - Final row/chunk counts can end up higher than the real file count (duplicate inserts from the overlapping runs), not just truncated — we observed a codebase with ~3,266 real files reporting 35,700 indexed chunks after a couple of overlapping force-reindex attempts combined with the periodic background sync (see the related sync issue).
Repro
index_codebase(path)on a codebase large enough to take more than a few seconds.- While it's still indexing, call
index_codebase(path, force: true). - Watch
~/.context/mcp-codebase-snapshot.jsondirectly (to avoid also triggering the separateget_indexing_statussync bug) — the reportedindexingPercentageis non-monotonic across successive reads, evidence of two runs writing concurrently. clear_index(path)(which correctly aborts+awaits via the tracked controller) stops it; a subsequent single clean run shows genuinely monotonic progress.
Suggested fix
In the force: true branch, cancel and await the existing tracked task before starting a new one — essentially reuse the same cancel-then-clear logic clear_index already has:
if (this.snapshotManager.getIndexingCodebases().includes(absolutePath)) {
if (forceReindex) {
const existing = this.indexingTasks.get(absolutePath);
if (existing) {
existing.controller.abort();
await existing.promise.catch(() => {}); // IndexAbortError is expected here
}
this.snapshotManager.removeCodebaseCompletely(absolutePath);
this.snapshotManager.saveCodebaseSnapshot();
}
...Happy to open a PR if useful.
Source: zilliztech/claude-context