From 039b73dfaefe93452501a48914eaeeb2f83c572b Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 13 Jul 2026 11:33:39 +0200 Subject: [PATCH] fix(cli): avoid independent worktree indexing scans --- .changeset/calm-worktree-indexing.md | 6 ++ .../kilo-indexing/src/indexing/manager.ts | 24 +++++++- .../src/indexing/orchestrator.ts | 3 +- .../kilocode/indexing/orchestrator.test.ts | 29 --------- .../test/kilocode/indexing-worker.test.ts | 61 +++++++++++++++++++ 5 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 .changeset/calm-worktree-indexing.md diff --git a/.changeset/calm-worktree-indexing.md b/.changeset/calm-worktree-indexing.md new file mode 100644 index 0000000000..de68121f98 --- /dev/null +++ b/.changeset/calm-worktree-indexing.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-indexing": patch +--- + +Wait for the primary codebase index before indexing a linked worktree, preventing large worktrees from consuming excessive CPU during startup. diff --git a/packages/kilo-indexing/src/indexing/manager.ts b/packages/kilo-indexing/src/indexing/manager.ts index 05e4c5fc15..dcaed039b1 100644 --- a/packages/kilo-indexing/src/indexing/manager.ts +++ b/packages/kilo-indexing/src/indexing/manager.ts @@ -18,6 +18,7 @@ import { WorktreeOverlay } from "./worktree-overlay" const log = Log.create({ service: "indexing-manager" }) const BASELINE_CHECK_INTERVAL = 1_000 const BASELINE_SIGNATURE_INTERVAL = 30_000 +const BASELINE_PENDING = "Waiting for the primary worktree index to become available." type Baseline = { store?: IVectorStore @@ -129,6 +130,12 @@ export class CodeIndexManager { this.clearRetryTimer() } + private waiting(): boolean { + if (!this.baselinePath || this._baselineStore) return false + this._stateManager.setSystemState("Standby", BASELINE_PENDING) + return true + } + private async waitForRetry(delay: number): Promise { await new Promise((resolve) => { this._retryResolve = resolve @@ -191,6 +198,11 @@ export class CodeIndexManager { try { await this._recreateServices() if (this._disposed) return + if (this.waiting()) { + this.resetRetryState() + this._isRecoveringFromError = false + return + } this.emitStart(trigger) await this._orchestrator!.startIndexing(trigger) if (this._disposed) return @@ -344,6 +356,8 @@ export class CodeIndexManager { } } + if (this.waiting()) return { requiresRestart } + const shouldStartOrRestart = requiresRestart || (needsServiceRecreation && (!this._orchestrator || this._orchestrator.state !== "Indexing")) @@ -365,6 +379,9 @@ export class CodeIndexManager { if (this._disposed) return if (!this.isFeatureEnabled) return + await this.refreshBaseline() + if (this.waiting()) return + log.info("manual indexing start requested", { workspacePath: this.workspacePath }) const currentStatus = this.getCurrentStatus() @@ -453,8 +470,9 @@ export class CodeIndexManager { public async searchIndex(query: string, directoryPrefix?: string): Promise { if (!this.isFeatureEnabled) return [] - this.assertInitialized() await this.refreshBaseline() + if (this.waiting()) return [] + this.assertInitialized() return this._searchService!.searchIndex(query, directoryPrefix) } @@ -483,6 +501,7 @@ export class CodeIndexManager { this._baselineSigned = now if (!baseline?.store) { if (!this._baselineStore) this._baselineSignature = signature + this.waiting() return } @@ -528,7 +547,7 @@ export class CodeIndexManager { } } catch (err) { await store.close?.() - log.warn("shared indexing baseline is unavailable; using an independent worktree index", { + log.info("shared indexing baseline is unavailable; waiting for the primary worktree index", { workspacePath: this.workspacePath, baselinePath: this.baselinePath, err, @@ -586,7 +605,6 @@ export class CodeIndexManager { fileWatcher, (event) => this.handleTelemetry(event), baseline?.overlay, - Boolean(this.baselinePath && !baseline?.store), ) const search = new CodeIndexSearchService( this._configManager!, diff --git a/packages/kilo-indexing/src/indexing/orchestrator.ts b/packages/kilo-indexing/src/indexing/orchestrator.ts index 8e2d08ef8b..9f4d376eca 100644 --- a/packages/kilo-indexing/src/indexing/orchestrator.ts +++ b/packages/kilo-indexing/src/indexing/orchestrator.ts @@ -37,7 +37,6 @@ export class CodeIndexOrchestrator { private readonly fileWatcher: IFileWatcher, private readonly onTelemetry?: IndexingTelemetryReporter, private readonly overlay?: WorktreeOverlay, - private readonly independent = false, ) {} private getTelemetryMeta(): IndexingTelemetryMeta { @@ -215,7 +214,7 @@ export class CodeIndexOrchestrator { }) } - const hasExistingData = this.overlay || this.independent ? false : await this.vectorStore.hasIndexedData() + const hasExistingData = this.overlay ? false : await this.vectorStore.hasIndexedData() if (!this.overlay && !hasExistingData) { if (!collectionCreated) await this.vectorStore.clearCollection() await this.cacheManager.clearCacheFile() diff --git a/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts b/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts index cac7663998..f2fc3f046d 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts @@ -357,35 +357,6 @@ describe("CodeIndexOrchestrator telemetry", () => { expect(orchestrator.state).toBe("Error") }) - test("rebuilds a complete independent index when the shared baseline is unavailable", async () => { - const cache = { - clears: 0, - async clearCacheFile() { - this.clears += 1 - }, - async flush() {}, - } - const store = new Store(true, false) - const orchestrator = new CodeIndexOrchestrator( - createConfig(), - new CodeIndexStateManager(), - "/tmp/ws", - cache as unknown as CacheManager, - store as unknown as IVectorStore, - new Scanner(1, 1, 1) as unknown as DirectoryScanner, - new Watcher() as unknown as IFileWatcher, - undefined, - undefined, - true, - ) - - await orchestrator.startIndexing("background") - - expect(store.clearCount).toBe(1) - expect(cache.clears).toBe(1) - expect(orchestrator.state).toBe("Indexed") - }) - test("preserves cache and collection data on retryable start failures", async () => { const events: IndexingTelemetryEvent[] = [] const cache = { diff --git a/packages/opencode/test/kilocode/indexing-worker.test.ts b/packages/opencode/test/kilocode/indexing-worker.test.ts index a3c44002ad..12214b0a07 100644 --- a/packages/opencode/test/kilocode/indexing-worker.test.ts +++ b/packages/opencode/test/kilocode/indexing-worker.test.ts @@ -1,4 +1,6 @@ import { expect, test } from "bun:test" +import { mkdir } from "node:fs/promises" +import path from "node:path" import { IndexingWorker } from "../../src/kilocode/indexing-worker-client" import { tmpdir } from "../fixture/fixture" @@ -55,6 +57,65 @@ test("routes multiple directories through the shared indexing worker", async () expect(failures).toEqual([]) }) +test("waits for the primary index instead of scanning a worktree independently", async () => { + await using tmp = await tmpdir() + const main = path.join(tmp.path, "main") + const worktree = path.join(tmp.path, "worktree") + await mkdir(main) + await mkdir(worktree) + await Bun.write(path.join(worktree, "file.ts"), "export function value() { return 1 }\n") + + const requests: string[] = [] + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(req) { + requests.push(new URL(req.url).pathname) + const body = (await req.json()) as { input: string | string[] } + const input = Array.isArray(body.input) ? body.input : [body.input] + return Response.json({ + object: "list", + model: "fixture-model", + data: input.map((_, index) => ({ object: "embedding", index, embedding: [0.1, 0.2, 0.3] })), + usage: { prompt_tokens: 1, total_tokens: 1 }, + }) + }, + }) + const failures: unknown[] = [] + const engine = IndexingWorker.create(worktree, tmp.path, { + status() {}, + telemetry() {}, + warning() {}, + log() {}, + failure(err) { + failures.push(err) + }, + }) + + try { + const status = await engine.init( + { + enabled: true, + embedderProvider: "openai-compatible", + vectorStoreProvider: "lancedb", + modelId: "fixture-model", + modelDimension: 3, + openAiCompatibleBaseUrl: `http://127.0.0.1:${server.port}/v1`, + }, + main, + ) + + expect(status.state).toBe("Standby") + expect(status.message).toContain("primary worktree index") + expect(requests).toEqual(["/v1/embeddings"]) + } finally { + await engine.dispose() + server.stop(true) + } + + expect(failures).toEqual([]) +}) + test("allows same-directory recreation while disposal is pending", async () => { await using tmp = await tmpdir() const hooks = {