Merge pull request #12164 from Kilo-Org/fix-kilo-serve-busy-loop-linux

fix(cli): avoid independent worktree indexing scans
This commit is contained in:
Marius
2026-07-13 12:16:21 +02:00
committed by GitHub
5 changed files with 89 additions and 34 deletions
+6
View File
@@ -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.
+21 -3
View File
@@ -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<void> {
await new Promise<void>((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<VectorStoreSearchResult[]> {
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!,
@@ -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()
@@ -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 = {
@@ -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 = {