Skip to content

fix(task-lifecycle): preserve parent-child link when delegated subtask is interrupted #560

@edelauna

Description

@edelauna

Problem

When a delegated subtask is interrupted mid-execution (user hits stop), cancelTask immediately severs the parent–child link:

  • Parent: status: "delegated""active", awaitingChildIdundefined
  • Child: retains parentTaskId but is now effectively standalone

When the user resumes the subtask and it calls attempt_completion, AttemptCompletionTool checks parentHistory.awaitingChildId === task.taskId — which is now false — and falls through to the standalone "Start New Task" flow. The subtask never reports back.

Users experience this as: "subtask ended with Start New Task instead of Complete Subtask and Keep Going" — particularly when the subtask was interrupted midway.

Confirmed reproducible after #510 (which fixed a different variant: profile-switch causing status: "active" on parent while awaitingChildId was still set).

Root cause: cancelTask conflates stream abortion (always correct) with parent link severance (should be explicit). There is no persisted record that the child was interrupted from a delegated context, so the relationship cannot survive a reload.

The in-memory cancelledDelegationChildIds Set is a partial guard but resets on extension reload.

Proposed Fix

ClineProvider#cancelTask — when cancelling a delegated child, do not clear the parent link. Instead mark the child as interrupted:

// Before (ClineProvider.ts ~L3151)
if (parentHistory?.status === "delegated" && parentHistory?.awaitingChildId === task.taskId) {
  await this.updateTaskHistory({
    ...parentHistory,
    status: "active",
    awaitingChildId: undefined,
  })
}

// After
if (parentHistory?.status === "delegated" && parentHistory?.awaitingChildId === task.taskId) {
  await this.updateTaskHistory({
    ...historyItem,
    status: "interrupted",  // new status — child is paused, not detached
  })
  // parent stays delegated with awaitingChildId intact
}

packages/typesHistoryItem — add "interrupted" to the status union:

status: "active" | "delegated" | "interrupted" | "completed"

AttemptCompletionTool — extend the child status check to accept "interrupted" alongside "active" (L100):

} else if (status === "active" || status === "interrupted") {

UI (task list) — interrupted subtask state needs a label and an "Abandon" action so users can explicitly break the link if they do not want to resume.

Files to Change

  • packages/types/src/HistoryItem.ts (or wherever the type lives) — add "interrupted" to status union
  • src/core/webview/ClineProvider.tscancelTask (~L3151), removeClineFromStack (~L523), startup reconciliation
  • src/core/tools/AttemptCompletionTool.ts — accept "interrupted" child status (~L100)
  • src/core/webview/ClineProvider.tsreopenParentFromDelegation guard may need "interrupted" added
  • webview-ui/ — task list UI for interrupted subtask state

Acceptance Criteria

  • Interrupt a delegated subtask mid-execution → resume it → it reports back to parent (no "Start New Task")
  • Parent–child link survives extension reload after interruption
  • User can explicitly abandon the interrupted subtask, which detaches the parent (current auto-detach behavior, now user-initiated)
  • Non-delegated task cancellation is unaffected
  • Existing subtask e2e suite passes (apps/vscode-e2e subtasks)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions