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
3 changes: 3 additions & 0 deletions packages/types/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const historyItemSchema = z.object({
size: z.number().optional(),
workspace: z.string().optional(),
mode: z.string().optional(),
parentTaskId: z.string().optional(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would be helpful to add JSDoc comments explaining these new fields:

Suggested change
parentTaskId: z.string().optional(),
/** ID of the parent task if this is a subtask */
parentTaskId: z.string().optional(),
/** ID of the root task in the hierarchy */
rootTaskId: z.string().optional(),
/** Array of task IDs representing the full hierarchy from root to parent */
taskHierarchy: z.array(z.string()).optional(),

rootTaskId: z.string().optional(),
taskHierarchy: z.array(z.string()).optional(),
})

export type HistoryItem = z.infer<typeof historyItemSchema>
9 changes: 9 additions & 0 deletions src/core/task-persistence/taskMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type TaskMetadataOptions = {
globalStoragePath: string
workspace: string
mode?: string
parentTaskId?: string
rootTaskId?: string
taskHierarchy?: string[]
}

export async function taskMetadata({
Expand All @@ -28,6 +31,9 @@ export async function taskMetadata({
globalStoragePath,
workspace,
mode,
parentTaskId,
rootTaskId,
taskHierarchy,
}: TaskMetadataOptions) {
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)

Expand Down Expand Up @@ -95,6 +101,9 @@ export async function taskMetadata({
size: taskDirSize,
workspace,
mode,
parentTaskId,
rootTaskId,
taskHierarchy,
}

return { historyItem, tokenUsage }
Expand Down
24 changes: 24 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
readonly taskNumber: number
readonly workspacePath: string

// Store task IDs for persistence
readonly rootTaskId: string | undefined = undefined
readonly parentTaskId: string | undefined = undefined

/**
* The mode associated with this task. Persisted across sessions
* to maintain user context when reopening tasks from history.
Expand Down Expand Up @@ -307,6 +311,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.parentTask = parentTask
this.taskNumber = taskNumber

// Store task IDs for persistence
this.rootTaskId = rootTask?.taskId
this.parentTaskId = parentTask?.taskId

// Store the task's mode when it's created.
// For history items, use the stored mode; for new tasks, we'll set it
// after getting state.
Expand Down Expand Up @@ -582,6 +590,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
globalStoragePath: this.globalStoragePath,
workspace: this.cwd,
mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode
parentTaskId: this.parentTaskId,
rootTaskId: this.rootTaskId,
taskHierarchy: this.getTaskHierarchy(),
})

this.emit(RooCodeEventName.TaskTokenUsageUpdated, this.taskId, tokenUsage)
Expand Down Expand Up @@ -2152,4 +2163,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
public get cwd() {
return this.workspacePath
}

// Get task hierarchy for persistence
public getTaskHierarchy(): string[] {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method could cause an infinite loop if task data becomes corrupted and creates circular references. Consider:

Suggested change
public getTaskHierarchy(): string[] {
public getTaskHierarchy(): string[] {
const hierarchy: string[] = []
const visited = new Set<string>()
let currentTask: Task | undefined = this.parentTask
while (currentTask && !visited.has(currentTask.taskId)) {
visited.add(currentTask.taskId)
hierarchy.unshift(currentTask.taskId)
currentTask = currentTask.parentTask
}
return hierarchy
}

const hierarchy: string[] = []
let currentTask: Task | undefined = this.parentTask

while (currentTask) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consider adding a maximum depth limit to prevent performance issues with excessively deep task hierarchies. You could add a MAX_HIERARCHY_DEPTH constant and check against it in the while loop.

hierarchy.unshift(currentTask.taskId)
currentTask = currentTask.parentTask
}

return hierarchy
}
}
33 changes: 31 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,18 @@ export class ClineProvider
experiments,
} = await this.getState()

// Restore parent and root tasks if their IDs are stored in the history item
let rootTask: Task | undefined = historyItem.rootTask
let parentTask: Task | undefined = historyItem.parentTask

// If we don't have the actual task objects but have their IDs, try to find them in the stack
if (!rootTask && historyItem.rootTaskId) {
rootTask = this.clineStack.find((task) => task.taskId === historyItem.rootTaskId)
}
if (!parentTask && historyItem.parentTaskId) {
parentTask = this.clineStack.find((task) => task.taskId === historyItem.parentTaskId)
}

const task = new Task({
provider: this,
apiConfiguration,
Expand All @@ -749,8 +761,8 @@ export class ClineProvider
consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
historyItem,
experiments,
rootTask: historyItem.rootTask,
parentTask: historyItem.parentTask,
rootTask,
parentTask,
taskNumber: historyItem.number,
onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance),
})
Expand Down Expand Up @@ -1344,6 +1356,23 @@ export class ClineProvider
if (id !== this.getCurrentCline()?.taskId) {
// Non-current task.
const { historyItem } = await this.getTaskWithId(id)

// Check if this task has parent/child relationships that need to be restored
if (historyItem.taskHierarchy && historyItem.taskHierarchy.length > 0) {
// Restore the entire task hierarchy from root to this task
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consider adding debug logging here to help troubleshoot hierarchy restoration issues in production:

Suggested change
// Restore the entire task hierarchy from root to this task
// Restore the entire task hierarchy from root to this task
console.log(`[showTaskWithId] Restoring task hierarchy for task ${id} with ${historyItem.taskHierarchy.length} parent tasks`)
const taskHistory = this.getGlobalState("taskHistory") ?? []

const taskHistory = this.getGlobalState("taskHistory") ?? []

// First, restore all parent tasks in the hierarchy
for (const taskId of historyItem.taskHierarchy) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missing error handling here. If a parent task fails to restore, the entire hierarchy restoration could fail. Consider wrapping this in a try-catch:

Suggested change
for (const taskId of historyItem.taskHierarchy) {
for (const taskId of historyItem.taskHierarchy) {
try {
const parentHistoryItem = taskHistory.find((item: HistoryItem) => item.id === taskId)
if (parentHistoryItem && !this.clineStack.find((task) => task.taskId === taskId)) {
await this.initClineWithHistoryItem(parentHistoryItem)
}
} catch (error) {
console.error(`Failed to restore parent task ${taskId}:`, error)
// Continue with other tasks in hierarchy
}
}

const parentHistoryItem = taskHistory.find((item: HistoryItem) => item.id === taskId)
if (parentHistoryItem && !this.clineStack.find((task) => task.taskId === taskId)) {
// This parent task is not in the stack, so restore it
await this.initClineWithHistoryItem(parentHistoryItem)
}
}
}

// Now restore the requested task
await this.initClineWithHistoryItem(historyItem) // Clears existing task.
}

Expand Down
Loading