Repository navigation
Expand file tree
/
Copy pathorchestrator.ts
More file actions
295 lines (261 loc) · 10.2 KB
/
Copy pathorchestrator.ts
File metadata and controls
295 lines (261 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import * as vscode from "vscode"
import * as path from "path"
import { CodeIndexConfigManager } from "./config-manager"
import { CodeIndexStateManager, IndexingState } from "./state-manager"
import { IFileWatcher, IVectorStore, BatchProcessingSummary } from "./interfaces"
import { DirectoryScanner } from "./processors"
import { CacheManager } from "./cache-manager"
import { CodeIndexScanExecutor } from "./code-index-scan-executor"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
import { t } from "../../i18n"
/**
* Manages the code indexing workflow, coordinating between different services and managers.
*/
export class CodeIndexOrchestrator {
private _fileWatcherSubscriptions: vscode.Disposable[] = []
private _isProcessing: boolean = false
private _abortController: AbortController | null = null
private readonly scanExecutor: CodeIndexScanExecutor
constructor(
private readonly configManager: CodeIndexConfigManager,
private readonly stateManager: CodeIndexStateManager,
workspacePath: string,
private readonly cacheManager: CacheManager,
private readonly vectorStore: IVectorStore,
scanner: DirectoryScanner,
private readonly fileWatcher: IFileWatcher,
) {
this.scanExecutor = new CodeIndexScanExecutor(workspacePath, scanner, vectorStore, stateManager)
}
/**
* Starts the file watcher if not already running.
*/
private async _startWatcher(): Promise<void> {
if (!this.configManager.isFeatureConfigured) {
throw new Error("Cannot start watcher: Service not configured.")
}
this.stateManager.setSystemState("Indexing", "Initializing file watcher...")
try {
await this.fileWatcher.initialize()
this._fileWatcherSubscriptions = [
this.fileWatcher.onDidStartBatchProcessing((filePaths: string[]) => {}),
this.fileWatcher.onBatchProgressUpdate(({ processedInBatch, totalInBatch, currentFile }) => {
if (totalInBatch > 0 && this.stateManager.state !== "Indexing") {
this.stateManager.setSystemState("Indexing", "Processing file changes...")
}
this.stateManager.reportFileQueueProgress(
processedInBatch,
totalInBatch,
currentFile ? path.basename(currentFile) : undefined,
)
if (processedInBatch === totalInBatch) {
// Covers (N/N) and (0/0)
if (totalInBatch > 0) {
// Batch with items completed
this.stateManager.setSystemState("Indexed", "File changes processed. Index up-to-date.")
} else {
if (this.stateManager.state === "Indexing") {
// Only transition if it was "Indexing"
this.stateManager.setSystemState("Indexed", "Index up-to-date. File queue empty.")
}
}
}
}),
this.fileWatcher.onDidFinishBatchProcessing((summary: BatchProcessingSummary) => {
if (summary.batchError) {
console.error(`[CodeIndexOrchestrator] Batch processing failed:`, summary.batchError)
} else {
const successCount = summary.processedFiles.filter(
(f: { status: string }) => f.status === "success",
).length
const errorCount = summary.processedFiles.filter(
(f: { status: string }) => f.status === "error" || f.status === "local_error",
).length
}
}),
]
} catch (error) {
console.error("[CodeIndexOrchestrator] Failed to start file watcher:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "_startWatcher",
})
throw error
}
}
/**
* Updates the status of a file in the state manager.
*/
/**
* Initiates the indexing process (initial scan and starts watcher).
*/
public async startIndexing(): Promise<void> {
// Check if workspace is available first
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
this.stateManager.setSystemState("Error", t("embeddings:orchestrator.indexingRequiresWorkspace"))
console.warn("[CodeIndexOrchestrator] Start rejected: No workspace folder open.")
return
}
if (!this.configManager.isFeatureConfigured) {
this.stateManager.setSystemState("Standby", "Missing configuration. Save your settings to start indexing.")
console.warn("[CodeIndexOrchestrator] Start rejected: Missing configuration.")
return
}
if (
this._isProcessing ||
(this.stateManager.state !== "Standby" &&
this.stateManager.state !== "Error" &&
this.stateManager.state !== "Indexed")
) {
console.warn(
`[CodeIndexOrchestrator] Start rejected: Already processing or in state ${this.stateManager.state}.`,
)
return
}
this._isProcessing = true
this._abortController = new AbortController()
const signal = this._abortController.signal
this.stateManager.setSystemState("Indexing", "Initializing services...")
// Only clean up collections created by this run; existing data must survive failed retries.
let clearIndexOnError = false
try {
const collectionCreated = await this.vectorStore.initialize()
clearIndexOnError = collectionCreated
if (collectionCreated) {
await this.cacheManager.clearCacheFile()
}
// Check if the collection already has indexed data
// If it does, we can skip the full scan and just start the watcher
const hasExistingData = await this.vectorStore.hasIndexedData()
if (hasExistingData && !collectionCreated) {
if (!(await this.scanExecutor.runIncrementalScan(signal))) {
await this.cacheManager.flush()
this.stopWatcher()
this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.indexingStopped"))
return
}
await this._startWatcher()
// Mark indexing as complete after successful incremental scan
await this.vectorStore.markIndexingComplete()
this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted"))
} else {
if (!(await this.scanExecutor.runFullScan(signal))) {
await this.cacheManager.flush()
this.stopWatcher()
this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.indexingStopped"))
return
}
await this._startWatcher()
// Mark indexing as complete after successful full scan
await this.vectorStore.markIndexingComplete()
this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted"))
}
} catch (error: any) {
// Handle abort gracefully — not an error, just a user-initiated stop
if (error?.name === "AbortError" || signal.aborted) {
console.log("[CodeIndexOrchestrator] Indexing aborted by user.")
await this.cacheManager.flush()
this.stopWatcher()
this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.indexingStopped"))
return
}
console.error("[CodeIndexOrchestrator] Error during indexing:", error)
// Scanner/provider errors and stacks may contain local paths or other private data.
// Keep details local; send only a fixed failure category across the telemetry boundary.
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: "Indexing failed",
location: "startIndexing",
})
if (clearIndexOnError) {
try {
await this.vectorStore.clearCollection()
} catch (cleanupError) {
console.error("[CodeIndexOrchestrator] Failed to clean up after error:", cleanupError)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: "Index cleanup failed",
location: "startIndexing.cleanup",
})
}
// Indexing started but failed mid-way - clear cache to avoid cache-Qdrant mismatch
await this.cacheManager.clearCacheFile()
console.log(
"[CodeIndexOrchestrator] Indexing failed after starting. Clearing cache to avoid inconsistency.",
)
} else {
console.log("[CodeIndexOrchestrator] Preserving existing index and cache for a retry.")
}
this.stateManager.setSystemState(
"Error",
t("embeddings:orchestrator.failedDuringInitialScan", {
errorMessage: error.message || t("embeddings:orchestrator.unknownError"),
}),
)
this.stopWatcher()
} finally {
this._isProcessing = false
this._abortController = null
}
}
/**
* Stops any in-progress indexing by aborting the scan and stopping the file watcher.
*/
public stopIndexing(): void {
if (this._abortController) {
this.stateManager.setSystemState("Stopping", t("embeddings:orchestrator.indexingStoppedPartial"))
this._abortController.abort()
this._abortController = null
}
this.stopWatcher()
}
/**
* Stops the file watcher and cleans up resources.
*/
public stopWatcher(): void {
this.fileWatcher.dispose()
this._fileWatcherSubscriptions.forEach((sub) => sub.dispose())
this._fileWatcherSubscriptions = []
if (this.stateManager.state !== "Error" && this.stateManager.state !== "Stopping") {
this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.fileWatcherStopped"))
}
this._isProcessing = false
}
/**
* Clears all index data by stopping the watcher, clearing the vector store,
* and resetting the cache file.
*/
public async clearIndexData(): Promise<void> {
this._isProcessing = true
try {
await this.stopWatcher()
try {
if (this.configManager.isFeatureConfigured) {
await this.vectorStore.deleteCollection()
} else {
console.warn("[CodeIndexOrchestrator] Service not configured, skipping vector collection clear.")
}
} catch (error: any) {
console.error("[CodeIndexOrchestrator] Failed to clear vector collection:", error)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
location: "clearIndexData",
})
this.stateManager.setSystemState("Error", `Failed to clear vector collection: ${error.message}`)
}
await this.cacheManager.clearCacheFile()
if (this.stateManager.state !== "Error") {
this.stateManager.setSystemState("Standby", "Index data cleared successfully.")
}
} finally {
this._isProcessing = false
}
}
/**
* Gets the current state of the indexing system.
*/
public get state(): IndexingState {
return this.stateManager.state
}
}