@@ -6,6 +6,7 @@ import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
66import { getReadablePath } from "../../utils/path"
77import { isPathOutsideWorkspace } from "../../utils/pathUtils"
88import { Task } from "../task/Task"
9+ import { checkpointSave } from "../checkpoints"
910import { formatResponse } from "../prompts/responses"
1011import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1112import { fileExistsAtPath } from "../../utils/fs"
@@ -102,7 +103,10 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
102103 return
103104 }
104105
105- // Process each file change
106+ // Process each file change. The handlers report whether their file
107+ // operation succeeded, so a rejected approval or a failed local write
108+ // does not get checkpointed as if the patch had succeeded.
109+ let patchSucceeded = true
106110 for ( const change of changes ) {
107111 const relPath = change . path
108112 const absolutePath = path . resolve ( task . cwd , relPath )
@@ -120,17 +124,39 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
120124
121125 if ( change . type === "add" ) {
122126 // Create new file
123- await this . handleAddFile ( change , absolutePath , relPath , task , callbacks , isWriteProtected )
127+ patchSucceeded =
128+ ( await this . handleAddFile ( change , absolutePath , relPath , task , callbacks , isWriteProtected ) ) &&
129+ patchSucceeded
124130 } else if ( change . type === "delete" ) {
125131 // Delete file
126- await this . handleDeleteFile ( absolutePath , relPath , task , callbacks , isWriteProtected )
132+ patchSucceeded =
133+ ( await this . handleDeleteFile ( absolutePath , relPath , task , callbacks , isWriteProtected ) ) &&
134+ patchSucceeded
127135 } else if ( change . type === "update" ) {
128136 // Update file
129- await this . handleUpdateFile ( change , absolutePath , relPath , task , callbacks , isWriteProtected )
137+ patchSucceeded =
138+ ( await this . handleUpdateFile (
139+ change ,
140+ absolutePath ,
141+ relPath ,
142+ task ,
143+ callbacks ,
144+ isWriteProtected ,
145+ ) ) && patchSucceeded
130146 }
131147 }
132148
133149 task . consecutiveMistakeCount = 0
150+
151+ // B1: one checkpoint for the whole patch (not per file), and only when
152+ // every file operation succeeded. Live setting with default-on
153+ // semantics: skip only when explicitly false.
154+ if ( patchSucceeded ) {
155+ const perWriteCheckpoints = ( await task . providerRef ?. deref ( ) ?. getState ( ) ) ?. perWriteCheckpoints
156+ if ( perWriteCheckpoints !== false ) {
157+ void checkpointSave ( task , false , true ) . catch ( ( ) => { } )
158+ }
159+ }
134160 } catch ( error ) {
135161 await handleError ( "apply patch" , error as Error )
136162 await task . diffViewProvider . reset ( )
@@ -144,7 +170,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
144170 task : Task ,
145171 callbacks : ToolCallbacks ,
146172 isWriteProtected : boolean ,
147- ) : Promise < void > {
173+ ) : Promise < boolean > {
148174 const { askApproval, pushToolResult } = callbacks
149175
150176 // Check if file already exists
@@ -155,7 +181,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
155181 const errorMessage = `File already exists: ${ relPath } . Use Update File instead.`
156182 await task . say ( "error" , errorMessage )
157183 pushToolResult ( formatResponse . toolError ( errorMessage ) )
158- return
184+ return false
159185 }
160186
161187 const newContent = change . newContent || ""
@@ -209,7 +235,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
209235 }
210236 pushToolResult ( "Changes were rejected by the user." )
211237 await task . diffViewProvider . reset ( )
212- return
238+ return false
213239 }
214240
215241 // Save the changes
@@ -227,6 +253,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
227253 pushToolResult ( message )
228254 await task . diffViewProvider . reset ( )
229255 task . processQueuedMessages ( )
256+ return true
230257 }
231258
232259 private async handleDeleteFile (
@@ -235,7 +262,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
235262 task : Task ,
236263 callbacks : ToolCallbacks ,
237264 isWriteProtected : boolean ,
238- ) : Promise < void > {
265+ ) : Promise < boolean > {
239266 const { askApproval, pushToolResult } = callbacks
240267
241268 // Check if file exists
@@ -246,7 +273,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
246273 const errorMessage = `File not found: ${ relPath } . Cannot delete a non-existent file.`
247274 await task . say ( "error" , errorMessage )
248275 pushToolResult ( formatResponse . toolError ( errorMessage ) )
249- return
276+ return false
250277 }
251278
252279 const isOutsideWorkspace = isPathOutsideWorkspace ( absolutePath )
@@ -268,7 +295,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
268295
269296 if ( ! didApprove ) {
270297 pushToolResult ( "Delete operation was rejected by the user." )
271- return
298+ return false
272299 }
273300
274301 // Delete the file
@@ -278,12 +305,13 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
278305 const errorMessage = `Failed to delete file '${ relPath } ': ${ error instanceof Error ? error . message : String ( error ) } `
279306 await task . say ( "error" , errorMessage )
280307 pushToolResult ( formatResponse . toolError ( errorMessage ) )
281- return
308+ return false
282309 }
283310
284311 task . didEditFile = true
285312 pushToolResult ( `Successfully deleted ${ relPath } ` )
286313 task . processQueuedMessages ( )
314+ return true
287315 }
288316
289317 private async handleUpdateFile (
@@ -293,7 +321,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
293321 task : Task ,
294322 callbacks : ToolCallbacks ,
295323 isWriteProtected : boolean ,
296- ) : Promise < void > {
324+ ) : Promise < boolean > {
297325 const { askApproval, pushToolResult } = callbacks
298326
299327 // Check if file exists
@@ -304,7 +332,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
304332 const errorMessage = `File not found: ${ relPath } . Cannot update a non-existent file.`
305333 await task . say ( "error" , errorMessage )
306334 pushToolResult ( formatResponse . toolError ( errorMessage ) )
307- return
335+ return false
308336 }
309337
310338 const originalContent = change . originalContent || ""
@@ -318,9 +346,11 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
318346 // Generate and validate diff
319347 const diff = formatResponse . createPrettyPatch ( relPath , originalContent , newContent )
320348 if ( ! diff ) {
349+ // A no-op change is not a failure: the patch processed cleanly and
350+ // nothing was written, so the whole-patch success state is kept.
321351 pushToolResult ( `No changes needed for '${ relPath } '` )
322352 await task . diffViewProvider . reset ( )
323- return
353+ return true
324354 }
325355
326356 // Check experiment settings
@@ -366,7 +396,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
366396 }
367397 pushToolResult ( "Changes were rejected by the user." )
368398 await task . diffViewProvider . reset ( )
369- return
399+ return false
370400 }
371401
372402 // Handle file move if specified
@@ -379,7 +409,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
379409 await task . say ( "rooignore_error" , change . movePath )
380410 pushToolResult ( formatResponse . rooIgnoreError ( change . movePath ) )
381411 await task . diffViewProvider . reset ( )
382- return
412+ return false
383413 }
384414
385415 // Check if destination path is write-protected
@@ -391,7 +421,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
391421 await task . say ( "error" , errorMessage )
392422 pushToolResult ( formatResponse . toolError ( errorMessage ) )
393423 await task . diffViewProvider . reset ( )
394- return
424+ return false
395425 }
396426
397427 // Check if destination path is outside workspace
@@ -403,7 +433,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
403433 await task . say ( "error" , errorMessage )
404434 pushToolResult ( formatResponse . toolError ( errorMessage ) )
405435 await task . diffViewProvider . reset ( )
406- return
436+ return false
407437 }
408438
409439 // Save new content to the new path
@@ -447,6 +477,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
447477 pushToolResult ( message )
448478 await task . diffViewProvider . reset ( )
449479 task . processQueuedMessages ( )
480+ return true
450481 }
451482
452483 override async handlePartial ( task : Task , block : ToolUse < "apply_patch" > ) : Promise < void > {
0 commit comments