@@ -1273,6 +1273,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12731273 }
12741274 }
12751275
1276+ /**
1277+ * Aborts the current task and optionally saves messages.
1278+ *
1279+ * @param isAbandoned - If true, marks the task as abandoned (no cleanup needed)
1280+ * @param skipSave - If true, skips saving messages (used during user cancellation when messages are already saved)
1281+ */
12761282 public async abortTask ( isAbandoned = false , skipSave = false ) {
12771283 console . log ( `[subtasks] aborting task ${ this . taskId } .${ this . instanceId } ` )
12781284
@@ -1305,6 +1311,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
13051311 /**
13061312 * Reset the task to a resumable state without recreating the instance.
13071313 * This is used when canceling a task to avoid unnecessary rerenders.
1314+ *
1315+ * IMPORTANT: This method cleans up resources to prevent memory leaks
1316+ * while preserving the task instance for resumption.
13081317 */
13091318 public async resetToResumableState ( ) {
13101319 console . log ( `[subtasks] resetting task ${ this . taskId } .${ this . instanceId } to resumable state` )
@@ -1350,9 +1359,27 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
13501359 await this . diffViewProvider . reset ( )
13511360 }
13521361
1353- // The task is now ready to be resumed
1354- // The API request status has already been updated by abortStream
1355- // We don't add the resume_task message here because ask() will add it
1362+ // Dispose of resources that could accumulate if tasks are repeatedly cancelled
1363+ // These will be recreated as needed when the task resumes
1364+ try {
1365+ // Close browser sessions to free memory and browser processes
1366+ if ( this . urlContentFetcher ) {
1367+ this . urlContentFetcher . closeBrowser ( )
1368+ }
1369+ if ( this . browserSession ) {
1370+ this . browserSession . closeBrowser ( )
1371+ }
1372+
1373+ // Release any terminals associated with this task
1374+ // They will be recreated if needed when the task resumes
1375+ if ( this . terminalProcess ) {
1376+ this . terminalProcess . abort ( )
1377+ this . terminalProcess = undefined
1378+ }
1379+ } catch ( error ) {
1380+ console . error ( `Error disposing resources during resetToResumableState: ${ error } ` )
1381+ // Continue even if resource cleanup fails
1382+ }
13561383
13571384 // Keep messages and history intact for resumption
13581385 // The task is now ready to be resumed without recreation
@@ -1758,30 +1785,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
17581785
17591786 // Need to call here in case the stream was aborted.
17601787 if ( this . abort || this . abandoned ) {
1761- // If this was a user cancellation, reset and show resume prompt
1788+ // If this was a user cancellation, handle resumption
17621789 if ( this . abort && ! this . abandoned ) {
1763- // Reset the task to a resumable state
1764- this . abort = false
1765- await this . resetToResumableState ( )
1766-
1767- // Show the resume prompt
1768- const { response, text, images } = await this . ask ( "resume_task" )
1769-
1770- if ( response === "messageResponse" ) {
1771- await this . say ( "user_feedback" , text , images )
1772- // Continue with the new user input
1773- const newUserContent : Anthropic . Messages . ContentBlockParam [ ] = [ ]
1774- if ( text ) {
1775- newUserContent . push ( { type : "text" , text } )
1776- }
1777- if ( images && images . length > 0 ) {
1778- newUserContent . push ( ...formatResponse . imageBlocks ( images ) )
1779- }
1780- // Recursively continue with the new content
1781- return await this . recursivelyMakeClineRequests ( newUserContent )
1782- }
1783- // If not messageResponse, the task will end
1784- return true
1790+ return await this . handleUserCancellationResume ( )
17851791 }
17861792 throw new Error ( `[RooCode#recursivelyMakeRooRequests] task ${ this . taskId } .${ this . instanceId } aborted` )
17871793 }
@@ -2253,6 +2259,45 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
22532259 }
22542260 }
22552261
2262+ /**
2263+ * Handles the resumption flow after a user cancels a task.
2264+ * This method resets the task state, shows the resume prompt,
2265+ * and continues with new user input if provided.
2266+ *
2267+ * @returns Promise<boolean> - true if the task should end, false to continue
2268+ */
2269+ private async handleUserCancellationResume ( ) : Promise < boolean > {
2270+ try {
2271+ // Reset the task to a resumable state
2272+ this . abort = false
2273+ await this . resetToResumableState ( )
2274+
2275+ // Show the resume prompt
2276+ const { response, text, images } = await this . ask ( "resume_task" )
2277+
2278+ if ( response === "messageResponse" ) {
2279+ await this . say ( "user_feedback" , text , images )
2280+ // Continue with the new user input
2281+ const newUserContent : Anthropic . Messages . ContentBlockParam [ ] = [ ]
2282+ if ( text ) {
2283+ newUserContent . push ( { type : "text" , text } )
2284+ }
2285+ if ( images && images . length > 0 ) {
2286+ newUserContent . push ( ...formatResponse . imageBlocks ( images ) )
2287+ }
2288+ // Recursively continue with the new content
2289+ return await this . recursivelyMakeClineRequests ( newUserContent )
2290+ }
2291+ // If not messageResponse, the task will end
2292+ return true
2293+ } catch ( error ) {
2294+ // If there's an error during resumption, log it and end the task
2295+ console . error ( `Error during user cancellation resume: ${ error } ` )
2296+ // Re-throw to maintain existing error handling behavior
2297+ throw error
2298+ }
2299+ }
2300+
22562301 // Getters
22572302
22582303 public get cwd ( ) {
0 commit comments