index_codebase(force: true) doesn't cancel the previous in-flight indexing task, causing concurrent duplicate runs

Author: murad-mocartCreated Aug 10, 2026Updated Aug 10, 2026

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:

javascript
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:

javascript
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 entry

The 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

  1. index_codebase(path) on a codebase large enough to take more than a few seconds.
  2. While it's still indexing, call index_codebase(path, force: true).
  3. Watch ~/.context/mcp-codebase-snapshot.json directly (to avoid also triggering the separate get_indexing_status sync bug) — the reported indexingPercentage is non-monotonic across successive reads, evidence of two runs writing concurrently.
  4. 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:

javascript
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