Skip to content

Commit a5b55da

Browse files
NaccOlldaniel-lxs
andauthored
Changing checkpoint timing and ensuring checkpoints work (#6359)
* feat: Before requesting, ensure checkpoint is initialized * Generate a checkpoint before modifying the code * refactor: streamline checkpoint handling and enhance getCheckpoints method * Blocked waiting for checkpoint initialization timing to change * cancel checkpoint restore limit * fix: ensure checkpoint service is undefined on initialization error and improve checkpoint diff handling * refactor: simplify checkpoint service initialization and cleanup unused variables in CheckpointMenu * 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 * fix: remove checkpoint save from presentAssistantMessage for update_todo_list case --------- Co-authored-by: Daniel Riccio <[email protected]>
1 parent a88238f commit a5b55da

File tree

5 files changed

+134
-145
lines changed

5 files changed

+134
-145
lines changed

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { switchModeTool } from "../tools/switchModeTool"
2525
import { attemptCompletionTool } from "../tools/attemptCompletionTool"
2626
import { newTaskTool } from "../tools/newTaskTool"
2727

28-
import { checkpointSave } from "../checkpoints"
2928
import { updateTodoListTool } from "../tools/updateTodoListTool"
3029

3130
import { formatResponse } from "../prompts/responses"
@@ -411,6 +410,7 @@ export async function presentAssistantMessage(cline: Task) {
411410

412411
switch (block.name) {
413412
case "write_to_file":
413+
await checkpointSaveAndMark(cline)
414414
await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
415415
break
416416
case "update_todo_list":
@@ -430,8 +430,10 @@ export async function presentAssistantMessage(cline: Task) {
430430
}
431431

432432
if (isMultiFileApplyDiffEnabled) {
433+
await checkpointSaveAndMark(cline)
433434
await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
434435
} else {
436+
await checkpointSaveAndMark(cline)
435437
await applyDiffToolLegacy(
436438
cline,
437439
block,
@@ -444,9 +446,11 @@ export async function presentAssistantMessage(cline: Task) {
444446
break
445447
}
446448
case "insert_content":
449+
await checkpointSaveAndMark(cline)
447450
await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
448451
break
449452
case "search_and_replace":
453+
await checkpointSaveAndMark(cline)
450454
await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
451455
break
452456
case "read_file":
@@ -527,14 +531,6 @@ export async function presentAssistantMessage(cline: Task) {
527531
break
528532
}
529533

530-
const recentlyModifiedFiles = cline.fileContextTracker.getAndClearCheckpointPossibleFile()
531-
532-
if (recentlyModifiedFiles.length > 0) {
533-
// TODO: We can track what file changes were made and only
534-
// checkpoint those files, this will be save storage.
535-
await checkpointSave(cline)
536-
}
537-
538534
// Seeing out of bounds is fine, it means that the next too call is being
539535
// built up and ready to add to assistantMessageContent to present.
540536
// When you see the UI inactive during this, it means that a tool is
@@ -583,3 +579,20 @@ export async function presentAssistantMessage(cline: Task) {
583579
presentAssistantMessage(cline)
584580
}
585581
}
582+
583+
/**
584+
* save checkpoint and mark done in the current streaming task.
585+
* @param task The Task instance to checkpoint save and mark.
586+
* @returns
587+
*/
588+
async function checkpointSaveAndMark(task: Task) {
589+
if (task.currentStreamingDidCheckpoint) {
590+
return
591+
}
592+
try {
593+
await task.checkpointSave(true)
594+
task.currentStreamingDidCheckpoint = true
595+
} catch (error) {
596+
console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error)
597+
}
598+
}

src/core/checkpoints/index.ts

Lines changed: 46 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,29 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider
1616

1717
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
1818

19-
export function getCheckpointService(cline: Task) {
19+
export async function getCheckpointService(
20+
cline: Task,
21+
{ interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {},
22+
) {
2023
if (!cline.enableCheckpoints) {
2124
return undefined
2225
}
2326

2427
if (cline.checkpointService) {
25-
return cline.checkpointService
26-
}
27-
28-
if (cline.checkpointServiceInitializing) {
29-
console.log("[Task#getCheckpointService] checkpoint service is still initializing")
30-
return undefined
28+
if (cline.checkpointServiceInitializing) {
29+
console.log("[Task#getCheckpointService] checkpoint service is still initializing")
30+
const service = cline.checkpointService
31+
await pWaitFor(
32+
() => {
33+
console.log("[Task#getCheckpointService] waiting for service to initialize")
34+
return service.isInitialized
35+
},
36+
{ interval, timeout },
37+
)
38+
return service.isInitialized ? cline.checkpointService : undefined
39+
} else {
40+
return cline.checkpointService
41+
}
3142
}
3243

3344
const provider = cline.providerRef.deref()
@@ -69,15 +80,20 @@ export function getCheckpointService(cline: Task) {
6980
}
7081

7182
const service = RepoPerTaskCheckpointService.create(options)
72-
7383
cline.checkpointServiceInitializing = true
7484

7585
// Check if Git is installed before initializing the service
76-
// Note: This is intentionally fire-and-forget to match the original IIFE pattern
77-
// The service is returned immediately while Git check happens asynchronously
78-
checkGitInstallation(cline, service, log, provider)
79-
80-
return service
86+
// Only assign the service after successful initialization
87+
try {
88+
await checkGitInstallation(cline, service, log, provider)
89+
cline.checkpointService = service
90+
return service
91+
} catch (err) {
92+
// Clean up on failure
93+
cline.checkpointServiceInitializing = false
94+
cline.enableCheckpoints = false
95+
throw err
96+
}
8197
} catch (err) {
8298
log(`[Task#getCheckpointService] ${err.message}`)
8399
cline.enableCheckpoints = false
@@ -115,22 +131,7 @@ async function checkGitInstallation(
115131
// Git is installed, proceed with initialization
116132
service.on("initialize", () => {
117133
log("[Task#getCheckpointService] service initialized")
118-
119-
try {
120-
const isCheckpointNeeded =
121-
typeof cline.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined"
122-
123-
cline.checkpointService = service
124-
cline.checkpointServiceInitializing = false
125-
126-
if (isCheckpointNeeded) {
127-
log("[Task#getCheckpointService] no checkpoints found, saving initial checkpoint")
128-
checkpointSave(cline)
129-
}
130-
} catch (err) {
131-
log("[Task#getCheckpointService] caught error in on('initialize'), disabling checkpoints")
132-
cline.enableCheckpoints = false
133-
}
134+
cline.checkpointServiceInitializing = false
134135
})
135136

136137
service.on("checkpoint", ({ isFirst, fromHash: from, toHash: to }) => {
@@ -153,11 +154,12 @@ async function checkGitInstallation(
153154
})
154155

155156
log("[Task#getCheckpointService] initializing shadow git")
156-
157-
service.initShadowGit().catch((err) => {
157+
try {
158+
await service.initShadowGit()
159+
} catch (err) {
158160
log(`[Task#getCheckpointService] initShadowGit -> ${err.message}`)
159161
cline.enableCheckpoints = false
160-
})
162+
}
161163
} catch (err) {
162164
log(`[Task#getCheckpointService] Unexpected error during Git check: ${err.message}`)
163165
console.error("Git check error:", err)
@@ -166,33 +168,8 @@ async function checkGitInstallation(
166168
}
167169
}
168170

169-
async function getInitializedCheckpointService(
170-
cline: Task,
171-
{ interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {},
172-
) {
173-
const service = getCheckpointService(cline)
174-
175-
if (!service || service.isInitialized) {
176-
return service
177-
}
178-
179-
try {
180-
await pWaitFor(
181-
() => {
182-
console.log("[Task#getCheckpointService] waiting for service to initialize")
183-
return service.isInitialized
184-
},
185-
{ interval, timeout },
186-
)
187-
188-
return service
189-
} catch (err) {
190-
return undefined
191-
}
192-
}
193-
194171
export async function checkpointSave(cline: Task, force = false) {
195-
const service = getCheckpointService(cline)
172+
const service = await getCheckpointService(cline)
196173

197174
if (!service) {
198175
return
@@ -221,7 +198,7 @@ export type CheckpointRestoreOptions = {
221198
}
222199

223200
export async function checkpointRestore(cline: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) {
224-
const service = await getInitializedCheckpointService(cline)
201+
const service = await getCheckpointService(cline)
225202

226203
if (!service) {
227204
return
@@ -289,25 +266,27 @@ export type CheckpointDiffOptions = {
289266
}
290267

291268
export async function checkpointDiff(cline: Task, { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions) {
292-
const service = await getInitializedCheckpointService(cline)
269+
const service = await getCheckpointService(cline)
293270

294271
if (!service) {
295272
return
296273
}
297274

298275
TelemetryService.instance.captureCheckpointDiffed(cline.taskId)
299276

300-
if (!previousCommitHash && mode === "checkpoint") {
301-
const previousCheckpoint = cline.clineMessages
302-
.filter(({ say }) => say === "checkpoint_saved")
303-
.sort((a, b) => b.ts - a.ts)
304-
.find((message) => message.ts < ts)
277+
let prevHash = commitHash
278+
let nextHash: string | undefined
305279

306-
previousCommitHash = previousCheckpoint?.text
280+
const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : []
281+
const idx = checkpoints.indexOf(commitHash)
282+
if (idx !== -1 && idx < checkpoints.length - 1) {
283+
nextHash = checkpoints[idx + 1]
284+
} else {
285+
nextHash = undefined
307286
}
308287

309288
try {
310-
const changes = await service.getDiff({ from: previousCommitHash, to: commitHash })
289+
const changes = await service.getDiff({ from: prevHash, to: nextHash })
311290

312291
if (!changes?.length) {
313292
vscode.window.showInformationMessage("No changes found.")

src/core/task/Task.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
240240
isWaitingForFirstChunk = false
241241
isStreaming = false
242242
currentStreamingContentIndex = 0
243+
currentStreamingDidCheckpoint = false
243244
assistantMessageContent: AssistantMessageContent[] = []
244245
presentAssistantMessageLocked = false
245246
presentAssistantMessageHasPendingUpdates = false
@@ -1543,6 +1544,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15431544

15441545
// Reset streaming state.
15451546
this.currentStreamingContentIndex = 0
1547+
this.currentStreamingDidCheckpoint = false
15461548
this.assistantMessageContent = []
15471549
this.didCompleteReadingStream = false
15481550
this.userMessageContent = []

src/services/checkpoints/ShadowCheckpointService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export abstract class ShadowCheckpointService extends EventEmitter {
3838
return !!this.git
3939
}
4040

41+
public getCheckpoints(): string[] {
42+
return this._checkpoints.slice()
43+
}
44+
4145
constructor(taskId: string, checkpointsDir: string, workspaceDir: string, log: (message: string) => void) {
4246
super()
4347

0 commit comments

Comments
 (0)