Checkpoints:タスク再生のシャドウ Git
役割
Checkpoints は Cline がタスクに追加した「タイムマシン (time machine)」である。LLM が手を動かす前、Cline は現在のワークスペースのスナップショットを撮って保存する。ユーザーがツール呼び出し結果に満足しなければ、コード、ファイル、さらに対話履歴全体を任意のスナップショット時点までロールバックし、そこから会話を再開できる。
全体の仕組みはシャドウ Git リポジトリ (shadow git) の上に成り立つ。ユーザーのワークスペースにある .git ではなく、Cline が自身の globalStorage 内でひっそり管理するもう 1 つの Git リポジトリで、core.worktree がユーザーの作業ディレクトリを指す。各 commit が 1 つのチェックポイントで、git reset --hard でロールバックが完了する。これにより Git の diff/stage/commit 能力を再利用しつつ、ユーザー自身のリポジトリ状態を汚染しない。
外部に晒す中核は TaskCheckpointManager で、「スナップショットを作るべきか、commit hash をどのメッセージに貼るか、ロールバック後に対話履歴をどこまで削るか」のビジネスロジックを包み込む。実際の Git 操作を担うのはその下の CheckpointTracker と GitOperations である。
設計動機
- ユーザーの .git を変更できない:ユーザーのワークスペースが既に別の Git リポジトリ配下にあるかもしれず、直接
git add .すれば彼らの index が汚染される。シャドウ Git は独立の.gitディレクトリ +core.worktreeで逆参照し、2 つのリポジトリを完全に隔離する。 - 各ツール呼び出しをロールバック可能に:LLM のファイル編集は予測不能で、ユーザーは「このツール呼び出しの前後」を比較し、任意の 1 ステップ前に戻せる必要がある。そのため commit のタイミングは必ずツール実行境界に合わせる。
- メッセージ履歴を一緒にロールバックしない:ロールバックは 3 種——
workspaceはファイルのみ、taskは対話メッセージのみ、taskAndWorkspaceは両方を動かす。これにより「対話を残したままコードだけ戻す」や「対話ごと戻ってやり直す」ができる。 - マルチルートワークスペースは個別処理:1 つの workspace に複数ディレクトリがぶら下がることがあり、単一ルートのシャドウ Git では足りないため、外側で
MultiRootCheckpointManagerがディスパッチする。 - ネストされた .git が git add を壊す:Git はサブディレクトリの
.gitをデフォルトでサブモジュール扱いするため、シャドウ Git は add のたびにネスト.gitを一時的にリネームして無効化し、add 完了後に戻す必要がある。 - 保護ディレクトリをブロック:home / Desktop / Documents / Downloads は範囲が大きすぎて無関係ファイルを大量にスキャンしてしまうため、
validateWorkspacePathで直接拒否する。
主要ファイル
class CheckpointTracker:50— 1 タスクに対応するシャドウ Git 操作器。taskIdとcwdHashを保持。commit:212— ロック取得 →git add .→git commit --allow-empty --no-verifyで commit hash を返す。resetHead:336—git reset --hard <hash>でワークスペースを指定 checkpoint に復元。getDiffSet:397— 2 commit間 (または commit ↔ ワークスペース) のファイル差分。前後内容を含む。initShadowGit:59— 初回シャドウ Git リポジトリ作成。core.worktree/user.email/ excludes を設定。renameNestedGitRepos:148— ネスト.gitディレクトリの一時無効化/復元。サブモジュール制限を回避。addCheckpointFiles:203— ネスト git 無効化 →git add . --ignore-errors→ ネスト git 復元。getShadowGitPath:20— シャドウ Git パスglobalStorage/checkpoints/{cwdHash}/.git。hashWorkingDir:103— 作業ディレクトリパスを 13 桁数字にハッシュ化し、シャドウ Git ディレクトリ名にする。saveCheckpoint:118— ビジネスエントリ。commit 要否を判断し、hash を対応メッセージに貼る。restoreCheckpoint:238—messageTsで checkpoint を探し、restoreType で分岐復元。buildCheckpointManager:59— workspace が単一かマルチルートかでTaskCheckpointManagerかMultiRootCheckpointManagerを選ぶ。saveCheckpointCallback:1113— Task がツール実行器に晒すコールバック。ツール完了後に呼ぶ。ensureCheckpointInitialized:2920— 初回 API リクエスト前にシャドウ Git が初期化済みであることを保証。
データフロー
各タスクは初回 API リクエスト時にチェックポイントシステムを立ち上げる。初期化がタイムアウトすれば、以降のタスク全体でチェックポイントを再試行しない——毎ラウンドでタイムアウトに引っかかるのを避けるため:
// 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}`,
});
}
}初期化成功後、タスクは 2 つのタイミングで saveCheckpoint を発火する。(1) 各 attempt_completion の完了 (AttemptCompletionHandler:156)。(2) 1 つの assistant メッセージ内の全ツール実行完了後 (saveCheckpoint after tools:3811)。saveCheckpoint は内部で直接 commit せず、まず say("checkpoint_created") でメッセージを 1 件確保し、その後非同期で commit し、commit hash をこのメッセージに埋め戻す:
// 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,
)
})
}
}非同期な理由は、LLM の次ラウンドリクエストが commit 完了を待たずに継続できるようにするためである。lastCheckpointHash フィールドが後のロールバックの錨点となる。
ロールバックは restoreCheckpoint を経由し、restoreType で分岐する。コードのみ戻すなら resetHead(hash)、対話のみ戻すなら conversationHistoryDeletedRange を変更、両方なら両方を行う。ワークスペース復元時に tracker が未起動なら、その場で CheckpointTracker.create する。
境界と失敗
- home / Desktop / Documents / Downloads は直接拒否 (
validateWorkspacePath:59)。範囲が大きすぎてスキャンできず、権限にも抵触しやすい。 - 初期化タイムアウトは 1 回でタスク全体を諦める (
timeout guard:123)。checkpointManagerErrorMessageにCheckpoints initialization timed out.マーカーを入れ、以降のsaveCheckpointは入った瞬間に return して再試行しない。 - ネスト
.gitのリネーム失敗はリトライ (retryWithBackoff:223)。addCheckpointFilesは finally で 3 回指数バックオフでネスト git を復元し、失敗しても error を log するだけ——さもなくばユーザーのサブプロジェクトがずっと.git_disabled状態に留まる。 - フォルダロックで並行性を防止 (
tryAcquireCheckpointLockWithRetry:220)。同一 cwdHash 配下で複数の Cline インスタンスが互いに踏み合う可能性があり、cwdHashをロックキーにする。VS Code 内部シナリオではロックをスキップ。 - 連続 checkpoint_created は重複除去 (
back-to-back guard:160)。直前のメッセージが既にcheckpoint_createdなら直接 return し、空 commit が積もるのを防ぐ。attempt_completionにはさらに直近 3 メッセージの重複除去がある (completion dedup:192)。 - 空 commit は
--allow-empty(empty commit:251)。ワークスペースに変更がなくても commit プレースホルダーを残し、hash 連鎖を切らさないようにする。これによりロールバックが常に錨点を見つけられる。 - バイナリファイルは diff 結果から除外 (
binary skip:434)。拡張子なしやドットファイルのパスはisBinaryFileで調査し、バイナリならスキップする。さもなくば diff ビューが乱码で埋まる。 - マルチルートワークスペースは警告のみ (
multiroot warn:461)。マルチルートを検出したらエラー情報をtaskState.checkpointManagerErrorMessageに詰め、UI に警告を表示するが、タスクは中断しない。
まとめ
Checkpoints は Cline の安全網の底盤である。シャドウ Git でワークスペース状態を一連の commit に直列化し、commit hash を対話メッセージに貼ることで、「時点」と「対話点」を 1 対 1 に対応させる。上位は saveCheckpoint / restoreCheckpoint の 2 つの API を知るだけで、底层の Git 操作、ネスト .git 処理、フォルダロックといった泥臭い仕事を気にする必要がない。
ロールバックチェーンの次のステップは agent-loop の再帰ループ を参照——recursivelyMakeClineRequests がどのステップで saveCheckpoint を発火するか、ロールバック後にどう対話履歴の一点から LLM に再供給するかは、そのページに詳しい。ツール実行器が saveCheckpoint を全 handler に晒す詳細は ツール実行 にある。
公式資料: Cline 文档 · CheckpointTracker.ts