Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).
Depends on: Story 3.2d (Webview-side task-scoping guard — must land before fan-out is first exercised in user-visible UI).
Relates to #126 (concurrent conversation sidebar — this story completes the fan-out loop).
Context
When a child completes, the parent needs to receive the result and resume. Currently this is embedded entirely in reopenParentFromDelegation (disk-based: reads messages from disk, injects tool_result, reopens parent instance). For fan-out, the parent may still be running and its state is live in memory — disk I/O is unnecessary and would race against the live instance. Two injection paths are needed: one per parent state.
This story assumes Stories 3.2b-3.2d have already established task isolation:
- 3.2b: parent and child execution contexts do not leak into each other during child setup
- 3.2c: non-focused tasks do not post into the single webview
- 3.2d: the webview rejects cross-task messages/state and uses per-task sequence guards
Developer Notes
In src/core/task/TaskScheduler.ts:
- Register an
onComplete callback per child via RooCodeEventName.TaskCompleted or TaskDelegationCompleted.
- On child completion: (1) release semaphore permit; (2) check if parent is still active in
TaskRegistry (fan-out path) or was suspended (sequential path); (3) call the appropriate injection path.
In src/core/webview/ClineProvider.ts:
- Sequential path (parent was suspended): Existing
reopenParentFromDelegation flow — reads messages from disk, injects tool_result, reopens parent Task instance. No change to this path.
- Fan-out path (parent is still running): In-memory injection directly into the parent's live
userMessageContent array via pushToolResultToUserContent(). Critical: the child completion callback must NOT call parent.setUserMessageContentReady(true). The parent's own dispatch loop is the sole owner of userMessageContentReady — it sets it to true only after all tool results for the current turn are collected. This follows the Single Writer Principle.
- Parent lookup must be by explicit
parentTaskId, not by getCurrentTask(). During fan-out the focused task may be the child or another live task; during sequential delegation the parent may be persisted but not live.
Add /** @invariant Single Writer — only the parent's dispatch loop may set this to true. See Single Writer Principle (Thompson, 2011). */ JSDoc at the userMessageContentReady field in Task.ts.
Files: src/core/task/TaskScheduler.ts, src/core/webview/ClineProvider.ts
Tests (extend src/core/task/__tests__/TaskScheduler.spec.ts and delegation tests):
- Child completion releases semaphore permit (
semaphore.available increases by 1).
- Sequential path: assert
reopenParentFromDelegation is called with correct completionResultSummary.
- Fan-out path: assert
pushToolResultToUserContent is called on live parent instance and userMessageContentReady is NOT set by the callback.
- Fan-out path: assert that if the parent has in-flight tools,
userMessageContentReady remains false after child completion injection.
- Fan-out path with child focused: completion still injects into the parent by
parentTaskId, not into whichever task is current.
- Semaphore permit restored on child abort and on child unhandled crash (the
finally guard from Story 3.1 covers this).
- Completed child
Task instances removed from activeTasks map after callback.
- Orphan handling: Parent aborts while child is in-flight → assert
abortTask(true) called on the orphaned child, semaphore permit released, child removed from activeTasks.
Acceptance Criteria
- Parent receives
completionResultSummary as a tool_result in its next API call regardless of which path is taken.
- No memory leak: completed task instances are removed from the scheduler's map.
- Correct injection path selected based on parent's live vs suspended state.
- Completion injection is scoped by explicit parent/child ids and does not depend on focused task.
- Orphan handling:
TaskScheduler listens for RooCodeEventName.TaskAborted on the parent and calls abortTask(true) on orphaned children.
- Invariant: Only the parent's dispatch loop sets
userMessageContentReady = true. No external callback may set this flag.
Out of Scope
Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).
Depends on: Story 3.2d (Webview-side task-scoping guard — must land before fan-out is first exercised in user-visible UI).
Relates to #126 (concurrent conversation sidebar — this story completes the fan-out loop).
Context
When a child completes, the parent needs to receive the result and resume. Currently this is embedded entirely in
reopenParentFromDelegation(disk-based: reads messages from disk, injectstool_result, reopens parent instance). For fan-out, the parent may still be running and its state is live in memory — disk I/O is unnecessary and would race against the live instance. Two injection paths are needed: one per parent state.This story assumes Stories 3.2b-3.2d have already established task isolation:
Developer Notes
In
src/core/task/TaskScheduler.ts:onCompletecallback per child viaRooCodeEventName.TaskCompletedorTaskDelegationCompleted.TaskRegistry(fan-out path) or was suspended (sequential path); (3) call the appropriate injection path.In
src/core/webview/ClineProvider.ts:reopenParentFromDelegationflow — reads messages from disk, injectstool_result, reopens parent Task instance. No change to this path.userMessageContentarray viapushToolResultToUserContent(). Critical: the child completion callback must NOT callparent.setUserMessageContentReady(true). The parent's own dispatch loop is the sole owner ofuserMessageContentReady— it sets it totrueonly after all tool results for the current turn are collected. This follows the Single Writer Principle.parentTaskId, not bygetCurrentTask(). During fan-out the focused task may be the child or another live task; during sequential delegation the parent may be persisted but not live.Add
/** @invariant Single Writer — only the parent's dispatch loop may set this to true. See Single Writer Principle (Thompson, 2011). */JSDoc at theuserMessageContentReadyfield inTask.ts.Files:
src/core/task/TaskScheduler.ts,src/core/webview/ClineProvider.tsTests (extend
src/core/task/__tests__/TaskScheduler.spec.tsand delegation tests):semaphore.availableincreases by 1).reopenParentFromDelegationis called with correctcompletionResultSummary.pushToolResultToUserContentis called on live parent instance anduserMessageContentReadyis NOT set by the callback.userMessageContentReadyremainsfalseafter child completion injection.parentTaskId, not into whichever task is current.finallyguard from Story 3.1 covers this).Taskinstances removed fromactiveTasksmap after callback.abortTask(true)called on the orphaned child, semaphore permit released, child removed fromactiveTasks.Acceptance Criteria
completionResultSummaryas atool_resultin its next API call regardless of which path is taken.TaskSchedulerlistens forRooCodeEventName.TaskAbortedon the parent and callsabortTask(true)on orphaned children.userMessageContentReady = true. No external callback may set this flag.Out of Scope