fix: getPendingSubtasksFromContent now checks both in-memory and persisted tool_results

When resuming after delegation, tool_results are already in the API history.
The function now looks at user messages AFTER the last assistant message,
not just in-memory pendingToolResults. This prevents the infinite loop
where the same subtask was being created repeatedly on resume.
This commit is contained in:
daniel-lxs 2025-12-03 13:49:47 -05:00
parent 56f7be6c7f
commit ff77e7c990
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
9 changed files with 487 additions and 412 deletions

View file

@ -33,6 +33,19 @@ vi.mock("../core/task-persistence", () => ({
readApiMessages: vi.fn().mockResolvedValue([]),
saveApiMessages: vi.fn().mockResolvedValue(undefined),
saveTaskMessages: vi.fn().mockResolvedValue(undefined),
getPendingSubtasks: vi.fn().mockReturnValue([]),
// appendToolResult should actually add the tool_result to the messages
appendToolResult: vi.fn().mockImplementation((msgs, toolUseId, result) => {
const newMsgs = [...msgs]
newMsgs.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUseId, content: result }],
ts: Date.now(),
})
return newMsgs
}),
getOtherToolResults: vi.fn().mockReturnValue([]),
hasPendingSubtasksInHistory: vi.fn().mockReturnValue(false),
}))
import { ClineProvider } from "../core/webview/ClineProvider"
@ -69,7 +82,6 @@ describe("History resume delegation - parent metadata transitions", () => {
taskId: "parent-1",
skipPrevResponseIdOnce: false,
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
})
@ -81,9 +93,6 @@ describe("History resume delegation - parent metadata transitions", () => {
removeClineFromStack,
createTaskWithHistoryItem,
updateTaskHistory,
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
// Mock persistence reads to return empty arrays
@ -148,13 +157,9 @@ describe("History resume delegation - parent metadata transitions", () => {
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
// Start with existing messages in history
@ -219,13 +224,9 @@ describe("History resume delegation - parent metadata transitions", () => {
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
// Include an assistant message with new_task tool_use to exercise the tool_result path
@ -295,7 +296,6 @@ describe("History resume delegation - parent metadata transitions", () => {
}),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}
@ -319,9 +319,6 @@ describe("History resume delegation - parent metadata transitions", () => {
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTaskWithHistoryItem: vi.fn().mockResolvedValue(parentInstance),
updateTaskHistory: vi.fn().mockResolvedValue([]),
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
vi.mocked(readTaskMessages).mockResolvedValue([])
@ -363,13 +360,9 @@ describe("History resume delegation - parent metadata transitions", () => {
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
vi.mocked(readTaskMessages).mockResolvedValue([])
@ -418,13 +411,9 @@ describe("History resume delegation - parent metadata transitions", () => {
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
vi.mocked(readTaskMessages).mockResolvedValue([])
@ -466,13 +455,9 @@ describe("History resume delegation - parent metadata transitions", () => {
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
} as unknown as ClineProvider
// Mock read failures or empty returns

View file

@ -47,6 +47,19 @@ vi.mock("../core/task-persistence", () => ({
readApiMessages: vi.fn().mockResolvedValue([]),
saveApiMessages: vi.fn().mockResolvedValue(undefined),
saveTaskMessages: vi.fn().mockResolvedValue(undefined),
getPendingSubtasks: vi.fn().mockReturnValue([]),
// appendToolResult should actually add the tool_result to the messages
appendToolResult: vi.fn().mockImplementation((msgs, toolUseId, result) => {
const newMsgs = [...msgs]
newMsgs.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUseId, content: result }],
ts: Date.now(),
})
return newMsgs
}),
getOtherToolResults: vi.fn().mockReturnValue([]),
hasPendingSubtasksInHistory: vi.fn().mockReturnValue(false),
}))
import { attemptCompletionTool } from "../core/tools/AttemptCompletionTool"
@ -57,7 +70,7 @@ import { readApiMessages, saveApiMessages, saveTaskMessages } from "../core/task
describe("Nested delegation resume (A → B → C)", () => {
beforeEach(() => {
vi.restoreAllMocks()
vi.clearAllMocks()
})
it("C completes → reopens B; then B completes → reopens A; emits correct events; no resume_task asks", async () => {
@ -128,7 +141,6 @@ describe("Nested delegation resume (A → B → C)", () => {
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
loadPendingSubtasks: vi.fn(),
hasPendingSubtasks: vi.fn().mockReturnValue(false),
}
})
@ -158,9 +170,6 @@ describe("Nested delegation resume (A → B → C)", () => {
removeClineFromStack,
createTaskWithHistoryItem,
updateTaskHistory,
getSubtaskState: vi.fn().mockReturnValue(undefined),
setSubtaskState: vi.fn().mockResolvedValue(undefined),
clearSubtaskState: vi.fn().mockResolvedValue(undefined),
// Wire through provider method so attemptCompletionTool can call it
reopenParentFromDelegation: vi.fn(async (params: any) => {
return await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, params)

View file

@ -2,6 +2,18 @@
import { describe, it, expect, vi } from "vitest"
import { RooCodeEventName } from "@roo-code/types"
// Mock task-persistence to avoid undefined apiMessages error
vi.mock("../core/task-persistence", () => ({
readApiMessages: vi.fn().mockResolvedValue([]),
saveApiMessages: vi.fn().mockResolvedValue(undefined),
saveTaskMessages: vi.fn().mockResolvedValue(undefined),
getPendingSubtasks: vi.fn().mockReturnValue([]),
appendToolResult: vi.fn().mockImplementation((msgs) => msgs),
getOtherToolResults: vi.fn().mockReturnValue([]),
hasPendingSubtasksInHistory: vi.fn().mockReturnValue(false),
}))
import { ClineProvider } from "../core/webview/ClineProvider"
describe("ClineProvider.delegateParentAndOpenChild()", () => {
@ -47,7 +59,6 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
updateTaskHistory,
handleModeSwitch,
log: vi.fn(),
getSubtaskState: vi.fn().mockReturnValue(undefined),
} as unknown as ClineProvider
const params = {

View file

@ -1,3 +1,15 @@
export { type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages"
export { readTaskMessages, saveTaskMessages } from "./taskMessages"
export { taskMetadata } from "./taskMetadata"
export {
type PendingSubtask,
getPendingSubtasks,
getPendingSubtasksFromContent,
getFirstPendingSubtaskId,
hasPendingSubtasksInHistory,
appendToolResult,
getOtherToolResults,
areAllSubtasksComplete,
getCompletedSubtaskCount,
getTotalSubtaskCount,
} from "./subtaskState"

View file

@ -0,0 +1,305 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { TodoItem } from "@roo-code/types"
import { parseMarkdownChecklist } from "../tools/UpdateTodoListTool"
import { type ApiMessage } from "./apiMessages"
/**
* Represents a pending subtask derived from the API conversation history.
*/
export interface PendingSubtask {
toolCallId: string
message: string
mode: string
todoItems: TodoItem[]
}
/**
* Helper to extract pending subtasks from assistant content and existing tool results.
* Used internally by both getPendingSubtasks and getPendingSubtasksFromContent.
*/
function extractPendingSubtasks(
assistantContent: Array<Anthropic.Messages.ContentBlockParam | Anthropic.ToolResultBlockParam>,
completedToolIds: Set<string>,
): PendingSubtask[] {
const newTaskToolUses = assistantContent.filter(
(block): block is Anthropic.Messages.ToolUseBlock => block.type === "tool_use" && block.name === "new_task",
)
return newTaskToolUses
.filter((toolUse) => !completedToolIds.has(toolUse.id))
.map((toolUse) => {
const input = toolUse.input as Record<string, unknown>
let todoItems: TodoItem[] = []
if (input.todos && typeof input.todos === "string") {
try {
todoItems = parseMarkdownChecklist(input.todos)
} catch {
todoItems = []
}
}
return {
toolCallId: toolUse.id,
message: (input.message as string) || "",
mode: (input.mode as string) || "",
todoItems,
}
})
}
/**
* Gets pending new_task tool calls that don't have tool_results yet.
* This derives the pending subtasks by comparing tool_use blocks (with name: "new_task")
* in the last assistant message against tool_result blocks in the last user message.
*
* @param apiMessages - The API conversation history
* @returns Array of pending subtasks with their parameters
*/
export function getPendingSubtasks(apiMessages: ApiMessage[]): PendingSubtask[] {
if (apiMessages.length < 2) return []
const lastAssistant = apiMessages[apiMessages.length - 2]
const lastUser = apiMessages[apiMessages.length - 1]
if (lastAssistant?.role !== "assistant" || lastUser?.role !== "user") return []
const assistantContent = Array.isArray(lastAssistant.content) ? lastAssistant.content : []
const userContent = Array.isArray(lastUser.content) ? lastUser.content : []
const completedIds = new Set(
userContent
.filter((block): block is Anthropic.ToolResultBlockParam => block.type === "tool_result")
.map((block) => block.tool_use_id),
)
return extractPendingSubtasks(assistantContent, completedIds)
}
/**
* Gets pending new_task tool calls by examining both API history and in-memory state.
* This handles two scenarios:
*
* 1. DURING streaming: user message not in history yet, tool_results in pendingToolResults
* - apiMessages ends with assistant message
* - pendingToolResults has executed tool results
*
* 2. AFTER delegation resume: user message already in history with tool_results
* - apiMessages ends with user message (containing tool_result)
* - pendingToolResults is empty
*
* @param apiMessages - The API conversation history
* @param pendingToolResults - In-memory tool_result blocks not yet saved to history
* @returns Array of pending subtasks with their parameters
*/
export function getPendingSubtasksFromContent(
apiMessages: ApiMessage[],
pendingToolResults: Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam>,
): PendingSubtask[] {
if (apiMessages.length < 1) return []
// Find the last assistant message
let lastAssistant: ApiMessage | undefined
let lastAssistantIndex = -1
for (let i = apiMessages.length - 1; i >= 0; i--) {
if (apiMessages[i].role === "assistant") {
lastAssistant = apiMessages[i]
lastAssistantIndex = i
break
}
}
if (!lastAssistant) return []
const assistantContent = Array.isArray(lastAssistant.content) ? lastAssistant.content : []
// Collect completed IDs from multiple sources:
// 1. In-memory pendingToolResults (during streaming)
// 2. Any user message that comes AFTER the last assistant message (after delegation resume)
const completedIds = new Set<string>()
// Source 1: In-memory tool results
for (const block of pendingToolResults) {
if (block.type === "tool_result") {
completedIds.add(block.tool_use_id)
}
}
// Source 2: User messages after the last assistant message
for (let i = lastAssistantIndex + 1; i < apiMessages.length; i++) {
const msg = apiMessages[i]
if (msg.role === "user" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_result") {
completedIds.add((block as Anthropic.ToolResultBlockParam).tool_use_id)
}
}
}
}
return extractPendingSubtasks(assistantContent, completedIds)
}
/**
* Gets the tool_use ID of the first pending new_task subtask.
* Used to determine which subtask is currently executing.
*
* @param apiMessages - The API conversation history
* @returns The tool_use ID of the first pending subtask, or undefined if none
*/
export function getFirstPendingSubtaskId(apiMessages: ApiMessage[]): string | undefined {
const pending = getPendingSubtasks(apiMessages)
return pending.length > 0 ? pending[0].toolCallId : undefined
}
/**
* Checks if there are any pending new_task subtasks.
*
* @param apiMessages - The API conversation history
* @returns True if there are pending subtasks
*/
export function hasPendingSubtasksInHistory(apiMessages: ApiMessage[]): boolean {
return getPendingSubtasks(apiMessages).length > 0
}
/**
* Appends a tool_result to the last user message in the API conversation history.
* If the last message is not a user message, creates a new user message.
*
* This function modifies the input array in place and returns it.
*
* @param apiMessages - The API conversation history (modified in place)
* @param toolUseId - The tool_use_id to reference in the tool_result
* @param result - The result content for the tool_result
* @returns The modified apiMessages array
*/
export function appendToolResult(apiMessages: ApiMessage[], toolUseId: string, result: string): ApiMessage[] {
if (apiMessages.length === 0) {
apiMessages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUseId, content: result }],
ts: Date.now(),
})
return apiMessages
}
const lastMsg = apiMessages[apiMessages.length - 1]
if (lastMsg?.role !== "user") {
apiMessages.push({
role: "user",
content: [{ type: "tool_result", tool_use_id: toolUseId, content: result }],
ts: Date.now(),
})
} else {
if (!Array.isArray(lastMsg.content)) {
lastMsg.content = lastMsg.content ? [{ type: "text", text: lastMsg.content }] : []
}
;(lastMsg.content as Anthropic.ToolResultBlockParam[]).push({
type: "tool_result",
tool_use_id: toolUseId,
content: result,
})
}
return apiMessages
}
/**
* Gets tool_result blocks from the last user message that are NOT for new_task tool calls.
* These are "other" tool results (like update_todo_list) that were called in the same turn.
*
* @param apiMessages - The API conversation history
* @returns Array of tool_result blocks for non-new_task tools
*/
export function getOtherToolResults(apiMessages: ApiMessage[]): Anthropic.ToolResultBlockParam[] {
if (apiMessages.length < 2) return []
const lastAssistant = apiMessages[apiMessages.length - 2]
const lastUser = apiMessages[apiMessages.length - 1]
if (lastAssistant?.role !== "assistant" || lastUser?.role !== "user") return []
const assistantContent = Array.isArray(lastAssistant.content) ? lastAssistant.content : []
const newTaskToolIds = new Set(
assistantContent
.filter(
(block): block is Anthropic.Messages.ToolUseBlock =>
block.type === "tool_use" && block.name === "new_task",
)
.map((block) => block.id),
)
const userContent = Array.isArray(lastUser.content) ? lastUser.content : []
return userContent.filter(
(block): block is Anthropic.ToolResultBlockParam =>
block.type === "tool_result" && !newTaskToolIds.has(block.tool_use_id),
)
}
/**
* Checks if all new_task tool calls have corresponding tool_results.
* This indicates all subtasks have completed.
*
* @param apiMessages - The API conversation history
* @returns True if all new_task tool calls have tool_results
*/
export function areAllSubtasksComplete(apiMessages: ApiMessage[]): boolean {
return getPendingSubtasks(apiMessages).length === 0
}
/**
* Gets the count of completed new_task subtasks (those with tool_results).
*
* @param apiMessages - The API conversation history
* @returns Number of completed subtasks
*/
export function getCompletedSubtaskCount(apiMessages: ApiMessage[]): number {
if (apiMessages.length < 2) return 0
const lastAssistant = apiMessages[apiMessages.length - 2]
const lastUser = apiMessages[apiMessages.length - 1]
if (lastAssistant?.role !== "assistant" || lastUser?.role !== "user") return 0
const assistantContent = Array.isArray(lastAssistant.content) ? lastAssistant.content : []
const newTaskToolIds = new Set(
assistantContent
.filter(
(block): block is Anthropic.Messages.ToolUseBlock =>
block.type === "tool_use" && block.name === "new_task",
)
.map((block) => block.id),
)
const userContent = Array.isArray(lastUser.content) ? lastUser.content : []
return userContent.filter(
(block): block is Anthropic.ToolResultBlockParam =>
block.type === "tool_result" && newTaskToolIds.has(block.tool_use_id),
).length
}
/**
* Gets the total count of new_task tool calls in the last assistant message.
*
* @param apiMessages - The API conversation history
* @returns Total number of new_task tool calls
*/
export function getTotalSubtaskCount(apiMessages: ApiMessage[]): number {
if (apiMessages.length < 1) return 0
let lastAssistant: ApiMessage | undefined
for (let i = apiMessages.length - 1; i >= 0; i--) {
if (apiMessages[i].role === "assistant") {
lastAssistant = apiMessages[i]
break
}
}
if (!lastAssistant) return 0
const assistantContent = Array.isArray(lastAssistant.content) ? lastAssistant.content : []
return assistantContent.filter(
(block): block is Anthropic.Messages.ToolUseBlock => block.type === "tool_use" && block.name === "new_task",
).length
}

View file

@ -109,6 +109,9 @@ import {
readTaskMessages,
saveTaskMessages,
taskMetadata,
getPendingSubtasks,
getPendingSubtasksFromContent,
getOtherToolResults,
} from "../task-persistence"
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
import { checkContextWindowExceededError } from "../context/context-management/context-error-handling"
@ -161,27 +164,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
childTaskId?: string
pendingNewTaskToolCallId?: string
/** Queue of subtasks to execute sequentially when multiple new_task calls are made in parallel. */
pendingSubtasks: Array<{
toolCallId: string
message: string
mode: string
todoItems: TodoItem[]
}> = []
/** Results from completed subtasks, added to conversation only after ALL subtasks finish. */
completedSubtaskResults: Array<{
toolCallId: string
result: string
}> = []
/** Pending tool results from OTHER tools called in the same turn.
* Saved with subtask state so they're combined with subtask results at the end. */
pendingOtherToolResults: Array<Anthropic.ToolResultBlockParam> = []
/** Tool call ID of the currently-executing subtask. */
currentSubtaskToolCallId?: string
readonly instanceId: string
readonly metadata: TaskMetadata
@ -842,9 +824,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.userMessageContent = []
}
/** Execute pending subtasks sequentially. Returns empty array since delegation suspends the parent. */
/**
* Execute pending subtasks sequentially, deriving pending subtasks from api_conversation_history
* and in-memory userMessageContent.
* Returns empty array since delegation suspends the parent.
*/
public async executePendingSubtasks(): Promise<Array<{ toolCallId: string; result: string }>> {
if (this.pendingSubtasks.length === 0) {
// Derive pending subtasks - use the in-memory version since user message may not be in history yet
const pendingSubtasks = getPendingSubtasksFromContent(this.apiConversationHistory, this.userMessageContent)
if (pendingSubtasks.length === 0) {
return []
}
@ -853,35 +841,31 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
throw new Error("Provider not available for subtask execution")
}
// Save other tool results (like update_todo_list) that were called in the same turn.
// Don't flush them to history yet - they'll be combined with subtask results later.
// Only save non-new_task tool results (filter out new_task tool results which are pending subtasks).
const pendingSubtaskToolIds = new Set(this.pendingSubtasks.map((s) => s.toolCallId))
if (this.currentSubtaskToolCallId) {
pendingSubtaskToolIds.add(this.currentSubtaskToolCallId)
// Before delegating first subtask, flush other tool results to api_conversation_history.
// We need to create a user message with ALL tool_results including a placeholder for this new_task.
// This ensures the API conversation is valid (every tool_use has a tool_result).
// Get all new_task tool IDs from the last assistant message
const newTaskToolIds = new Set(pendingSubtasks.map((s) => s.toolCallId))
// Find non-new_task tool results in userMessageContent
const nonNewTaskToolResults = this.userMessageContent.filter(
(block): block is Anthropic.ToolResultBlockParam =>
block.type === "tool_result" && !newTaskToolIds.has(block.tool_use_id),
)
// Flush all pending tool results (for non-new_task tools) to history
if (nonNewTaskToolResults.length > 0) {
await this.flushPendingToolResultsToHistory()
} else {
// Clear userMessageContent since we're about to delegate
this.userMessageContent = []
}
// Extract tool_result blocks from userMessageContent that are NOT subtask-related
const otherToolResults = this.userMessageContent.filter((block): block is Anthropic.ToolResultBlockParam => {
if (block.type !== "tool_result") return false
return !pendingSubtaskToolIds.has(block.tool_use_id)
})
// If this is the first subtask, save the other tool results
if (this.pendingOtherToolResults.length === 0 && otherToolResults.length > 0) {
this.pendingOtherToolResults = otherToolResults
}
// Clear userMessageContent since we're about to delegate (don't flush to history)
this.userMessageContent = []
const currentSubtask = this.pendingSubtasks.shift()!
this.currentSubtaskToolCallId = currentSubtask.toolCallId
// Get the first pending subtask to execute
const currentSubtask = pendingSubtasks[0]
try {
// State must be saved before delegating since a new parent instance is created on resume
await this.savePendingSubtasks()
await (provider as any).delegateParentAndOpenChild({
parentTaskId: this.taskId,
message: currentSubtask.message,
@ -890,91 +874,21 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.completedSubtaskResults.push({
toolCallId: currentSubtask.toolCallId,
result: `Failed to execute subtask: ${errorMessage}`,
})
this.currentSubtaskToolCallId = undefined
await this.savePendingSubtasks()
console.error(`[Task#executePendingSubtasks] Failed to execute subtask: ${errorMessage}`)
}
return []
}
/**
* Check if there are pending subtasks by examining api_conversation_history
* and in-memory userMessageContent.
* Pending = new_task tool_use blocks without corresponding tool_result blocks.
*/
public hasPendingSubtasks(): boolean {
return this.pendingSubtasks.length > 0
}
/**
* Save subtask state to VSCode's workspaceState via the provider.
* The workspaceState persists across extension restarts, so this state survives
* when the parent is disposed and recreated after a child task completes.
*/
public async savePendingSubtasks(): Promise<void> {
const provider = this.providerRef.deref()
if (!provider) {
console.error(`[Task#savePendingSubtasks] Provider not available`)
return
}
const hasState =
this.pendingSubtasks.length > 0 ||
this.completedSubtaskResults.length > 0 ||
this.currentSubtaskToolCallId ||
this.pendingOtherToolResults.length > 0
if (hasState) {
await provider.setSubtaskState(this.taskId, {
pendingSubtasks: this.pendingSubtasks,
completedSubtaskResults: this.completedSubtaskResults,
currentSubtaskToolCallId: this.currentSubtaskToolCallId,
pendingOtherToolResults: this.pendingOtherToolResults as any,
})
} else {
// Clean up state if nothing to store
await provider.clearSubtaskState(this.taskId)
}
}
/**
* Load subtask state from workspaceState via the provider.
* Called when the parent resumes after a child task completes.
*/
public loadPendingSubtasks(): void {
const provider = this.providerRef.deref()
if (!provider) {
return
}
const state = provider.getSubtaskState(this.taskId)
if (state) {
if (Array.isArray(state.pendingSubtasks)) {
this.pendingSubtasks = state.pendingSubtasks
}
if (Array.isArray(state.completedSubtaskResults)) {
this.completedSubtaskResults = state.completedSubtaskResults
}
if (state.currentSubtaskToolCallId) {
this.currentSubtaskToolCallId = state.currentSubtaskToolCallId
}
if (Array.isArray(state.pendingOtherToolResults)) {
this.pendingOtherToolResults = state.pendingOtherToolResults as Anthropic.ToolResultBlockParam[]
}
}
}
/**
* Clear subtask state from workspaceState after all subtasks are complete.
*/
public async clearSubtaskState(): Promise<void> {
const provider = this.providerRef.deref()
if (provider) {
await provider.clearSubtaskState(this.taskId)
}
this.pendingSubtasks = []
this.completedSubtaskResults = []
this.currentSubtaskToolCallId = undefined
this.pendingOtherToolResults = []
// Use the in-memory version since user message may not be in history yet
// This is called after streaming completes but before the user message is saved
return getPendingSubtasksFromContent(this.apiConversationHistory, this.userMessageContent).length > 0
}
private async saveApiConversationHistory() {
@ -2202,11 +2116,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Save the updated history
await this.saveApiConversationHistory()
// Load any pending subtasks from the provider's in-memory storage.
// When multiple new_task tools are called in parallel, remaining subtasks are
// saved before each delegation. We load them here on resume.
this.loadPendingSubtasks()
// Check if there are pending subtasks to execute sequentially.
// This happens when multiple new_task tools were called in parallel -
// they are queued and executed one at a time.

View file

@ -147,44 +147,25 @@ export class NewTaskTool extends BaseTool<"new_task"> {
task.checkpointSave(true)
}
// Queue this new_task if using native tool protocol AND there are:
// 1. Multiple new_task blocks (to execute sequentially), OR
// 2. Any remaining tool blocks after this one (so they can execute before delegation)
// For native tool protocol with multiple new_task blocks or remaining tools:
// Don't execute immediately - let the tool result accumulate in userMessageContent.
// After all tools process, executePendingSubtasks() will derive pending tasks from
// api_conversation_history (comparing tool_use vs tool_result blocks).
// NOTE: XML protocol processes tools one at a time, so this condition is always false for XML.
// We add an explicit check for clarity and defensive safety.
const isNativeToolProtocol = toolProtocol === "native"
const newTaskBlockCount = countNewTaskBlocks(task)
const hasRemainingTools = hasRemainingToolBlocks(task)
if (isNativeToolProtocol && (newTaskBlockCount > 1 || hasRemainingTools)) {
task.pendingSubtasks.push({
toolCallId: toolCallId ?? "",
message: unescapedMessage,
mode,
todoItems,
})
// Don't push to pendingSubtasks - the info is already in the assistant message's tool_use block.
// Just return without executing, and executePendingSubtasks() will handle it later.
return
}
// Save other tool results (e.g., update_todo_list) that were called in the same turn.
// This prevents them from being lost or incorrectly ordered when the parent resumes.
const currentToolCallId = toolCallId ?? ""
const otherToolResults = task.userMessageContent.filter(
(block): block is Anthropic.ToolResultBlockParam =>
block.type === "tool_result" && block.tool_use_id !== currentToolCallId,
)
if (otherToolResults.length > 0) {
task.pendingOtherToolResults = otherToolResults
}
// Track this subtask and clear userMessageContent to prevent incorrect flushing
task.currentSubtaskToolCallId = currentToolCallId
// For single new_task call or XML protocol, delegate immediately.
// Clear userMessageContent to prevent incorrect flushing during delegation.
task.userMessageContent = []
// Save state before delegation so it survives parent disposal
await task.savePendingSubtasks()
const child = await (provider as any).delegateParentAndOpenChild({
parentTaskId: task.taskId,
message: unescapedMessage,

View file

@ -99,9 +99,6 @@ const mockCline = {
checkpointSave: mockCheckpointSave,
startSubtask: mockStartSubtask,
userMessageContent: [] as any[],
pendingOtherToolResults: [] as any[],
currentSubtaskToolCallId: undefined as string | undefined,
savePendingSubtasks: vi.fn().mockResolvedValue(undefined),
providerRef: {
deref: vi.fn(() => ({
getState: vi.fn(() => ({ customModes: [], mode: "ask" })),
@ -656,9 +653,6 @@ describe("newTaskTool delegation flow", () => {
checkpointSave: mockCheckpointSave,
startSubtask: localStartSubtask,
userMessageContent: [] as any[],
pendingOtherToolResults: [] as any[],
currentSubtaskToolCallId: undefined as string | undefined,
savePendingSubtasks: vi.fn().mockResolvedValue(undefined),
providerRef: {
deref: vi.fn(() => providerSpy),
},
@ -707,7 +701,10 @@ describe("newTaskTool delegation flow", () => {
})
describe("newTaskTool parallel execution", () => {
it("should queue subtasks when multiple new_task blocks are detected", async () => {
it("should defer delegation when multiple new_task blocks are detected (native protocol)", async () => {
// With the new approach, when multiple new_task blocks exist in native protocol,
// the tool just returns without executing - the subtask info is in the assistant message's
// tool_use blocks and will be derived by executePendingSubtasks() later.
const providerSpy = {
getState: vi.fn().mockResolvedValue({
mode: "ask",
@ -717,7 +714,6 @@ describe("newTaskTool parallel execution", () => {
handleModeSwitch: vi.fn(),
} as any
const pendingSubtasks: any[] = []
const localCline = {
ask: vi.fn(),
sayAndCreateMissingParamError: vi.fn(),
@ -731,9 +727,6 @@ describe("newTaskTool parallel execution", () => {
checkpointSave: vi.fn(),
startSubtask: vi.fn(),
userMessageContent: [] as any[],
pendingOtherToolResults: [] as any[],
currentSubtaskToolCallId: undefined as string | undefined,
savePendingSubtasks: vi.fn().mockResolvedValue(undefined),
providerRef: {
deref: vi.fn(() => providerSpy),
},
@ -753,7 +746,7 @@ describe("newTaskTool parallel execution", () => {
partial: false,
},
],
pendingSubtasks,
currentStreamingContentIndex: 0,
}
const mockPushToolResult = vi.fn()
@ -779,38 +772,10 @@ describe("newTaskTool parallel execution", () => {
toolCallId: "tool-1",
})
expect(pendingSubtasks.length).toBe(1)
expect(pendingSubtasks[0].mode).toBe("code")
expect(pendingSubtasks[0].message).toBe("First task")
expect(pendingSubtasks[0].toolCallId).toBe("tool-1")
expect(mockPushToolResult).not.toHaveBeenCalled()
expect(providerSpy.delegateParentAndOpenChild).not.toHaveBeenCalled()
const block2: ToolUse = {
type: "tool_use",
id: "tool-2",
name: "new_task",
params: {
mode: "code",
message: "Second task",
},
partial: false,
}
await newTaskTool.handle(localCline as any, block2 as ToolUse<"new_task">, {
askApproval: mockAskApproval,
handleError: vi.fn(),
pushToolResult: mockPushToolResult,
removeClosingTag: vi.fn((_: string, v?: string) => v ?? ""),
toolProtocol: "native", // Native protocol is required for parallel tool execution
toolCallId: "tool-2",
})
expect(pendingSubtasks.length).toBe(2)
expect(pendingSubtasks[1].mode).toBe("code")
expect(pendingSubtasks[1].message).toBe("Second task")
expect(pendingSubtasks[1].toolCallId).toBe("tool-2")
// With new approach: tool returns without delegating when multiple new_task blocks exist
// No pushToolResult call (deferred)
expect(mockPushToolResult).not.toHaveBeenCalled()
// No delegation yet (deferred to executePendingSubtasks)
expect(providerSpy.delegateParentAndOpenChild).not.toHaveBeenCalled()
})
@ -824,7 +789,6 @@ describe("newTaskTool parallel execution", () => {
handleModeSwitch: vi.fn(),
} as any
const pendingSubtasks: any[] = []
const localCline = {
ask: vi.fn(),
sayAndCreateMissingParamError: vi.fn(),
@ -838,9 +802,6 @@ describe("newTaskTool parallel execution", () => {
checkpointSave: vi.fn(),
startSubtask: vi.fn(),
userMessageContent: [] as any[],
pendingOtherToolResults: [] as any[],
currentSubtaskToolCallId: undefined as string | undefined,
savePendingSubtasks: vi.fn().mockResolvedValue(undefined),
providerRef: {
deref: vi.fn(() => providerSpy),
},
@ -853,7 +814,7 @@ describe("newTaskTool parallel execution", () => {
partial: false,
},
],
pendingSubtasks,
currentStreamingContentIndex: 0,
}
const mockPushToolResult = vi.fn()
@ -879,7 +840,6 @@ describe("newTaskTool parallel execution", () => {
toolCallId: "tool-1",
})
expect(pendingSubtasks.length).toBe(0)
expect(providerSpy.delegateParentAndOpenChild).toHaveBeenCalledWith({
parentTaskId: "mock-parent-task-id",
message: "Single task",

View file

@ -93,7 +93,15 @@ import { getSystemPromptFilePath } from "../prompts/sections/custom-system-promp
import { webviewMessageHandler } from "./webviewMessageHandler"
import type { ClineMessage, TodoItem } from "@roo-code/types"
import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
import {
readApiMessages,
saveApiMessages,
saveTaskMessages,
getPendingSubtasks,
appendToolResult,
getOtherToolResults,
hasPendingSubtasksInHistory,
} from "../task-persistence"
import { readTaskMessages } from "../task-persistence/taskMessages"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
@ -118,32 +126,6 @@ interface PendingEditOperation {
createdAt: number
}
/**
* State for pending subtasks during parallel new_task execution.
* Stored in VSCode's workspaceState for persistence across extension restarts.
*/
export interface SubtaskState {
pendingSubtasks: Array<{
toolCallId: string
message: string
mode: string
todoItems: TodoItem[]
}>
completedSubtaskResults: Array<{
toolCallId: string
result: string
}>
currentSubtaskToolCallId?: string
/** Pending tool results from OTHER tools called in the same turn as new_task.
* These are saved before the first subtask executes so they can be combined
* with subtask results into a single user message when all complete. */
pendingOtherToolResults?: Array<{
type: "tool_result"
tool_use_id: string
content: string | Array<{ type: "text"; text: string } | { type: "image"; source: any }>
}>
}
export class ClineProvider
extends EventEmitter<TaskProviderEvents>
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
@ -172,9 +154,6 @@ export class ClineProvider
private pendingOperations: Map<string, PendingEditOperation> = new Map()
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
// Storage key prefix for subtask state in VSCode workspaceState
private static readonly SUBTASK_STATE_KEY_PREFIX = "subtaskState:"
private cloudOrganizationsCache: CloudOrganizationMembership[] | null = null
private cloudOrganizationsCacheTimestamp: number | null = null
private static readonly CLOUD_ORGANIZATIONS_CACHE_DURATION_MS = 5 * 1000 // 5 seconds
@ -579,47 +558,6 @@ export class ClineProvider
this.log(`[clearAllPendingEditOperations] Cleared all pending operations`)
}
// Subtask State Management
// These methods manage state for parallel new_task execution using VSCode's workspaceState.
// workspaceState persists data across extension restarts for the current workspace.
/**
* Gets the subtask state key for a given task ID.
*/
private getSubtaskStateKey(taskId: string): string {
return `${ClineProvider.SUBTASK_STATE_KEY_PREFIX}${taskId}`
}
/**
* Gets the subtask state for a given task ID from workspaceState.
*/
public getSubtaskState(taskId: string): SubtaskState | undefined {
const key = this.getSubtaskStateKey(taskId)
return this.context.workspaceState.get<SubtaskState>(key)
}
/**
* Sets the subtask state for a given task ID in workspaceState.
*/
public async setSubtaskState(taskId: string, state: SubtaskState): Promise<void> {
const key = this.getSubtaskStateKey(taskId)
await this.context.workspaceState.update(key, state)
this.log(`[setSubtaskState] Set subtask state for task ${taskId}`)
}
/**
* Clears the subtask state for a given task ID from workspaceState.
* Should be called after all subtasks complete.
*/
public async clearSubtaskState(taskId: string): Promise<void> {
const key = this.getSubtaskStateKey(taskId)
const exists = this.context.workspaceState.get(key) !== undefined
await this.context.workspaceState.update(key, undefined)
if (exists) {
this.log(`[clearSubtaskState] Cleared subtask state for task ${taskId}`)
}
}
/*
VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@ -3061,16 +2999,16 @@ export class ClineProvider
)
}
// 2) DON'T flush pending tool results to history when we're in subtask execution flow.
// The executePendingSubtasks() method saves other tool results (like update_todo_list)
// to pendingOtherToolResults, which will be combined with subtask results into a
// SINGLE user message when all subtasks complete. If we flush here, we'd create
// The executePendingSubtasks() method handles saving other tool results that were
// called in the same turn as new_task. These will be combined with subtask results
// into a SINGLE user message when all subtasks complete. If we flush here, we'd create
// a separate user message that breaks the conversation structure (tool results
// MUST follow the assistant message that called them in a single user message).
//
// Only flush if this is a direct delegation (no pending subtask state), which means
// Only flush if this is a direct delegation (no pending subtasks in history), which means
// the parent called new_task without the parallel subtask execution flow.
const hasSubtaskState = this.getSubtaskState(parentTaskId) !== undefined
if (!hasSubtaskState) {
const hasParallelSubtasks = hasPendingSubtasksInHistory(parent.apiConversationHistory)
if (!hasParallelSubtasks) {
try {
await parent.flushPendingToolResultsToHistory()
} catch (error) {
@ -3151,6 +3089,12 @@ export class ClineProvider
/**
* Reopen parent task from delegation with write-back and events.
*
* This method derives pending subtask state directly from api_conversation_history.json
* instead of using workspaceState. The state is determined by comparing:
* - tool_use blocks with name: "new_task" in the last assistant message
* - tool_result blocks in the last user message
* - Pending = tool_use.id NOT in tool_results
*/
public async reopenParentFromDelegation(params: {
parentTaskId: string
@ -3183,17 +3127,6 @@ export class ClineProvider
parentApiMessages = []
}
// Load subtask state from workspaceState
const subtaskState = this.getSubtaskState(parentTaskId)
let pendingSubtasks: Array<{ toolCallId: string; message: string; mode: string; todoItems: any[] }> =
subtaskState?.pendingSubtasks ?? []
let completedSubtaskResults: Array<{ toolCallId: string; result: string }> =
subtaskState?.completedSubtaskResults ?? []
let currentSubtaskToolCallId: string | undefined = subtaskState?.currentSubtaskToolCallId
// Other tool results (like update_todo_list) that were called in the same turn as new_task
const pendingOtherToolResults: Array<{ type: string; tool_use_id: string; content: any }> =
(subtaskState?.pendingOtherToolResults as any) ?? []
const ts = Date.now()
if (!Array.isArray(parentClineMessages)) parentClineMessages = []
if (!Array.isArray(parentApiMessages)) parentApiMessages = []
@ -3208,12 +3141,26 @@ export class ClineProvider
parentClineMessages.push(subtaskUiMessage)
await saveTaskMessages({ messages: parentClineMessages, taskId: parentTaskId, globalStoragePath })
// Determine the toolUseId for this subtask result
// Priority: 1) currentSubtaskToolCallId from state, 2) fallback to finding the last one
let toolUseId: string | undefined = currentSubtaskToolCallId
// Derive pending subtasks from the api_conversation_history
// The getPendingSubtasks function compares tool_use blocks vs tool_result blocks
const pendingSubtasksFromHistory = getPendingSubtasks(parentApiMessages)
if (!toolUseId) {
// Fallback: find the tool_use_id from the last assistant message's new_task tool_use
// Get other tool results (non-new_task tools) from the last assistant message
// that haven't been added to history yet
const otherToolResults = getOtherToolResults(parentApiMessages)
// Find the toolUseId for the child that just completed
// First, check if there's a pending subtask matching the child's details
// Since we execute subtasks in order, the first pending one should be the one that just completed
let toolUseId: string | undefined
// Look through assistant message to find the new_task tool_use that matches
// We need to find which one was being executed - look for the first pending one
if (pendingSubtasksFromHistory.length > 0) {
// The first pending subtask is the one we just completed
toolUseId = pendingSubtasksFromHistory[0].toolCallId
} else {
// Fallback: find the last new_task tool_use in the assistant message
for (let i = parentApiMessages.length - 1; i >= 0; i--) {
const msg = parentApiMessages[i]
if (msg.role === "assistant" && Array.isArray(msg.content)) {
@ -3228,83 +3175,41 @@ export class ClineProvider
}
}
// Store this subtask's result
// Append the completed subtask's tool_result to the conversation history
if (toolUseId) {
completedSubtaskResults.push({
toolCallId: toolUseId,
result: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
})
const resultContent = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
parentApiMessages = appendToolResult(parentApiMessages, toolUseId, resultContent)
}
// Clear currentSubtaskToolCallId since this subtask is complete
currentSubtaskToolCallId = undefined
// Check if there are more pending subtasks to execute after adding this result
const remainingPendingSubtasks = getPendingSubtasks(parentApiMessages)
const hasMoreSubtasks = remainingPendingSubtasks.length > 0
// Check if there are more pending subtasks to execute
const hasMoreSubtasks = pendingSubtasks.length > 0
if (!hasMoreSubtasks && completedSubtaskResults.length > 0) {
// All subtasks complete - add ALL tool_results to the conversation now
// The API expects: user → assistant (with tool_use) → user (with tool_result)
// Check if the last message is already a user message we can append to
// If no more subtasks and we have other tool results that need to be flushed,
// add them to the conversation (they come before subtask results)
if (!hasMoreSubtasks && otherToolResults.length > 0) {
// The other tool results should be added to the last user message
const lastMsg = parentApiMessages[parentApiMessages.length - 1]
let userMessageContent: any[] = []
if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
// Filter out any existing tool_results for our subtask IDs (to avoid duplicates)
const allToolResultIds = new Set([
...completedSubtaskResults.map((r) => r.toolCallId),
...pendingOtherToolResults.map((r) => r.tool_use_id),
])
userMessageContent = lastMsg.content.filter(
(block: any) => !(block.type === "tool_result" && allToolResultIds.has(block.tool_use_id)),
// Check if these results are already in the message
const existingToolResultIds = new Set(
lastMsg.content.filter((b: any) => b.type === "tool_result").map((b: any) => b.tool_use_id),
)
// Remove the last message so we can replace it with an updated one
parentApiMessages.pop()
// Add missing other tool results at the beginning (before subtask results)
const newContent = [...otherToolResults.filter((r) => !existingToolResultIds.has(r.tool_use_id))]
if (newContent.length > 0) {
// Insert other tool results at the beginning of the content array
// to maintain original tool call order
lastMsg.content = [...newContent, ...lastMsg.content]
}
}
// FIRST: Add other tool results (e.g., update_todo_list) that were called in the same turn
// These should come before subtask results to maintain the original tool call order
for (const toolResult of pendingOtherToolResults) {
userMessageContent.push({
type: "tool_result" as const,
tool_use_id: toolResult.tool_use_id,
content: toolResult.content,
})
}
// THEN: Add all completed subtask results as tool_result blocks
for (const result of completedSubtaskResults) {
userMessageContent.push({
type: "tool_result" as const,
tool_use_id: result.toolCallId,
content: result.result,
})
}
parentApiMessages.push({
role: "user",
content: userMessageContent,
ts,
})
// Clear completed results since they're now in the conversation
completedSubtaskResults = []
// Clean up the subtask state from workspaceState
await this.clearSubtaskState(parentTaskId)
} else {
// More subtasks remain - save state for the next resumption
await this.setSubtaskState(parentTaskId, {
pendingSubtasks,
completedSubtaskResults,
currentSubtaskToolCallId,
})
}
await saveApiMessages({ messages: parentApiMessages as any, taskId: parentTaskId, globalStoragePath })
// 3) Update child metadata to "completed" status
// 2) Update child metadata to "completed" status
try {
const { historyItem: childHistory } = await this.getTaskWithId(childTaskId)
await this.updateTaskHistory({
@ -3319,7 +3224,7 @@ export class ClineProvider
)
}
// 4) Update parent metadata and persist BEFORE emitting completion event
// 3) Update parent metadata and persist BEFORE emitting completion event
const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId]))
const updatedHistory: typeof historyItem = {
...historyItem,
@ -3331,24 +3236,24 @@ export class ClineProvider
}
await this.updateTaskHistory(updatedHistory)
// 5) Emit TaskDelegationCompleted (provider-level)
// 4) Emit TaskDelegationCompleted (provider-level)
try {
this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
} catch {
// non-fatal
}
// 6) Close child instance if still open (single-open-task invariant)
// 5) Close child instance if still open (single-open-task invariant)
const current = this.getCurrentTask()
if (current?.taskId === childTaskId) {
await this.removeClineFromStack()
}
// 7) Reopen the parent from history as the sole active task (restores saved mode)
// 6) Reopen the parent from history as the sole active task (restores saved mode)
// IMPORTANT: startTask=false to suppress resume-from-history ask scheduling
const parentInstance = await this.createTaskWithHistoryItem(updatedHistory, { startTask: false })
// 8) Inject restored histories into the in-memory instance before resuming
// 7) Inject restored histories into the in-memory instance before resuming
if (parentInstance) {
try {
await parentInstance.overwriteClineMessages(parentClineMessages)
@ -3361,15 +3266,13 @@ export class ClineProvider
// non-fatal
}
// Load subtask state into the parent instance (synchronous - reads from provider's in-memory state)
parentInstance.loadPendingSubtasks()
// Check if there are more pending subtasks to execute
if (parentInstance.hasPendingSubtasks()) {
// Check if there are more pending subtasks to execute (derived from history)
const pendingFromHistory = getPendingSubtasks(parentApiMessages)
if (pendingFromHistory.length > 0) {
// Execute the next pending subtask
// This will cause the parent to be "paused" again and a new child will run
this.log(
`[reopenParentFromDelegation] Parent ${parentTaskId} has ${parentInstance.pendingSubtasks.length} more subtasks, executing next one`,
`[reopenParentFromDelegation] Parent ${parentTaskId} has ${pendingFromHistory.length} more subtasks, executing next one`,
)
await parentInstance.executePendingSubtasks()
} else {
@ -3378,7 +3281,7 @@ export class ClineProvider
}
}
// 9) Emit TaskDelegationResumed (provider-level)
// 8) Emit TaskDelegationResumed (provider-level)
try {
this.emit(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId)
} catch {