fix: prevent conversation history loss when canceling during response

- Add explicit saveApiConversationHistory() call after adding interrupted message in abortStream
- Reverse order of abortStream/abortTask calls to save state before cleanup
- Add safety check in saveApiMessages to prevent overwriting non-empty history with empty array

Fixes #8153
This commit is contained in:
Roo Code 2025-09-18 19:30:38 +00:00
parent 87b45def18
commit 41789177d3
2 changed files with 32 additions and 2 deletions

View file

@ -79,5 +79,31 @@ export async function saveApiMessages({
}) {
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory)
// Safety check: prevent saving an empty array if a non-empty conversation previously existed
if (Array.isArray(messages) && messages.length === 0) {
// Check if there's an existing non-empty conversation history
if (await fileExistsAtPath(filePath)) {
try {
const existingContent = await fs.readFile(filePath, "utf8")
const existingData = JSON.parse(existingContent)
if (Array.isArray(existingData) && existingData.length > 0) {
console.error(
`[Roo-Debug] saveApiMessages: Attempted to save empty array over existing non-empty conversation. ` +
`TaskId: ${taskId}, Existing messages count: ${existingData.length}. ` +
`Skipping save to prevent data loss.`,
)
return // Don't save empty array over non-empty conversation
}
} catch (error) {
// If we can't read/parse the existing file, proceed with save
console.error(
`[Roo-Debug] saveApiMessages: Error checking existing conversation history. ` +
`TaskId: ${taskId}, Error: ${error}. Proceeding with save.`,
)
}
}
}
await safeWriteJson(filePath, messages)
}

View file

@ -1904,6 +1904,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
],
})
// Explicitly save the API conversation history after adding the interrupted message
await this.saveApiConversationHistory()
// Update `api_req_started` to have cancelled and cost, so that
// we can display the cost of the partial stream.
updateApiReqMsg(cancelReason, streamingFailedMessage)
@ -2196,9 +2199,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
? undefined
: (error.message ?? JSON.stringify(serializeError(error), null, 2))
// Now call abortTask after determining the cancel reason.
await this.abortTask()
// Reverse the order: call abortStream first to properly save the interrupted message
// before abortTask cleans up resources
await abortStream(cancelReason, streamingFailedMessage)
await this.abortTask()
const history = await provider?.getTaskWithId(this.taskId)