Checkpoints: el git paralelo para reproducir tareas
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
.gitdel usuario: el espacio de trabajo del usuario podría pertenecer a otro repositorio git; ungit add .directo contaminaría su índice. El git sombra usa un directorio.gitindependiente +core.worktreeque 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 —
workspacesolo toca archivos,tasksolo toca los mensajes de la conversación,taskAndWorkspacetoca 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
MultiRootCheckpointManagercomo dispatcher externo. - Los
.gitanidados rompengit add: git interpreta por defecto los.gitde subdirectorios como submódulos; el git sombra debe renombrar temporalmente y deshabilitar los.gitanidados 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
validateWorkspacePathlos rechaza directamente.
Archivos clave
class CheckpointTracker:50— manipulador del git sombra asociado a una tarea; mantienetaskIdycwdHash.commit:212— adquiere el lock →git add .→git commit --allow-empty --no-verify, devuelve el commit hash.resetHead:336—git 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, configuracore.worktree/user.email/ excludes.renameNestedGitRepos:148— deshabilita/restaura temporalmente los.gitanidados 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 sombraglobalStorage/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 pormessageTsy aplica la restauración segúnrestoreType.buildCheckpointManager:59— eligeTaskCheckpointManageroMultiRootCheckpointManagersegú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:
// 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:
// 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),checkpointManagerErrorMessagelleva la marcaCheckpoints initialization timed out.y lossaveCheckpointposteriores entran y hacen return directamente, sin reintentar. - Fallo al renombrar
.gitanidados se reintenta (retryWithBackoff:223):addCheckpointFilesusa enfinally3 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 mismocwdHashpueden pisarse entre sí, se usacwdHashcomo llave de lock. Dentro de VS Code el lock se omite. checkpoint_createdconsecutivos se deduplican (back-to-back guard:160): si el mensaje anterior ya escheckpoint_createdse hace return directamente, para evitar commits vacíos acumulados.attempt_completiontiene 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 conisBinaryFile, 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 entaskState.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