StateManager : persistance et migration d'état VSCode
Responsabilités
StateManager est le singleton que Cline construit au démarrage de l'Extension. Il prend en charge les trois stockages natifs VSCode — globalState / workspaceState / secrets — et les unifie derrière une couche de cache mémoire. Toute lecture passe directement par la mémoire ; toute écriture entre d'abord dans le cache puis est flushée de façon asynchrone vers le stockage sous-jacent avec un debounce de 500 ms. Son objectif est que les modules supérieurs (configuration API, task history, état workspace, configuration distante) n'aient plus à se soucier de « globalState ou workspaceState ou secret » lors de leurs lectures/écritures, et que plusieurs écritures consécutives soient fusionnées en un seul setBatch lors de la persistance.
Sa place dans l'architecture est très basse — presque tous les services récupèrent le singleton via StateManager.get(). Au démarrage de l'Extension, initialize lit l'intégralité de l'état depuis le fichier (initialize:128), remplit le cache, lance le watcher de fichiers de taskHistory (setupTaskHistoryWatcher:541), puis seulement autorise les autres modules à l'utiliser. L'évolution de la version d'état est confiée à un ensemble de fonctions de migration à usage unique dans state-migrations.ts (migrateWorkspaceToGlobalStorage:8).
Motivation de conception
- Singleton + lecture immédiate :
static get()récupère le singleton et lit synchro dans le cache mémoire (get instance:163), pour éviter d'attendre l'API asynchrone de VSCode à chaque fois. - Debounce 500 ms pour la persistance : plusieurs écritures consécutives sont fusionnées en un seul
persistPendingState(scheduleDebouncedPersistence:791), ce qui réduit les IO disque. - Cache segmenté par bucket : cinq caches indépendants pour globalState, taskState (settings workspace par task), secrets, workspaceState, remoteConfig (
caches:61). - taskHistory dans un fichier séparé : le volume de taskHistory grossit facilement, on l'a donc sorti de globalState vers un fichier dédié (
taskHistory routing:821). - File watcher anti-bouclage : quand le fichier taskHistory est édité depuis l'extérieur, on ne met à jour que le cache sans déclencher de réécriture (
syncTaskHistoryFromDisk:558). - Migrations centralisées et idempotentes : chaque fonction de migration vérifie d'abord « déjà migré ? » (
early return if empty:79).lastShownAnnouncementIdsert de marqueur global de migration (hasMigrated check:733). - flushPendingState force la persistance : les chemins critiques (changement de tâche, arrêt de l'Extension) peuvent contourner le debounce et persister immédiatement (
flushPendingState:777). - Cache model info indépendant : les listes de models tirées par les providers dynamiques sont stockées dans un cache mémoire à TTL d'1 heure (
MODEL_CACHE_TTL_MS:76), hors couche de persistance.
Fichiers clés
StateManager class:58— singleton, porte cinq buckets de cache + file watcher + model cache.initialize:128— lit asynchrone depuis le fichier globalState / secrets / workspaceState pour remplir le cache.readGlobalStateFromStorage:141— appelle la couche disk pour lire tout le globalState.get instance:163— récupère synchro le singleton, cache prêt.setupTaskHistoryWatcher:541— chokidar écoutetaskHistory.json, les modifications externes sont synchronisées dans le cache.persistPendingState:747— flush en parallèle les quatre ensembles pending.persistGlobalStateBatch:816— flush batch du globalState, taskHistory est routé vers un fichier dédié.persistTaskStateBatch:838— flush batch par taskId, un groupe de settings keys par taskId.flushPendingState:777— entrée de flush immédiat, contourne le debounce.reInitialize:698— réinitialise le cache puis relit depuis le disque, pour les scénarios de hot reload.migrateWorkspaceToGlobalStorage:8— ramène les clés de configuration API du workspace storage vers globalState.migrateTaskHistoryToFile:70— migre taskHistory de globalState vers un fichier JSON dédié.migrateCustomInstructionsToGlobalRules:147— fusionne l'anciencustom_instructions.mddans le répertoire global des Cline rules.migrateWelcomeViewCompleted:561— déduit le marqueur welcomeViewCompleted à partir des API key existantes.cleanupLegacyVSCodeStorage:729— entrée qui exécute toutes les migrations au démarrage de l'Extension.
Flux de données
Au démarrage, l'Extension exécute d'abord les migrations puis initialize le StateManager. Les migrations utilisent lastShownAnnouncementId comme garde idempotent : si déjà migré, on retourne directement :
// 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}` : ""))
}
}Ce bloc se trouve près de cleanupLegacyVSCodeStorage:729. Une fois les migrations terminées, StateManager.initialize charge le contenu des fichiers dans le cache. Au runtime, le chemin d'une écriture est le suivant :
// apps/vscode/src/core/storage/StateManager.ts
// 调用方写值
this.globalStateCache[key] = value
// 标记 pending
this.pendingGlobalState.add(key)
// 安排 500ms 后批刷
this.scheduleDebouncedPersistence()
// 到点后:
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 route taskHistory séparément vers writeTaskHistoryToState (taskHistory routing:823) ; les autres clés passent par setBatch en une seule écriture. Quand le fichier taskHistory est édité depuis l'extérieur, le watcher récupère les données onDisk : si le cache est vide, il les remplace ; s'il n'est pas vide, il conserve la version mémoire pour éviter d'écraser des mises à jour qu'on vient juste d'écrire (onDisk sync:564).
Limites et échecs
- initialize dupliqué lève une erreur : si
isInitializedest déjà true, on throw directement pour empêcher une double construction (already initialized:133). - Échec de migration non bloquant : chaque fonction
migrate*est enveloppée de try/catch. En cas d'échec, on se contente de loguer et on continue (continue on failure:188). - Vérification après écriture de taskHistory :
writeTaskHistoryToStaterelit après écriture et compare les longueurs ; si incohérence, on log une error sans nettoyer l'ancien globalState (write verification:109). - Callback onPersistenceError :
onPersistenceErrorlaisse la couche supérieure décider de la stratégie de reprise (onPersistenceError:116). - File watcher : fenêtre 300 ms pour ignorer ses propres écritures : après avoir écrit taskHistory, une fenêtre brève ignore les file change externes pendant un court laps afin d'éviter le bouclage (
unlink handler:577). - reInitialize vide le cache :
reInitializemet d'abord le cache à vide puis relit (clear caches:733). Pendant ce temps, toute lecture renvoie une valeur vide. - migrateWorkspaceToGlobalStorage ne déplace que ce qui n'est pas défini : on ne migre que si workspaceValue existe et globalValue n'est pas défini (
conditional migrate:56), pour éviter d'écraser les modifications manuelles de l'utilisateur dans la nouvelle position. - Secrets traités à part : les données sensibles du type API key passent par
persistSecretsBatch, jamais mélangées avec le global state ordinaire (persistSecretsBatch:864).
Résumé
StateManager est le module de base qui unifie les trois stockages VSCode en une couche de cache + debounce de persistance. La logique de migration est entièrement centralisée dans state-migrations.ts. Pour voir comment la configuration des serveurs MCP passe par StateManager, lire mcp/mcp-hub ; pour les lectures/écritures du répertoire de task liées à la compression de contexte, lire mcp/context-manager ; pour les interactions entre l'exécution des hooks et StateManager, lire hooks.
Voir la documentation officielle : documentation Cline · README.