Zoo Code version: 3.57.100116
API provider/model: Reproduces with per-mode profiles where the parent (orchestrator) and the spawned subtask use different model profiles (any providers).
Related: Part of #357 (Epic 2: Task Lifecycle Fixes). This is a distinct defect from the step 4/5 atomicity race — it is a return-guard that rejects the active parent state outright, so the parent never reaches those writes on the cross-profile hop.
What happens
When an orchestrator/parent task delegates to a subtask whose mode is bound to a different API-configuration profile than the parent, the subtask completes its work and calls attempt_completion, but control never returns to the parent. The run silently waits for a manual click instead of auto-resuming the parent. Delegating to subtasks that share the parent's profile works fine; the first delegation to a different-profile mode is what breaks. It is order-independent — whichever profile is used first works, and the first switch to a different one breaks the next return.
Steps to reproduce
- Enable per-mode API configs (
modeApiConfigs) so two modes use two different model profiles — e.g. an orchestrator on profile A and a sub-mode (e.g. verifier) on profile B.
- From the orchestrator, delegate to a same-profile subtask. It returns to the parent correctly.
- From the orchestrator, delegate (
new_task) to a subtask on the different profile.
- Let that subtask finish and call
attempt_completion.
Expected
Control returns to the parent task automatically (the parent resumes with the subtask result), the same as for same-profile subtasks.
Actual
The subtask emits its completion result, but the parent is never re-opened; the flow stalls waiting for manual input.
Root cause (verified)
The per-mode profile switch runs handleModeSwitch then activateProviderProfile, which leaves the parent task record as status: "active" while awaitingChildId is still set to the child. Two separate return-guards then reject because they require the parent status to be exactly "delegated":
- In the
attempt_completion tool handler — the pre-check that decides whether to call delegateToParent requires parent.status === "delegated". When it is "active", it skips delegation and falls through to the "wait for user" path.
- In
ClineProvider.reopenParentFromDelegation() — the guard rejects unless status === "delegated", logging [reopenParentFromDelegation] Aborting: ... status=active.
Because awaitingChildId === childTaskId already proves the parent is genuinely waiting for this exact child, both guards can safely also accept status === "active".
Suggested fix
Relax both guards to accept "active" in addition to "delegated", only while awaitingChildId still points at the returning child:
attempt_completion pre-check: (parent.status === "delegated" || parent.status === "active") && parent.awaitingChildId === child
reopenParentFromDelegation guard: abort only when status !== "delegated" && status !== "active"
The unrelated cleanup paths (stack removal on removeClineFromStack, and cancelTask) should keep the strict "delegated"-only check.
Workaround
Enabling Lock the API configuration across all modes (lockApiConfigAcrossModes) avoids the profile switch, so returns work — but every mode then runs on the same model, losing per-mode model routing.
Roadmap alignment: Reliability First — fixes a silent stall that blocks reliable multi-mode/agent workflows.
Zoo Code version: 3.57.100116
API provider/model: Reproduces with per-mode profiles where the parent (orchestrator) and the spawned subtask use different model profiles (any providers).
Related: Part of #357 (Epic 2: Task Lifecycle Fixes). This is a distinct defect from the step 4/5 atomicity race — it is a return-guard that rejects the
activeparent state outright, so the parent never reaches those writes on the cross-profile hop.What happens
When an orchestrator/parent task delegates to a subtask whose mode is bound to a different API-configuration profile than the parent, the subtask completes its work and calls
attempt_completion, but control never returns to the parent. The run silently waits for a manual click instead of auto-resuming the parent. Delegating to subtasks that share the parent's profile works fine; the first delegation to a different-profile mode is what breaks. It is order-independent — whichever profile is used first works, and the first switch to a different one breaks the next return.Steps to reproduce
modeApiConfigs) so two modes use two different model profiles — e.g. an orchestrator on profile A and a sub-mode (e.g.verifier) on profile B.new_task) to a subtask on the different profile.attempt_completion.Expected
Control returns to the parent task automatically (the parent resumes with the subtask result), the same as for same-profile subtasks.
Actual
The subtask emits its completion result, but the parent is never re-opened; the flow stalls waiting for manual input.
Root cause (verified)
The per-mode profile switch runs
handleModeSwitchthenactivateProviderProfile, which leaves the parent task record asstatus: "active"whileawaitingChildIdis still set to the child. Two separate return-guards then reject because they require the parent status to be exactly"delegated":attempt_completiontool handler — the pre-check that decides whether to calldelegateToParentrequiresparent.status === "delegated". When it is"active", it skips delegation and falls through to the "wait for user" path.ClineProvider.reopenParentFromDelegation()— the guard rejects unlessstatus === "delegated", logging[reopenParentFromDelegation] Aborting: ... status=active.Because
awaitingChildId === childTaskIdalready proves the parent is genuinely waiting for this exact child, both guards can safely also acceptstatus === "active".Suggested fix
Relax both guards to accept
"active"in addition to"delegated", only whileawaitingChildIdstill points at the returning child:attempt_completionpre-check:(parent.status === "delegated" || parent.status === "active") && parent.awaitingChildId === childreopenParentFromDelegationguard: abort only whenstatus !== "delegated" && status !== "active"The unrelated cleanup paths (stack removal on
removeClineFromStack, andcancelTask) should keep the strict"delegated"-only check.Workaround
Enabling Lock the API configuration across all modes (
lockApiConfigAcrossModes) avoids the profile switch, so returns work — but every mode then runs on the same model, losing per-mode model routing.Roadmap alignment: Reliability First — fixes a silent stall that blocks reliable multi-mode/agent workflows.