From 7b22715557641264337f733f790fa8b7a77d2667 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 18 Sep 2025 16:22:23 +0000 Subject: [PATCH] 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 --- src/core/task-persistence/apiMessages.ts | 15 ++++++++++ src/core/task-persistence/taskMessages.ts | 15 ++++++++++ src/core/task/Task.ts | 34 +++++++++++++++++++++-- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/core/task-persistence/apiMessages.ts b/src/core/task-persistence/apiMessages.ts index f846aaf13f..04e9d5c6b5 100644 --- a/src/core/task-persistence/apiMessages.ts +++ b/src/core/task-persistence/apiMessages.ts @@ -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) diff --git a/src/core/task-persistence/taskMessages.ts b/src/core/task-persistence/taskMessages.ts index 63a2eefbaa..344cfaf207 100644 --- a/src/core/task-persistence/taskMessages.ts +++ b/src/core/task-persistence/taskMessages.ts @@ -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) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cf16df8dcc..83269eda8b 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -589,6 +589,14 @@ export class Task extends EventEmitter 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 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 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) }