Skip to content

Checkpoints : git fantôme pour rejouer une tâche

源码版本v4.0.10

Responsabilités

Les checkpoints (points de contrôle) sont la « machine à remonter le temps » que Cline attache à chaque tâche. Avant chaque action de l'LLM, Cline prend un snapshot de l'espace de travail courant. Si l'utilisateur n'est pas satisfait du résultat d'un appel d'outil, il peut restaurer le code, les fichiers, voire toute une section d'historique de conversation à n'importe quel snapshot, puis reprendre la conversation à partir de là.

Tout le mécanisme repose sur un dépôt git fantôme (shadow git) : pas le .git du workspace de l'utilisateur, mais un autre dépôt git que Cline maintient discrètement dans son globalStorage, avec core.worktree pointant vers le répertoire de travail de l'utilisateur. Chaque commit est un checkpoint, et git reset --hard effectue la restauration. On réutilise ainsi les capacités diff/stage/commit de git sans jamais polluer le dépôt de l'utilisateur.

L'API centrale exposée est TaskCheckpointManager. Elle encapsule la logique métier (« faut-il créer un snapshot ? », « à quel message coller le commit hash ? », « jusqu'où tronquer l'historique de conversation après une restauration ? »). Le travail git effectif est confié aux couches inférieures CheckpointTracker et GitOperations.

Motivation de conception

  • Ne pas toucher au .git de l'utilisateur : le workspace utilisateur peut déjà être sous un autre dépôt git. Un git add . direct polluerait leur index. Le git fantôme utilise un répertoire .git indépendant avec core.worktree qui pointe vers le workspace, les deux dépôts sont totalement isolés.
  • Chaque appel d'outil doit pouvoir être annulé : les éditions de fichiers par l'LLM sont imprévisibles. L'utilisateur doit pouvoir comparer « avant » et « après » un appel d'outil, et revenir à n'importe quelle étape antérieure. Le rythme des commits doit donc se caler sur la frontière d'exécution des outils.
  • Ne pas embarquer l'historique des messages dans la restauration : la restauration se divise en trois types — workspace ne touche qu'aux fichiers, task ne touche qu'aux messages de conversation, taskAndWorkspace touche aux deux. Ainsi l'utilisateur peut « garder la conversation mais restaurer le code » ou « tout revenir en arrière, conversation comprise ».
  • Workspaces multi-root traités séparément : un workspace peut monter plusieurs répertoires, un seul git fantôme à racine unique ne suffit pas. C'est pourquoi MultiRootCheckpointManager dispatche en amont.
  • Les .git imbriqués cassent git add : quand git rencontre un .git dans un sous-répertoire, il le traite par défaut en submodule. Le git fantôme doit donc renommer temporairement les .git imbriqués avant chaque add, puis les restaurer à la fin.
  • Répertoires protégés à bloquer : home / Desktop / Documents / Downloads couvrent une portée trop large et risquent de scanner une foule de fichiers sans rapport. validateWorkspacePath les refuse directement.

Fichiers clés

  • class CheckpointTracker:50 — manipulateur de git fantôme pour une tâche donnée, porte taskId et cwdHash.
  • commit:212 — prend le lock → git add .git commit --allow-empty --no-verify, renvoie le commit hash.
  • resetHead:336git reset --hard <hash> ramène le workspace au checkpoint spécifié.
  • getDiffSet:397 — diff de fichiers entre deux commits (ou commit ↔ workspace), avec contenu avant/après.
  • initShadowGit:59 — création initiale du dépôt git fantôme, configure core.worktree / user.email / excludes.
  • renameNestedGitRepos:148 — désactive/restaure temporairement les .git imbriqués pour contourner la limite submodule.
  • addCheckpointFiles:203 — désactive git imbriqués → git add . --ignore-errors → restaure git imbriqués.
  • getShadowGitPath:20 — chemin du git fantôme globalStorage/checkpoints/{cwdHash}/.git.
  • hashWorkingDir:103 — hashe le chemin du répertoire de travail en 13 chiffres, sert de nom de répertoire pour le git fantôme.
  • saveCheckpoint:118 — entrée métier, décide s'il faut committer, colle le hash sur le message correspondant.
  • restoreCheckpoint:238 — retrouve le checkpoint par messageTs, restore selon restoreType.
  • buildCheckpointManager:59 — selon workspace mono-racine ou multi-racine, choisit TaskCheckpointManager ou MultiRootCheckpointManager.
  • saveCheckpointCallback:1113 — callback que Task expose aux exécuteurs d'outils, invoqué après exécution de l'outil.
  • ensureCheckpointInitialized:2920 — avant la première requête API, s'assure que le git fantôme est prêt.

Flux de données

À la première requête API de chaque tâche, le système de checkpoint est également levé. En cas de timeout à l'initialisation, la tâche n'essaiera plus jamais de checkpoint, pour éviter de rester bloquée sur le timeout à chaque tour :

typescript
// 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}`,
        });
    }
}

Une fois l'initialisation réussie, la tâche déclenche saveCheckpoint à deux moments : (1) après chaque attempt_completion terminé (AttemptCompletionHandler:156) ; (2) une fois que tous les outils d'un message assistant ont fini de tourner (saveCheckpoint after tools:3811). saveCheckpoint ne commit pas directement : il émet d'abord say("checkpoint_created") pour occuper un message, puis lance le commit en asynchrone, et le commit hash est réinjecté dans ce message :

typescript
// 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,
                )
            })
    }
}

Pourquoi asynchrone : la requête LLM suivante peut continuer sans attendre la fin de l'écriture du commit. Le champ lastCheckpointHash sert d'ancre pour les restaurations ultérieures.

La restauration passe par restoreCheckpoint, qui emprunte différentes branches selon restoreType : code uniquement → resetHead(hash) ; conversation uniquement → modification de conversationHistoryDeletedRange ; les deux → les deux. À la restauration du workspace, si le tracker n'est pas encore levé, on crée un CheckpointTracker.create à la volée.

Limites et échecs

  • home / Desktop / Documents / Downloads refusés directement (validateWorkspacePath:59) : portée trop large, le scan serait impraticable et risquerait de heurter des permissions.
  • Un seul timeout d'initialisation abandonne toute la tâche (timeout guard:123). checkpointManagerErrorMessage contient Checkpoints initialization timed out., et tout appel ultérieur à saveCheckpoint retourne immédiatement sans réessayer.
  • Échec du renommage des .git imbriqués : retry (retryWithBackoff:223). addCheckpointFiles restaure les git imbriqués dans un bloc finally avec 3 retries à backoff exponentiel ; en cas d'échec, on se contente de loguer une error sans la propager — sinon le sous-projet utilisateur resterait bloqué dans l'état .git_disabled.
  • Lock de dossier contre la concurrence (tryAcquireCheckpointLockWithRetry:220). Plusieurs instances Cline sous le même cwdHash pourraient se marcher dessus ; on utilise cwdHash comme clé de lock. Le scénario interne à VS Code saute le lock.
  • Les checkpoint_created consécutifs sont dédupliqués (back-to-back guard:160). Si le message précédent est déjà checkpoint_created, on retourne directement pour éviter l'empilement de commits vides. attempt_completion dispose en plus d'une déduplication sur les 3 derniers messages (completion dedup:192).
  • Commit vide via --allow-empty (empty commit:251). Même sans modification du workspace, on garde un commit placeholder pour que la chaîne de hash ne se casse pas et que la restauration trouve toujours une ancre.
  • Fichiers binaires exclus du diff (binary skip:434). Les chemins sans extension ou dotfile sont sonnés par isBinaryFile ; si binaire, on saute pour éviter que la vue diff ne se retrouve remplie de caractères illisibles.
  • Workspace multi-root : warning sans erreur (multiroot warn:461). Quand un multi-root est détecté, on injecte directement l'erreur dans taskState.checkpointManagerErrorMessage et on affiche un warning dans l'UI, sans interrompre la tâche.

Résumé

Les checkpoints sont la base du filet de sécurité de Cline : un git fantôme sérialise l'état du workspace en une chaîne de commits, et les commit hash sont collés sur les messages de conversation, si bien que « point temporel » et « point de conversation » se correspondent un à un. La couche supérieure n'a qu'à connaître les deux API saveCheckpoint / restoreCheckpoint, sans se soucier du travail sale en dessous : opérations git, traitement des .git imbriqués, lock de dossier.

La suite de la chaîne de restauration se trouve dans la boucle récursive d'agent-loop — à quelle étape recursivelyMakeClineRequests déclenche saveCheckpoint, et comment, après restauration, on renvoie à l'LLM un point précis de l'historique de conversation. Le détail de l'exécuteur d'outils qui expose saveCheckpoint à tous les handlers est dans exécution d'outils.

Voir la documentation officielle : documentation Cline · CheckpointTracker.ts.