Skip to content

[Story 3.2b] Fan-out: parent stays active while child runs #369

Description

@edelauna

Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).

Depends on: Story 3.2a (TaskRegistry), Story 3.1 (TaskScheduler).

Relates to #126 (concurrent conversation sidebar — this story implements the fan-out code path that makes concurrent tasks possible).

Context

delegateParentAndOpenChild always calls removeClineFromStack (parent suspends). For fan-out, the parent should keep running while the child executes concurrently. This story adds the second code path in ClineProvider/task delegation: only when maxConcurrency > 1 and a scheduler permit can be reserved immediately, the parent remains live in TaskRegistry while the child is created and focused.

This story is also the correctness baseline for fan-out. It is not enough for both tasks to be present in TaskRegistry: the parent must continue with its own execution context while the child is being initialized. Historically getCurrentTask() was a safe implicit parameter because only one task could be alive; fan-out creates a window where getCurrentTask() still returns the parent while setup operations are logically about the child.

Developer Notes

In src/core/task/TaskScheduler.ts / src/utils/TaskSemaphore.ts:

  • Expose maxConcurrency and available capacity.
  • Add a non-queueing reservation path (tryReserve() / tryAcquire()) and run a task with a previously reserved permit (runWithReservation()).
  • Do not decide fan-out via available > 0 followed by awaits before scheduling. Reserve the permit at the decision point.
  • Every reserved permit must be released on all failure paths before the child starts running.

In src/core/webview/ClineProvider.ts:

  • When fan-out path is taken: keep the parent in the registry, create/focus the child, and expose all running tasks via getRunningTasks().
  • getCurrentTask() remains the UI-focused task. Do not assume it is the task an operation is logically about during child setup.
  • Thread explicit task scope through fan-out-reachable operations. At minimum, the parent must not be mutated by the child's setup path:
    • parent _taskMode / task-history mode must not be overwritten by child mode
    • parent live API handler must not be rebuilt to the child's saved provider/model
    • parent sticky provider profile must not be overwritten by child profile
    • parent clineMessages / taskId must not be posted to the webview as if it were the newly focused child during the pre-child-registration window
  • Protect against the broader execution-context leak: if child mode/profile activation changes global provider state that a still-running parent can read later (for example system prompt generation or provider settings reads), snapshot or explicitly scope the parent execution context so parent follow-up requests continue using the parent's mode/profile/config.
  • If parent eviction happens before child creation in the non-fan-out path, any createTask() failure must restore the parent.
  • If a permit is reserved in the fan-out path, any createTask() or metadata-persistence failure before child start must release the permit.
  • Rollback cleanup must remove the just-created child by taskId, not only if it is currently focused.

Files: src/core/task/TaskScheduler.ts, src/utils/TaskSemaphore.ts, src/core/webview/ClineProvider.ts

Tests (src/core/task/__tests__/delegation-concurrent.spec.ts, existing delegation tests, and focused e2e coverage):

  • maxConcurrency = 1: existing delegation behavior is preserved; parent is suspended/removed as before.
  • maxConcurrency = 2: parent task's request loop remains in flight while child run() actually starts.
  • Both parent and child appear in taskRegistry.getRunning() and do not share clineMessages.
  • Parent persisted HistoryItem.status remains the intended delegated state while the live parent remains in memory.
  • tryReserve() proves two concurrent reservations cannot both win the final permit.
  • Fan-out with different parent/child modes and saved profiles: parent follow-up request continues using the parent's mode/profile/API config, while child request uses the child's requested mode/profile/API config.
  • createTask() failure in fan-out releases the reserved permit.
  • createTask() failure in non-fan-out restores the evicted parent.
  • Metadata-persistence failure evicts the created child by id and rolls back or releases resources correctly.
  • E2E subtask suite includes a fan-out smoke test and the full subtasks.test.ts remains green.

Acceptance Criteria

  • maxConcurrency = 1 path is unchanged — all existing delegation tests pass.
  • maxConcurrency = 2 fan-out path is only reachable via explicit config and an immediately reserved permit.
  • Both parent and child are independently tracked in TaskRegistry with no shared message state.
  • The running parent is isolated from child setup: no child mode/profile/API config leaks into parent task mode, live API handler, sticky profile, system prompt, or subsequent API request.
  • All rollback paths release reserved permits and/or restore the parent before rethrowing.
  • Focused unit/integration tests and the full subtask e2e file pass.

Explicitly Out of Scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions