|
|
@@ -89,6 +89,8 @@ import { Task } from "../task/Task"
|
|
|
import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
|
|
|
|
|
|
import { webviewMessageHandler } from "./webviewMessageHandler"
|
|
|
+import type { ClineMessage } from "@roo-code/types"
|
|
|
+import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
|
|
|
import { getNonce } from "./getNonce"
|
|
|
import { getUri } from "./getUri"
|
|
|
|
|
|
@@ -196,7 +198,35 @@ export class ClineProvider
|
|
|
const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
|
|
|
const onTaskCompleted = (taskId: string, tokenUsage: any, toolUsage: any) =>
|
|
|
this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
|
|
|
- const onTaskAborted = () => this.emit(RooCodeEventName.TaskAborted, instance.taskId)
|
|
|
+ const onTaskAborted = async () => {
|
|
|
+ this.emit(RooCodeEventName.TaskAborted, instance.taskId)
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Only rehydrate on genuine streaming failures.
|
|
|
+ // User-initiated cancels are handled by cancelTask().
|
|
|
+ if (instance.abortReason === "streaming_failed") {
|
|
|
+ // Defensive safeguard: if another path already replaced this instance, skip
|
|
|
+ const current = this.getCurrentTask()
|
|
|
+ if (current && current.instanceId !== instance.instanceId) {
|
|
|
+ this.log(
|
|
|
+ `[onTaskAborted] Skipping rehydrate: current instance ${current.instanceId} != aborted ${instance.instanceId}`,
|
|
|
+ )
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const { historyItem } = await this.getTaskWithId(instance.taskId)
|
|
|
+ const rootTask = instance.rootTask
|
|
|
+ const parentTask = instance.parentTask
|
|
|
+ await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.log(
|
|
|
+ `[onTaskAborted] Failed to rehydrate after streaming failure: ${
|
|
|
+ error instanceof Error ? error.message : String(error)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId)
|
|
|
const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId)
|
|
|
const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId)
|
|
|
@@ -2525,14 +2555,24 @@ export class ClineProvider
|
|
|
|
|
|
console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
|
|
|
|
|
|
- const { historyItem } = await this.getTaskWithId(task.taskId)
|
|
|
+ const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId)
|
|
|
|
|
|
// Preserve parent and root task information for history item.
|
|
|
const rootTask = task.rootTask
|
|
|
const parentTask = task.parentTask
|
|
|
|
|
|
+ // Mark this as a user-initiated cancellation so provider-only rehydration can occur
|
|
|
+ task.abortReason = "user_cancelled"
|
|
|
+
|
|
|
+ // Capture the current instance to detect if rehydrate already occurred elsewhere
|
|
|
+ const originalInstanceId = task.instanceId
|
|
|
+
|
|
|
+ // Begin abort (non-blocking)
|
|
|
task.abortTask()
|
|
|
|
|
|
+ // Immediately mark the original instance as abandoned to prevent any residual activity
|
|
|
+ task.abandoned = true
|
|
|
+
|
|
|
await pWaitFor(
|
|
|
() =>
|
|
|
this.getCurrentTask()! === undefined ||
|
|
|
@@ -2549,11 +2589,24 @@ export class ClineProvider
|
|
|
console.error("Failed to abort task")
|
|
|
})
|
|
|
|
|
|
- if (this.getCurrentTask()) {
|
|
|
- // 'abandoned' will prevent this Cline instance from affecting
|
|
|
- // future Cline instances. This may happen if its hanging on a
|
|
|
- // streaming request.
|
|
|
- this.getCurrentTask()!.abandoned = true
|
|
|
+ // Defensive safeguard: if current instance already changed, skip rehydrate
|
|
|
+ const current = this.getCurrentTask()
|
|
|
+ if (current && current.instanceId !== originalInstanceId) {
|
|
|
+ this.log(
|
|
|
+ `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
|
|
|
+ )
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Final race check before rehydrate to avoid duplicate rehydration
|
|
|
+ {
|
|
|
+ const currentAfterCheck = this.getCurrentTask()
|
|
|
+ if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) {
|
|
|
+ this.log(
|
|
|
+ `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
|
|
|
+ )
|
|
|
+ return
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Clears task again, so we need to abortTask manually above.
|