Checkpoints: Schatten-Git für die Aufgaben-Wiederholung
Verantwortung
Checkpoints sind die «Zeitmaschine», die Cline einer Task (Aufgabe) zur Seite stellt. Bevor ein LLM etwas in die Hand nimmt, legt Cline einen Snapshot (Schatten-Git-Commit) des aktuellen Arbeitsbereichs an; wenn ein Benutzer mit dem Ergebnis eines bestimmten Werkzeugaufrufs unzufrieden ist, kann er Code, Dateien oder sogar ganze Gesprächsverläufe auf einen beliebigen Snapshot zurückrollen und von dort aus weiter conversieren.
Der gesamte Mechanismus baut auf einem Schatten-Git-Repository (shadow git) auf: nicht das .git im Arbeitsbereich des Benutzers, sondern ein weiteres Git-Repository, das Cline heimlich in seinem globalStorage pflegt und dessen core.worktree auf das Arbeitsverzeichnis des Benutzers zeigt. Jeder Commit ist ein Checkpoint, ein git reset --hard vollzieht die Wiederherstellung. So lassen sich die diff-/stage-/commit-Fähigkeiten von Git wiederverwenden, ohne den eigenen Repository-Zustand des Benutzers zu verschmutzen.
Nach aussen hin sichtbar ist die TaskCheckpointManager-Klasse, die die Geschäftslogik kapselt: «Soll ein Snapshot angelegt werden, an welche Nachricht wird der Commit-Hash geheftet und wie weit wird der Gesprächsverlauf nach einer Wiederherstellung abgeschnitten?» Die eigentliche Git-Arbeit erledigen darunter CheckpointTracker und GitOperations.
Entwurfsmotivation
- Das
.gitdes Benutzers darf nicht angetastet werden: Der Arbeitsbereich des Benutzers kann bereits zu einem anderen Git-Repository gehören; ein direktesgit add .würde dessen Index verschmutzen. Das Schatten-Git nutzt ein unabhängiges.git-Verzeichnis und zeigt mitcore.worktreedorthin zurück; die beiden Repositories bleiben vollständig isoliert. - Jeder Werkzeugaufruf muss rückgängig machbar sein: Dateibearbeitungen durch LLMs sind unvorhersehbar; der Benutzer braucht Vorher-/Nachher-Vergleiche und muss zu jedem beliebigen Schritt zurückspringen können. Deshalb muss das Commit-Tempo genau an den Werkzeugausführungsgrenzen ansetzen.
- Den Nachrichtenverlauf nicht mit zurückrollen: Es gibt drei Arten der Wiederherstellung –
workspacebewegt nur Dateien,tasknur die Konversationsnachrichten,taskAndWorkspacebewegt beides. So kann der Benutzer entweder «das Gespräch behalten, aber den Code zurücksetzen» oder «das Gespräch mit zurückrollen und von vorne beginnen». - Multi-Root-Arbeitsbereiche müssen separat behandelt werden: Ein Workspace kann mehrere Verzeichnisse mounten; ein einzelnes Schatten-Git reicht nicht aus, daher gibt es aussen einen
MultiRootCheckpointManagerals Verteiler. - Verschachtelte
.git-Verzeichnisse legengit addlahm: Git behandelt.gitin Unterverzeichnissen standardmässig als Submodul; das Schatten-Git muss vor jedemadddie verschachtelten.git-Verzeichnisse temporär umbenennen und danach wiederherstellen. - Geschützte Verzeichnisse werden abgewiesen: home / Desktop / Documents / Downloads sind zu gross und würden eine Menge irrelevanter Dateien erfassen; sie werden in
validateWorkspacePathdirekt abgewiesen.
Schlüsseldateien
class CheckpointTracker:50— Der Schatten-Git-Controller für eine einzelne Task; hälttaskIdundcwdHash.commit:212— Holt die Sperre →git add .→git commit --allow-empty --no-verify; liefert den Commit-Hash zurück.resetHead:336—git reset --hard <hash>setzt den Arbeitsbereich auf den angegebenen Checkpoint zurück.getDiffSet:397— Datei-Diffs zwischen zwei Commits (oder Commit ↔ Arbeitsbereich) inklusive Vorher-/Nachher-Inhalt.initShadowGit:59— Legt beim ersten Mal das Schatten-Git-Repository an und konfiguriertcore.worktree/user.email/ excludes.renameNestedGitRepos:148— Deaktiviert und stellt verschachtelte.git-Verzeichnisse wieder her, um das Submodul-Problem zu umgehen.addCheckpointFiles:203— Verschachtelte Git-Verzeichnisse deaktivieren →git add . --ignore-errors→ wiederherstellen.getShadowGitPath:20— Pfad des Schatten-Git:globalStorage/checkpoints/{cwdHash}/.git.hashWorkingDir:103— Hasht den Arbeitsverzeichnispfad zu einer 13-stelligen Zahl, die als Verzeichnisname des Schatten-Git dient.saveCheckpoint:118— Geschäftseinstieg; entscheidet ob ein Commit erstellt wird und heftet den Hash an die entsprechende Nachricht.restoreCheckpoint:238— Sucht den Checkpoint permessageTsund verzweigt nach restoreType.buildCheckpointManager:59— Wählt je nach Workspace (Single-Root oder Multi-Root) zwischenTaskCheckpointManageroderMultiRootCheckpointManager.saveCheckpointCallback:1113— Callback, das die Task dem Werkzeugexecutor zur Verfügung stellt; wird nach Werkzeugabschluss aufgerufen.ensureCheckpointInitialized:2920— Stellt vor der ersten API-Anfrage sicher, dass das Schatten-Git initialisiert ist.
Datenfluss
Bei der ersten API-Anfrage einer Task wird das Checkpoint-System hochgezogen – wenn die Initialisierung abbricht (Timeout), wird für die gesamte restliche Task auf Checkpoints verzichtet, um nicht in jeder Runde am Timeout hängen zu bleiben:
// core/task/index.ts:2906-2933
// Save checkpoint if this is the first API request
const isFirstRequest =
this.messageStateHandler
.getClineMessages()
.filter((m) => m.say === "api_req_started").length === 0;
// Initialize checkpointManager first if enabled and it's the first request
if (
isFirstRequest &&
this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting") &&
this.checkpointManager && // TODO REVIEW: may be able to implement a replacement for the 15s timer
!this.taskState.checkpointManagerErrorMessage
) {
try {
await ensureCheckpointInitialized({
checkpointManager: this.checkpointManager,
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
Logger.error("Failed to initialize checkpoint manager:", errorMessage);
this.taskState.checkpointManagerErrorMessage = errorMessage;
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Checkpoint initialization timed out: ${errorMessage}`,
});
}
}Nach erfolgreicher Initialisierung wird saveCheckpoint an zwei Stellen getriggert: (1) nach jedem attempt_completion (AttemptCompletionHandler:156); (2) nachdem alle Werkzeuge einer Assistant-Nachricht durchgelaufen sind (saveCheckpoint after tools:3811). saveCheckpoint selbst committet nicht direkt, sondern reserviert zuerst per say("checkpoint_created") eine Nachricht und committet dann asynchron; der Commit-Hash wird nachträglich in diese Nachricht eingetragen:
// integrations/checkpoints/index.ts:166-187
const messageTs = await this.callbacks.say("checkpoint_created")
if (messageTs) {
const messages = this.services.messageStateHandler.getClineMessages()
const targetMessage = messages.find((m) => m.ts === messageTs)
if (targetMessage) {
this.state.checkpointTracker
?.commit()
.then(async (commitHash) => {
if (commitHash) {
targetMessage.lastCheckpointHash = commitHash
await this.services.messageStateHandler.saveClineMessagesAndUpdateHistory()
}
})
.catch((error) => {
Logger.error(
`[TaskCheckpointManager] Failed to create checkpoint commit for task ${this.task.taskId}:`,
error,
)
})
}
}Warum asynchron: Die nächste LLM-Runde muss nicht auf das Beenden des Commits warten. Das Feld lastCheckpointHash ist der Ankerpunkt für künftige Wiederherstellungen.
Die Wiederherstellung läuft über restoreCheckpoint und verzweigt nach restoreType: Nur Code zurücksetzen heisst resetHead(hash), nur das Gespräch zurückrollen heisst conversationHistoryDeletedRange anpassen, beides bedeutet beides. Wird der Arbeitsbereich wiederhergestellt und der Tracker ist noch nicht hochgefahren, wird vor Ort per CheckpointTracker.create einer erzeugt.
Grenzen und Fehler
- home / Desktop / Documents / Downloads werden direkt abgewiesen (
validateWorkspacePath:59), da die Reichweite zu gross ist und es an Berechtigungen scheitern kann. - Einmaliges Initialisierungs-Timeout gibt die gesamte Task auf (
timeout guard:123);checkpointManagerErrorMessageträgt die MarkierungCheckpoints initialization timed out.ein und nachfolgendesaveCheckpoint-Aufrufe kehren sofort zurück, ohne erneut zu versuchen. - Fehlschlag beim Umbenennen verschachtelter
.git-Verzeichnisse wird retried (retryWithBackoff:223);addCheckpointFilesstellt imfinally-Block mit dreifachem exponentiellem Backoff die verschachtelten Git-Verzeichnisse wieder her – schlägt das fehl, wird nur ein error geloggt und nicht geworfen, sonst stünde das Benutzer-Subprojekt dauerhaft unter.git_disabled. - Verzeichnissperre gegen Nebenläufigkeit (
tryAcquireCheckpointLockWithRetry:220): Mehrere Cline-Instanzen mit demselben cwdHash könnten sich sonst in die Quere kommen;cwdHashdient als Sperrschlüssel. Innerhalb von VS Code wird die Sperre übersprungen. - Aufeinanderfolgende
checkpoint_createdwerden dedupliziert (back-to-back guard:160): Wenn die vorhergehende Nachricht bereitscheckpoint_createdist, kehrt der Aufruf sofort zurück, um leere Commits zu vermeiden. Beiattempt_completiongibt es zusätzlich eine Deduplizierung über die letzten drei Nachrichten (completion dedup:192). - Leere Commits mit
--allow-empty(empty commit:251): Auch wenn sich im Arbeitsbereich nichts geändert hat, wird ein Commit als Platzhalter angelegt, damit die Hash-Kette nicht abreisst und eine Wiederherstellung immer einen Anker findet. - Binärdateien werden aus dem Diff entfernt (
binary skip:434): Pfade ohne Erweiterung oder Dotfiles werden mitisBinaryFilegeprüft; Binärdateien werden übersprungen, damit die Diff-Ansicht nicht mit Zeichensalat vollgemüllt wird. - Multi-Root-Workspaces warnen nur statt zu werfen (
multiroot warn:461): Wird Multi-Root erkannt, wird die Fehlermeldung intaskState.checkpointManagerErrorMessagegeschrieben und im UI angezeigt, die Task aber nicht unterbrochen.
Zusammenfassung
Checkpoints sind das Fundament von Clines Sicherheitsnetz: Das Schatten-Git serialisiert den Arbeitsbereichszustand in eine Kette von Commits, deren Hashes an die Konversationsnachrichten geheftet werden; so entsprechen sich «Zeitpunkt» und «Gesprächspunkt» eins zu eins. Die obere Schicht braucht nur die beiden APIs saveCheckpoint und restoreCheckpoint und muss sich nicht um die Git-Operationen, verschachtelte .git-Verzeichnisse oder Verzeichnissperren kümmern.
Der nächste Schritt auf dem Rückroll-Pfad findet sich in der Rekursionsschleife des Agent-Loops – wo recursivelyMakeClineRequests den saveCheckpoint triggert und wie nach einer Wiederherstellung der Gesprächsverlauf ab einem bestimmten Punkt wieder dem LLM zugeführt wird. Wie der Werkzeugexecutor saveCheckpoint allen Handlern zugänglich macht, ist unter Tool-Ausführung beschrieben.
Siehe offizielle Dokumentation: Cline-Dokumentation · CheckpointTracker.ts