Skip to content

feat(TaskScheduler): fan-out — parent stays active while child runs - #1046

Draft
edelauna wants to merge 1 commit into
mainfrom
issue/369
Draft

feat(TaskScheduler): fan-out — parent stays active while child runs#1046
edelauna wants to merge 1 commit into
mainfrom
issue/369

Conversation

@edelauna

@edelauna edelauna commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Closes: #

Description

Test Procedure

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Visual Snapshot (UI changes only): If a user would notice this change at a glance (layout, theme tokens, brand elements, empty/error states), I've added or updated a *.visual.tsx snapshot in webview-ui/. See webview-ui/AGENTS.md → "When a UI change needs a snapshot".
  • Documentation Impact: I have considered if my changes require documentation updates (see "Documentation Updates" section below).
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Visual Snapshots

Videos (interaction / animation only)

Documentation Updates

Additional Notes

Get in Touch

Summary by CodeRabbit

  • New Features

    • Added configurable task concurrency, allowing parent and child tasks to run simultaneously when capacity is available.
    • Added API support for setting the maximum number of concurrent tasks.
    • Added visibility into currently running tasks.
  • Bug Fixes

    • Improved delegation rollback to correctly remove newly created child tasks and release reserved capacity.
  • Tests

    • Added coverage for concurrent delegation, task focus, fallback behavior, rollback, and end-to-end fan-out execution.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c728254-cbb6-4871-8ee0-948034e2c5e4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.76744% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 64.70% 10 Missing and 2 partials ⚠️
src/extension/api.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/core/webview/ClineProvider.ts (2)

1531-1601: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

targetTask=null doesn't protect the parent's API config, only its _taskMode.

The doc for the new targetTask param promises that passing null will "skip the per-task mutation and only apply the global mode/API-config side effects... without stomping the still-running parent's own _taskMode." But the task/targetTask gate only wraps the _taskMode + task-history mutation (the if (task) {...} block). The subsequent activateProviderProfile({ name: profile.name }) call (further down in this same function, when the new mode has a saved API config) is unconditional and internally rebuilds whichever task this.getCurrentTask() returns via updateTaskApiHandlerIfNeeded.

In the fan-out delegation path (delegateParentAndOpenChild), handleModeSwitch(requestedMode, null) runs before the child exists and before the parent is removed from the registry — so getCurrentTask() at that point is still the parent. If the child's requested mode has a different saved/sticky API config than the parent's, this silently rebuilds and swaps the running parent's apiConfiguration/API handler to the child's mode's provider settings mid-task, potentially rerouting the parent's next request to an unintended provider/model/key.

Fix direction: thread an explicit target (or "skip current-task rebuild") flag through activateProviderProfile/updateTaskApiHandlerIfNeeded instead of relying on getCurrentTask().

🐛 Sketch of one possible fix
-			if (hasActualSettings) {
-				await this.activateProviderProfile({ name: profile.name })
-			} else {
+			if (hasActualSettings) {
+				await this.activateProviderProfile({ name: profile.name }, { skipCurrentTaskApiRebuild: targetTask === null })
+			} else {
 				// The task will continue with the current/default configuration.
 			}

and in activateProviderProfile/updateTaskApiHandlerIfNeeded, skip the getCurrentTask() rebuild when that flag is set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/ClineProvider.ts` around lines 1531 - 1601, Update
handleModeSwitch and the activateProviderProfile/updateTaskApiHandlerIfNeeded
flow so passing targetTask=null also skips rebuilding the current task’s API
configuration and handler. Thread an explicit skip-current-task-rebuild or
target-task control through these methods, ensuring delegation fan-out applies
global mode/config side effects without using getCurrentTask() to mutate the
still-running parent.

3666-3703: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Reserved permit (and parent) can be lost if createTask() throws.

childReservedRelease is captured at line 3627, but the only cleanup that releases it (fan-out) or restores the parent (non-fan-out) lives in the catch wrapping atomicReadAndUpdate starting at line 3703 — which only runs for errors after createTask() returns. The createTask() call itself (3679-3683) is not wrapped in try/catch, yet it has real unguarded throw paths (OrganizationAllowListViolationError, Task constructor validation, addClineToStack()/getState() failures).

If it throws:

  • Fan-out case: childReservedRelease is never released — a permanent concurrency-permit leak for the life of this TaskScheduler instance.
  • Non-fan-out case: the parent was already evicted in step 3 and is never restored — the user is left with no active task.
🐛 Proposed fix
-		const child = await this.createTask(message, undefined, parent as any, {
-			initialTodos,
-			initialStatus: "active",
-			startTask: false,
-		})
+		let child: Task
+		try {
+			child = await this.createTask(message, undefined, parent as any, {
+				initialTodos,
+				initialStatus: "active",
+				startTask: false,
+			})
+		} catch (err) {
+			// createTask() can throw before any cleanup below runs — release the
+			// reserved permit (fan-out) so it doesn't leak, and let the caller see the error.
+			childReservedRelease?.()
+			throw err
+		}

Note: the non-fan-out parent-restore case still needs equivalent handling here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/ClineProvider.ts` around lines 3666 - 3703, Wrap the
createTask call in the same failure-handling flow as atomicReadAndUpdate so any
exception during child creation triggers cleanup. In the catch path, invoke
childReservedRelease for fan-out and restore the evicted parent for non-fan-out,
while preserving existing post-creation rollback behavior and avoiding duplicate
cleanup.
🧹 Nitpick comments (1)
src/core/task/__tests__/delegation-concurrent.spec.ts (1)

136-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fragile microtask-count synchronization instead of polling a deterministic signal.

Both tests occupy scheduler permits with never-resolving run() functions and then assume sem.acquire() has settled after a fixed number of await Promise.resolve() calls (2 ticks at Lines 149-150; 1 tick at Line 207, then 2 more at Lines 219-220). This hardcodes an implementation detail (exact microtask depth of sem.acquire()); if TaskSemaphore's internal promise chaining ever grows by one hop, these tests could start failing/flaking without a clear signal why. The atomic-reservation test at Lines 270-273 already demonstrates a more robust alternative: polling scheduler.available until it reflects the expected state.

♻️ Suggested pattern for deterministic settling
-		void scheduler.schedule(makeParent({ taskId: "occupant-1" }), () => new Promise<void>(() => {}))
-		void scheduler.schedule(makeParent({ taskId: "occupant-2" }), () => new Promise<void>(() => {}))
-		// Let both schedule() calls' internal sem.acquire() microtasks settle so
-		// both permits are actually held before we check availability.
-		await Promise.resolve()
-		await Promise.resolve()
+		void scheduler.schedule(makeParent({ taskId: "occupant-1" }), () => new Promise<void>(() => {}))
+		void scheduler.schedule(makeParent({ taskId: "occupant-2" }), () => new Promise<void>(() => {}))
+		// Poll the deterministic signal instead of assuming a fixed microtask depth.
+		while (scheduler.available > 0) {
+			await Promise.resolve()
+		}

Also applies to: 185-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/task/__tests__/delegation-concurrent.spec.ts` around lines 136 -
163, Replace the fixed Promise.resolve() microtask waits in the permit-occupancy
tests around callDelegate and the related concurrency cases with polling of
scheduler.available until it reaches the expected exhausted state, matching the
deterministic approach used by the atomic-reservation test. Keep the
never-resolving occupancy tasks and assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/core/webview/ClineProvider.ts`:
- Around line 1531-1601: Update handleModeSwitch and the
activateProviderProfile/updateTaskApiHandlerIfNeeded flow so passing
targetTask=null also skips rebuilding the current task’s API configuration and
handler. Thread an explicit skip-current-task-rebuild or target-task control
through these methods, ensuring delegation fan-out applies global mode/config
side effects without using getCurrentTask() to mutate the still-running parent.
- Around line 3666-3703: Wrap the createTask call in the same failure-handling
flow as atomicReadAndUpdate so any exception during child creation triggers
cleanup. In the catch path, invoke childReservedRelease for fan-out and restore
the evicted parent for non-fan-out, while preserving existing post-creation
rollback behavior and avoiding duplicate cleanup.

---

Nitpick comments:
In `@src/core/task/__tests__/delegation-concurrent.spec.ts`:
- Around line 136-163: Replace the fixed Promise.resolve() microtask waits in
the permit-occupancy tests around callDelegate and the related concurrency cases
with polling of scheduler.available until it reaches the expected exhausted
state, matching the deterministic approach used by the atomic-reservation test.
Keep the never-resolving occupancy tasks and assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 038f0437-f573-41dc-b617-b71c4911555b

📥 Commits

Reviewing files that changed from the base of the PR and between dcaa3cb and 0cd7dea.

📒 Files selected for processing (10)
  • apps/vscode-e2e/src/fixtures/subtasks.ts
  • apps/vscode-e2e/src/suite/subtasks.test.ts
  • packages/types/src/api.ts
  • src/__tests__/provider-delegation.spec.ts
  • src/core/task/TaskScheduler.ts
  • src/core/task/__tests__/delegation-concurrent.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/eslint-suppressions.json
  • src/extension/api.ts
  • src/utils/TaskSemaphore.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant