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).
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.
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.
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):
- 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.
- 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.
- 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.
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).
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.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.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.ts):semaphore.availableincreases by 1).reopenParentFromDelegationis called with correctcompletionResultSummary.pushToolResultToUserContentis called on live parent instance anduserMessageContentReadyis NOT set by the callback.userMessageContentReadyremainsfalseafter child completion injection.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.