enhance snapshot manager

Signed-off-by: ChengZi <chen.zhang@zilliz.com>
This commit is contained in:
ChengZi
2025-08-20 21:01:52 +08:00
committed by Cheney Zhang
parent 325bcd158f
commit a01a487a1a
3 changed files with 391 additions and 101 deletions

View File

@@ -19,12 +19,53 @@ export interface ContextMcpConfig {
milvusToken?: string;
}
export interface CodebaseSnapshot {
// Legacy format (v1) - for backward compatibility
export interface CodebaseSnapshotV1 {
indexedCodebases: string[];
indexingCodebases: string[] | Record<string, number>; // Array (legacy) or Map of codebase path to progress percentage
lastUpdated: string;
}
// New format (v2) - structured with codebase information
// Base interface for common fields
interface CodebaseInfoBase {
lastUpdated: string;
}
// Indexing state - when indexing is in progress
export interface CodebaseInfoIndexing extends CodebaseInfoBase {
status: 'indexing';
indexingPercentage: number; // Current progress percentage
}
// Indexed state - when indexing completed successfully
export interface CodebaseInfoIndexed extends CodebaseInfoBase {
status: 'indexed';
indexedFiles: number; // Number of files indexed
totalChunks: number; // Total number of chunks generated
indexStatus: 'completed' | 'limit_reached'; // Status from indexing result
}
// Index failed state - when indexing failed
export interface CodebaseInfoIndexFailed extends CodebaseInfoBase {
status: 'indexfailed';
errorMessage: string; // Error message from the failure
lastAttemptedPercentage?: number; // Progress when failure occurred
}
// Union type for all codebase information states
export type CodebaseInfo = CodebaseInfoIndexing | CodebaseInfoIndexed | CodebaseInfoIndexFailed;
export interface CodebaseSnapshotV2 {
formatVersion: 'v2';
codebases: Record<string, CodebaseInfo>; // codebasePath -> CodebaseInfo
lastUpdated: string;
}
// Union type for all supported formats
export type CodebaseSnapshot = CodebaseSnapshotV1 | CodebaseSnapshotV2;
// Helper function to get default model for each provider
export function getDefaultModelForProvider(provider: string): string {
switch (provider) {

View File

@@ -270,8 +270,15 @@ export class ToolHandlers {
this.context.addCustomIgnorePatterns(customIgnorePatterns);
}
// Add to indexing list and save snapshot immediately
this.snapshotManager.addIndexingCodebase(absolutePath);
// Check current status and log if retrying after failure
const currentStatus = this.snapshotManager.getCodebaseStatus(absolutePath);
if (currentStatus === 'indexfailed') {
const failedInfo = this.snapshotManager.getCodebaseInfo(absolutePath) as any;
console.log(`[BACKGROUND-INDEX] Retrying indexing for previously failed codebase. Previous error: ${failedInfo?.errorMessage || 'Unknown error'}`);
}
// Set to indexing status and save snapshot immediately
this.snapshotManager.setCodebaseIndexing(absolutePath, 0);
this.snapshotManager.saveCodebaseSnapshot();
// Track the codebase path for syncing
@@ -359,8 +366,8 @@ export class ToolHandlers {
// Start indexing with the appropriate context and progress tracking
console.log(`[BACKGROUND-INDEX] 🚀 Beginning codebase indexing process...`);
const stats = await contextForThisTask.indexCodebase(absolutePath, (progress) => {
// Update progress in snapshot manager
this.snapshotManager.updateIndexingProgress(absolutePath, progress.percentage);
// Update progress in snapshot manager using new method
this.snapshotManager.setCodebaseIndexing(absolutePath, progress.percentage);
// Save snapshot periodically (every 2 seconds to avoid too frequent saves)
const currentTime = Date.now();
@@ -374,8 +381,8 @@ export class ToolHandlers {
});
console.log(`[BACKGROUND-INDEX] ✅ Indexing completed successfully! Files: ${stats.indexedFiles}, Chunks: ${stats.totalChunks}`);
// Move from indexing to indexed list
this.snapshotManager.moveFromIndexingToIndexed(absolutePath);
// Set codebase to indexed status with complete statistics
this.snapshotManager.setCodebaseIndexed(absolutePath, stats);
this.indexingStats = { indexedFiles: stats.indexedFiles, totalChunks: stats.totalChunks };
// Save snapshot after updating codebase lists
@@ -390,12 +397,17 @@ export class ToolHandlers {
} catch (error: any) {
console.error(`[BACKGROUND-INDEX] Error during indexing for ${absolutePath}:`, error);
// Remove from indexing list on error
this.snapshotManager.removeIndexingCodebase(absolutePath);
// Get the last attempted progress
const lastProgress = this.snapshotManager.getIndexingProgress(absolutePath);
// Set codebase to failed status with error information
const errorMessage = error.message || String(error);
this.snapshotManager.setCodebaseIndexFailed(absolutePath, errorMessage, lastProgress);
this.snapshotManager.saveCodebaseSnapshot();
// Log error but don't crash MCP service - indexing errors are handled gracefully
console.error(`[BACKGROUND-INDEX] Indexing failed for ${absolutePath}: ${error.message || error}`);
console.error(`[BACKGROUND-INDEX] Indexing failed for ${absolutePath}: ${errorMessage}`);
}
}
@@ -707,27 +719,62 @@ export class ToolHandlers {
};
}
// Check indexing status
const isIndexed = this.snapshotManager.getIndexedCodebases().includes(absolutePath);
const isIndexing = this.snapshotManager.getIndexingCodebases().includes(absolutePath);
const indexingProgress = this.snapshotManager.getIndexingProgress(absolutePath);
// Check indexing status using new status system
const status = this.snapshotManager.getCodebaseStatus(absolutePath);
const info = this.snapshotManager.getCodebaseInfo(absolutePath);
let statusMessage = '';
if (isIndexed) {
statusMessage = `✅ Codebase '${absolutePath}' is fully indexed and ready for search.`;
} else if (isIndexing) {
const progressPercentage = indexingProgress !== undefined ? indexingProgress : 0;
statusMessage = `🔄 Codebase '${absolutePath}' is currently being indexed. Progress: ${progressPercentage.toFixed(1)}%`;
switch (status) {
case 'indexed':
if (info && 'indexedFiles' in info) {
const indexedInfo = info as any;
statusMessage = ` Codebase '${absolutePath}' is fully indexed and ready for search.`;
statusMessage += `\n📊 Statistics: ${indexedInfo.indexedFiles} files, ${indexedInfo.totalChunks} chunks`;
statusMessage += `\n📅 Status: ${indexedInfo.indexStatus}`;
statusMessage += `\n🕐 Last updated: ${new Date(indexedInfo.lastUpdated).toLocaleString()}`;
} else {
statusMessage = `✅ Codebase '${absolutePath}' is fully indexed and ready for search.`;
}
break;
// Add more detailed status based on progress
if (progressPercentage < 10) {
statusMessage += ' (Preparing and scanning files...)';
} else if (progressPercentage < 100) {
statusMessage += ' (Processing files and generating embeddings...)';
}
} else {
statusMessage = `❌ Codebase '${absolutePath}' is not indexed. Please use the index_codebase tool to index it first.`;
case 'indexing':
if (info && 'indexingPercentage' in info) {
const indexingInfo = info as any;
const progressPercentage = indexingInfo.indexingPercentage || 0;
statusMessage = `🔄 Codebase '${absolutePath}' is currently being indexed. Progress: ${progressPercentage.toFixed(1)}%`;
// Add more detailed status based on progress
if (progressPercentage < 10) {
statusMessage += ' (Preparing and scanning files...)';
} else if (progressPercentage < 100) {
statusMessage += ' (Processing files and generating embeddings...)';
}
statusMessage += `\n🕐 Last updated: ${new Date(indexingInfo.lastUpdated).toLocaleString()}`;
} else {
statusMessage = `🔄 Codebase '${absolutePath}' is currently being indexed.`;
}
break;
case 'indexfailed':
if (info && 'errorMessage' in info) {
const failedInfo = info as any;
statusMessage = `❌ Codebase '${absolutePath}' indexing failed.`;
statusMessage += `\n🚨 Error: ${failedInfo.errorMessage}`;
if (failedInfo.lastAttemptedPercentage !== undefined) {
statusMessage += `\n📊 Failed at: ${failedInfo.lastAttemptedPercentage.toFixed(1)}% progress`;
}
statusMessage += `\n🕐 Failed at: ${new Date(failedInfo.lastUpdated).toLocaleString()}`;
statusMessage += `\n💡 You can retry indexing by running the index_codebase command again.`;
} else {
statusMessage = `❌ Codebase '${absolutePath}' indexing failed. You can retry indexing.`;
}
break;
case 'not_found':
default:
statusMessage = `❌ Codebase '${absolutePath}' is not indexed. Please use the index_codebase tool to index it first.`;
break;
}
const pathInfo = codebasePath !== absolutePath

View File

@@ -1,18 +1,125 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { CodebaseSnapshot } from "./config.js";
import {
CodebaseSnapshot,
CodebaseSnapshotV1,
CodebaseSnapshotV2,
CodebaseInfo,
CodebaseInfoIndexing,
CodebaseInfoIndexed,
CodebaseInfoIndexFailed
} from "./config.js";
export class SnapshotManager {
private snapshotFilePath: string;
private indexedCodebases: string[] = [];
private indexingCodebases: Map<string, number> = new Map(); // Map of codebase path to progress percentage
private codebaseFileCount: Map<string, number> = new Map(); // Map of codebase path to indexed file count
private codebaseInfoMap: Map<string, CodebaseInfo> = new Map(); // Map of codebase path to complete info
constructor() {
// Initialize snapshot file path
this.snapshotFilePath = path.join(os.homedir(), '.context', 'mcp-codebase-snapshot.json');
}
/**
* Check if snapshot is v2 format
*/
private isV2Format(snapshot: any): snapshot is CodebaseSnapshotV2 {
return snapshot && snapshot.formatVersion === 'v2';
}
/**
* Convert v1 format to internal state
*/
private loadV1Format(snapshot: CodebaseSnapshotV1): void {
console.log('[SNAPSHOT-DEBUG] Loading v1 format snapshot');
// Validate that the codebases still exist
const validCodebases: string[] = [];
for (const codebasePath of snapshot.indexedCodebases) {
if (fs.existsSync(codebasePath)) {
validCodebases.push(codebasePath);
console.log(`[SNAPSHOT-DEBUG] Validated codebase: ${codebasePath}`);
} else {
console.warn(`[SNAPSHOT-DEBUG] Codebase no longer exists, removing: ${codebasePath}`);
}
}
// Handle indexing codebases - treat them as not indexed since they were interrupted
let indexingCodebasesList: string[] = [];
if (Array.isArray(snapshot.indexingCodebases)) {
// Legacy format: string[]
indexingCodebasesList = snapshot.indexingCodebases;
console.log(`[SNAPSHOT-DEBUG] Found legacy indexingCodebases array format with ${indexingCodebasesList.length} entries`);
} else if (snapshot.indexingCodebases && typeof snapshot.indexingCodebases === 'object') {
// New format: Record<string, number>
indexingCodebasesList = Object.keys(snapshot.indexingCodebases);
console.log(`[SNAPSHOT-DEBUG] Found new indexingCodebases object format with ${indexingCodebasesList.length} entries`);
}
for (const codebasePath of indexingCodebasesList) {
if (fs.existsSync(codebasePath)) {
console.warn(`[SNAPSHOT-DEBUG] Found interrupted indexing codebase: ${codebasePath}. Treating as not indexed.`);
// Don't add to validIndexingCodebases - treat as not indexed
} else {
console.warn(`[SNAPSHOT-DEBUG] Interrupted indexing codebase no longer exists: ${codebasePath}`);
}
}
// Restore state - only fully indexed codebases
this.indexedCodebases = validCodebases;
this.indexingCodebases = new Map(); // Reset indexing codebases since they were interrupted
this.codebaseFileCount = new Map(); // No file count info in v1 format
}
/**
* Convert v2 format to internal state
*/
private loadV2Format(snapshot: CodebaseSnapshotV2): void {
console.log('[SNAPSHOT-DEBUG] Loading v2 format snapshot');
const validIndexedCodebases: string[] = [];
const validIndexingCodebases = new Map<string, number>();
const validFileCount = new Map<string, number>();
const validCodebaseInfoMap = new Map<string, CodebaseInfo>();
for (const [codebasePath, info] of Object.entries(snapshot.codebases)) {
if (!fs.existsSync(codebasePath)) {
console.warn(`[SNAPSHOT-DEBUG] Codebase no longer exists, removing: ${codebasePath}`);
continue;
}
// Store the complete info for this codebase
validCodebaseInfoMap.set(codebasePath, info);
if (info.status === 'indexed') {
validIndexedCodebases.push(codebasePath);
if ('indexedFiles' in info) {
validFileCount.set(codebasePath, info.indexedFiles);
}
console.log(`[SNAPSHOT-DEBUG] Validated indexed codebase: ${codebasePath} (${info.indexedFiles || 'unknown'} files, ${info.totalChunks || 'unknown'} chunks)`);
} else if (info.status === 'indexing') {
if ('indexingPercentage' in info) {
validIndexingCodebases.set(codebasePath, info.indexingPercentage);
}
console.warn(`[SNAPSHOT-DEBUG] Found interrupted indexing codebase: ${codebasePath} (${info.indexingPercentage || 0}%). Treating as not indexed.`);
// Don't add to indexed - treat interrupted indexing as not indexed
} else if (info.status === 'indexfailed') {
console.warn(`[SNAPSHOT-DEBUG] Found failed indexing codebase: ${codebasePath}. Error: ${info.errorMessage}`);
// Failed indexing codebases are not added to indexed or indexing lists
// But we keep the info for potential retry
}
}
// Restore state
this.indexedCodebases = validIndexedCodebases;
this.indexingCodebases = new Map(); // Reset indexing codebases since they were interrupted
this.codebaseFileCount = validFileCount;
this.codebaseInfoMap = validCodebaseInfoMap;
}
public getIndexedCodebases(): string[] {
// Read from JSON file to ensure consistency and persistence
try {
@@ -23,7 +130,14 @@ export class SnapshotManager {
const snapshotData = fs.readFileSync(this.snapshotFilePath, 'utf8');
const snapshot: CodebaseSnapshot = JSON.parse(snapshotData);
return snapshot.indexedCodebases || [];
if (this.isV2Format(snapshot)) {
return Object.entries(snapshot.codebases)
.filter(([_, info]) => info.status === 'indexed')
.map(([path, _]) => path);
} else {
// V1 format
return snapshot.indexedCodebases || [];
}
} catch (error) {
console.warn(`[SNAPSHOT-DEBUG] Error reading indexed codebases from file:`, error);
// Fallback to memory if file reading fails
@@ -41,13 +155,19 @@ export class SnapshotManager {
const snapshotData = fs.readFileSync(this.snapshotFilePath, 'utf8');
const snapshot: CodebaseSnapshot = JSON.parse(snapshotData);
// Handle both legacy array format and new object format
if (Array.isArray(snapshot.indexingCodebases)) {
// Legacy format: return the array directly
return snapshot.indexingCodebases;
} else if (snapshot.indexingCodebases && typeof snapshot.indexingCodebases === 'object') {
// New format: return the keys of the object
return Object.keys(snapshot.indexingCodebases);
if (this.isV2Format(snapshot)) {
return Object.entries(snapshot.codebases)
.filter(([_, info]) => info.status === 'indexing')
.map(([path, _]) => path);
} else {
// V1 format - Handle both legacy array format and new object format
if (Array.isArray(snapshot.indexingCodebases)) {
// Legacy format: return the array directly
return snapshot.indexingCodebases;
} else if (snapshot.indexingCodebases && typeof snapshot.indexingCodebases === 'object') {
// New format: return the keys of the object
return Object.keys(snapshot.indexingCodebases);
}
}
return [];
@@ -72,13 +192,21 @@ export class SnapshotManager {
const snapshotData = fs.readFileSync(this.snapshotFilePath, 'utf8');
const snapshot: CodebaseSnapshot = JSON.parse(snapshotData);
// Handle both legacy array format and new object format
if (Array.isArray(snapshot.indexingCodebases)) {
// Legacy format: if path exists in array, assume 0% progress
return snapshot.indexingCodebases.includes(codebasePath) ? 0 : undefined;
} else if (snapshot.indexingCodebases && typeof snapshot.indexingCodebases === 'object') {
// New format: return the actual progress percentage
return snapshot.indexingCodebases[codebasePath];
if (this.isV2Format(snapshot)) {
const info = snapshot.codebases[codebasePath];
if (info && info.status === 'indexing') {
return info.indexingPercentage || 0;
}
return undefined;
} else {
// V1 format - Handle both legacy array format and new object format
if (Array.isArray(snapshot.indexingCodebases)) {
// Legacy format: if path exists in array, assume 0% progress
return snapshot.indexingCodebases.includes(codebasePath) ? 0 : undefined;
} else if (snapshot.indexingCodebases && typeof snapshot.indexingCodebases === 'object') {
// New format: return the actual progress percentage
return snapshot.indexingCodebases[codebasePath];
}
}
return undefined;
@@ -103,19 +231,126 @@ export class SnapshotManager {
this.indexingCodebases.delete(codebasePath);
}
public addIndexedCodebase(codebasePath: string): void {
public addIndexedCodebase(codebasePath: string, fileCount?: number): void {
if (!this.indexedCodebases.includes(codebasePath)) {
this.indexedCodebases.push(codebasePath);
}
if (fileCount !== undefined) {
this.codebaseFileCount.set(codebasePath, fileCount);
}
}
public removeIndexedCodebase(codebasePath: string): void {
this.indexedCodebases = this.indexedCodebases.filter(path => path !== codebasePath);
this.codebaseFileCount.delete(codebasePath);
}
public moveFromIndexingToIndexed(codebasePath: string): void {
public moveFromIndexingToIndexed(codebasePath: string, fileCount?: number): void {
this.removeIndexingCodebase(codebasePath);
this.addIndexedCodebase(codebasePath);
this.addIndexedCodebase(codebasePath, fileCount);
}
public getIndexedFileCount(codebasePath: string): number | undefined {
return this.codebaseFileCount.get(codebasePath);
}
public setIndexedFileCount(codebasePath: string, fileCount: number): void {
this.codebaseFileCount.set(codebasePath, fileCount);
}
/**
* Set codebase to indexing status
*/
public setCodebaseIndexing(codebasePath: string, progress: number = 0): void {
this.indexingCodebases.set(codebasePath, progress);
// Remove from other states
this.indexedCodebases = this.indexedCodebases.filter(path => path !== codebasePath);
this.codebaseFileCount.delete(codebasePath);
// Update info map
const info: CodebaseInfoIndexing = {
status: 'indexing',
indexingPercentage: progress,
lastUpdated: new Date().toISOString()
};
this.codebaseInfoMap.set(codebasePath, info);
}
/**
* Set codebase to indexed status with complete statistics
*/
public setCodebaseIndexed(
codebasePath: string,
stats: { indexedFiles: number; totalChunks: number; status: 'completed' | 'limit_reached' }
): void {
// Add to indexed list if not already there
if (!this.indexedCodebases.includes(codebasePath)) {
this.indexedCodebases.push(codebasePath);
}
// Remove from indexing state
this.indexingCodebases.delete(codebasePath);
// Update file count and info
this.codebaseFileCount.set(codebasePath, stats.indexedFiles);
const info: CodebaseInfoIndexed = {
status: 'indexed',
indexedFiles: stats.indexedFiles,
totalChunks: stats.totalChunks,
indexStatus: stats.status,
lastUpdated: new Date().toISOString()
};
this.codebaseInfoMap.set(codebasePath, info);
}
/**
* Set codebase to failed status
*/
public setCodebaseIndexFailed(
codebasePath: string,
errorMessage: string,
lastAttemptedPercentage?: number
): void {
// Remove from other states
this.indexedCodebases = this.indexedCodebases.filter(path => path !== codebasePath);
this.indexingCodebases.delete(codebasePath);
this.codebaseFileCount.delete(codebasePath);
// Update info map
const info: CodebaseInfoIndexFailed = {
status: 'indexfailed',
errorMessage: errorMessage,
lastAttemptedPercentage: lastAttemptedPercentage,
lastUpdated: new Date().toISOString()
};
this.codebaseInfoMap.set(codebasePath, info);
}
/**
* Get codebase status
*/
public getCodebaseStatus(codebasePath: string): 'indexed' | 'indexing' | 'indexfailed' | 'not_found' {
const info = this.codebaseInfoMap.get(codebasePath);
if (!info) return 'not_found';
return info.status;
}
/**
* Get complete codebase information
*/
public getCodebaseInfo(codebasePath: string): CodebaseInfo | undefined {
return this.codebaseInfoMap.get(codebasePath);
}
/**
* Get all failed codebases
*/
public getFailedCodebases(): string[] {
return Array.from(this.codebaseInfoMap.entries())
.filter(([_, info]) => info.status === 'indexfailed')
.map(([path, _]) => path);
}
public loadCodebaseSnapshot(): void {
@@ -132,54 +367,14 @@ export class SnapshotManager {
console.log('[SNAPSHOT-DEBUG] Loaded snapshot:', snapshot);
// Validate that the codebases still exist
const validCodebases: string[] = [];
for (const codebasePath of snapshot.indexedCodebases) {
if (fs.existsSync(codebasePath)) {
validCodebases.push(codebasePath);
console.log(`[SNAPSHOT-DEBUG] Validated codebase: ${codebasePath}`);
} else {
console.warn(`[SNAPSHOT-DEBUG] Codebase no longer exists, removing: ${codebasePath}`);
}
if (this.isV2Format(snapshot)) {
this.loadV2Format(snapshot);
} else {
this.loadV1Format(snapshot);
}
// Handle indexing codebases - treat them as not indexed since they were interrupted
// Support both legacy array format and new object format
let indexingCodebasesList: string[] = [];
if (Array.isArray(snapshot.indexingCodebases)) {
// Legacy format: string[]
indexingCodebasesList = snapshot.indexingCodebases;
console.log(`[SNAPSHOT-DEBUG] Found legacy indexingCodebases array format with ${indexingCodebasesList.length} entries`);
} else if (snapshot.indexingCodebases && typeof snapshot.indexingCodebases === 'object') {
// New format: Record<string, number>
indexingCodebasesList = Object.keys(snapshot.indexingCodebases);
console.log(`[SNAPSHOT-DEBUG] Found new indexingCodebases object format with ${indexingCodebasesList.length} entries`);
}
for (const codebasePath of indexingCodebasesList) {
if (fs.existsSync(codebasePath)) {
console.warn(`[SNAPSHOT-DEBUG] Found interrupted indexing codebase: ${codebasePath}. Treating as not indexed.`);
// Don't add to validIndexingCodebases - treat as not indexed
} else {
console.warn(`[SNAPSHOT-DEBUG] Interrupted indexing codebase no longer exists: ${codebasePath}`);
}
}
// Restore state - only fully indexed codebases
this.indexedCodebases = validCodebases;
this.indexingCodebases = new Map(); // Reset indexing codebases since they were interrupted
console.log(`[SNAPSHOT-DEBUG] Restored ${validCodebases.length} fully indexed codebases.`);
console.log(`[SNAPSHOT-DEBUG] Reset ${indexingCodebasesList.length} interrupted indexing codebases.`);
// Save updated snapshot if we removed any invalid paths or reset indexing codebases
const originalIndexingCount = Array.isArray(snapshot.indexingCodebases)
? snapshot.indexingCodebases.length
: Object.keys(snapshot.indexingCodebases || {}).length;
if (validCodebases.length !== snapshot.indexedCodebases.length || originalIndexingCount > 0) {
this.saveCodebaseSnapshot();
}
// Always save in v2 format after loading (migration)
this.saveCodebaseSnapshot();
} catch (error: any) {
console.error('[SNAPSHOT-DEBUG] Error loading snapshot:', error);
@@ -198,20 +393,27 @@ export class SnapshotManager {
console.log('[SNAPSHOT-DEBUG] Created snapshot directory:', snapshotDir);
}
// Convert Map to object for JSON serialization
const indexingCodebasesObject: Record<string, number> = {};
this.indexingCodebases.forEach((progress, path) => {
indexingCodebasesObject[path] = progress;
});
// Build v2 format snapshot using the complete info map
const codebases: Record<string, CodebaseInfo> = {};
const snapshot: CodebaseSnapshot = {
indexedCodebases: this.indexedCodebases,
indexingCodebases: indexingCodebasesObject,
// Add all codebases from the info map
for (const [codebasePath, info] of this.codebaseInfoMap) {
codebases[codebasePath] = info;
}
const snapshot: CodebaseSnapshotV2 = {
formatVersion: 'v2',
codebases: codebases,
lastUpdated: new Date().toISOString()
};
fs.writeFileSync(this.snapshotFilePath, JSON.stringify(snapshot, null, 2));
console.log('[SNAPSHOT-DEBUG] Snapshot saved successfully. Indexed codebases:', this.indexedCodebases.length, 'Indexing codebases:', this.indexingCodebases.size);
const indexedCount = this.indexedCodebases.length;
const indexingCount = this.indexingCodebases.size;
const failedCount = this.getFailedCodebases().length;
console.log(`[SNAPSHOT-DEBUG] Snapshot saved successfully in v2 format. Indexed: ${indexedCount}, Indexing: ${indexingCount}, Failed: ${failedCount}`);
} catch (error: any) {
console.error('[SNAPSHOT-DEBUG] Error saving snapshot:', error);