fix: prevent conversation history corruption when cancelling during response

- Add validation in abortTask to skip saving messages during active streaming
- Add empty array checks in save methods to prevent corruption
- Add validation in persistence layer to ensure valid message arrays
- This fixes the race condition where cancelling during streaming could save empty arrays and wipe conversation history

Fixes #8153
This commit is contained in:
Roo Code 2025-09-18 16:22:23 +00:00
parent 87b45def18
commit 7b22715557
3 changed files with 61 additions and 3 deletions

View file

@ -77,6 +77,21 @@ export async function saveApiMessages({
taskId: string
globalStoragePath: string
}) {
// Validate messages array to prevent saving empty arrays that could corrupt history
if (!messages || !Array.isArray(messages)) {
console.error(
`[Roo-Debug] saveApiMessages: Invalid messages provided for taskId: ${taskId}. Messages must be an array.`,
)
throw new Error("Invalid messages: must be an array")
}
// Warn if saving empty array but allow it for new tasks
if (messages.length === 0) {
console.warn(
`[Roo-Debug] saveApiMessages: Saving empty messages array for taskId: ${taskId}. This may be intentional for new tasks.`,
)
}
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory)
await safeWriteJson(filePath, messages)

View file

@ -36,6 +36,21 @@ export type SaveTaskMessagesOptions = {
}
export async function saveTaskMessages({ messages, taskId, globalStoragePath }: SaveTaskMessagesOptions) {
// Validate messages array to prevent saving empty arrays that could corrupt history
if (!messages || !Array.isArray(messages)) {
console.error(
`[Roo-Debug] saveTaskMessages: Invalid messages provided for taskId: ${taskId}. Messages must be an array.`,
)
throw new Error("Invalid messages: must be an array")
}
// Warn if saving empty array but allow it for new tasks
if (messages.length === 0) {
console.warn(
`[Roo-Debug] saveTaskMessages: Saving empty messages array for taskId: ${taskId}. This may be intentional for new tasks.`,
)
}
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await safeWriteJson(filePath, messages)

View file

@ -589,6 +589,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
private async saveApiConversationHistory() {
try {
// Validate that we have messages before saving to prevent corruption
if (!this.apiConversationHistory || this.apiConversationHistory.length === 0) {
console.warn(
`[Task#saveApiConversationHistory] Skipping save of empty API conversation history for task ${this.taskId}`,
)
return
}
await saveApiMessages({
messages: this.apiConversationHistory,
taskId: this.taskId,
@ -658,6 +666,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
private async saveClineMessages() {
try {
// Validate that we have messages before saving to prevent corruption
if (!this.clineMessages || this.clineMessages.length === 0) {
console.warn(`[Task#saveClineMessages] Skipping save of empty Cline messages for task ${this.taskId}`)
return
}
await saveTaskMessages({
messages: this.clineMessages,
taskId: this.taskId,
@ -1504,10 +1518,24 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
console.error(`Error during task ${this.taskId}.${this.instanceId} disposal:`, error)
// Don't rethrow - we want abort to always succeed
}
// Save the countdown message in the automatic retry or other content.
// Only save messages if they are not empty to prevent corruption
// This prevents the race condition where cancelling during streaming
// could save an empty array and wipe the conversation history
try {
// Save the countdown message in the automatic retry or other content.
await this.saveClineMessages()
// Check if we have valid messages before saving
// Don't save if messages are empty or if we're in the middle of streaming
if (this.clineMessages && this.clineMessages.length > 0 && !this.isStreaming) {
await this.saveClineMessages()
} else if (this.isStreaming) {
console.log(
`[Task#abortTask] Skipping message save during active streaming for task ${this.taskId}.${this.instanceId}`,
)
} else {
console.log(
`[Task#abortTask] Skipping message save due to empty messages for task ${this.taskId}.${this.instanceId}`,
)
}
} catch (error) {
console.error(`Error saving messages during abort for task ${this.taskId}.${this.instanceId}:`, error)
}