Skip to content

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

@edelauna

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).

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions