Skip to content

Checkpoints: el git paralelo para reproducir tareas

源码版本v4.0.10

Responsabilidades

Checkpoints es la «máquina del tiempo» que Cline añade a cada tarea. Antes de que el LLM haga cambios, Cline toma una instantánea (punto de control / checkpoint) del espacio de trabajo actual y la guarda; si el usuario no está conforme con el resultado de una llamada a herramienta, puede revertir el código, los archivos o incluso el historial de conversación a cualquier punto de control y continuar la conversación desde ahí.

Todo el mecanismo se apoya en un repositorio git sombra (shadow git): no es el .git del espacio de trabajo del usuario, sino otro repositorio git que Cline mantiene oculto en su globalStorage, con core.worktree apuntando al directorio de trabajo del usuario. Cada commit es un checkpoint, y git reset --hard completa la reversion. Así se reutilizan las capacidades de diff/stage/commit de git sin contaminar el repositorio del usuario.

El núcleo expuesto al exterior es TaskCheckpointManager, que envuelve la lógica de negocio («¿debo crear una instantánea?», «¿a qué mensaje pegar el commit hash?», «tras revertir, hasta qué punto borrar el historial de conversación»); el trabajo git real lo hacen CheckpointTracker y CheckpointGitOperations por debajo.

Motivación de diseño

  • No tocar el .git del usuario: el espacio de trabajo del usuario podría pertenecer a otro repositorio git; un git add . directo contaminaría su índice. El git sombra usa un directorio .git independiente + core.worktree que apunta de vuelta, manteniendo ambos repositorios totalmente aislados.
  • Cada llamada a herramienta debe poder deshacerse: las ediciones del LLM son impredecibles, el usuario necesita ver el contraste «antes» y «después» de una llamada a herramienta y poder deshacer hasta cualquier paso previo. Por eso el ritmo de commits debe atarse al borde de ejecución de herramientas.
  • No arrastrar el historial de mensajes al revertir: la reversion se divide en tres tipos —workspace solo toca archivos, task solo toca los mensajes de la conversación, taskAndWorkspace toca ambos—. Así el usuario puede «conservar la conversación pero restaurar el código» o «retroceder también la conversación y rehacer el camino».
  • Workspaces multi-root se tratan por separado: un workspace puede montar varios directorios, un único git sombra de raíz no basta, por eso existe MultiRootCheckpointManager como dispatcher externo.
  • Los .git anidados rompen git add: git interpreta por defecto los .git de subdirectorios como submódulos; el git sombra debe renombrar temporalmente y deshabilitar los .git anidados antes de cada add, y restaurarlos al terminar.
  • Los directorios protegidos se bloquean: home / Desktop / Documents / Downloads son demasiado amplios y arrastrarían un montón de archivos irrelevantes, por lo que validateWorkspacePath los rechaza directamente.

Archivos clave

  • class CheckpointTracker:50 — manipulador del git sombra asociado a una tarea; mantiene taskId y cwdHash.
  • commit:212 — adquiere el lock → git add .git commit --allow-empty --no-verify, devuelve el commit hash.
  • resetHead:336git reset --hard <hash> restaura el espacio de trabajo al checkpoint indicado.
  • getDiffSet:397 — diff de archivos entre dos commits (o commit ↔ workspace), con contenido anterior y posterior.
  • initShadowGit:59 — crea el repositorio git sombra por primera vez, configura core.worktree / user.email / excludes.
  • renameNestedGitRepos:148 — deshabilita/restaura temporalmente los .git anidados para eludir la restricción de submódulos.
  • addCheckpointFiles:203 — deshabilita git anidado → git add . --ignore-errors → restaura git anidado.
  • getShadowGitPath:20 — ruta del git sombra globalStorage/checkpoints/{cwdHash}/.git.
  • hashWorkingDir:103 — hashea la ruta del directorio de trabajo a 13 dígitos, usado como nombre del directorio del git sombra.
  • saveCheckpoint:118 — entrada de negocio, decide si crear commit y pega el hash al mensaje correspondiente.
  • restoreCheckpoint:238 — busca el checkpoint por messageTs y aplica la restauración según restoreType.
  • buildCheckpointManager:59 — elige TaskCheckpointManager o MultiRootCheckpointManager según si el workspace es mono-root o multi-root.
  • saveCheckpointCallback:1113 — callback que Task expone al ejecutor de herramientas; se invoca al terminar una herramienta.
  • ensureCheckpointInitialized:2920 — antes de la primera petición API asegura que el git sombra ya esté inicializado.

Flujo de datos

En la primera petición API de cada tarea, se levanta el sistema de checkpoints —si la inicialización expira, esa tarea ya no reintenta checkpoints en lo sucesivo, para no trabar cada ronda en el timeout:

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

Tras la inicialización, la tarea dispara saveCheckpoint en dos momentos: (1) al completar cada attempt_completion (AttemptCompletionHandler:156); (2) tras terminar todas las herramientas de un mismo mensaje del asistente (saveCheckpoint after tools:3811). saveCheckpoint no commitea directamente: primero say("checkpoint_created") para reservar un mensaje y luego hace commit asíncrono, rellenando el commit hash en ese mensaje:

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

Es asíncrono para que la siguiente ronda del LLM pueda continuar sin esperar a que termine el commit. El campo lastCheckpointHash es el ancla de la reversion futura.

La reversion pasa por restoreCheckpoint, que toma distintas ramas según restoreType: solo restaurar código es resetHead(hash), solo retroceder la conversión modifica conversationHistoryDeletedRange, y ambos hace las dos cosas. Al restaurar el workspace, si el tracker aún no está levantado, se crea uno al vuelo con CheckpointTracker.create.

Límites y fallos

  • home / Desktop / Documents / Downloads se rechazan directamente (validateWorkspacePath:59), su alcance es demasiado amplio y es fácil chocar con permisos.
  • Un timeout de inicialización aborta checkpoints en toda la tarea (timeout guard:123), checkpointManagerErrorMessage lleva la marca Checkpoints initialization timed out. y los saveCheckpoint posteriores entran y hacen return directamente, sin reintentar.
  • Fallo al renombrar .git anidados se reintenta (retryWithBackoff:223): addCheckpointFiles usa en finally 3 intentos con backoff exponencial para restaurar los git anidados; si falla solo registra el error sin lanzar —de lo contrario los subproyectos del usuario quedarían atascados en estado .git_disabled.
  • Lock de carpeta anti-concurrencia (tryAcquireCheckpointLockWithRetry:220): varias instancias de Cline bajo el mismo cwdHash pueden pisarse entre sí, se usa cwdHash como llave de lock. Dentro de VS Code el lock se omite.
  • checkpoint_created consecutivos se deduplican (back-to-back guard:160): si el mensaje anterior ya es checkpoint_created se hace return directamente, para evitar commits vacíos acumulados. attempt_completion tiene además una dedup adicional sobre los últimos 3 mensajes (completion dedup:192).
  • Commit vacío con --allow-empty (empty commit:251): aunque el workspace no cambie, se deja un commit de marcador para que la cadena de hashes no se rompa y la reversion siempre encuentre un ancla.
  • Archivos binarios se eliminan del diff (binary skip:434): las rutas sin extensión o que son dotfile se prueban con isBinaryFile, y los binarios se saltan; de lo contrario la vista de diff se llenaría de basura.
  • Workspace multi-root solo advierte, no falla (multiroot warn:461): al detectar multi-root se introduce el error en taskState.checkpointManagerErrorMessage, se muestra una advertencia en la UI, pero la tarea no se interrumpe.

Resumen

Checkpoints es la base de la red de seguridad de Cline: usa un git sombra para serializar el estado del espacio de trabajo en una cadena de commits y pega el commit hash a los mensajes de la conversación, de modo que «puntos temporales» y «puntos de la conversación» se corresponden uno a uno. La capa superior solo necesita conocer las dos API saveCheckpoint / restoreCheckpoint, sin preocuparse del trabajo git subyacente, los .git anidados ni los locks de carpeta.

El siguiente paso de la cadena de reversion está en el bucle recursivo del agent-loop —en qué punto recursivelyMakeClineRequests dispara saveCheckpoint y cómo, tras revertir, se vuelve a alimentar al LLM desde un punto concreto del historial de conversación. Los detalles de cómo el ejecutor de herramientas expone saveCheckpoint a todos los handlers están en ejecución de herramientas.

Véase la documentación oficial: Cline 文档 · CheckpointTracker.ts