Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,26 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const iterator = stream[Symbol.asyncIterator]()
let item = await iterator.next()
while (!item.done) {
// Check abort flag BEFORE processing the chunk
if (this.abort) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding tests for this critical cancel functionality to prevent regression. The fix looks good, but tests would verify:

  • The abort flag is set immediately when cancel is clicked
  • The early abort check works before processing chunks
  • Iterator cleanup happens properly when aborting

console.log(`aborting stream (early check), this.abandoned = ${this.abandoned}`)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abort check and cleanup logic is duplicated here and at lines 1730-1747. Consider extracting this into a helper method like handleStreamAbort() to reduce duplication and ensure consistency:

Suggested change
console.log(`aborting stream (early check), this.abandoned = ${this.abandoned}`)
private async handleStreamAbort(iterator: AsyncIterator<any>, cancelReason: string) {
console.log(`aborting stream (${cancelReason}), this.abandoned = ${this.abandoned}`)
if (!this.abandoned) {
await abortStream(cancelReason)
}
if (iterator.return) {
await iterator.return(undefined).catch(() => {})
}
}


if (!this.abandoned) {
// Only need to gracefully abort if this instance
// isn't abandoned (sometimes OpenRouter stream
// hangs, in which case this would affect future
// instances of Cline).
await abortStream("user_cancelled")
}

// Clean up the iterator if it has a return method
if (iterator.return) {
await iterator.return(undefined).catch(() => {})
}

break // Aborts the stream.
}

const chunk = item.value
item = await iterator.next()
if (!chunk) {
Expand Down Expand Up @@ -1707,8 +1727,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}

// Check abort flag AFTER processing the chunk as well
if (this.abort) {
console.log(`aborting stream, this.abandoned = ${this.abandoned}`)
console.log(`aborting stream (after chunk), this.abandoned = ${this.abandoned}`)

if (!this.abandoned) {
// Only need to gracefully abort if this instance
Expand All @@ -1718,6 +1739,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await abortStream("user_cancelled")
}

// Clean up the iterator if it has a return method
if (iterator.return) {
await iterator.return(undefined).catch(() => {})
}

break // Aborts the stream.
}

Expand Down
6 changes: 5 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,10 @@ export class ClineProvider
const rootTask = cline.rootTask
const parentTask = cline.parentTask

// Set the abort flag immediately to signal cancellation
cline.abort = true
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix! Setting the abort flag immediately before calling abortTask() eliminates the race condition. This ensures the flag is set even if the stream check happens before abortTask() propagates the flag internally.


// Then call abortTask to handle cleanup
cline.abortTask()

await pWaitFor(
Expand All @@ -1243,7 +1247,7 @@ export class ClineProvider
timeout: 3_000,
},
).catch(() => {
console.error("Failed to abort task")
console.error("Failed to abort task gracefully")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Could be more specific about the timeout duration:

Suggested change
console.error("Failed to abort task gracefully")
console.error("Failed to abort task gracefully within 3 second timeout")

})

if (this.getCurrentCline()) {
Expand Down
Loading