Context
Tracked under the #355 parallelizable-tasks refactor — proposed addition to Epic 1 (Foundation) or Epic 2 (Lifecycle Fixes).
Problem
HistoryItem.status is an ad-hoc state machine: valid states ("active", "delegated", "completed") are implicit string literals with no enforced transition table. Guards are scattered across AttemptCompletionTool.ts, ClineProvider#cancelTask, ClineProvider#reopenParentFromDelegation, and ClineProvider#removeClineFromStack, leading to the class of bugs fixed in #457 and #510.
A concrete open gap: when a delegated subtask is interrupted mid-execution (cancelTask fires), the code permanently severs the parent link (awaitingChildId → undefined, parent status → "active"). When the subtask resumes and calls attempt_completion, it has no way to re-attach to its parent and falls through to a standalone "Start New Task" flow. Users experience this as "subtask didn't report back to parent" — confirmed by user reports.
Root cause: cancelTask conflates two distinct operations:
- Aborting the running LLM stream (transient, always correct)
- Severing the parent–child delegation link (should be an explicit user choice, not an automatic side effect)
There is no history pseudo-state (in the HSM sense) — no persisted record that a task was interrupted from a delegated context — so the relationship cannot be restored after reload.
The cancelledDelegationChildIds in-memory Set is a partial workaround but resets on extension reload.
Proposed Solution
Phase 1 — Minimal (ship independently, low risk)
Add "interrupted" as a valid HistoryItem status:
// Before
status: "active" | "delegated" | "completed"
// After
status: "active" | "delegated" | "interrupted" | "completed"
Transition changes:
cancelTask on a delegated child → child status: "active" → "interrupted", parent retains status: "delegated" and awaitingChildId (link preserved)
AttemptCompletionTool already handles parentStatus === "delegated" — no change needed there
reopenParentFromDelegation guard already accepts "delegated" — no change needed
- UI: parent in
"delegated" state with child in "interrupted" shows "Subtask was interrupted — resume or abandon" rather than silently detaching
Files touched: HistoryItem type, ClineProvider#cancelTask, ClineProvider#removeClineFromStack, UI task list rendering for the interrupted state.
Phase 2 — Full state machine (part of #355 Epic 1)
Replace scattered status guards with an explicit transition table (XState or lightweight equivalent):
IDLE → ACTIVE → DELEGATED → ACTIVE (reopen) → COMPLETED
↘ INTERRUPTED → ACTIVE (resume) / COMPLETED (finish after interrupt)
ACTIVE → INTERRUPTED → ACTIVE (resume) → COMPLETED
- Transitions enforced as discriminated unions at compile time
- Single
TaskStateMachine owns all valid transitions — no more scattered if (status === "delegated") guards
- Slots into the
TaskRegistry (Story 3.2a) as the authoritative state authority per task
Acceptance Criteria
Related
Context
Tracked under the #355 parallelizable-tasks refactor — proposed addition to Epic 1 (Foundation) or Epic 2 (Lifecycle Fixes).
Problem
HistoryItem.statusis an ad-hoc state machine: valid states ("active","delegated","completed") are implicit string literals with no enforced transition table. Guards are scattered acrossAttemptCompletionTool.ts,ClineProvider#cancelTask,ClineProvider#reopenParentFromDelegation, andClineProvider#removeClineFromStack, leading to the class of bugs fixed in #457 and #510.A concrete open gap: when a delegated subtask is interrupted mid-execution (
cancelTaskfires), the code permanently severs the parent link (awaitingChildId → undefined, parentstatus → "active"). When the subtask resumes and callsattempt_completion, it has no way to re-attach to its parent and falls through to a standalone "Start New Task" flow. Users experience this as "subtask didn't report back to parent" — confirmed by user reports.Root cause:
cancelTaskconflates two distinct operations:There is no history pseudo-state (in the HSM sense) — no persisted record that a task was interrupted from a delegated context — so the relationship cannot be restored after reload.
The
cancelledDelegationChildIdsin-memory Set is a partial workaround but resets on extension reload.Proposed Solution
Phase 1 — Minimal (ship independently, low risk)
Add
"interrupted"as a validHistoryItemstatus:Transition changes:
cancelTaskon a delegated child → child status:"active" → "interrupted", parent retainsstatus: "delegated"andawaitingChildId(link preserved)AttemptCompletionToolalready handlesparentStatus === "delegated"— no change needed therereopenParentFromDelegationguard already accepts"delegated"— no change needed"delegated"state with child in"interrupted"shows "Subtask was interrupted — resume or abandon" rather than silently detachingFiles touched:
HistoryItemtype,ClineProvider#cancelTask,ClineProvider#removeClineFromStack, UI task list rendering for the interrupted state.Phase 2 — Full state machine (part of #355 Epic 1)
Replace scattered status guards with an explicit transition table (XState or lightweight equivalent):
TaskStateMachineowns all valid transitions — no more scatteredif (status === "delegated")guardsTaskRegistry(Story 3.2a) as the authoritative state authority per taskAcceptance Criteria
apps/vscode-e2esubtasks suite)cancelTask→ standalone-child path (non-delegated tasks unaffected)Related