From 5f46f32dabe36fd293fd5df5f3899fb6b8a908ae Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 3 Feb 2026 07:42:16 +0000 Subject: [PATCH] fix(EXT-696): prevent parent task deletion when API history file missing This fixes the issue where parent tasks disappear from the UI in orchestrator mode after delegating to a child task via new_task tool. Root cause: In getTaskWithId(), if the historyItem existed in taskHistory but the apiConversationHistory.json file did not exist (due to race conditions during delegation), the method would incorrectly delete the task from state. Fix: When historyItem exists but the API history file does not, return the task with an empty apiConversationHistory array instead of deleting it. This preserves the task metadata during delegation and prevents the parent task from disappearing from the UI. Added tests to verify: - Task is preserved with empty API history when file is missing - Task is properly deleted when historyItem does not exist --- src/core/webview/ClineProvider.ts | 18 ++++++- .../ClineProvider.taskHistory.spec.ts | 54 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bc3f6bd6ef..ce8120e82e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1684,10 +1684,24 @@ export class ClineProvider apiConversationHistory, } } + + // EXT-696: historyItem exists but API conversation file doesn't. + // This can happen during race conditions (e.g., delegation before file is flushed). + // Return with empty apiConversationHistory instead of deleting the task. + // The task's metadata is still valid and should be preserved. + this.log( + `[getTaskWithId] Task ${id} exists in history but API conversation file not found. Returning with empty history.`, + ) + return { + historyItem, + taskDirPath, + apiConversationHistoryFilePath, + uiMessagesFilePath, + apiConversationHistory: [], + } } - // if we tried to get a task that doesn't exist, remove it from state - // FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason + // Only delete from state if historyItem truly doesn't exist await this.deleteTaskFromState(id) throw new Error("Task not found") } diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index f5e6afa7f0..a88d2b0967 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -592,4 +592,58 @@ describe("ClineProvider Task History Synchronization", () => { expect(state.taskHistory.some((item: HistoryItem) => item.workspace === "/different/workspace")).toBe(true) }) }) + + describe("getTaskWithId", () => { + it("returns task with empty apiConversationHistory when file does not exist but historyItem exists (EXT-696 fix)", async () => { + await provider.resolveWebviewView(mockWebviewView) + provider.isViewLaunched = true + + const historyItem = createHistoryItem({ + id: "delegated-parent-task", + task: "Parent task that was delegated", + }) + + // Add the task to history + await provider.updateTaskHistory(historyItem) + + // Mock fileExistsAtPath to return false (simulating race condition during delegation) + const fsUtils = await import("../../../utils/fs") + const fileExistsSpy = vi.spyOn(fsUtils, "fileExistsAtPath").mockResolvedValue(false) + + // Track if deleteTaskFromState was called + const deleteTaskSpy = vi.spyOn(provider, "deleteTaskFromState") + + // Call getTaskWithId + const result = await provider.getTaskWithId("delegated-parent-task") + + // Should return the task with empty API conversation history, NOT delete it + expect(result.historyItem.id).toBe("delegated-parent-task") + expect(result.historyItem.task).toBe("Parent task that was delegated") + expect(result.apiConversationHistory).toEqual([]) + + // Should NOT have called deleteTaskFromState + expect(deleteTaskSpy).not.toHaveBeenCalled() + + // Clean up + fileExistsSpy.mockRestore() + deleteTaskSpy.mockRestore() + }) + + it("throws error and deletes task when historyItem does not exist", async () => { + await provider.resolveWebviewView(mockWebviewView) + provider.isViewLaunched = true + + // Track if deleteTaskFromState was called + const deleteTaskSpy = vi.spyOn(provider, "deleteTaskFromState").mockResolvedValue() + + // Call getTaskWithId with a task that doesn't exist + await expect(provider.getTaskWithId("non-existent-task")).rejects.toThrow("Task not found") + + // Should have called deleteTaskFromState + expect(deleteTaskSpy).toHaveBeenCalledWith("non-existent-task") + + // Clean up + deleteTaskSpy.mockRestore() + }) + }) })