fix: preserve conversation state when cancelling reasoning model mid-thinking

- Modified ClineProvider.cancelTask() to preserve current conversation state instead of reverting to saved history
- Enhanced abortStream() to properly handle partial reasoning messages during cancellation
- Ensures frontend displays correct current conversation rather than previous conversation from disk
- Fixes issue #5810 where cancelling reasoning models caused conversation replacement

The fix works by:
1. Capturing current clineMessages and apiConversationHistory before cancellation
2. Creating new task instance with preserved state instead of loading from saved history
3. Properly handling partial reasoning messages during stream abortion
4. Maintaining conversation integrity throughout the cancellation process
This commit is contained in:
Roo Code 2025-07-17 11:40:29 +00:00
parent 6cf376f832
commit 32d70c7541
3 changed files with 7664 additions and 3 deletions

7625
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1306,7 +1306,14 @@ export class Task extends EventEmitter<ClineEvents> {
lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
console.log("updating partial message", lastMessage)
// await this.saveClineMessages()
// For reasoning messages, preserve the current content
if (lastMessage.say === "reasoning" && reasoningMessage) {
lastMessage.text = reasoningMessage
}
// Save the updated partial message to preserve current state
await this.saveClineMessages()
}
// Let assistant know their response was interrupted for when task is resumed

View file

@ -969,6 +969,10 @@ export class ClineProvider
console.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`)
// Preserve current conversation state before cancellation
const currentClineMessages = [...cline.clineMessages]
const currentApiHistory = [...cline.apiConversationHistory]
const { historyItem } = await this.getTaskWithId(cline.taskId)
// Preserve parent and root task information for history item.
const rootTask = cline.rootTask
@ -999,8 +1003,33 @@ export class ClineProvider
this.getCurrentCline()!.abandoned = true
}
// Clears task again, so we need to abortTask manually above.
await this.initClineWithHistoryItem({ ...historyItem, rootTask, parentTask })
// Create new task with preserved conversation state instead of reverting to saved history
const newCline = new Task({
provider: this,
apiConfiguration: cline.apiConfiguration,
enableDiff: cline.diffEnabled,
enableCheckpoints: cline.enableCheckpoints,
fuzzyMatchThreshold: cline.fuzzyMatchThreshold,
consecutiveMistakeLimit: cline.consecutiveMistakeLimit,
historyItem: historyItem,
experiments: (await this.getState()).experiments,
startTask: false,
rootTask,
parentTask,
taskNumber: cline.taskNumber,
onCreated: (cline) => this.emit("clineCreated", cline),
})
// Restore the current conversation state instead of loading from disk
await newCline.overwriteClineMessages(currentClineMessages)
await newCline.overwriteApiConversationHistory(currentApiHistory)
// Mark as initialized and add to stack
newCline.isInitialized = true
await this.addClineToStack(newCline)
// Update webview with current state
await this.postStateToWebview()
}
async updateCustomInstructions(instructions?: string) {