Skip to content

Subtask never returns to parent after a per-mode model/profile switch (attempt_completion silently waits for human) #457

@Bamamana

Description

@Bamamana

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

  1. 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.
  2. From the orchestrator, delegate to a same-profile subtask. It returns to the parent correctly.
  3. From the orchestrator, delegate (new_task) to a subtask on the different profile.
  4. 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":

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions