From a2b47ccb13cdaf331e18f085be64ff6249987732 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Tue, 29 Jul 2025 18:06:28 +0800 Subject: [PATCH 1/9] feat: Before requesting, ensure checkpoint is initialized --- src/core/checkpoints/index.ts | 15 ++++++++------- src/core/task/Task.ts | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index 02fb5dfc5a..5d7ab23533 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -16,7 +16,7 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints" -export function getCheckpointService(cline: Task) { +export async function getCheckpointService(cline: Task) { if (!cline.enableCheckpoints) { return undefined } @@ -75,7 +75,7 @@ export function getCheckpointService(cline: Task) { // Check if Git is installed before initializing the service // Note: This is intentionally fire-and-forget to match the original IIFE pattern // The service is returned immediately while Git check happens asynchronously - checkGitInstallation(cline, service, log, provider) + await checkGitInstallation(cline, service, log, provider) return service } catch (err) { @@ -153,11 +153,12 @@ async function checkGitInstallation( }) log("[Task#getCheckpointService] initializing shadow git") - - service.initShadowGit().catch((err) => { + try { + await service.initShadowGit() + } catch (err) { log(`[Task#getCheckpointService] initShadowGit -> ${err.message}`) cline.enableCheckpoints = false - }) + } } catch (err) { log(`[Task#getCheckpointService] Unexpected error during Git check: ${err.message}`) console.error("Git check error:", err) @@ -170,7 +171,7 @@ async function getInitializedCheckpointService( cline: Task, { interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {}, ) { - const service = getCheckpointService(cline) + const service = await getCheckpointService(cline) if (!service || service.isInitialized) { return service @@ -192,7 +193,7 @@ async function getInitializedCheckpointService( } export async function checkpointSave(cline: Task, force = false) { - const service = getCheckpointService(cline) + const service = await getCheckpointService(cline) if (!service) { return diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index edbde32ea7..c5fe6bbacc 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1295,7 +1295,7 @@ export class Task extends EventEmitter { private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise { // Kicks off the checkpoints initialization process in the background. - getCheckpointService(this) + await getCheckpointService(this) let nextUserContent = userContent let includeFileDetails = true From 958a4373e8fb0c60de6e6922eac8f69893da8cd1 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Tue, 29 Jul 2025 18:07:55 +0800 Subject: [PATCH 2/9] Generate a checkpoint before modifying the code --- .../assistant-message/presentAssistantMessage.ts | 12 ++++++++++++ src/core/task/Task.ts | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ee3fa148b4..58f443ed34 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -411,9 +411,11 @@ export async function presentAssistantMessage(cline: Task) { switch (block.name) { case "write_to_file": + await checkpointSaveAndMark(cline) await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break case "update_todo_list": + await checkpointSaveAndMark(cline) await updateTodoListTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break case "apply_diff": { @@ -429,6 +431,7 @@ export async function presentAssistantMessage(cline: Task) { ) } + await checkpointSaveAndMark(cline) if (isMultiFileApplyDiffEnabled) { await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) } else { @@ -444,9 +447,11 @@ export async function presentAssistantMessage(cline: Task) { break } case "insert_content": + await checkpointSaveAndMark(cline) await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break case "search_and_replace": + await checkpointSaveAndMark(cline) await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break case "read_file": @@ -583,3 +588,10 @@ export async function presentAssistantMessage(cline: Task) { presentAssistantMessage(cline) } } +async function checkpointSaveAndMark(cline: Task) { + if (cline.currentStreamingDidCheckpoint) { + return + } + await cline.checkpointSave(true) + cline.currentStreamingDidCheckpoint = true +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index c5fe6bbacc..d2b943ab7f 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -247,6 +247,7 @@ export class Task extends EventEmitter { isWaitingForFirstChunk = false isStreaming = false currentStreamingContentIndex = 0 + currentStreamingDidCheckpoint = false assistantMessageContent: AssistantMessageContent[] = [] presentAssistantMessageLocked = false presentAssistantMessageHasPendingUpdates = false @@ -1523,6 +1524,7 @@ export class Task extends EventEmitter { // Reset streaming state. this.currentStreamingContentIndex = 0 + this.currentStreamingDidCheckpoint = false this.assistantMessageContent = [] this.didCompleteReadingStream = false this.userMessageContent = [] From f93427610252af801f4007a83c16d80ebfa1d32f Mon Sep 17 00:00:00 2001 From: NaccOll Date: Tue, 29 Jul 2025 18:14:49 +0800 Subject: [PATCH 3/9] refactor: streamline checkpoint handling and enhance getCheckpoints method --- .../presentAssistantMessage.ts | 8 -------- src/core/checkpoints/index.ts | 18 ++++++++++-------- .../checkpoints/ShadowCheckpointService.ts | 4 ++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 58f443ed34..5497fc7634 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -532,14 +532,6 @@ export async function presentAssistantMessage(cline: Task) { break } - const recentlyModifiedFiles = cline.fileContextTracker.getAndClearCheckpointPossibleFile() - - if (recentlyModifiedFiles.length > 0) { - // TODO: We can track what file changes were made and only - // checkpoint those files, this will be save storage. - await checkpointSave(cline) - } - // Seeing out of bounds is fine, it means that the next too call is being // built up and ready to add to assistantMessageContent to present. // When you see the UI inactive during this, it means that a tool is diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index 5d7ab23533..c700384948 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -298,17 +298,19 @@ export async function checkpointDiff(cline: Task, { ts, previousCommitHash, comm TelemetryService.instance.captureCheckpointDiffed(cline.taskId) - if (!previousCommitHash && mode === "checkpoint") { - const previousCheckpoint = cline.clineMessages - .filter(({ say }) => say === "checkpoint_saved") - .sort((a, b) => b.ts - a.ts) - .find((message) => message.ts < ts) - - previousCommitHash = previousCheckpoint?.text + let from = commitHash + let to: string | undefined + + const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : [] + const idx = checkpoints.indexOf(commitHash) + if (idx !== -1 && idx < checkpoints.length - 1) { + to = checkpoints[idx + 1] + } else { + to = undefined } try { - const changes = await service.getDiff({ from: previousCommitHash, to: commitHash }) + const changes = await service.getDiff({ from, to }) if (!changes?.length) { vscode.window.showInformationMessage("No changes found.") diff --git a/src/services/checkpoints/ShadowCheckpointService.ts b/src/services/checkpoints/ShadowCheckpointService.ts index be2c86852a..280cbd8118 100644 --- a/src/services/checkpoints/ShadowCheckpointService.ts +++ b/src/services/checkpoints/ShadowCheckpointService.ts @@ -38,6 +38,10 @@ export abstract class ShadowCheckpointService extends EventEmitter { return !!this.git } + public getCheckpoints(): string[] { + return this._checkpoints.slice() + } + constructor(taskId: string, checkpointsDir: string, workspaceDir: string, log: (message: string) => void) { super() From cf200c573d2306ad97e92eb20e9a982443bc74bd Mon Sep 17 00:00:00 2001 From: NaccOll Date: Tue, 29 Jul 2025 23:48:32 +0800 Subject: [PATCH 4/9] Blocked waiting for checkpoint initialization timing to change --- .../presentAssistantMessage.ts | 4 +- src/core/checkpoints/index.ts | 57 +++++++------------ src/core/task/Task.ts | 2 +- 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 5497fc7634..d59052c8a1 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -25,7 +25,6 @@ import { switchModeTool } from "../tools/switchModeTool" import { attemptCompletionTool } from "../tools/attemptCompletionTool" import { newTaskTool } from "../tools/newTaskTool" -import { checkpointSave } from "../checkpoints" import { updateTodoListTool } from "../tools/updateTodoListTool" import { formatResponse } from "../prompts/responses" @@ -431,10 +430,11 @@ export async function presentAssistantMessage(cline: Task) { ) } - await checkpointSaveAndMark(cline) if (isMultiFileApplyDiffEnabled) { + await checkpointSaveAndMark(cline) await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) } else { + await checkpointSaveAndMark(cline) await applyDiffToolLegacy( cline, block, diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index c700384948..e27005721e 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -16,18 +16,29 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints" -export async function getCheckpointService(cline: Task) { +export async function getCheckpointService( + cline: Task, + { interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {}, +) { if (!cline.enableCheckpoints) { return undefined } if (cline.checkpointService) { - return cline.checkpointService - } - - if (cline.checkpointServiceInitializing) { - console.log("[Task#getCheckpointService] checkpoint service is still initializing") - return undefined + if (cline.checkpointServiceInitializing) { + console.log("[Task#getCheckpointService] checkpoint service is still initializing") + const service = cline.checkpointService + await pWaitFor( + () => { + console.log("[Task#getCheckpointService] waiting for service to initialize") + return service.isInitialized + }, + { interval, timeout }, + ) + return service.isInitialized ? cline.checkpointService : undefined + } else { + return cline.checkpointService + } } const provider = cline.providerRef.deref() @@ -69,8 +80,8 @@ export async function getCheckpointService(cline: Task) { } const service = RepoPerTaskCheckpointService.create(options) - cline.checkpointServiceInitializing = true + cline.checkpointService = service // Check if Git is installed before initializing the service // Note: This is intentionally fire-and-forget to match the original IIFE pattern @@ -120,7 +131,6 @@ async function checkGitInstallation( const isCheckpointNeeded = typeof cline.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined" - cline.checkpointService = service cline.checkpointServiceInitializing = false if (isCheckpointNeeded) { @@ -167,31 +177,6 @@ async function checkGitInstallation( } } -async function getInitializedCheckpointService( - cline: Task, - { interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {}, -) { - const service = await getCheckpointService(cline) - - if (!service || service.isInitialized) { - return service - } - - try { - await pWaitFor( - () => { - console.log("[Task#getCheckpointService] waiting for service to initialize") - return service.isInitialized - }, - { interval, timeout }, - ) - - return service - } catch (err) { - return undefined - } -} - export async function checkpointSave(cline: Task, force = false) { const service = await getCheckpointService(cline) @@ -222,7 +207,7 @@ export type CheckpointRestoreOptions = { } export async function checkpointRestore(cline: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) { - const service = await getInitializedCheckpointService(cline) + const service = await getCheckpointService(cline) if (!service) { return @@ -290,7 +275,7 @@ export type CheckpointDiffOptions = { } export async function checkpointDiff(cline: Task, { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions) { - const service = await getInitializedCheckpointService(cline) + const service = await getCheckpointService(cline) if (!service) { return diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d2b943ab7f..e1511568b0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1296,7 +1296,7 @@ export class Task extends EventEmitter { private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise { // Kicks off the checkpoints initialization process in the background. - await getCheckpointService(this) + getCheckpointService(this) let nextUserContent = userContent let includeFileDetails = true From ff3e40fbb6ca531835e1666a95b6666e04834f47 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Tue, 29 Jul 2025 23:50:45 +0800 Subject: [PATCH 5/9] cancel checkpoint restore limit --- .../chat/checkpoints/CheckpointMenu.tsx | 117 +++++++++--------- 1 file changed, 56 insertions(+), 61 deletions(-) diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 21b4f486c7..bb0d599d55 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -24,7 +24,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec const isCurrent = currentHash === commitHash const isFirst = checkpoint.isFirst const isDiffAvailable = !isFirst - const isRestoreAvailable = !isFirst || !isCurrent const previousCommitHash = checkpoint?.from @@ -54,71 +53,67 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec )} - {isRestoreAvailable && ( - { - setIsOpen(open) - setIsConfirming(false) - }}> - - - - - - -
- {!isCurrent && ( -
- -
- {t("chat:checkpoint.menu.restoreFilesDescription")} -
+ { + setIsOpen(open) + setIsConfirming(false) + }}> + + + + + + +
+ {!isCurrent && ( +
+ +
+ {t("chat:checkpoint.menu.restoreFilesDescription")}
- )} - {!isFirst && ( -
-
- {!isConfirming ? ( - - ) : ( - <> - - - - )} - {isConfirming ? ( -
- {t("chat:checkpoint.menu.cannotUndo")} +
+ )} +
+
+ {!isConfirming ? ( + + ) : ( + <> + + + + )} + {isConfirming ? ( +
+ {t("chat:checkpoint.menu.cannotUndo")}
-
- )} + ) : ( +
+ {t("chat:checkpoint.menu.restoreFilesAndTaskDescription")} +
+ )} +
- - - )} +
+ +
) } From 4df4b51ae86aefd93c929b20fe2e259a6dd52a74 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Wed, 30 Jul 2025 00:17:10 +0800 Subject: [PATCH 6/9] fix: ensure checkpoint service is undefined on initialization error and improve checkpoint diff handling --- .../presentAssistantMessage.ts | 18 ++++++++++++++---- src/core/checkpoints/index.ts | 11 ++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index d59052c8a1..ce37468547 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -580,10 +580,20 @@ export async function presentAssistantMessage(cline: Task) { presentAssistantMessage(cline) } } -async function checkpointSaveAndMark(cline: Task) { - if (cline.currentStreamingDidCheckpoint) { + +/** + * save checkpoint and mark done in the current streaming task. + * @param task The Task instance to checkpoint save and mark. + * @returns + */ +async function checkpointSaveAndMark(task: Task) { + if (task.currentStreamingDidCheckpoint) { return } - await cline.checkpointSave(true) - cline.currentStreamingDidCheckpoint = true + try { + await task.checkpointSave(true) + task.currentStreamingDidCheckpoint = true + } catch (error) { + console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error) + } } diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index e27005721e..e8768a2234 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -140,6 +140,7 @@ async function checkGitInstallation( } catch (err) { log("[Task#getCheckpointService] caught error in on('initialize'), disabling checkpoints") cline.enableCheckpoints = false + cline.checkpointService = undefined } }) @@ -283,19 +284,19 @@ export async function checkpointDiff(cline: Task, { ts, previousCommitHash, comm TelemetryService.instance.captureCheckpointDiffed(cline.taskId) - let from = commitHash - let to: string | undefined + let prevHash = commitHash + let nextHash: string | undefined const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : [] const idx = checkpoints.indexOf(commitHash) if (idx !== -1 && idx < checkpoints.length - 1) { - to = checkpoints[idx + 1] + nextHash = checkpoints[idx + 1] } else { - to = undefined + nextHash = undefined } try { - const changes = await service.getDiff({ from, to }) + const changes = await service.getDiff({ from: prevHash, to: nextHash }) if (!changes?.length) { vscode.window.showInformationMessage("No changes found.") From 8ae1a2fbd780db95906d2b5ce5a58b168a1dc301 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Sun, 3 Aug 2025 21:26:55 +0800 Subject: [PATCH 7/9] refactor: simplify checkpoint service initialization and cleanup unused variables in CheckpointMenu --- src/core/checkpoints/index.ts | 17 +---------------- .../chat/checkpoints/CheckpointMenu.tsx | 14 +++++--------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index e8768a2234..1be1100d31 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -126,22 +126,7 @@ async function checkGitInstallation( // Git is installed, proceed with initialization service.on("initialize", () => { log("[Task#getCheckpointService] service initialized") - - try { - const isCheckpointNeeded = - typeof cline.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined" - - cline.checkpointServiceInitializing = false - - if (isCheckpointNeeded) { - log("[Task#getCheckpointService] no checkpoints found, saving initial checkpoint") - checkpointSave(cline) - } - } catch (err) { - log("[Task#getCheckpointService] caught error in on('initialize'), disabling checkpoints") - cline.enableCheckpoints = false - cline.checkpointService = undefined - } + cline.checkpointServiceInitializing = false }) service.on("checkpoint", ({ isFirst, fromHash: from, toHash: to }) => { diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index bb0d599d55..eba47699ab 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -22,8 +22,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec const portalContainer = useRooPortal("roo-portal") const isCurrent = currentHash === commitHash - const isFirst = checkpoint.isFirst - const isDiffAvailable = !isFirst const previousCommitHash = checkpoint?.from @@ -46,13 +44,11 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec return (
- {isDiffAvailable && ( - - - - )} + + + { From 8c5aa5538e3a2b8cd4bf32579e69966e466f7290 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Sun, 3 Aug 2025 12:02:37 -0500 Subject: [PATCH 8/9] fix: prevent race condition in checkpoint service initialization - Only assign service to cline.checkpointService after successful initialization - Add proper cleanup on initialization failure - Prevents service from being in inconsistent state if Git check fails --- src/core/checkpoints/index.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index 1be1100d31..f08dc24e16 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -81,14 +81,19 @@ export async function getCheckpointService( const service = RepoPerTaskCheckpointService.create(options) cline.checkpointServiceInitializing = true - cline.checkpointService = service // Check if Git is installed before initializing the service - // Note: This is intentionally fire-and-forget to match the original IIFE pattern - // The service is returned immediately while Git check happens asynchronously - await checkGitInstallation(cline, service, log, provider) - - return service + // Only assign the service after successful initialization + try { + await checkGitInstallation(cline, service, log, provider) + cline.checkpointService = service + return service + } catch (err) { + // Clean up on failure + cline.checkpointServiceInitializing = false + cline.enableCheckpoints = false + throw err + } } catch (err) { log(`[Task#getCheckpointService] ${err.message}`) cline.enableCheckpoints = false From 9983c60795bd2ef1a14762c950d601e54af49da7 Mon Sep 17 00:00:00 2001 From: NaccOll Date: Mon, 4 Aug 2025 06:32:39 +0800 Subject: [PATCH 9/9] fix: remove checkpoint save from presentAssistantMessage for update_todo_list case --- src/core/assistant-message/presentAssistantMessage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ce37468547..acdc7f5412 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -414,7 +414,6 @@ export async function presentAssistantMessage(cline: Task) { await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break case "update_todo_list": - await checkpointSaveAndMark(cline) await updateTodoListTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) break case "apply_diff": {