Skip to content

StateManager:VSCode 状態の永続化とマイグレーション

源码版本v4.0.10

役割

StateManager は Cline が Extension 起動時に構築するシングルトンで、VSCode ネイティブの globalState / workspaceState / secrets の 3 種ストレージを統合して 1 層のメモリキャッシュとして受け持つ。すべての読み取りは直接メモリから行われ、書き込みはまずキャッシュに入れた後 500ms の debounce で非同期バッチで底层ストレージに書き戻す。狙いは上位モジュール (API 設定、task history、workspace 状態、リモート設定) が読み書きする際に「globalState か workspaceState か secret か」を気にしなくてよいようにすること、そして連続書き込みを 1 回の setBatch にマージしてディスクに落とすことである。

アーキテクチャ上の位置は非常に低位で、ほぼすべてのサービスが StateManager.get() でこのシングルトンを取得する。Extension 起動時に initialize がファイルから全状態を読み出し (initialize:128)、キャッシュを充填し、taskHistory ファイル watcher を起動 (setupTaskHistoryWatcher:541) してから他モジュールの利用を許可する。状態バージョンの進化は state-migrations.ts の一連のワンショットマイグレーション関数が処理する (migrateWorkspaceToGlobalStorage:8)。

設計動機

  • シングルトン + 即時読み取り:static get() でメモリキャッシュを取得後、同期読み取り (get instance:163)。毎回 VSCode の非同期 storage API を await するのを避ける。
  • 500ms debounce でディスク書き込み:連続書き込みを 1 回の persistPendingState にマージ (scheduleDebouncedPersistence:791)、ディスク IO を減らす。
  • バケット別キャッシュ:globalState、taskState (per-task workspace settings)、secrets、workspaceState、remoteConfig の 5 つの独立キャッシュ (caches:61)。
  • taskHistory は独立ファイル:task history はデータ量が膨らみやすいため、globalState から切り離して独立ファイルに (taskHistory routing:821)。
  • ファイル watcher でループを防止:外部エディタが task history ファイルを編集した時はキャッシュを更新するだけで、書き戻しを発火しない (syncTaskHistoryFromDisk:558)。
  • マイグレーションは集中かつ冪等:各マイグレーション関数は「既に移行済みか」を先にチェック (early return if empty:79) し、lastShownAnnouncementId を全体マイグレーションマーカーとして使う (hasMigrated check:733)。
  • flushPendingState で強制フラッシュ:重要パス (task 切り替え、Extension 停止など) は debounce をバイパスして即時ディスクに落とせる (flushPendingState:777)。
  • model info cache は独立:動的 provider が models リストを取得した結果は 1 時間 TTL のメモリキャッシュに格納 (MODEL_CACHE_TTL_MS:76) され、永続化層には入らない。

主要ファイル

データフロー

起動時、Extension はまずマイグレーションを実行してから StateManager を initialize する。マイグレーションは lastShownAnnouncementId で冪等ゲートを作り、移行済みならそのまま return する:

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}` : ""))
  }
}

この処理は cleanupLegacyVSCodeStorage:729 付近にある。マイグレーション完了後、StateManager.initialize がファイルの内容をキャッシュへロードする。実行時の 1 書き込みパスはこうなる:

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()
  // ...
}

persistGlobalStateBatchtaskHistory を個別に writeTaskHistoryToState にルーティングし (taskHistory routing:823)、残りの key は setBatch で一括書き込みする。外部から taskHistory ファイルが編集された時、watcher は onDisk データを取得し、キャッシュが空なら置換、非空ならメモリ版を保持して刚写入更新の上書きを回避する (onDisk sync:564)。

境界と失敗

  • 重複 initialize は例外:isInitialized が既に true の時は直接 throw し、重複構築を防止 (already initialized:133)。
  • マイグレーション失敗は起動をブロックしない:各 migrate* 関数は try/catch し、失敗時は log して次へ進む (continue on failure:188)。
  • taskHistory 書き込み後検証:writeTaskHistoryToState は書き込み後にもう一度読み戻して長さを比較し、不一致なら error を log するが旧 globalState はクリアしない (write verification:109)。
  • persistence error コールバック:onPersistenceError で上位がエラーリカバリ戦略を決定 (onPersistenceError:116)。
  • ファイル watcher は 300ms 間自身の更新をスキップ:taskHistory 書き込み後の短い窓では外部 file change を無視し、ループを回避 (unlink handler:577)。
  • reInitialize はキャッシュをクリア:reInitialize はまずキャッシュを空にしてから再読み込み (clear caches:733)。この間の読み取りはすべて空値になる。
  • migrateWorkspaceToGlobalStorage は未定義のもののみ移行:workspaceValue が存在し globalValue が未定義の場合のみ移行 (conditional migrate:56)。新位置でのユーザー手動変更を上書きしないため。
  • secrets は独立バッチ:API key のような機密データは persistSecretsBatch を経由し、通常 global state と混ぜてフラッシュしない (persistSecretsBatch:864)。

まとめ

StateManager は Cline が VSCode の 3 種ストレージを 1 層キャッシュ + debounce 書き込みに統合した底层モジュールで、マイグレーションロジックは state-migrations.ts に集中している。MCP サーバー設定がどう StateManager を経由するかは mcp/mcp-hub、コンテキスト圧縮関連の task ディレクトリ読み書きは mcp/context-manager、hook 実行がどう StateManager とやり取りするかは hooks を参照のこと。

公式資料: Cline 文档 · README