Repository navigation
[Story 3.2b] Fan-out: parent stays active while child runs #369
Description
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
maxConcurrencyand 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 > 0followed 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/taskIdmust not be posted to the webview as if it were the newly focused child during the pre-child-registration window
- parent
- 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 childrun()actually starts.- Both parent and child appear in
taskRegistry.getRunning()and do not shareclineMessages. - Parent persisted
HistoryItem.statusremains 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.tsremains green.
Acceptance Criteria
maxConcurrency = 1path is unchanged — all existing delegation tests pass.maxConcurrency = 2fan-out path is only reachable via explicit config and an immediately reserved permit.- Both parent and child are independently tracked in
TaskRegistrywith 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
showTaskWithIdlive-task focus switching and extension-side non-focused post suppression are Story [Story 3.2c] Extension-side task-scoping guard #370.- Webview-side message rejection and per-task sequence handling are Story [Story 3.2d] Webview-side task-scoping guard + per-task seq #371.
- Child completion callback and live-parent result injection are Story [Story 3.3] Child completion callback and parent wakeup #372.
Activity