StateManager:VSCode 状態の永続化とマイグレーション
役割
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) され、永続化層には入らない。
主要ファイル
StateManager class:58— シングルトン。5 つのキャッシュバケット + ファイル watcher + model cache を保持。initialize:128— 非同期でファイルから globalState / secrets / workspaceState を読み込みキャッシュを充填。readGlobalStateFromStorage:141— disk 層を呼んで全 globalState を読み込み。get instance:163— 同期でシングルトンを取得。キャッシュ準備済み。setupTaskHistoryWatcher:541— chokidar でtaskHistory.jsonを監視。外部変更をキャッシュへ同期。persistPendingState:747— 4 種の pending sets を並列フラッシュ。persistGlobalStateBatch:816— globalState バッチフラッシュ。taskHistory は独立ファイルへルーティング。persistTaskStateBatch:838— 各 taskId ごとに settings key をグループ化してバッチフラッシュ。flushPendingState:777— 即時フラッシュエントリ。debounce をバイパス。reInitialize:698— キャッシュをリセットしてからディスクを再読み込み。ホットリロードシナリオ用。migrateWorkspaceToGlobalStorage:8— workspace storage 内の API 設定 key を globalState に戻す。migrateTaskHistoryToFile:70— taskHistory を globalState から独立 JSON ファイルへ移行。migrateCustomInstructionsToGlobalRules:147— 旧custom_instructions.mdをグローバル Cline rules ディレクトリに統合。migrateWelcomeViewCompleted:561— 既存 API key から welcomeViewCompleted マーカーを逆推。cleanupLegacyVSCodeStorage:729— Extension 起動時に全マイグレーションを実行するエントリ。
データフロー
起動時、Extension はまずマイグレーションを実行してから StateManager を initialize する。マイグレーションは lastShownAnnouncementId で冪等ゲートを作り、移行済みならそのまま return する:
// 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 書き込みパスはこうなる:
// 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 は taskHistory を個別に 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 を参照のこと。