@@ -19,14 +19,12 @@ export class ToolHandlers {
1919 }
2020
2121 /**
22- * Sync indexed codebases from Zilliz Cloud collections
23- * This method fetches all collections from the vector database,
24- * gets the first document from each collection to extract codebasePath from metadata,
25- * and updates the snapshot with discovered codebases.
26- *
27- * Logic: Compare mcp-codebase-snapshot.json with zilliz cloud collections
28- * - If local snapshot has extra directories (not in cloud), remove them
29- * - If local snapshot is missing directories (exist in cloud), ignore them
22+ * Best-effort cloud sync for diagnostics.
23+ *
24+ * IMPORTANT SAFETY RULE:
25+ * Never remove local snapshot entries based only on cloud list/query results.
26+ * Different CLI sessions may run with different credentials/clusters, and
27+ * transient cloud visibility issues can cause false negatives.
3028 */
3129 private async syncIndexedCodebasesFromCloud ( ) : Promise < void > {
3230 try {
@@ -41,18 +39,7 @@ export class ToolHandlers {
4139 console . log ( `[SYNC-CLOUD] 📋 Found ${ collections . length } collections in Zilliz Cloud` ) ;
4240
4341 if ( collections . length === 0 ) {
44- console . log ( `[SYNC-CLOUD] ✅ No collections found in cloud` ) ;
45- // If no collections in cloud, remove all local codebases
46- const localCodebases = this . snapshotManager . getIndexedCodebases ( ) ;
47- if ( localCodebases . length > 0 ) {
48- console . log ( `[SYNC-CLOUD] 🧹 Removing ${ localCodebases . length } local codebases as cloud has no collections` ) ;
49- for ( const codebasePath of localCodebases ) {
50- this . snapshotManager . removeIndexedCodebase ( codebasePath ) ;
51- console . log ( `[SYNC-CLOUD] ➖ Removed local codebase: ${ codebasePath } ` ) ;
52- }
53- this . snapshotManager . saveCodebaseSnapshot ( ) ;
54- console . log ( `[SYNC-CLOUD] 💾 Updated snapshot to match empty cloud state` ) ;
55- }
42+ console . warn ( `[SYNC-CLOUD] ⚠️ Cloud returned zero collections. Skipping local snapshot cleanup to avoid false negatives.` ) ;
5643 return ;
5744 }
5845
@@ -110,30 +97,19 @@ export class ToolHandlers {
11097 console . log ( `[SYNC-CLOUD] 📊 Found ${ cloudCodebases . size } valid codebases in cloud` ) ;
11198
11299 // Get current local codebases
113- const localCodebases = new Set ( this . snapshotManager . getIndexedCodebases ( ) ) ;
114- console . log ( `[SYNC-CLOUD] 📊 Found ${ localCodebases . size } local codebases in snapshot` ) ;
115-
116- let hasChanges = false ;
100+ const localCodebases = this . snapshotManager . getIndexedCodebases ( ) ;
101+ console . log ( `[SYNC-CLOUD] 📊 Found ${ localCodebases . length } local codebases in snapshot` ) ;
117102
118- // Remove local codebases that don't exist in cloud
119- for ( const localCodebase of localCodebases ) {
120- if ( ! cloudCodebases . has ( localCodebase ) ) {
121- this . snapshotManager . removeIndexedCodebase ( localCodebase ) ;
122- hasChanges = true ;
123- console . log ( `[SYNC-CLOUD] ➖ Removed local codebase (not in cloud): ${ localCodebase } ` ) ;
124- }
125- }
126-
127- // Note: We don't add cloud codebases that are missing locally (as per user requirement)
128- console . log ( `[SYNC-CLOUD] ℹ️ Skipping addition of cloud codebases not present locally (per sync policy)` ) ;
129-
130- if ( hasChanges ) {
131- this . snapshotManager . saveCodebaseSnapshot ( ) ;
132- console . log ( `[SYNC-CLOUD] 💾 Updated snapshot to match cloud state` ) ;
133- } else {
134- console . log ( `[SYNC-CLOUD] ✅ Local snapshot already matches cloud state` ) ;
103+ const missingInCloud = localCodebases . filter ( ( localCodebase ) => ! cloudCodebases . has ( localCodebase ) ) ;
104+ if ( missingInCloud . length > 0 ) {
105+ console . warn (
106+ `[SYNC-CLOUD] ⚠️ ${ missingInCloud . length } local codebase(s) were not found in cloud metadata. ` +
107+ `Keeping local snapshot unchanged for safety.`
108+ ) ;
135109 }
136110
111+ // Note: intentionally no snapshot mutation here.
112+ console . log ( `[SYNC-CLOUD] ℹ️ Cloud sync is non-destructive; local snapshot was not modified.` ) ;
137113 console . log ( `[SYNC-CLOUD] ✅ Cloud sync completed successfully` ) ;
138114 } catch ( error : any ) {
139115 console . error ( `[SYNC-CLOUD] ❌ Error syncing codebases from cloud:` , error . message || error ) ;
@@ -199,13 +175,29 @@ export class ToolHandlers {
199175 } ;
200176 }
201177
202- //Check if the snapshot and cloud index are in sync
203- if ( this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) !== await this . context . hasIndex ( absolutePath ) ) {
178+ const snapshotHasIndex = this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) ;
179+ const cloudHasIndex = await this . context . hasIndex ( absolutePath ) ;
180+
181+ // Reconcile local snapshot with cloud truth for this specific codebase
182+ if ( snapshotHasIndex !== cloudHasIndex ) {
204183 console . warn ( `[INDEX-VALIDATION] ❌ Snapshot and cloud index mismatch: ${ absolutePath } ` ) ;
184+ if ( cloudHasIndex && ! snapshotHasIndex ) {
185+ this . snapshotManager . setCodebaseIndexed ( absolutePath , {
186+ indexedFiles : 0 ,
187+ totalChunks : 0 ,
188+ status : 'completed'
189+ } ) ;
190+ await this . snapshotManager . saveCodebaseSnapshot ( 'index-reconcile-cloud-present' ) ;
191+ console . log ( `[INDEX-VALIDATION] 🛠️ Recovered missing snapshot entry from cloud index: ${ absolutePath } ` ) ;
192+ } else if ( ! cloudHasIndex && snapshotHasIndex ) {
193+ this . snapshotManager . removeCodebaseCompletely ( absolutePath ) ;
194+ await this . snapshotManager . saveCodebaseSnapshot ( 'index-reconcile-cloud-missing' ) ;
195+ console . log ( `[INDEX-VALIDATION] 🧹 Removed stale snapshot entry without cloud index: ${ absolutePath } ` ) ;
196+ }
205197 }
206198
207- // Check if already indexed (unless force is true)
208- if ( ! forceReindex && this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) ) {
199+ // Check if already indexed in cloud (unless force is true)
200+ if ( ! forceReindex && cloudHasIndex ) {
209201 return {
210202 content : [ {
211203 type : "text" ,
@@ -219,9 +211,9 @@ export class ToolHandlers {
219211 if ( forceReindex ) {
220212 if ( this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) ) {
221213 console . log ( `[FORCE-REINDEX] 🔄 Removing '${ absolutePath } ' from indexed list for re-indexing` ) ;
222- this . snapshotManager . removeIndexedCodebase ( absolutePath ) ;
214+ this . snapshotManager . removeCodebaseCompletely ( absolutePath ) ;
223215 }
224- if ( await this . context . hasIndex ( absolutePath ) ) {
216+ if ( cloudHasIndex ) {
225217 console . log ( `[FORCE-REINDEX] 🔄 Clearing index for '${ absolutePath } '` ) ;
226218 await this . context . clearIndex ( absolutePath ) ;
227219 }
@@ -279,7 +271,7 @@ export class ToolHandlers {
279271
280272 // Set to indexing status and save snapshot immediately
281273 this . snapshotManager . setCodebaseIndexing ( absolutePath , 0 ) ;
282- this . snapshotManager . saveCodebaseSnapshot ( ) ;
274+ await this . snapshotManager . saveCodebaseSnapshot ( 'index-started' ) ;
283275
284276 // Track the codebase path for syncing
285277 trackCodebasePath ( absolutePath ) ;
@@ -323,7 +315,7 @@ export class ToolHandlers {
323315
324316 private async startBackgroundIndexing ( codebasePath : string , forceReindex : boolean , splitterType : string ) {
325317 const absolutePath = codebasePath ;
326- let lastSaveTime = 0 ; // Track last save timestamp
318+ let lastPersistedProgress = - 1 ;
327319
328320 try {
329321 console . log ( `[BACKGROUND-INDEX] Starting background indexing for: ${ absolutePath } ` ) ;
@@ -369,12 +361,16 @@ export class ToolHandlers {
369361 // Update progress in snapshot manager using new method
370362 this . snapshotManager . setCodebaseIndexing ( absolutePath , progress . percentage ) ;
371363
372- // Save snapshot periodically (every 2 seconds to avoid too frequent saves)
373- const currentTime = Date . now ( ) ;
374- if ( currentTime - lastSaveTime >= 2000 ) { // 2 seconds = 2000ms
375- this . snapshotManager . saveCodebaseSnapshot ( ) ;
376- lastSaveTime = currentTime ;
377- console . log ( `[BACKGROUND-INDEX] 💾 Saved progress snapshot at ${ progress . percentage . toFixed ( 1 ) } %` ) ;
364+ // Coalesce disk writes: persist only meaningful progress jumps.
365+ const shouldPersistProgress =
366+ lastPersistedProgress < 0 ||
367+ progress . percentage >= 100 ||
368+ Math . abs ( progress . percentage - lastPersistedProgress ) >= 2 ;
369+
370+ if ( shouldPersistProgress ) {
371+ this . snapshotManager . scheduleSaveCodebaseSnapshot ( 'index-progress' ) ;
372+ lastPersistedProgress = progress . percentage ;
373+ console . log ( `[BACKGROUND-INDEX] 💾 Scheduled progress snapshot at ${ progress . percentage . toFixed ( 1 ) } %` ) ;
378374 }
379375
380376 console . log ( `[BACKGROUND-INDEX] Progress: ${ progress . phase } - ${ progress . percentage } % (${ progress . current } /${ progress . total } )` ) ;
@@ -386,7 +382,7 @@ export class ToolHandlers {
386382 this . indexingStats = { indexedFiles : stats . indexedFiles , totalChunks : stats . totalChunks } ;
387383
388384 // Save snapshot after updating codebase lists
389- this . snapshotManager . saveCodebaseSnapshot ( ) ;
385+ await this . snapshotManager . saveCodebaseSnapshot ( 'index-completed' ) ;
390386
391387 let message = `Background indexing completed for '${ absolutePath } ' using ${ splitterType . toUpperCase ( ) } splitter.\nIndexed ${ stats . indexedFiles } files, ${ stats . totalChunks } chunks.` ;
392388 if ( stats . status === 'limit_reached' ) {
@@ -404,7 +400,7 @@ export class ToolHandlers {
404400 // Set codebase to failed status with error information
405401 const errorMessage = error . message || String ( error ) ;
406402 this . snapshotManager . setCodebaseIndexFailed ( absolutePath , errorMessage , lastProgress ) ;
407- this . snapshotManager . saveCodebaseSnapshot ( ) ;
403+ await this . snapshotManager . saveCodebaseSnapshot ( 'index-failed' ) ;
408404
409405 // Log error but don't crash MCP service - indexing errors are handled gracefully
410406 console . error ( `[BACKGROUND-INDEX] Indexing failed for ${ absolutePath } : ${ errorMessage } ` ) ;
@@ -447,11 +443,23 @@ export class ToolHandlers {
447443
448444 trackCodebasePath ( absolutePath ) ;
449445
450- // Check if this codebase is indexed or being indexed
451- const isIndexed = this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) ;
446+ // Check status with cloud as source of truth and snapshot as progress source
447+ const isIndexedInSnapshot = this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) ;
452448 const isIndexing = this . snapshotManager . getIndexingCodebases ( ) . includes ( absolutePath ) ;
449+ const hasCloudIndex = await this . context . hasIndex ( absolutePath ) ;
450+
451+ // Self-heal snapshot if index exists in cloud but local snapshot is missing
452+ if ( hasCloudIndex && ! isIndexedInSnapshot && ! isIndexing ) {
453+ this . snapshotManager . setCodebaseIndexed ( absolutePath , {
454+ indexedFiles : 0 ,
455+ totalChunks : 0 ,
456+ status : 'completed'
457+ } ) ;
458+ await this . snapshotManager . saveCodebaseSnapshot ( 'search-reconcile-cloud-present' ) ;
459+ console . log ( `[SEARCH] 🛠️ Restored missing snapshot entry from cloud index for: ${ absolutePath } ` ) ;
460+ }
453461
454- if ( ! isIndexed && ! isIndexing ) {
462+ if ( ! hasCloudIndex && ! isIndexing ) {
455463 return {
456464 content : [ {
457465 type : "text" ,
@@ -469,7 +477,7 @@ export class ToolHandlers {
469477
470478 console . log ( `[SEARCH] Searching in codebase: ${ absolutePath } ` ) ;
471479 console . log ( `[SEARCH] Query: "${ query } "` ) ;
472- console . log ( `[SEARCH] Indexing status: ${ isIndexing ? 'In Progress' : 'Completed' } ` ) ;
480+ console . log ( `[SEARCH] Indexing status: ${ isIndexing ? 'In Progress' : ( hasCloudIndex ? 'Completed' : 'No collection yet' ) } ` ) ;
473481
474482 // Log embedding provider information before search
475483 const embeddingProvider = this . context . getEmbedding ( ) ;
@@ -571,15 +579,6 @@ export class ToolHandlers {
571579 public async handleClearIndex ( args : any ) {
572580 const { path : codebasePath } = args ;
573581
574- if ( this . snapshotManager . getIndexedCodebases ( ) . length === 0 && this . snapshotManager . getIndexingCodebases ( ) . length === 0 ) {
575- return {
576- content : [ {
577- type : "text" ,
578- text : "No codebases are currently indexed or being indexed."
579- } ]
580- } ;
581- }
582-
583582 try {
584583 // Force absolute path resolution - warn if relative path provided
585584 const absolutePath = ensureAbsolutePath ( codebasePath ) ;
@@ -610,8 +609,9 @@ export class ToolHandlers {
610609 // Check if this codebase is indexed or being indexed
611610 const isIndexed = this . snapshotManager . getIndexedCodebases ( ) . includes ( absolutePath ) ;
612611 const isIndexing = this . snapshotManager . getIndexingCodebases ( ) . includes ( absolutePath ) ;
612+ const hasCloudIndex = await this . context . hasIndex ( absolutePath ) ;
613613
614- if ( ! isIndexed && ! isIndexing ) {
614+ if ( ! isIndexed && ! isIndexing && ! hasCloudIndex ) {
615615 return {
616616 content : [ {
617617 type : "text" ,
@@ -623,19 +623,23 @@ export class ToolHandlers {
623623
624624 console . log ( `[CLEAR] Clearing codebase: ${ absolutePath } ` ) ;
625625
626- try {
627- await this . context . clearIndex ( absolutePath ) ;
628- console . log ( `[CLEAR] Successfully cleared index for: ${ absolutePath } ` ) ;
629- } catch ( error : any ) {
630- const errorMsg = `Failed to clear ${ absolutePath } : ${ error . message } ` ;
631- console . error ( `[CLEAR] ${ errorMsg } ` ) ;
632- return {
633- content : [ {
634- type : "text" ,
635- text : errorMsg
636- } ] ,
637- isError : true
638- } ;
626+ if ( hasCloudIndex ) {
627+ try {
628+ await this . context . clearIndex ( absolutePath ) ;
629+ console . log ( `[CLEAR] Successfully cleared index for: ${ absolutePath } ` ) ;
630+ } catch ( error : any ) {
631+ const errorMsg = `Failed to clear ${ absolutePath } : ${ error . message } ` ;
632+ console . error ( `[CLEAR] ${ errorMsg } ` ) ;
633+ return {
634+ content : [ {
635+ type : "text" ,
636+ text : errorMsg
637+ } ] ,
638+ isError : true
639+ } ;
640+ }
641+ } else {
642+ console . log ( `[CLEAR] ℹ️ No cloud collection found for ${ absolutePath } , cleaning snapshot only` ) ;
639643 }
640644
641645 // Completely remove the cleared codebase from snapshot
@@ -645,7 +649,7 @@ export class ToolHandlers {
645649 this . indexingStats = null ;
646650
647651 // Save snapshot after clearing index
648- this . snapshotManager . saveCodebaseSnapshot ( ) ;
652+ await this . snapshotManager . saveCodebaseSnapshot ( 'clear-index' ) ;
649653
650654 let resultText = `Successfully cleared codebase '${ absolutePath } '` ;
651655
@@ -718,9 +722,34 @@ export class ToolHandlers {
718722 } ;
719723 }
720724
721- // Check indexing status using new status system
722- const status = this . snapshotManager . getCodebaseStatus ( absolutePath ) ;
723- const info = this . snapshotManager . getCodebaseInfo ( absolutePath ) ;
725+ // Check indexing status using snapshot plus cloud truth
726+ let status = this . snapshotManager . getCodebaseStatus ( absolutePath ) ;
727+ let info = this . snapshotManager . getCodebaseInfo ( absolutePath ) ;
728+ let recoveredFromCloud = false ;
729+ const hasCloudIndex = await this . context . hasIndex ( absolutePath ) ;
730+
731+ // Self-heal snapshot if cloud has index but local status is missing
732+ if ( status === 'not_found' && hasCloudIndex ) {
733+ this . snapshotManager . setCodebaseIndexed ( absolutePath , {
734+ indexedFiles : 0 ,
735+ totalChunks : 0 ,
736+ status : 'completed'
737+ } ) ;
738+ await this . snapshotManager . saveCodebaseSnapshot ( 'status-reconcile-cloud-present' ) ;
739+ status = 'indexed' ;
740+ info = this . snapshotManager . getCodebaseInfo ( absolutePath ) ;
741+ recoveredFromCloud = true ;
742+ console . log ( `[STATUS] 🛠️ Restored missing snapshot entry from cloud index for: ${ absolutePath } ` ) ;
743+ }
744+
745+ // Cleanup stale snapshot entries if cloud index no longer exists
746+ if ( status === 'indexed' && ! hasCloudIndex ) {
747+ this . snapshotManager . removeCodebaseCompletely ( absolutePath ) ;
748+ await this . snapshotManager . saveCodebaseSnapshot ( 'status-reconcile-cloud-missing' ) ;
749+ status = 'not_found' ;
750+ info = undefined ;
751+ console . log ( `[STATUS] 🧹 Removed stale indexed snapshot entry without cloud index for: ${ absolutePath } ` ) ;
752+ }
724753
725754 let statusMessage = '' ;
726755
@@ -735,6 +764,9 @@ export class ToolHandlers {
735764 } else {
736765 statusMessage = `✅ Codebase '${ absolutePath } ' is fully indexed and ready for search.` ;
737766 }
767+ if ( recoveredFromCloud ) {
768+ statusMessage += `\nℹ️ Index was detected directly in vector database and local snapshot state was restored.` ;
769+ }
738770 break ;
739771
740772 case 'indexing' :
@@ -797,4 +829,4 @@ export class ToolHandlers {
797829 } ;
798830 }
799831 }
800- }
832+ }
0 commit comments