Related to #1100.
Summary
When a delegated child delegates a grandchild and the chain is interrupted externally, the intermediate child stays persisted as delegated with no live session. The parent re-delegation guard sees delegated, not interrupted, and blocks forever. Startup reconciliation explicitly leaves this state as-is. No status transition can move a dead delegated child to a terminal status.
Reproduction
- Start a parent task in a mode that can delegate, for example
orchestrator.
- Delegate a child with
new_task.
- From within that child, delegate a grandchild.
- Skip or interrupt the grandchild mid-run so the child never completes. Control returns to the parent with the delegation tool call interrupted.
- Ask the parent to delegate again.
- The delegation fails:
Cannot re-delegate while the awaited child is not interrupted.
- Restart the extension host and retry. The failure persists.
Live persisted evidence from one session:
- Parent
01a097bc-f04a: status delegated, awaits 01a0982c-7c1b.
- Child
01a0982c-7c1b: status delegated, awaits 01a0982c-d446.
- Grandchild
01a0982c-d446: status interrupted.
Expected behavior
The parent can re-delegate after the whole descendant chain is dead, or a recovery transition resolves the dead chain.
Actual behavior
The guard throws and the parent can never delegate again. The state survives an extension restart.
Root cause
- The transition table at
src/core/task-persistence/taskLifecycle.ts line 6 allows delegated to active only. interruptDelegatedChild requires child status active. abandonDelegatedChild requires child status interrupted. completeDelegatedChild requires a running child.
reconcileDelegationStateCore at src/core/task-persistence/TaskHistoryStore.ts line 499 leaves awaited children with status interrupted or delegated as-is. A dead delegated intermediate child is never repaired.
- The re-delegation guard at
src/core/webview/ClineProvider.ts line 3887 inspects only the direct awaited child and throws without reporting the actual status.
Relation to #1100
#1100 fixed the phantom active awaited child through startup reconciliation. That fix explicitly skips the delegated intermediate child case. This issue is the uncovered sibling case.
Why the existing lifecycle model did not flag this
The stuck state is reachable through valid transitions and the model considers it valid. Trace: delegate parent to child, delegate child to grandchild, then interrupt the grandchild. That yields exactly the persisted chain above.
The model invariants pass on that state. Invariant 2 allows a delegated child that awaits a nested child. Invariant 4 allows interrupted lineage to persist. The model action set in scripts/check-task-lifecycle.ts contains only valid lifecycle operations. A delegated child is implicitly immortal in the model: it can always resume or complete later. Real task death, a process kill or a session skip, is a fault injection with no model action. Reconciliation then completes the trap, because its conservative leave-as-is branch is safe for a live child and a livelock for a dead one.
The gap is therefore not interleaving coverage. It is one missing fault action and one missing liveness invariant.
Suggested fix
Model and reducer first, then runtime:
- Add a
killDelegatedChild fault action to scripts/check-task-lifecycle.ts that terminates a child in active or delegated status without a terminal transition.
- Add a recovery invariant: every reachable state with a
delegated parent returns to re-delegable in bounded steps after chain death.
- Extend
VALID_TASK_STATUS_TRANSITIONS in src/core/task-persistence/taskLifecycle.ts or handle severing inside the reducer, so model and production share the same rule.
- Walk the
awaitingChildId chain in the re-delegation guard or in reconciliation. Sever a chain that terminates in interrupted or completed when no live task instance exists for any link.
- Include the actual awaited-child status in the guard error message at
src/core/webview/ClineProvider.ts line 3888.
- Run
pnpm lifecycle:model-check.
Related to #1100.
Summary
When a delegated child delegates a grandchild and the chain is interrupted externally, the intermediate child stays persisted as
delegatedwith no live session. The parent re-delegation guard seesdelegated, notinterrupted, and blocks forever. Startup reconciliation explicitly leaves this state as-is. No status transition can move a deaddelegatedchild to a terminal status.Reproduction
orchestrator.new_task.Cannot re-delegate while the awaited child is not interrupted.Live persisted evidence from one session:
01a097bc-f04a: statusdelegated, awaits01a0982c-7c1b.01a0982c-7c1b: statusdelegated, awaits01a0982c-d446.01a0982c-d446: statusinterrupted.Expected behavior
The parent can re-delegate after the whole descendant chain is dead, or a recovery transition resolves the dead chain.
Actual behavior
The guard throws and the parent can never delegate again. The state survives an extension restart.
Root cause
src/core/task-persistence/taskLifecycle.tsline 6 allowsdelegated to activeonly.interruptDelegatedChildrequires child statusactive.abandonDelegatedChildrequires child statusinterrupted.completeDelegatedChildrequires a running child.reconcileDelegationStateCoreatsrc/core/task-persistence/TaskHistoryStore.tsline 499 leaves awaited children with statusinterruptedordelegatedas-is. A deaddelegatedintermediate child is never repaired.src/core/webview/ClineProvider.tsline 3887 inspects only the direct awaited child and throws without reporting the actual status.Relation to #1100
#1100 fixed the phantom
activeawaited child through startup reconciliation. That fix explicitly skips thedelegatedintermediate child case. This issue is the uncovered sibling case.Why the existing lifecycle model did not flag this
The stuck state is reachable through valid transitions and the model considers it valid. Trace: delegate parent to child, delegate child to grandchild, then interrupt the grandchild. That yields exactly the persisted chain above.
The model invariants pass on that state. Invariant 2 allows a delegated child that awaits a nested child. Invariant 4 allows interrupted lineage to persist. The model action set in
scripts/check-task-lifecycle.tscontains only valid lifecycle operations. A delegated child is implicitly immortal in the model: it can always resume or complete later. Real task death, a process kill or a session skip, is a fault injection with no model action. Reconciliation then completes the trap, because its conservative leave-as-is branch is safe for a live child and a livelock for a dead one.The gap is therefore not interleaving coverage. It is one missing fault action and one missing liveness invariant.
Suggested fix
Model and reducer first, then runtime:
killDelegatedChildfault action toscripts/check-task-lifecycle.tsthat terminates a child inactiveordelegatedstatus without a terminal transition.delegatedparent returns to re-delegable in bounded steps after chain death.VALID_TASK_STATUS_TRANSITIONSinsrc/core/task-persistence/taskLifecycle.tsor handle severing inside the reducer, so model and production share the same rule.awaitingChildIdchain in the re-delegation guard or in reconciliation. Sever a chain that terminates ininterruptedorcompletedwhen no live task instance exists for any link.src/core/webview/ClineProvider.tsline 3888.pnpm lifecycle:model-check.