Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/core/tools/attemptCompletionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,18 @@ export async function attemptCompletionTool(
TelemetryService.instance.captureTaskCompleted(cline.taskId)
cline.emit(RooCodeEventName.TaskCompleted, cline.taskId, cline.getTokenUsage(), cline.toolUsage)

if (cline.parentTask) {
// Check both parentTask (direct reference) and parentTaskId (persisted ID)
// This handles cases where VSCode was closed and the direct reference was lost
if (cline.parentTask || cline.parentTaskId) {
const didApprove = await askFinishSubTaskApproval()

if (!didApprove) {
return
}

// Return the completion content as a tool result for the child task before finishing
pushToolResult(formatResponse.toolResult(result))

// tell the provider to remove the current subtask and resume the previous task in the stack
await cline.providerRef.deref()?.finishSubTask(result)
return
Expand Down
53 changes: 50 additions & 3 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,59 @@ export class ClineProvider
// This is used when a subtask is finished and the parent task needs to be
// resumed.
async finishSubTask(lastMessage: string) {
// Get the current task before removing it from the stack
const currentTask = this.getCurrentTask()
const parentTaskId = currentTask?.parentTaskId
let didRestoreParentFromHistory = false

// Remove the last cline instance from the stack (this is the finished
// subtask).
await this.removeClineFromStack()
// Resume the last cline instance in the stack (if it exists - this is
// the 'parent' calling task).
await this.getCurrentTask()?.completeSubtask(lastMessage)

// Check if there's a parent task in the stack
let parentTask = this.getCurrentTask()

// If no parent task in stack but we have a parentTaskId,
// we need to restore the parent task (happens when VSCode was closed)
if (!parentTask && parentTaskId) {
try {
// Restore the parent task from history
const { historyItem } = await this.getTaskWithId(parentTaskId)
parentTask = await this.createTaskWithHistoryItem(historyItem)
didRestoreParentFromHistory = true
this.log(`[finishSubTask] Restored parent task ${parentTaskId} from history to receive subtask result`)
} catch (error) {
this.log(
`[finishSubTask] Failed to restore parent task ${parentTaskId}: ${error instanceof Error ? error.message : String(error)}`,
)
// Even if parent restoration fails, we should still show completion
// The user can manually resume the parent from task history
return
}
}

Comment on lines +499 to +515
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the parent task is restored from history using createTaskWithHistoryItem, the Task constructor automatically calls resumeTaskFromHistory() in the background (without awaiting). This method will show a "resume task" dialog to the user and wait for their response. Meanwhile, completeSubtask is called on line 515 immediately after the task is created, causing a race condition where the task is simultaneously asking the user to resume while trying to add the subtask result to its conversation. This will likely result in incorrect state or unexpected behavior. The parent task restoration should use createTaskWithHistoryItem with startTask: false option to prevent automatic resumption, or you need to await the task initialization before calling completeSubtask.

// Resume the parent task with the subtask result
await parentTask?.completeSubtask(lastMessage)

// If the parent was restored from history, auto-approve the resume prompt so execution continues
if (didRestoreParentFromHistory && parentTask) {
try {
await pWaitFor(
() => {
const ask = parentTask!.taskAsk
return !!ask && (ask.ask === "resume_task" || ask.ask === "resume_completed_task")
},
{ timeout: 3000 },
).catch(() => undefined)
parentTask.approveAsk()
} catch (error) {
this.log(
`[finishSubTask] Auto-approve resume failed for parent ${parentTask.taskId}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
}
// Pending Edit Operations Management

Expand Down