-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Fix duplicate rehydrate during reasoning; centralize rehydrate and preserve cancel metadata #8171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
bcf2241
3bbf984
e5be214
054764c
b357f4c
b1958e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 as any).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,28 @@ export class ClineProvider | |
|
|
||
| console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`) | ||
|
|
||
| const { historyItem } = await this.getTaskWithId(task.taskId) | ||
| const { historyItem, uiMessagesFilePath, apiConversationHistoryFilePath } = 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 current instance as abandoned to prevent any residual activity | ||
| if (this.getCurrentTask()) { | ||
| this.getCurrentTask()!.abandoned = true | ||
| } | ||
|
|
||
| await pWaitFor( | ||
| () => | ||
| this.getCurrentTask()! === undefined || | ||
|
|
@@ -2549,11 +2593,71 @@ 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 cancel bookkeeping and rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`, | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| // Provider-side cancel bookkeeping to mirror abortStream effects for user_cancelled | ||
| try { | ||
| // Update ui_messages: add cancelReason to last api_req_started | ||
|
||
| const messagesJson = await fs.readFile(uiMessagesFilePath, "utf8").catch(() => undefined) | ||
| if (messagesJson) { | ||
| const uiMsgs = JSON.parse(messagesJson) as ClineMessage[] | ||
|
||
| if (Array.isArray(uiMsgs)) { | ||
| const revIdx = uiMsgs | ||
| .slice() | ||
| .reverse() | ||
| .findIndex((m) => m?.type === "say" && (m as any)?.say === "api_req_started") | ||
| if (revIdx !== -1) { | ||
| const idx = uiMsgs.length - 1 - revIdx | ||
| try { | ||
| const existing = uiMsgs[idx]?.text ? JSON.parse(uiMsgs[idx].text as string) : {} | ||
| uiMsgs[idx].text = JSON.stringify({ ...existing, cancelReason: "user_cancelled" }) | ||
| await saveTaskMessages({ | ||
| messages: uiMsgs as any, | ||
| taskId: task.taskId, | ||
| globalStoragePath: this.contextProxy.globalStorageUri.fsPath, | ||
| }) | ||
| } catch { | ||
| // non-fatal | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Update api_conversation_history: append assistant interruption if last isn't assistant | ||
| try { | ||
| const apiMsgs = await readApiMessages({ | ||
| taskId: task.taskId, | ||
| globalStoragePath: this.contextProxy.globalStorageUri.fsPath, | ||
| }) | ||
| const last = apiMsgs.at(-1) | ||
| if (!last || last.role !== "assistant") { | ||
| apiMsgs.push({ | ||
| role: "assistant", | ||
| content: [{ type: "text", text: "[Response interrupted by user]" }], | ||
| ts: Date.now(), | ||
| } as any) | ||
| await saveApiMessages({ | ||
| messages: apiMsgs as any, | ||
| taskId: task.taskId, | ||
| globalStoragePath: this.contextProxy.globalStorageUri.fsPath, | ||
| }) | ||
| } | ||
| } catch (e) { | ||
| this.log( | ||
| `[cancelTask] Failed to update API history for user_cancelled: ${ | ||
| e instanceof Error ? e.message : String(e) | ||
| }`, | ||
| ) | ||
| } | ||
| } catch (e) { | ||
| this.log(`[cancelTask] Cancel bookkeeping failed: ${e instanceof Error ? e.message : String(e)}`) | ||
| } | ||
|
|
||
| // Clears task again, so we need to abortTask manually above. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using type assertion
(instance as any)bypasses type safety. Consider addingabortReasonto the Task interface or creating a proper type guard to check for this property safely.