StateManager: VSCode-Zustandspersistenz und Migration
Verantwortung
StateManager ist ein Singleton, das Cline beim Start der Erweiterung (Extension) aufbaut; es vereinheitlicht die drei nativen VSCode-Speicher globalState / workspaceState / secrets hinter einer In-Memory-Cache-Schicht. Alle Lesezugriffe erfolgen direkt gegen den Speicher, Schreibzugriffe werden zunächst im Cache aktualisiert und per 500 ms Debounce asynchron und gebündelt in den zugrundeliegenden Speicher zurückgeschrieben. Ziel: Obere Module (API-Konfiguration, Task-Historie, Workspace-Zustand, Remote-Konfiguration) sollen beim Lesen und Schreiben nicht mehr zwischen globalState, workspaceState oder secret unterscheiden müssen; mehrere aufeinanderfolgende Schreibvorgänge werden zu einem einzigen setBatch zusammengefasst.
Seine Position in der Architektur ist sehr weit unten – fast jeder Dienst bezieht über StateManager.get() das Singleton. Beim Start der Erweiterung liest initialize den gesamten Zustand aus den Dateien (initialize:128), befüllt den Cache, startet den File-Watcher für taskHistory (setupTaskHistoryWatcher:541) und gibt danach den anderen Modulen frei. Zustandsversionen werden durch eine Gruppe einmaliger Migrationsfunktionen in state-migrations.ts behandelt (migrateWorkspaceToGlobalStorage:8).
Entwurfsmotivation
- Singleton + sofortiges Lesen:
static get()liefert den Cache, aus dem synchron gelesen wird (get instance:163), damit nicht jeder Zugriff die asynchrone VSCode-Storage-API abwarten muss. - 500 ms Debounce beim Zurückschreiben: Mehrere aufeinanderfolgende Schreibvorgänge werden zu einem
persistPendingStatezusammengefasst (scheduleDebouncedPersistence:791), was die Disk-IO reduziert. - Getrennte Cache-Buckets: globalState, taskState (per-Task Workspace-Settings), secrets, workspaceState und remoteConfig als fünf unabhängige Caches (
caches:61). - taskHistory in eigener Datei: Die Task-Historie kann stark anwachsen; sie ist aus globalState in eine eigenständige Datei ausgelagert (
taskHistory routing:821). - File-Watcher gegen Rückkopplung: Wenn die Task-History-Datei extern bearbeitet wird, wird nur der Cache aktualisiert, ohne einen Zurückschreibvorgang auszulösen (
syncTaskHistoryFromDisk:558). - Migrationen zentral und idempotent: Jede Migrationsfunktion prüft zuerst, ob sie bereits gelaufen ist (
early return if empty:79);lastShownAnnouncementId` dient als Gesamt-Migrationsmarker (hasMigrated check:733). - flushPendingState erzwingt Zurückschreiben: Kritische Pfade (etwa Task-Wechsel, Deaktivieren der Extension) können den Debounce umgehen und sofort zurückschreiben (
flushPendingState:777). - model-info-Cache getrennt: Die von dynamischen Providern gezogenen Modelllisten werden in einem Speicher-Cache mit 1-Stunden-TTL gehalten (
MODEL_CACHE_TTL_MS:76), der nicht in die Persistenzschicht eingeht.
Schlüsseldateien
StateManager class:58— Singleton; hält fünf Cache-Buckets, den File-Watcher und den Model-Cache.initialize:128— Liest globalState / secrets / workspaceState asynchron aus Dateien in den Cache.readGlobalStateFromStorage:141— Ruft die Disk-Schicht auf, um den gesamten globalState zu lesen.get instance:163— Liefert synchron das Singleton; der Cache ist bereit.setupTaskHistoryWatcher:541— chokidar überwachttaskHistory.json; externe Änderungen werden in den Cache synchronisiert.persistPendingState:747— Parallelisierter Vierfach-Flush der pending-Sets.persistGlobalStateBatch:816— Batch-Flush des globalState; taskHistory wird in eine eigene Datei geroutet.persistTaskStateBatch:838— Pro taskId wird ein Satz Settings-Keys gebatcht.flushPendingState:777— Sofortiger Flush-Einstieg, umgeht den Debounce.reInitialize:698— Setzt den Cache zurück und liest danach neu von der Disk; für Hot-Reload-Szenarien.migrateWorkspaceToGlobalStorage:8— Überführt API-Konfigurations-Keys aus dem Workspace-Storage zurück in den globalState.migrateTaskHistoryToFile:70— Verlegt taskHistory aus globalState in eine eigenständige JSON-Datei.migrateCustomInstructionsToGlobalRules:147— Altecustom_instructions.mdwird ins globale Cline-rules-Verzeichnis überführt.migrateWelcomeViewCompleted:561— Setzt den Marker welcomeViewCompleted zurück, basierend auf existierenden API-Keys.cleanupLegacyVSCodeStorage:729— Einstieg beim Extension-Start, der sämtliche Migrationen ausführt.
Datenfluss
Beim Extension-Start werden erst Migrationen, dann StateManager.initialize ausgeführt. Die Migration nutzt lastShownAnnouncementId als idempotente Schleuse; ist sie bereits gelaufen, erfolgt ein direkter 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}` : ""))
}
}Diese Stelle liegt in der Nähe von cleanupLegacyVSCodeStorage:729. Nach Abschluss der Migrationen lädt StateManager.initialize die Inhalte der Dateien in den Cache. Der Pfad eines Schreibvorgangs zur Laufzeit sieht so aus:
// apps/vscode/src/core/storage/StateManager.ts
// Aufrufer schreibt Wert
this.globalStateCache[key] = value
// als pending markieren
this.pendingGlobalState.add(key)
// 500 ms später gebündelt zurückschreiben
this.scheduleDebouncedPersistence()
// bei Ablauf:
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 routet taskHistory separat nach writeTaskHistoryToState (taskHistory routing:823), die übrigen Keys werden über setBatch in einem Aufruf geschrieben. Wird die taskHistory-Datei extern bearbeitet, holt der Watcher die onDisk-Daten und ersetzt sie im Cache, falls dieser leer ist; ist der Cache nicht leer, wird die In-Memory-Version behalten, um soeben geschriebene Aktualisierungen nicht zu überschreiben (onDisk sync:564`).
Grenzen und Fehler
- Doppeltes initialize wirft: Ist
isInitializedbereits true, wird direkt geworfen, um eine doppelte Konstruktion zu verhindern (already initialized:133). - Migrationsfehler blockieren den Start nicht: Jede
migrate*-Funktion ist in try/catch eingebettet; bei Misserfolg wird nur geloggt und weitergelaufen (continue on failure:188). - taskHistory wird nach dem Schreiben verifiziert:
writeTaskHistoryToStateliest nach dem Schreiben die Länge zurück und vergleicht; bei Abweichung wird ein Fehler geloggt, ohne den alten globalState zu löschen (write verification:109). - persistence-error-Callback:
onPersistenceErrorlässt der obere Schicht die Fehlerwiederherstellungsstrategie wählen (onPersistenceError:116). - File-Watcher ignoriert 300 ms lang eigene Aktualisierungen: Nach dem Schreiben der taskHistory werden externe Dateiänderungen in einem kurzen Fenster ignoriert, um Rückkopplungen zu vermeiden (
unlink handler:577). - reInitialize leert den Cache:
reInitializesetzt den Cache zuerst auf leer und liest danach neu ein (clear caches:733), sodass jeder Lesezugriff in diesem Zeitraum leer ausfällt. - migrateWorkspaceToGlobalStorage überführt nur Undefiniertes: Nur wenn workspaceValue existiert und globalValue undefined ist, wird migriert (
conditional migrate:56)`, um manuelle Änderungen des Benutzers am neuen Ort nicht zu überschreiben. - secrets in separatem Batch: Sensible Daten wie API-Keys gehen durch
persistSecretsBatchund werden nicht zusammen mit dem normalen globalState geflushed (persistSecretsBatch:864).
Zusammenfassung
StateManager ist das Modul, das die drei VSCode-Speicher zu einer einzigen Cache-Schicht mit Debounce-Zurückschreiben vereinigt; die Migrationslogik ist vollständig in state-migrations.ts zentralisiert. Wie MCP-Server-Konfigurationen über den StateManager laufen, steht in mcp/mcp-hub; wie Task-Verzeichnisse für die kontextbezogene Komprimierung gelesen und geschrieben werden, in mcp/context-manager; wie die Hook-Ausführung mit dem StateManager interagiert, in hooks.
Siehe offizielle Dokumentation: Cline-Dokumentation · README