|
|
@@ -110,6 +110,7 @@ import {
|
|
|
getToolCallName,
|
|
|
getToolResultCallId,
|
|
|
} from "../task-persistence"
|
|
|
+import { type DelegationMeta, readDelegationMeta, saveDelegationMeta } from "../task-persistence/delegationMeta"
|
|
|
import { readTaskMessages } from "../task-persistence/taskMessages"
|
|
|
import { getNonce } from "./getNonce"
|
|
|
import { getUri } from "./getUri"
|
|
|
@@ -160,6 +161,7 @@ export class ClineProvider
|
|
|
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
|
|
|
private currentWorkspacePath: string | undefined
|
|
|
private _disposed = false
|
|
|
+ private delegationInProgress = false
|
|
|
|
|
|
private recentTasksCache?: string[]
|
|
|
private taskHistoryWriteLock: Promise<void> = Promise.resolve()
|
|
|
@@ -177,6 +179,7 @@ export class ClineProvider
|
|
|
private clineMessagesSeq = 0
|
|
|
|
|
|
public isViewLaunched = false
|
|
|
+ public isTaskCreationInProgress = false
|
|
|
public settingsImportedAt?: number
|
|
|
public readonly latestAnnouncementId = "feb-2026-v3.47.0-opus-4.6-gpt-5.3-codex" // v3.47.0 Claude Opus 4.6 & GPT-5.3-Codex
|
|
|
public readonly providerSettingsManager: ProviderSettingsManager
|
|
|
@@ -528,17 +531,63 @@ export class ClineProvider
|
|
|
status: "active",
|
|
|
awaitingChildId: undefined,
|
|
|
})
|
|
|
+ const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: {
|
|
|
+ status: "active",
|
|
|
+ awaitingChildId: null,
|
|
|
+ delegatedToId: parentHistory.delegatedToId,
|
|
|
+ childIds: parentHistory.childIds,
|
|
|
+ completedByChildId: parentHistory.completedByChildId,
|
|
|
+ completionResultSummary: parentHistory.completionResultSummary,
|
|
|
+ },
|
|
|
+ })
|
|
|
this.log(
|
|
|
`[ClineProvider#removeClineFromStack] Repaired parent ${parentTaskId} metadata: delegated → active (child ${childTaskId} removed)`,
|
|
|
)
|
|
|
}
|
|
|
} catch (err) {
|
|
|
- // Non-fatal: log but do not block the pop operation.
|
|
|
- this.log(
|
|
|
- `[ClineProvider#removeClineFromStack] Failed to repair parent metadata for ${parentTaskId} (non-fatal): ${
|
|
|
- err instanceof Error ? err.message : String(err)
|
|
|
- }`,
|
|
|
- )
|
|
|
+ // Disk-only fallback when parent is missing from globalState
|
|
|
+ if (err instanceof Error && err.message === "Task not found") {
|
|
|
+ try {
|
|
|
+ const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
+ const delegationMeta = await readDelegationMeta({ taskId: parentTaskId, globalStoragePath })
|
|
|
+ if (
|
|
|
+ delegationMeta?.status === "delegated" &&
|
|
|
+ delegationMeta?.awaitingChildId === childTaskId
|
|
|
+ ) {
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: {
|
|
|
+ status: "active",
|
|
|
+ awaitingChildId: null,
|
|
|
+ delegatedToId: delegationMeta.delegatedToId,
|
|
|
+ childIds: delegationMeta.childIds,
|
|
|
+ completedByChildId: delegationMeta.completedByChildId,
|
|
|
+ completionResultSummary: delegationMeta.completionResultSummary,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ this.log(
|
|
|
+ `[ClineProvider#removeClineFromStack] Repaired parent ${parentTaskId} via disk fallback (not in globalState)`,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } catch (diskErr) {
|
|
|
+ this.log(
|
|
|
+ `[ClineProvider#removeClineFromStack] Disk fallback repair also failed for ${parentTaskId}: ${
|
|
|
+ diskErr instanceof Error ? diskErr.message : String(diskErr)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.log(
|
|
|
+ `[ClineProvider#removeClineFromStack] Failed to repair parent metadata for ${parentTaskId} (non-fatal): ${
|
|
|
+ err instanceof Error ? err.message : String(err)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1045,8 +1094,6 @@ export class ClineProvider
|
|
|
onCreated: this.taskCreationCallback,
|
|
|
startTask: options?.startTask ?? true,
|
|
|
enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, taskSyncEnabled),
|
|
|
- // Preserve the status from the history item to avoid overwriting it when the task saves messages
|
|
|
- initialStatus: historyItem.status,
|
|
|
})
|
|
|
|
|
|
if (isRehydratingCurrentTask) {
|
|
|
@@ -1742,8 +1789,28 @@ export class ClineProvider
|
|
|
throw new Error("Task not found")
|
|
|
}
|
|
|
|
|
|
- const { getTaskDirectoryPath } = await import("../../utils/storage")
|
|
|
+ // Hoist globalStoragePath so the delegation-meta merge and the file-path
|
|
|
+ // resolution below share one computation.
|
|
|
const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
+
|
|
|
+ // Merge per-task delegation metadata (source of truth for delegation fields).
|
|
|
+ // Old tasks without a file are unchanged (null → no merge).
|
|
|
+ try {
|
|
|
+ const delegationMeta = await readDelegationMeta({ taskId: id, globalStoragePath })
|
|
|
+
|
|
|
+ if (delegationMeta) {
|
|
|
+ for (const [key, value] of Object.entries(delegationMeta)) {
|
|
|
+ ;(historyItem as Record<string, unknown>)[key] = value === null ? undefined : value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ // Non-fatal: fall back to globalState values
|
|
|
+ console.warn(
|
|
|
+ `[getTaskWithId] Failed to read delegation metadata for task ${id} (non-fatal): ${err instanceof Error ? err.message : String(err)}`,
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const { getTaskDirectoryPath } = await import("../../utils/storage")
|
|
|
const taskDirPath = await getTaskDirectoryPath(globalStoragePath, id)
|
|
|
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
|
|
|
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
|
|
|
@@ -1782,6 +1849,11 @@ export class ClineProvider
|
|
|
}
|
|
|
|
|
|
async showTaskWithId(id: string) {
|
|
|
+ if (this.delegationInProgress) {
|
|
|
+ this.log("[showTaskWithId] Skipped: delegation in progress")
|
|
|
+ vscode.window.showInformationMessage("Task delegation in progress, please wait...")
|
|
|
+ return
|
|
|
+ }
|
|
|
if (id !== this.getCurrentTask()?.taskId) {
|
|
|
// Non-current task.
|
|
|
const { historyItem } = await this.getTaskWithId(id)
|
|
|
@@ -1825,6 +1897,11 @@ export class ClineProvider
|
|
|
// If the task has subtasks (childIds), they will also be deleted recursively
|
|
|
async deleteTaskWithId(id: string, cascadeSubtasks: boolean = true) {
|
|
|
try {
|
|
|
+ if (this.delegationInProgress) {
|
|
|
+ this.log("[deleteTaskWithId] Skipped: delegation in progress")
|
|
|
+ vscode.window.showInformationMessage("Task delegation in progress, please wait...")
|
|
|
+ return
|
|
|
+ }
|
|
|
// get the task directory full path and history item
|
|
|
const { taskDirPath, historyItem } = await this.getTaskWithId(id)
|
|
|
|
|
|
@@ -2609,6 +2686,28 @@ export class ClineProvider
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Convenience wrapper around the standalone saveDelegationMeta function,
|
|
|
+ * injecting globalStoragePath from this provider's context.
|
|
|
+ * Exposed so tools (e.g. AttemptCompletionTool) can persist delegation
|
|
|
+ * metadata through the DelegationProvider interface.
|
|
|
+ */
|
|
|
+ async persistDelegationMeta(taskId: string, meta: DelegationMeta): Promise<void> {
|
|
|
+ const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
+ await saveDelegationMeta({ taskId, globalStoragePath, meta })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Convenience wrapper around the standalone readDelegationMeta function,
|
|
|
+ * injecting globalStoragePath from this provider's context.
|
|
|
+ * Exposed so tools (e.g. AttemptCompletionTool) can read existing delegation
|
|
|
+ * metadata for read-merge-write patterns through the DelegationProvider interface.
|
|
|
+ */
|
|
|
+ async readDelegationMeta(taskId: string): Promise<DelegationMeta | null> {
|
|
|
+ const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
+ return readDelegationMeta({ taskId, globalStoragePath })
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Broadcasts a task history update to the webview.
|
|
|
* This sends a lightweight message with just the task history, rather than the full state.
|
|
|
@@ -2923,6 +3022,12 @@ export class ClineProvider
|
|
|
options: CreateTaskOptions = {},
|
|
|
configuration: RooCodeSettings = {},
|
|
|
): Promise<Task> {
|
|
|
+ if (this.delegationInProgress && !parentTask) {
|
|
|
+ this.log("[createTask] Blocked: delegation in progress")
|
|
|
+ vscode.window.showInformationMessage("Task delegation in progress, please wait...")
|
|
|
+ throw new Error("Cannot create task while delegation is in progress")
|
|
|
+ }
|
|
|
+
|
|
|
if (configuration) {
|
|
|
await this.setValues(configuration)
|
|
|
|
|
|
@@ -3004,6 +3109,11 @@ export class ClineProvider
|
|
|
}
|
|
|
|
|
|
public async cancelTask(): Promise<void> {
|
|
|
+ if (this.delegationInProgress) {
|
|
|
+ this.log("[cancelTask] Skipped: delegation in progress")
|
|
|
+ vscode.window.showInformationMessage("Task delegation in progress, please wait...")
|
|
|
+ return
|
|
|
+ }
|
|
|
const task = this.getCurrentTask()
|
|
|
|
|
|
if (!task) {
|
|
|
@@ -3242,128 +3352,231 @@ export class ClineProvider
|
|
|
}): Promise<Task> {
|
|
|
const { parentTaskId, message, initialTodos, mode } = params
|
|
|
|
|
|
- // Metadata-driven delegation is always enabled
|
|
|
-
|
|
|
- // 1) Get parent (must be current task)
|
|
|
- const parent = this.getCurrentTask()
|
|
|
- if (!parent) {
|
|
|
- throw new Error("[delegateParentAndOpenChild] No current task")
|
|
|
+ if (this.delegationInProgress) {
|
|
|
+ throw new Error("[delegateParentAndOpenChild] Delegation already in progress")
|
|
|
}
|
|
|
- if (parent.taskId !== parentTaskId) {
|
|
|
- throw new Error(
|
|
|
- `[delegateParentAndOpenChild] Parent mismatch: expected ${parentTaskId}, current ${parent.taskId}`,
|
|
|
- )
|
|
|
- }
|
|
|
- // 2) Flush pending tool results to API history BEFORE disposing the parent.
|
|
|
- // This is critical: when tools are called before new_task,
|
|
|
- // their tool_result blocks are in userMessageContent but not yet saved to API history.
|
|
|
- // If we don't flush them, the parent's API conversation will be incomplete and
|
|
|
- // cause 400 errors when resumed (missing tool_result for tool_use blocks).
|
|
|
- //
|
|
|
- // NOTE: We do NOT pass the assistant message here because the assistant message
|
|
|
- // is already added to apiConversationHistory by the normal flow in
|
|
|
- // recursivelyMakeClineRequests BEFORE tools start executing. We only need to
|
|
|
- // flush the pending user message with tool_results.
|
|
|
+ this.delegationInProgress = true
|
|
|
+
|
|
|
+ const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
+
|
|
|
try {
|
|
|
- const flushSuccess = await parent.flushPendingToolResultsToHistory()
|
|
|
+ // Metadata-driven delegation is always enabled
|
|
|
+
|
|
|
+ // 1) Get parent (must be current task)
|
|
|
+ const parent = this.getCurrentTask()
|
|
|
+ if (!parent) {
|
|
|
+ throw new Error("[delegateParentAndOpenChild] No current task")
|
|
|
+ }
|
|
|
+ if (parent.taskId !== parentTaskId) {
|
|
|
+ throw new Error(
|
|
|
+ `[delegateParentAndOpenChild] Parent mismatch: expected ${parentTaskId}, current ${parent.taskId}`,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ // Capture parent metadata before the parent is removed from the stack (Change A)
|
|
|
+ const parentMetadata = { task: parent?.metadata?.task, taskNumber: parent?.taskNumber }
|
|
|
+
|
|
|
+ // 2) Flush pending tool results to API history BEFORE disposing the parent.
|
|
|
+ // This is critical: when tools are called before new_task,
|
|
|
+ // their tool_result blocks are in userMessageContent but not yet saved to API history.
|
|
|
+ // If we don't flush them, the parent's API conversation will be incomplete and
|
|
|
+ // cause 400 errors when resumed (missing tool_result for tool_use blocks).
|
|
|
+ //
|
|
|
+ // NOTE: We do NOT pass the assistant message here because the assistant message
|
|
|
+ // is already added to apiConversationHistory by the normal flow in
|
|
|
+ // recursivelyMakeClineRequests BEFORE tools start executing. We only need to
|
|
|
+ // flush the pending user message with tool_results.
|
|
|
+ try {
|
|
|
+ const flushSuccess = await parent.flushPendingToolResultsToHistory()
|
|
|
|
|
|
- if (!flushSuccess) {
|
|
|
- console.warn(`[delegateParentAndOpenChild] Flush failed for parent ${parentTaskId}, retrying...`)
|
|
|
- const retrySuccess = await parent.retrySaveApiConversationHistory()
|
|
|
+ if (!flushSuccess) {
|
|
|
+ console.warn(`[delegateParentAndOpenChild] Flush failed for parent ${parentTaskId}, retrying...`)
|
|
|
+ const retrySuccess = await parent.retrySaveApiConversationHistory()
|
|
|
|
|
|
- if (!retrySuccess) {
|
|
|
- console.error(
|
|
|
- `[delegateParentAndOpenChild] CRITICAL: Parent ${parentTaskId} API history not persisted to disk. Child return may produce stale state.`,
|
|
|
- )
|
|
|
- vscode.window.showWarningMessage(
|
|
|
- "Warning: Parent task state could not be saved. The parent task may lose recent context when resumed.",
|
|
|
- )
|
|
|
+ if (!retrySuccess) {
|
|
|
+ console.error(
|
|
|
+ `[delegateParentAndOpenChild] CRITICAL: Parent ${parentTaskId} API history not persisted to disk. Child return may produce stale state.`,
|
|
|
+ )
|
|
|
+ vscode.window.showWarningMessage(
|
|
|
+ "Warning: Parent task state could not be saved. The parent task may lose recent context when resumed.",
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ this.log(
|
|
|
+ `[delegateParentAndOpenChild] Error flushing pending tool results (non-fatal): ${
|
|
|
+ error instanceof Error ? error.message : String(error)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- this.log(
|
|
|
- `[delegateParentAndOpenChild] Error flushing pending tool results (non-fatal): ${
|
|
|
- error instanceof Error ? error.message : String(error)
|
|
|
- }`,
|
|
|
- )
|
|
|
- }
|
|
|
|
|
|
- // 3) Enforce single-open invariant by closing/disposing the parent first
|
|
|
- // This ensures we never have >1 tasks open at any time during delegation.
|
|
|
- // Await abort completion to ensure clean disposal and prevent unhandled rejections.
|
|
|
- try {
|
|
|
- await this.removeClineFromStack({ skipDelegationRepair: true })
|
|
|
- } catch (error) {
|
|
|
- this.log(
|
|
|
- `[delegateParentAndOpenChild] Error during parent disposal (non-fatal): ${
|
|
|
- error instanceof Error ? error.message : String(error)
|
|
|
- }`,
|
|
|
- )
|
|
|
- // Non-fatal: proceed with child creation even if parent cleanup had issues
|
|
|
- }
|
|
|
+ // 3) Enforce single-open invariant by closing/disposing the parent first
|
|
|
+ // This ensures we never have >1 tasks open at any time during delegation.
|
|
|
+ // Await abort completion to ensure clean disposal and prevent unhandled rejections.
|
|
|
+ try {
|
|
|
+ await this.removeClineFromStack({ skipDelegationRepair: true })
|
|
|
+ } catch (error) {
|
|
|
+ this.log(
|
|
|
+ `[delegateParentAndOpenChild] Error during parent disposal (non-fatal): ${
|
|
|
+ error instanceof Error ? error.message : String(error)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ // Non-fatal: proceed with child creation even if parent cleanup had issues
|
|
|
+ }
|
|
|
|
|
|
- // 3) Switch provider mode to child's requested mode BEFORE creating the child task
|
|
|
- // This ensures the child's system prompt and configuration are based on the correct mode.
|
|
|
- // The mode switch must happen before createTask() because the Task constructor
|
|
|
- // initializes its mode from provider.getState() during initializeTaskMode().
|
|
|
- try {
|
|
|
- await this.handleModeSwitch(mode as any)
|
|
|
- } catch (e) {
|
|
|
- this.log(
|
|
|
- `[delegateParentAndOpenChild] handleModeSwitch failed for mode '${mode}': ${
|
|
|
- (e as Error)?.message ?? String(e)
|
|
|
- }`,
|
|
|
- )
|
|
|
- }
|
|
|
+ // 3) Switch provider mode to child's requested mode BEFORE creating the child task
|
|
|
+ // This ensures the child's system prompt and configuration are based on the correct mode.
|
|
|
+ // The mode switch must happen before createTask() because the Task constructor
|
|
|
+ // initializes its mode from provider.getState() during initializeTaskMode().
|
|
|
+ try {
|
|
|
+ await this.handleModeSwitch(mode as any)
|
|
|
+ } catch (e) {
|
|
|
+ this.log(
|
|
|
+ `[delegateParentAndOpenChild] handleModeSwitch failed for mode '${mode}': ${
|
|
|
+ (e as Error)?.message ?? String(e)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- // 4) Create child as sole active (parent reference preserved for lineage)
|
|
|
- // Pass initialStatus: "active" to ensure the child task's historyItem is created
|
|
|
- // with status from the start, avoiding race conditions where the task might
|
|
|
- // call attempt_completion before status is persisted separately.
|
|
|
- //
|
|
|
- // Pass startTask: false to prevent the child from beginning its task loop
|
|
|
- // (and writing to globalState via saveClineMessages → updateTaskHistory)
|
|
|
- // before we persist the parent's delegation metadata in step 5.
|
|
|
- // Without this, the child's fire-and-forget startTask() races with step 5,
|
|
|
- // and the last writer to globalState overwrites the other's changes—
|
|
|
- // causing the parent's delegation fields to be lost.
|
|
|
- const child = await this.createTask(message, undefined, parent as any, {
|
|
|
- initialTodos,
|
|
|
- initialStatus: "active",
|
|
|
- startTask: false,
|
|
|
- })
|
|
|
+ // 4) Create child as sole active (parent reference preserved for lineage)
|
|
|
+ //
|
|
|
+ // Pass startTask: false to prevent the child from beginning its task loop
|
|
|
+ // (and writing to globalState via saveClineMessages → updateTaskHistory)
|
|
|
+ // before we persist the parent's delegation metadata in step 5.
|
|
|
+ // Without this, the child's fire-and-forget startTask() races with step 5,
|
|
|
+ // and the last writer to globalState overwrites the other's changes—
|
|
|
+ // causing the parent's delegation fields to be lost.
|
|
|
+ const child = await this.createTask(message, undefined, parent as any, {
|
|
|
+ initialTodos,
|
|
|
+ startTask: false,
|
|
|
+ })
|
|
|
|
|
|
- // 5) Persist parent delegation metadata BEFORE the child starts writing.
|
|
|
- try {
|
|
|
- const { historyItem } = await this.getTaskWithId(parentTaskId)
|
|
|
- const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId]))
|
|
|
- const updatedHistory: typeof historyItem = {
|
|
|
- ...historyItem,
|
|
|
- status: "delegated",
|
|
|
+ // 4b) Persist child's initial status in globalState (saveClineMessages no longer writes status)
|
|
|
+ // Build the history item directly from the child Task object instead of getTaskWithId,
|
|
|
+ // because the child isn't in globalState yet at this point.
|
|
|
+ await this.updateTaskHistory(
|
|
|
+ {
|
|
|
+ id: child.taskId,
|
|
|
+ ts: Date.now(),
|
|
|
+ task: message,
|
|
|
+ number: child.taskNumber,
|
|
|
+ tokensIn: 0,
|
|
|
+ tokensOut: 0,
|
|
|
+ totalCost: 0,
|
|
|
+ status: "active",
|
|
|
+ parentTaskId: parentTaskId,
|
|
|
+ rootTaskId: child.rootTaskId,
|
|
|
+ workspace: this.cwd,
|
|
|
+ } as HistoryItem,
|
|
|
+ { broadcast: false },
|
|
|
+ )
|
|
|
+
|
|
|
+ // 5) Persist parent delegation metadata BEFORE the child starts writing.
|
|
|
+ // updateTaskHistory (globalState) is critical — without it, parent won't show as delegated
|
|
|
+ let parentHistory: HistoryItem
|
|
|
+ try {
|
|
|
+ const result = await this.getTaskWithId(parentTaskId)
|
|
|
+ parentHistory = result.historyItem
|
|
|
+ } catch (err) {
|
|
|
+ console.error(
|
|
|
+ `[delegateParentAndOpenChild] Parent ${parentTaskId} not in globalState, using in-memory fallback: ${
|
|
|
+ err instanceof Error ? err.message : String(err)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ parentHistory = {
|
|
|
+ id: parentTaskId,
|
|
|
+ ts: Date.now(),
|
|
|
+ task: parentMetadata.task ?? "",
|
|
|
+ number: parentMetadata.taskNumber ?? 0,
|
|
|
+ tokensIn: 0,
|
|
|
+ tokensOut: 0,
|
|
|
+ totalCost: 0,
|
|
|
+ workspace: this.cwd,
|
|
|
+ } as HistoryItem
|
|
|
+ }
|
|
|
+ const childIds = Array.from(new Set([...(parentHistory.childIds ?? []), child.taskId]))
|
|
|
+ const updatedHistory = {
|
|
|
+ ...parentHistory,
|
|
|
+ status: "delegated" as const,
|
|
|
delegatedToId: child.taskId,
|
|
|
awaitingChildId: child.taskId,
|
|
|
childIds,
|
|
|
}
|
|
|
await this.updateTaskHistory(updatedHistory)
|
|
|
- } catch (err) {
|
|
|
- this.log(
|
|
|
- `[delegateParentAndOpenChild] Failed to persist parent metadata for ${parentTaskId} -> ${child.taskId}: ${
|
|
|
- (err as Error)?.message ?? String(err)
|
|
|
- }`,
|
|
|
- )
|
|
|
- }
|
|
|
|
|
|
- // 6) Start the child task now that parent metadata is safely persisted.
|
|
|
- child.start()
|
|
|
+ // Per-task file backup is non-critical — globalState is the primary source
|
|
|
+ try {
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: {
|
|
|
+ status: "delegated",
|
|
|
+ delegatedToId: child.taskId,
|
|
|
+ awaitingChildId: child.taskId,
|
|
|
+ childIds,
|
|
|
+ completedByChildId: parentHistory.completedByChildId ?? null,
|
|
|
+ completionResultSummary: parentHistory.completionResultSummary ?? null,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: child.taskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: { status: "active" },
|
|
|
+ })
|
|
|
+ } catch (err) {
|
|
|
+ this.log(
|
|
|
+ `[delegateParentAndOpenChild] Non-critical: Failed to write delegation metadata files for ${parentTaskId} -> ${child.taskId}: ${(err as Error)?.message ?? String(err)}`,
|
|
|
+ )
|
|
|
+ vscode.window.showWarningMessage(
|
|
|
+ "Delegation metadata could not be saved. Task delegation may be in a degraded state.",
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- // 7) Emit TaskDelegated (provider-level)
|
|
|
- try {
|
|
|
- this.emit(RooCodeEventName.TaskDelegated, parentTaskId, child.taskId)
|
|
|
- } catch {
|
|
|
- // non-fatal
|
|
|
- }
|
|
|
+ // 6) Start the child task now that parent metadata is safely persisted.
|
|
|
+ const startPromise = child.start()
|
|
|
+ if (startPromise) {
|
|
|
+ startPromise.catch(async (err) => {
|
|
|
+ this.log(
|
|
|
+ `[delegateParentAndOpenChild] child.start() failed for ${child.taskId}: ${
|
|
|
+ (err as Error)?.message ?? String(err)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ // Repair parent status back to active
|
|
|
+ try {
|
|
|
+ const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
|
|
|
+ await this.updateTaskHistory({ ...parentHistory, status: "active", awaitingChildId: undefined })
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: {
|
|
|
+ status: "active",
|
|
|
+ awaitingChildId: null,
|
|
|
+ delegatedToId: parentHistory.delegatedToId,
|
|
|
+ childIds: parentHistory.childIds,
|
|
|
+ completedByChildId: parentHistory.completedByChildId ?? null,
|
|
|
+ completionResultSummary: parentHistory.completionResultSummary ?? null,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ } catch (repairErr) {
|
|
|
+ this.log(
|
|
|
+ `[delegateParentAndOpenChild] Failed to repair parent after child.start() failure: ${
|
|
|
+ (repairErr as Error)?.message ?? String(repairErr)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- return child
|
|
|
+ // 7) Emit TaskDelegated (provider-level)
|
|
|
+ try {
|
|
|
+ this.emit(RooCodeEventName.TaskDelegated, parentTaskId, child.taskId)
|
|
|
+ } catch {
|
|
|
+ // non-fatal
|
|
|
+ }
|
|
|
+
|
|
|
+ return child
|
|
|
+ } finally {
|
|
|
+ this.delegationInProgress = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -3377,211 +3590,254 @@ export class ClineProvider
|
|
|
const { parentTaskId, childTaskId, completionResultSummary } = params
|
|
|
const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
|
|
|
|
|
|
- // 1) Load parent from history and current persisted messages
|
|
|
- const { historyItem } = await this.getTaskWithId(parentTaskId)
|
|
|
-
|
|
|
- let parentClineMessages: ClineMessage[] = []
|
|
|
- try {
|
|
|
- parentClineMessages = await readTaskMessages({
|
|
|
- taskId: parentTaskId,
|
|
|
- globalStoragePath,
|
|
|
- })
|
|
|
- } catch {
|
|
|
- parentClineMessages = []
|
|
|
+ if (this.delegationInProgress) {
|
|
|
+ throw new Error("[reopenParentFromDelegation] Delegation already in progress")
|
|
|
}
|
|
|
+ this.delegationInProgress = true
|
|
|
|
|
|
- let parentApiMessages: RooMessage[] = []
|
|
|
try {
|
|
|
- parentApiMessages = await readRooMessages({
|
|
|
- taskId: parentTaskId,
|
|
|
- globalStoragePath,
|
|
|
- })
|
|
|
- } catch {
|
|
|
- parentApiMessages = []
|
|
|
- }
|
|
|
-
|
|
|
- // 2) Inject synthetic records: UI subtask_result and update API tool_result
|
|
|
- const ts = Date.now()
|
|
|
-
|
|
|
- // Defensive: ensure arrays
|
|
|
- if (!Array.isArray(parentClineMessages)) parentClineMessages = []
|
|
|
- if (!Array.isArray(parentApiMessages)) parentApiMessages = []
|
|
|
-
|
|
|
- const subtaskUiMessage: ClineMessage = {
|
|
|
- type: "say",
|
|
|
- say: "subtask_result",
|
|
|
- text: completionResultSummary,
|
|
|
- ts,
|
|
|
- }
|
|
|
- parentClineMessages.push(subtaskUiMessage)
|
|
|
- await saveTaskMessages({ messages: parentClineMessages, taskId: parentTaskId, globalStoragePath })
|
|
|
-
|
|
|
- // Find the tool call ID from the last assistant message's new_task tool call
|
|
|
- let toolUseId: string | undefined
|
|
|
- for (let i = parentApiMessages.length - 1; i >= 0; i--) {
|
|
|
- const msg = parentApiMessages[i]
|
|
|
- if (isRooAssistantMessage(msg) && Array.isArray(msg.content)) {
|
|
|
- for (const block of msg.content) {
|
|
|
- const typedBlock = block as unknown as { type: string }
|
|
|
- if (isAnyToolCallBlock(typedBlock) && getToolCallName(typedBlock) === "new_task") {
|
|
|
- toolUseId = getToolCallId(typedBlock)
|
|
|
- break
|
|
|
+ // 1) Load parent from history and current persisted messages
|
|
|
+ const { historyItem } = await this.getTaskWithId(parentTaskId)
|
|
|
+
|
|
|
+ let parentClineMessages: ClineMessage[] = []
|
|
|
+ try {
|
|
|
+ parentClineMessages = await readTaskMessages({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ parentClineMessages = []
|
|
|
+ }
|
|
|
+
|
|
|
+ let parentApiMessages: RooMessage[] = []
|
|
|
+ try {
|
|
|
+ parentApiMessages = await readRooMessages({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ parentApiMessages = []
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2) Inject synthetic records: UI subtask_result and update API tool_result
|
|
|
+ const ts = Date.now()
|
|
|
+
|
|
|
+ // Defensive: ensure arrays
|
|
|
+ if (!Array.isArray(parentClineMessages)) parentClineMessages = []
|
|
|
+ if (!Array.isArray(parentApiMessages)) parentApiMessages = []
|
|
|
+
|
|
|
+ const subtaskUiMessage: ClineMessage = {
|
|
|
+ type: "say",
|
|
|
+ say: "subtask_result",
|
|
|
+ text: completionResultSummary,
|
|
|
+ ts,
|
|
|
+ }
|
|
|
+ parentClineMessages.push(subtaskUiMessage)
|
|
|
+ await saveTaskMessages({ messages: parentClineMessages, taskId: parentTaskId, globalStoragePath })
|
|
|
+
|
|
|
+ // Find the tool call ID from the last assistant message's new_task tool call
|
|
|
+ let toolUseId: string | undefined
|
|
|
+ for (let i = parentApiMessages.length - 1; i >= 0; i--) {
|
|
|
+ const msg = parentApiMessages[i]
|
|
|
+ if (isRooAssistantMessage(msg) && Array.isArray(msg.content)) {
|
|
|
+ for (const block of msg.content) {
|
|
|
+ const typedBlock = block as unknown as { type: string }
|
|
|
+ if (isAnyToolCallBlock(typedBlock) && getToolCallName(typedBlock) === "new_task") {
|
|
|
+ toolUseId = getToolCallId(typedBlock)
|
|
|
+ break
|
|
|
+ }
|
|
|
}
|
|
|
+ if (toolUseId) break
|
|
|
}
|
|
|
- if (toolUseId) break
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Preferred: if the parent history contains a new_task tool call,
|
|
|
- // inject a matching tool result for the model message contract.
|
|
|
- if (toolUseId) {
|
|
|
- // Check if the last message already contains a tool result for this tool call ID
|
|
|
- // (in case this is a retry or the history was already updated)
|
|
|
- const lastMsg = parentApiMessages[parentApiMessages.length - 1]
|
|
|
- let alreadyHasToolResult = false
|
|
|
- if (lastMsg && "role" in lastMsg && Array.isArray(lastMsg.content)) {
|
|
|
- for (const block of lastMsg.content) {
|
|
|
- const typedBlock = block as unknown as { type: string }
|
|
|
- if (isAnyToolResultBlock(typedBlock) && getToolResultCallId(typedBlock) === toolUseId) {
|
|
|
- const updatedText = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
|
|
|
- if (typedBlock.type === "tool-result") {
|
|
|
- ;(typedBlock as { output: { type: "text"; value: string } }).output = {
|
|
|
- type: "text",
|
|
|
- value: updatedText,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Preferred: if the parent history contains a new_task tool call,
|
|
|
+ // inject a matching tool result for the model message contract.
|
|
|
+ if (toolUseId) {
|
|
|
+ // Check if the last message already contains a tool result for this tool call ID
|
|
|
+ // (in case this is a retry or the history was already updated)
|
|
|
+ const lastMsg = parentApiMessages[parentApiMessages.length - 1]
|
|
|
+ let alreadyHasToolResult = false
|
|
|
+ if (lastMsg && "role" in lastMsg && Array.isArray(lastMsg.content)) {
|
|
|
+ for (const block of lastMsg.content) {
|
|
|
+ const typedBlock = block as unknown as { type: string }
|
|
|
+ if (isAnyToolResultBlock(typedBlock) && getToolResultCallId(typedBlock) === toolUseId) {
|
|
|
+ const updatedText = `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`
|
|
|
+ if (typedBlock.type === "tool-result") {
|
|
|
+ ;(typedBlock as { output: { type: "text"; value: string } }).output = {
|
|
|
+ type: "text",
|
|
|
+ value: updatedText,
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ;(typedBlock as { content: string }).content = updatedText
|
|
|
}
|
|
|
- } else {
|
|
|
- ;(typedBlock as { content: string }).content = updatedText
|
|
|
+ alreadyHasToolResult = true
|
|
|
+ break
|
|
|
}
|
|
|
- alreadyHasToolResult = true
|
|
|
- break
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // If no existing tool result found, create a NEW tool message with the tool result
|
|
|
- if (!alreadyHasToolResult) {
|
|
|
+ // If no existing tool result found, create a NEW tool message with the tool result
|
|
|
+ if (!alreadyHasToolResult) {
|
|
|
+ parentApiMessages.push({
|
|
|
+ role: "tool",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "tool-result" as const,
|
|
|
+ toolCallId: toolUseId,
|
|
|
+ toolName: "new_task",
|
|
|
+ output: {
|
|
|
+ type: "text" as const,
|
|
|
+ value: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ ts,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate the newly injected/updated tool result against the preceding assistant message.
|
|
|
+ const lastMessage = parentApiMessages[parentApiMessages.length - 1]
|
|
|
+ if (
|
|
|
+ lastMessage &&
|
|
|
+ (isRooToolMessage(lastMessage) || ("role" in lastMessage && lastMessage.role === "user"))
|
|
|
+ ) {
|
|
|
+ const validatedMessage = validateAndFixToolResultIds(lastMessage, parentApiMessages.slice(0, -1))
|
|
|
+ parentApiMessages[parentApiMessages.length - 1] = validatedMessage as RooMessage
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // If there is no corresponding tool call in the parent API history, we cannot emit a
|
|
|
+ // tool result. Fall back to a plain user text note so the parent can still resume.
|
|
|
parentApiMessages.push({
|
|
|
- role: "tool",
|
|
|
+ role: "user",
|
|
|
content: [
|
|
|
{
|
|
|
- type: "tool-result" as const,
|
|
|
- toolCallId: toolUseId,
|
|
|
- toolName: "new_task",
|
|
|
- output: {
|
|
|
- type: "text" as const,
|
|
|
- value: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
|
|
|
- },
|
|
|
+ type: "text" as const,
|
|
|
+ text: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
|
|
|
},
|
|
|
],
|
|
|
ts,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- // Validate the newly injected/updated tool result against the preceding assistant message.
|
|
|
- const lastMessage = parentApiMessages[parentApiMessages.length - 1]
|
|
|
- if (
|
|
|
- lastMessage &&
|
|
|
- (isRooToolMessage(lastMessage) || ("role" in lastMessage && lastMessage.role === "user"))
|
|
|
- ) {
|
|
|
- const validatedMessage = validateAndFixToolResultIds(lastMessage, parentApiMessages.slice(0, -1))
|
|
|
- parentApiMessages[parentApiMessages.length - 1] = validatedMessage as RooMessage
|
|
|
- }
|
|
|
- } else {
|
|
|
- // If there is no corresponding tool call in the parent API history, we cannot emit a
|
|
|
- // tool result. Fall back to a plain user text note so the parent can still resume.
|
|
|
- parentApiMessages.push({
|
|
|
- role: "user",
|
|
|
- content: [
|
|
|
- {
|
|
|
- type: "text" as const,
|
|
|
- text: `Subtask ${childTaskId} completed.\n\nResult:\n${completionResultSummary}`,
|
|
|
- },
|
|
|
- ],
|
|
|
- ts,
|
|
|
+ const savedApiMessages = await saveRooMessages({
|
|
|
+ messages: parentApiMessages,
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
})
|
|
|
- }
|
|
|
-
|
|
|
- const savedApiMessages = await saveRooMessages({
|
|
|
- messages: parentApiMessages,
|
|
|
- taskId: parentTaskId,
|
|
|
- globalStoragePath,
|
|
|
- })
|
|
|
- if (savedApiMessages === false) {
|
|
|
- this.log(`[reopenParentFromDelegation] Failed to save API messages for parent ${parentTaskId}`)
|
|
|
- }
|
|
|
-
|
|
|
- // 3) Close child instance if still open (single-open-task invariant).
|
|
|
- // This MUST happen BEFORE updating the child's status to "completed" because
|
|
|
- // removeClineFromStack() → abortTask(true) → saveClineMessages() writes
|
|
|
- // the historyItem with initialStatus (typically "active"), which would
|
|
|
- // overwrite a "completed" status set earlier.
|
|
|
- const current = this.getCurrentTask()
|
|
|
- if (current?.taskId === childTaskId) {
|
|
|
- await this.removeClineFromStack()
|
|
|
- }
|
|
|
+ if (savedApiMessages === false) {
|
|
|
+ this.log(`[reopenParentFromDelegation] Failed to save API messages for parent ${parentTaskId}`)
|
|
|
+ }
|
|
|
|
|
|
- // 4) Update child metadata to "completed" status.
|
|
|
- // This runs after the abort so it overwrites the stale "active" status
|
|
|
- // that saveClineMessages() may have written during step 3.
|
|
|
- try {
|
|
|
- const { historyItem: childHistory } = await this.getTaskWithId(childTaskId)
|
|
|
- await this.updateTaskHistory({
|
|
|
- ...childHistory,
|
|
|
- status: "completed",
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- this.log(
|
|
|
- `[reopenParentFromDelegation] Failed to persist child completed status for ${childTaskId}: ${
|
|
|
- (err as Error)?.message ?? String(err)
|
|
|
- }`,
|
|
|
- )
|
|
|
- }
|
|
|
+ // 3) Close child instance if still open (single-open-task invariant).
|
|
|
+ // This MUST happen BEFORE updating the child's status to "completed" because
|
|
|
+ // removeClineFromStack() → abortTask(true) → saveClineMessages() calls
|
|
|
+ // updateTaskHistory which would overwrite a "completed" status set earlier
|
|
|
+ // (updateTaskHistory spreads incoming fields over existing ones).
|
|
|
+ const current = this.getCurrentTask()
|
|
|
+ if (current?.taskId === childTaskId) {
|
|
|
+ await this.removeClineFromStack({ skipDelegationRepair: true })
|
|
|
+ }
|
|
|
|
|
|
- // 5) Update parent metadata and persist BEFORE emitting completion event
|
|
|
- const childIds = Array.from(new Set([...(historyItem.childIds ?? []), childTaskId]))
|
|
|
- const updatedHistory: typeof historyItem = {
|
|
|
- ...historyItem,
|
|
|
- status: "active",
|
|
|
- completedByChildId: childTaskId,
|
|
|
- completionResultSummary,
|
|
|
- awaitingChildId: undefined,
|
|
|
- childIds,
|
|
|
- }
|
|
|
- await this.updateTaskHistory(updatedHistory)
|
|
|
+ // 4) Update child metadata to "completed" status.
|
|
|
+ // This runs after the abort so it overwrites the stale "active" status
|
|
|
+ // that saveClineMessages() may have written during step 3.
|
|
|
|
|
|
- // 6) Emit TaskDelegationCompleted (provider-level)
|
|
|
- try {
|
|
|
- this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
|
|
|
- } catch {
|
|
|
- // non-fatal
|
|
|
- }
|
|
|
+ try {
|
|
|
+ const { historyItem: childHistory } = await this.getTaskWithId(childTaskId)
|
|
|
+ await this.updateTaskHistory({
|
|
|
+ ...childHistory,
|
|
|
+ status: "completed",
|
|
|
+ })
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: childTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: { status: "completed" },
|
|
|
+ })
|
|
|
+ } catch (err) {
|
|
|
+ this.log(
|
|
|
+ `[reopenParentFromDelegation] Failed to persist child completed status for ${childTaskId}: ${
|
|
|
+ (err as Error)?.message ?? String(err)
|
|
|
+ }`,
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- // 7) 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 })
|
|
|
+ // 5) Update parent metadata and persist BEFORE emitting completion event
|
|
|
+ // Re-read parent to avoid stale-read TOCTOU: historyItem was loaded at step 1
|
|
|
+ // but many async operations (message injection, child abort) have elapsed since.
|
|
|
+ const { historyItem: freshParent } = await this.getTaskWithId(parentTaskId)
|
|
|
+ const childIds = Array.from(new Set([...(freshParent.childIds ?? []), childTaskId]))
|
|
|
+ const updatedHistory: typeof freshParent = {
|
|
|
+ ...freshParent,
|
|
|
+ status: "active",
|
|
|
+ completedByChildId: childTaskId,
|
|
|
+ completionResultSummary,
|
|
|
+ awaitingChildId: undefined,
|
|
|
+ childIds,
|
|
|
+ }
|
|
|
+ await this.updateTaskHistory(updatedHistory)
|
|
|
+ await saveDelegationMeta({
|
|
|
+ taskId: parentTaskId,
|
|
|
+ globalStoragePath,
|
|
|
+ meta: {
|
|
|
+ status: "active",
|
|
|
+ completedByChildId: childTaskId,
|
|
|
+ completionResultSummary,
|
|
|
+ awaitingChildId: null,
|
|
|
+ childIds,
|
|
|
+ delegatedToId: freshParent.delegatedToId ?? null,
|
|
|
+ },
|
|
|
+ })
|
|
|
|
|
|
- // 8) Inject restored histories into the in-memory instance before resuming
|
|
|
- if (parentInstance) {
|
|
|
+ // 6) Emit TaskDelegationCompleted (provider-level)
|
|
|
try {
|
|
|
- await parentInstance.overwriteClineMessages(parentClineMessages)
|
|
|
+ this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, completionResultSummary)
|
|
|
} catch {
|
|
|
// non-fatal
|
|
|
}
|
|
|
+
|
|
|
+ // 7) 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
|
|
|
+ if (parentInstance) {
|
|
|
+ try {
|
|
|
+ await parentInstance.overwriteClineMessages(parentClineMessages)
|
|
|
+ } catch {
|
|
|
+ // non-fatal
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await parentInstance.overwriteApiConversationHistory(parentApiMessages as any)
|
|
|
+ } catch {
|
|
|
+ // non-fatal
|
|
|
+ }
|
|
|
+
|
|
|
+ // Clear delegation guard BEFORE resuming parent so the parent's
|
|
|
+ // task loop can itself initiate a new delegation (new_task) without
|
|
|
+ // hitting the "delegation already in progress" guard. This early
|
|
|
+ // reset is safe because all metadata is persisted and the parent
|
|
|
+ // instance is fully reconstructed at this point.
|
|
|
+ //
|
|
|
+ // The `finally` block at the bottom of this method also sets
|
|
|
+ // `delegationInProgress = false` as a safety net for error paths —
|
|
|
+ // if an exception is thrown before reaching this line, the finally
|
|
|
+ // block ensures the guard is always released. On the happy path
|
|
|
+ // the finally reset is a harmless no-op.
|
|
|
+ this.delegationInProgress = false
|
|
|
+
|
|
|
+ // Auto-resume parent without ask("resume_task")
|
|
|
+ await parentInstance.resumeAfterDelegation()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 9) Emit TaskDelegationResumed (provider-level)
|
|
|
try {
|
|
|
- await parentInstance.overwriteApiConversationHistory(parentApiMessages as any)
|
|
|
+ this.emit(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId)
|
|
|
} catch {
|
|
|
// non-fatal
|
|
|
}
|
|
|
-
|
|
|
- // Auto-resume parent without ask("resume_task")
|
|
|
- await parentInstance.resumeAfterDelegation()
|
|
|
- }
|
|
|
-
|
|
|
- // 9) Emit TaskDelegationResumed (provider-level)
|
|
|
- try {
|
|
|
- this.emit(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId)
|
|
|
- } catch {
|
|
|
- // non-fatal
|
|
|
+ } finally {
|
|
|
+ this.delegationInProgress = false
|
|
|
}
|
|
|
}
|
|
|
|