Skip to content

[Story 3.3] Child completion callback and parent wakeup #372

Description

@edelauna

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

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