StateManager: persistencia y migración del estado de VSCode
Responsabilidades
StateManager es un singleton que Cline construye al arrancar la extensión; unifica los tres almacenes nativos de VSCode —globalState, workspaceState y secrets— en una capa de caché en memoria. Todas las lecturas van directo a memoria; las escrituras actualizan primero la caché y luego se vuelcan al almacenamiento subyacente de forma asíncrona con un debounce de 500 ms. Su objetivo es que los módulos superiores (configuración de API, historial de tareas, estado del workspace, configuración remota) ya no tengan que preocuparse por «si es globalState, workspaceState o secret» y que fusionen múltiples escrituras consecutivas en una sola llamada setBatch a disco.
Su posición en la arquitectura es muy baja: casi todos los servicios obtienen este singleton a través de StateManager.get(). Al arrancar la extensión, initialize lee todo el estado desde archivo (initialize:128), rellena la caché, arranca el watcher del archivo taskHistory (setupTaskHistoryWatcher:541) y solo entonces permite que otros módulos lo usen. La evolución de versiones del estado se confía a un conjunto de funciones de migración one-shot en state-migrations.ts (migrateWorkspaceToGlobalStorage:8).
Motivación de diseño
- Singleton + lectura inmediata:
static get()obtiene la caché en memoria y lee de forma síncrona (get instance:163), evitando await en cada llamada a la API asíncrona de VSCode. - Debounce de 500 ms al volcar: múltiples escrituras consecutivas se fusionan en un único
persistPendingState(scheduleDebouncedPersistence:791), reduciendo IO en disco. - Caché por buckets: cinco cachés independientes —globalState, taskState (settings del workspace por tarea), secrets, workspaceState, remoteConfig— (
caches:61). - taskHistory en archivo aparte: los datos del historial de tareas tienden a crecer, por lo que se separan de globalState a un archivo propio (
taskHistory routing:821). - Watcher de archivo anti-bucle: cuando el archivo taskHistory se edita externamente, solo se actualiza la caché, sin disparar escritura de vuelta (
syncTaskHistoryFromDisk:558). - Migraciones centralizadas e idempotentes: cada función de migración comprueba primero «si ya se migró» (
early return if empty:79), usandolastShownAnnouncementIdcomo marca global de migración (hasMigrated check:733). - flushPendingState para volcado forzado: las rutas críticas (cambio de tarea, desactivación de la extensión) pueden saltarse el debounce y volcar a disco al instante (
flushPendingState:777). - Caché de model info independiente: los resultados de listar modelos de providers dinámicos se guardan en una caché en memoria con TTL de 1 hora (
MODEL_CACHE_TTL_MS:76), sin entrar en la capa de persistencia.
Archivos clave
StateManager class:58— singleton, mantiene cinco buckets de caché + watcher + model cache.initialize:128— lee globalState / secrets / workspaceState desde archivo y rellena la caché.readGlobalStateFromStorage:141— llama a la capa de disco para leer todo el globalState.get instance:163— obtiene el singleton de forma síncrona con la caché lista.setupTaskHistoryWatcher:541— chokidar escuchataskHistory.jsony sincroniza cambios externos a la caché.persistPendingState:747— vuelca en paralelo los cuatro conjuntos pendientes.persistGlobalStateBatch:816— vuelca globalState por lotes; taskHistory se enruta a un archivo aparte.persistTaskStateBatch:838— vuelca por lotes un conjunto de keys de settings por taskId.flushPendingState:777— entrada de volcado inmediato, saltándose el debounce.reInitialize:698— vacía la caché y vuelve a leer desde disco, para escenarios de hot reload.migrateWorkspaceToGlobalStorage:8— traslada las keys de configuración de API del workspace storage de vuelta a globalState.migrateTaskHistoryToFile:70— mueve taskHistory de globalState a un archivo JSON aparte.migrateCustomInstructionsToGlobalRules:147— fusiona el viejocustom_instructions.mden el directorio global de Cline rules.migrateWelcomeViewCompleted:561— deduce la marca welcomeViewCompleted a partir de API keys existentes.cleanupLegacyVSCodeStorage:729— entrada que ejecuta todas las migraciones al arrancar la extensión.
Flujo de datos
Al arrancar, la extensión primero ejecuta las migraciones y luego inicializa StateManager. La migración usa lastShownAnnouncementId como compuerta idempotente: si ya se migró, hace return directamente:
// apps/vscode/src/extension.ts
async function cleanupLegacyVSCodeStorage(context: ExtensionContext): Promise<void> {
try {
await cleanupOldApiKey(context)
// Migrate is not done if the new storage does not have the lastShownAnnouncementId flag
const hasMigrated = context.globalState.get("lastShownAnnouncementId")
if (hasMigrated !== undefined) {
return
}
Logger.info("[VS Code Storage Migrations] Starting")
// Migrate custom instructions to global Cline rules (one-time cleanup)
await migrateCustomInstructionsToGlobalRules(context)
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
await migrateWelcomeViewCompleted(context)
// Migrate workspace storage values back to global storage (reverting previous migration)
await migrateWorkspaceToGlobalStorage(context)
// Ensure taskHistory.json exists and migrate legacy state (runs once)
await migrateTaskHistoryToFile(context)
// Clean up MCP marketplace catalog from global state (moved to disk cache)
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
// ...
} catch (error) {
Logger.warn("[VS Code Storage Migrations] Failed" + (error instanceof Error ? `: ${error.message}` : ""))
}
}Esto está cerca de cleanupLegacyVSCodeStorage:729. Tras las migraciones, StateManager.initialize carga el contenido del archivo en la caché. En runtime, una ruta de escritura es así:
// apps/vscode/src/core/storage/StateManager.ts
// llamador escribe valor
this.globalStateCache[key] = value
// marca como pendiente
this.pendingGlobalState.add(key)
// programa volcado por lotes en 500 ms
this.scheduleDebouncedPersistence()
// al expirar:
private async persistPendingState(): Promise<void> {
if (this.pendingGlobalState.size === 0 && /* ... */) {
return
}
await Promise.all([
this.persistGlobalStateBatch(this.pendingGlobalState),
this.persistSecretsBatch(this.pendingSecrets),
this.persistWorkspaceStateBatch(this.pendingWorkspaceState),
this.persistTaskStateBatch(this.pendingTaskState),
])
this.pendingGlobalState.clear()
// ...
}persistGlobalStateBatch enruta taskHistory a writeTaskHistoryToState (taskHistory routing:823); el resto de keys se escribe en una sola llamada setBatch. Al editar externamente el archivo taskHistory, el watcher toma los datos onDisk: si la caché está vacía los sustituye, y si no lo está conserva la versión en memoria para no sobrescribir actualizaciones recién escritas (onDisk sync:564).
Límites y fallos
initializeduplicado lanza error: siisInitializedya es true, lanza directamente, para evitar construir dos veces (already initialized:133).- Las migraciones fallidas no bloquean el arranque: cada
migrate*se envuelve en try/catch, en caso de fallo solo loguea y continúa (continue on failure:188). - Verificación tras escribir taskHistory:
writeTaskHistoryToStaterelee el archivo tras escribir y compara la longitud; si no coincide, loguea error y no limpia el globalState viejo (write verification:109). - Callback de error de persistencia:
onPersistenceErrordeja que la capa superior decida la estrategia de recuperación (onPersistenceError:116). - Watcher de archivo ignora 300 ms tras escritura propia: tras escribir taskHistory, los cambios externos en una ventana breve se ignoran para evitar bucles (
unlink handler:577). reInitializevacía la caché:reInitializeprimero vacía la caché y luego relee (clear caches:733); durante ese intervalo cualquier lectura obtiene un valor vacío.- migrateWorkspaceToGlobalStorage solo traslada lo no definido: solo migra si workspaceValue existe y globalValue no está definido (
conditional migrate:56), para no sobrescribir ediciones manuales del usuario en la nueva ubicación. - secrets en lote aparte: los datos sensibles tipo API keys pasan por
persistSecretsBatch, sin mezclarse con el globalState normal (persistSecretsBatch:864).
Resumen
StateManager es el módulo subyacente de Cline que unifica los tres almacenes de VSCode en una capa de caché + debounce; la lógica de migración se concentra toda en state-migrations.ts. Para ver cómo la configuración del servidor MCP pasa por StateManager, lee mcp/mcp-hub; para ver la lectura/escritura del directorio de tareas relacionada con la compresión de contexto, lee mcp/context-manager; para ver cómo la ejecución de hooks interactúa con StateManager, lee hooks.