Skip to content

Commit 4b237d2

Browse files
committed
fix: parent-child task relationships across extension reloads
1 parent 8fee312 commit 4b237d2

File tree

4 files changed

+737
-4
lines changed

4 files changed

+737
-4
lines changed

.changeset/rude-pans-throw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Fix parent-child task relationships across extension reloads

src/core/task/Task.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
572572

573573
// API Messages
574574

575-
private async getSavedApiConversationHistory(): Promise<ApiMessage[]> {
575+
public async getSavedApiConversationHistory(): Promise<ApiMessage[]> {
576576
return readApiMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
577577
}
578578

@@ -602,7 +602,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
602602

603603
// Cline Messages
604604

605-
private async getSavedClineMessages(): Promise<ClineMessage[]> {
605+
public async getSavedClineMessages(): Promise<ClineMessage[]> {
606606
return readTaskMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
607607
}
608608

src/core/webview/ClineProvider.ts

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ export class ClineProvider
456456
await this.removeClineFromStack()
457457
// Resume the last cline instance in the stack (if it exists - this is
458458
// the 'parent' calling task).
459-
await this.getCurrentTask()?.completeSubtask(lastMessage)
459+
await this.continueParentTask(lastMessage)
460460
}
461461
// Pending Edit Operations Management
462462

@@ -1482,12 +1482,209 @@ export class ClineProvider
14821482
if (id !== this.getCurrentTask()?.taskId) {
14831483
// Non-current task.
14841484
const { historyItem } = await this.getTaskWithId(id)
1485-
await this.createTaskWithHistoryItem(historyItem) // Clears existing task.
1485+
// If this is a subtask, we need to reconstruct the entire task stack
1486+
if (historyItem.parentTaskId || historyItem.rootTaskId) {
1487+
await this.reconstructTaskStack(historyItem)
1488+
} else {
1489+
// For standalone tasks, use the normal flow
1490+
await this.createTaskWithHistoryItem(historyItem)
1491+
}
14861492
}
14871493

14881494
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
14891495
}
14901496

1497+
private async continueParentTask(lastMessage: string): Promise<void> {
1498+
const parentTask = this.getCurrentTask()
1499+
if (parentTask) {
1500+
this.log(`[continueParentTask] Found parent task ${parentTask.taskId}, isPaused: ${parentTask.isPaused}`)
1501+
this.log(`[continueParentTask] Parent task isInitialized: ${parentTask.isInitialized}`)
1502+
1503+
try {
1504+
// If the parent task is not initialized, we need to initialize it properly
1505+
if (!parentTask.isInitialized) {
1506+
this.log(`[continueParentTask] Initializing parent task from history`)
1507+
// Load the parent task's saved messages and API conversation
1508+
parentTask.clineMessages = await parentTask.getSavedClineMessages()
1509+
parentTask.apiConversationHistory = await parentTask.getSavedApiConversationHistory()
1510+
parentTask.isInitialized = true
1511+
this.log(
1512+
`[continueParentTask] Parent task initialized with ${parentTask.clineMessages.length} messages`,
1513+
)
1514+
}
1515+
1516+
// Complete the subtask on the existing parent task
1517+
// This will add the subtask result to the parent's conversation and unpause it
1518+
await parentTask.completeSubtask(lastMessage)
1519+
this.log(`[continueParentTask] Parent task ${parentTask.taskId} subtask completed`)
1520+
1521+
// Check if the parent task needs to continue its execution
1522+
// If the parent task was created from history reconstruction, it may not have
1523+
// an active execution loop running, so we need to continue it manually
1524+
if (!parentTask.isPaused && parentTask.isInitialized) {
1525+
this.log(`[continueParentTask] Parent task is unpaused and initialized, continuing execution`)
1526+
1527+
// Continue the parent task's execution with the subtask result
1528+
// The subtask result has already been added to the conversation by completeSubtask
1529+
// Now we need to continue the execution loop
1530+
const continueExecution = async () => {
1531+
try {
1532+
// Continue the task loop with an empty user content since the subtask result
1533+
// has already been added to the API conversation history
1534+
await parentTask.recursivelyMakeClineRequests([], false)
1535+
} catch (error) {
1536+
this.log(
1537+
`[continueParentTask] Error continuing parent task execution: ${error instanceof Error ? error.message : String(error)}`,
1538+
)
1539+
}
1540+
}
1541+
// Start the continuation in the background to avoid blocking
1542+
continueExecution()
1543+
}
1544+
1545+
// Update the webview to show the parent task
1546+
this.log(`[continueParentTask] Updating webview state`)
1547+
await this.postStateToWebview()
1548+
this.log(`[continueParentTask] Webview state updated`)
1549+
} catch (error) {
1550+
this.log(
1551+
`[continueParentTask] Error during parent task resumption: ${error instanceof Error ? error.message : String(error)}`,
1552+
)
1553+
throw error
1554+
}
1555+
} else {
1556+
this.log(`[continueParentTask] No parent task found in stack`)
1557+
}
1558+
}
1559+
1560+
/**
1561+
* Reconstructs the entire task stack for a subtask by loading and adding
1562+
* all parent tasks to the stack in the correct order, then adding the target subtask.
1563+
* This ensures that when the subtask finishes, control returns to the parent task.
1564+
*/
1565+
private async reconstructTaskStack(targetHistoryItem: HistoryItem): Promise<void> {
1566+
// Clear the current stack
1567+
await this.removeClineFromStack()
1568+
1569+
// Build the task hierarchy from root to target
1570+
const taskHierarchy = await this.buildTaskHierarchy(targetHistoryItem)
1571+
1572+
this.log(`[reconstructTaskStack] Reconstructing stack with ${taskHierarchy.length} tasks`)
1573+
1574+
const createdTasks: Task[] = []
1575+
1576+
// Create all tasks in the hierarchy with proper parent/root references
1577+
for (let i = 0; i < taskHierarchy.length; i++) {
1578+
const historyItem = taskHierarchy[i]
1579+
const isTargetTask = i === taskHierarchy.length - 1
1580+
1581+
// Determine parent and root task references
1582+
const parentTask = i > 0 ? createdTasks[i - 1] : undefined
1583+
const rootTask = createdTasks[0] || undefined
1584+
1585+
// Create the task with proper parent/root references
1586+
const task = await this.createTaskFromHistoryItem(historyItem, isTargetTask, parentTask, rootTask)
1587+
1588+
// Pause parent tasks so only the target runs
1589+
if (!isTargetTask) {
1590+
task.isPaused = true
1591+
this.log(`[reconstructTaskStack] Added paused parent task ${task.taskId}`)
1592+
} else {
1593+
this.log(`[reconstructTaskStack] Added and started target task ${task.taskId}`)
1594+
}
1595+
1596+
createdTasks.push(task)
1597+
await this.addClineToStack(task)
1598+
}
1599+
1600+
// Establish parent-child relationships after all tasks are created
1601+
for (let i = 0; i < createdTasks.length - 1; i++) {
1602+
const parentTask = createdTasks[i]
1603+
const childTask = createdTasks[i + 1]
1604+
1605+
// Set the childTaskId on the parent to point to the child
1606+
parentTask.childTaskId = childTask.taskId
1607+
this.log(`[reconstructTaskStack] Linked parent ${parentTask.taskId} to child ${childTask.taskId}`)
1608+
}
1609+
}
1610+
1611+
/**
1612+
* Builds the complete task hierarchy from root to target task.
1613+
* Returns an array of HistoryItems in execution order (root first, target last).
1614+
*/
1615+
private async buildTaskHierarchy(targetHistoryItem: HistoryItem): Promise<HistoryItem[]> {
1616+
const hierarchy: HistoryItem[] = []
1617+
const visited = new Set<string>()
1618+
1619+
// Recursive function to build hierarchy
1620+
const addToHierarchy = async (historyItem: HistoryItem): Promise<void> => {
1621+
// Prevent infinite loops
1622+
if (visited.has(historyItem.id)) {
1623+
return
1624+
}
1625+
visited.add(historyItem.id)
1626+
1627+
// If this task has a parent, add the parent first
1628+
if (historyItem.parentTaskId) {
1629+
try {
1630+
const { historyItem: parentHistoryItem } = await this.getTaskWithId(historyItem.parentTaskId)
1631+
await addToHierarchy(parentHistoryItem)
1632+
} catch (error) {
1633+
this.log(
1634+
`[buildTaskHierarchy] Failed to load parent task ${historyItem.parentTaskId}: ${error instanceof Error ? error.message : String(error)}`,
1635+
)
1636+
}
1637+
}
1638+
1639+
// Add this task to the hierarchy
1640+
hierarchy.push(historyItem)
1641+
}
1642+
1643+
await addToHierarchy(targetHistoryItem)
1644+
return hierarchy
1645+
}
1646+
1647+
/**
1648+
* Creates a Task instance from a HistoryItem.
1649+
* Used for reconstructing the task stack.
1650+
*/
1651+
private async createTaskFromHistoryItem(
1652+
historyItem: HistoryItem,
1653+
shouldStart: boolean = false,
1654+
parentTask?: Task,
1655+
rootTask?: Task,
1656+
): Promise<Task> {
1657+
const {
1658+
apiConfiguration,
1659+
diffEnabled: enableDiff,
1660+
enableCheckpoints,
1661+
fuzzyMatchThreshold,
1662+
experiments,
1663+
cloudUserInfo,
1664+
remoteControlEnabled,
1665+
} = await this.getState()
1666+
1667+
const task = new Task({
1668+
provider: this,
1669+
apiConfiguration,
1670+
enableDiff,
1671+
enableCheckpoints,
1672+
fuzzyMatchThreshold,
1673+
consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
1674+
historyItem,
1675+
experiments,
1676+
parentTask, // Pass the actual parent Task object
1677+
rootTask, // Pass the actual root Task object
1678+
taskNumber: historyItem.number,
1679+
workspacePath: historyItem.workspace,
1680+
onCreated: this.taskCreationCallback,
1681+
enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled),
1682+
startTask: shouldStart, // Only start the target task
1683+
})
1684+
1685+
return task
1686+
}
1687+
14911688
async exportTaskWithId(id: string) {
14921689
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
14931690
await downloadTask(historyItem.ts, apiConversationHistory)

0 commit comments

Comments
 (0)