Skip to content

StateManager : persistance et migration d'état VSCode

源码版本v4.0.10

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). lastShownAnnouncementId sert 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

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 :

typescript
// 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 :

typescript
// 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 isInitialized est 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 : writeTaskHistoryToState relit après écriture et compare les longueurs ; si incohérence, on log une error sans nettoyer l'ancien globalState (write verification:109).
  • Callback onPersistenceError : onPersistenceError laisse 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 : reInitialize met 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.