Repository navigation
fix(task-lifecycle): preserve parent-child link when delegated subtask is interrupted #560
Description
Part of #357 (Epic 2: Task Lifecycle Fixes).
Problem
When a delegated subtask is interrupted mid-execution (user hits stop), cancelTask immediately severs the parent–child link:
- Parent:
status: "delegated"→"active",awaitingChildId→undefined - Child: retains
parentTaskIdbut 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/types — HistoryItem — 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 unionsrc/core/webview/ClineProvider.ts—cancelTask(~L3151),removeClineFromStack(~L523), startup reconciliationsrc/core/tools/AttemptCompletionTool.ts— accept"interrupted"child status (~L100)src/core/webview/ClineProvider.ts—reopenParentFromDelegationguard may need"interrupted"addedwebview-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-e2esubtasks)
Related
- Part of the broader state machine refactor tracked in feat(task-lifecycle): introduce explicit TaskStatus state machine with "interrupted" history state #559 (sub-issue of [Tracking] Parallelizable Tasks — Implementation Roadmap #355)
- Builds on fix(AttemptCompletionTool): child tasks return to parent when parent status is active #510, Subtask never returns to parent after a per-mode model/profile switch (attempt_completion silently waits for human) #457
Activity
edelauna commentedon Jun 14, 2026
Deferring in favor of #355 Epic 1+2 foundation
A prototype implementation was built on
issue/560(PR #613) and put through a full multi-agent code review. The review surfaced 3 blocking concurrency bugs — all stemming from the same structural root cause: new delegation-state mutations (deleteTaskWithIdparent repair,createTaskWithHistoryItemself-heal) bypassed therunDelegationTransitionserialization lock, and theremoveClineFromStackfail-closed path relied on a volatile in-memory Set that doesn't survive provider reload.These were fixed in the prototype, but the pattern is clear: every new state or transition added to the current ad-hoc system requires manually remembering to wrap mutations in the correct lock, persist the correct fallback state, and add idempotency guards — with no compile-time enforcement. This is exactly the class of bugs that #355 Epic 2 Story 2.3 (transition guards + startup reconciliation) and
atomicReadAndUpdate()onTaskHistoryStoreare designed to eliminate structurally.Recommendation: Land #355 Epics 1+2 first (Stories 1.1–1.3, 2.1–2.3), then implement
interruptedas a single story on top of the transition guard from #366. Theinterruptedstatus becomes a one-line addition to the transition map rather than a multi-file patch with hand-coded guards.The prototype branch (
issue/560, PR #613) is preserved for reference — the test coverage (E2E + unit) and the behavioral design (child marked interrupted, parent stays delegated, self-heal on rehydration) are directly reusable.fix(task-lifecycle): preserve parent-child link when delegated subtas…