Skip to content

feat(task-scheduler): enable concurrent delegated task fan-out - #1046

Open
edelauna wants to merge 9 commits into
mainfrom
issue/369
Open

feat(task-scheduler): enable concurrent delegated task fan-out#1046
edelauna wants to merge 9 commits into
mainfrom
issue/369

Conversation

@edelauna

@edelauna edelauna commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR 3 of 3 for #369; follows #1085 and #1087. Fan-out is disabled by default and scheduler concurrency is exposed only for extension-host test coverage.

Summary

  • Add immediate, non-queuing scheduler permit reservation and reserved-task execution with exactly-once release across completion, error, abort, and rollback.
  • Keep the default scheduler concurrency at 1, preserving sequential delegated-task behavior.
  • Enable parent/child fan-out only when a permit is immediately available and concurrency is explicitly greater than 1.
  • Snapshot child mode/profile/API configuration before startup so concurrent children cannot inherit mutable parent state.
  • Expose the test-only scheduler concurrency API with validation and active-task protection.
  • Preserve exact child cleanup and parent restoration behavior on delegation failures.

Testing

  • Targeted scheduler, semaphore, delegation, provider, and Task tests.
  • Full mocked subtask E2E coverage, including parent continuation during delayed child execution and distinct parent/child profile-model request routing.
  • Root typecheck and E2E typecheck.
  • Scoped and package-wide ESLint with suppression pruning; suppression counts did not increase.
  • Root bundle/build validation.

Scope

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds concurrent subtask fan-out with scheduler reservations, task-local startup configuration, provider rollback handling, configurable scheduler concurrency, and VS Code E2E coverage for concurrent and cross-profile execution.

Changes

Concurrent delegation infrastructure

Layer / File(s) Summary
Scheduler reservations and API configuration
src/utils/TaskSemaphore.ts, src/core/task/TaskScheduler.ts, src/extension/api.ts, packages/types/src/api.ts, src/**/__tests__/*
Adds non-queuing permit reservation, reserved task execution, scheduler concurrency validation and replacement, and the public API method for changing concurrency.
Task-local execution configuration
src/core/task/Task.ts, src/core/task/__tests__/Task.spec.ts
Adds immutable startup snapshots and uses task-local mode and provider configuration for initialization and API request metadata.
Profile mutation and child startup snapshots
src/core/webview/ClineProvider.ts, src/core/webview/__tests__/ClineProvider.spec.ts
Captures child mode and profile state, creates children from snapshots, exposes running tasks, and suppresses intermediate fan-out state updates.
Concurrent delegation and rollback
src/core/webview/ClineProvider.ts, src/__tests__/helpers/provider-stub.ts, src/__tests__/provider-delegation.spec.ts, src/core/task/__tests__/delegation-concurrent.spec.ts
Keeps parents registered during fan-out, starts children with reserved permits, handles creation and persistence failures, and verifies registry and rollback behavior.
End-to-end fan-out scenarios
apps/vscode-e2e/src/fixtures/subtasks.ts, apps/vscode-e2e/src/suite/subtasks.test.ts, apps/vscode-e2e/src/fixtures/search-files.ts, apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts
Adds concurrent and cross-profile fan-out fixtures and tests, plus updated search and terminal fixture matching.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: enabling concurrent delegated task fan-out through the task scheduler.
Description check ✅ Passed The description explains the implementation, scope, and testing, but it omits the template checklist and explicit Closes issue format.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue/369

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 86.44068% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 83.13% 7 Missing and 7 partials ⚠️
src/__tests__/helpers/provider-stub.ts 93.33% 0 Missing and 1 partial ⚠️
src/core/task/Task.ts 88.88% 0 Missing and 1 partial ⚠️

📢 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

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/core/task/__tests__/delegation-concurrent.spec.ts (1)

251-272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Restore the vscode.window.showErrorMessage spy.

Unless a global restoreMocks/beforeEach resets it, this spy leaks into later tests in the file.

♻️ Restore after use
 		expect(showErrorMessage).toHaveBeenCalledWith(
 			"Failed to restore the parent task after subtask creation failed. Reopen the task from history to continue.",
 		)
+		showErrorMessage.mockRestore()
 	})
🤖 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 251 -
272, Restore the vscode.window.showErrorMessage spy created in the test after
its assertions complete, ensuring cleanup runs even when the test fails. Update
the test around showErrorMessage and preserve the existing error and call-count
assertions.
src/core/webview/ClineProvider.ts (2)

213-236: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

A hung profile mutation silently blocks every later mode/profile switch.

The timeout only rejects the caller; providerProfileMutationQueue still chains on run, so if a mutation never settles the queue stalls forever with no signal. Consider logging when the timeout fires (and when the underlying op eventually settles) so this state is diagnosable.

♻️ Log on timeout
 	private withProviderProfileMutationTimeout<T>(operation: Promise<T>): Promise<T> {
 		let timeoutId: ReturnType<typeof setTimeout> | undefined
 		const timeout = new Promise<never>((_, reject) => {
 			timeoutId = setTimeout(() => {
+				this.log(
+					`[providerProfileMutation] mutation exceeded ${ClineProvider.PENDING_OPERATION_TIMEOUT_MS}ms; queued mutations remain blocked until it settles`,
+				)
 				reject(new Error("Provider profile mutation timed out"))
 			}, ClineProvider.PENDING_OPERATION_TIMEOUT_MS)
 		})
🤖 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 213 - 236, Update
enqueueProviderProfileMutation and withProviderProfileMutationTimeout so a
timeout emits a diagnostic log identifying the hung provider profile mutation,
and also log when the underlying operation eventually settles after timing out.
Preserve the existing caller rejection, queue chaining, and timeout cleanup
behavior.

3645-3679: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated restore block; a small loop reads better.

The two getTaskWithId + createTaskWithHistoryItem blocks are identical apart from log wording.

♻️ Retry loop
-		if (!fanOut) {
-			try {
-				const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
-				await this.createTaskWithHistoryItem(parentHistory)
-			} catch (firstRollbackError) {
-				...
-			}
-		} else {
+		if (!fanOut) {
+			for (let attempt = 1; attempt <= 2; attempt++) {
+				try {
+					const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
+					await this.createTaskWithHistoryItem(parentHistory)
+					return
+				} catch (error) {
+					this.log(
+						`[delegateParentAndOpenChild] Failed to restore parent ${parentTaskId} during rollback (attempt ${attempt}/2): ${
+							(error as Error)?.message ?? String(error)
+						}`,
+					)
+				}
+			}
+			vscode.window.showErrorMessage(
+				"Failed to restore the parent task after subtask creation failed. Reopen the task from history to continue.",
+			)
+		} else {
 			childReservedRelease?.()
 		}
🤖 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 3645 - 3679, Refactor the
non-fan-out branch of restoreParentOrReleasePermit into a small bounded retry
loop around getTaskWithId and createTaskWithHistoryItem, preserving the
initial-attempt and single-retry behavior. Keep distinct logging for the first
failure and retry failure, and retain the existing showErrorMessage handling
after both attempts fail; leave the fan-out permit release path unchanged.
src/__tests__/provider-delegation.spec.ts (1)

9-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate private-method binding logic.

bindRestoreParentOrReleasePermit re-implements the same proto.restoreParentOrReleasePermit.bind(s) pattern already centralized in src/__tests__/helpers/provider-stub.ts (makeProviderStub). Two copies of this binding logic now need to stay in sync if the private method's signature changes.

Consider exporting a shared binder from provider-stub.ts (or having this spec build its providers through makeProviderStub) instead of re-declaring the cast/bind locally.

♻️ Possible consolidation
-function bindRestoreParentOrReleasePermit(provider: ClineProvider): void {
-	type WithRestore = {
-		restoreParentOrReleasePermit: (
-			parentTaskId: string,
-			fanOut: boolean,
-			childReservedRelease: (() => void) | undefined,
-		) => Promise<void>
-	}
-	;(provider as unknown as WithRestore).restoreParentOrReleasePermit = (
-		ClineProvider.prototype as unknown as WithRestore
-	).restoreParentOrReleasePermit.bind(provider)
-}
+// import { bindRestoreParentOrReleasePermit } from "./helpers/provider-stub"

Run this to confirm whether the binder already exists/could be exported from provider-stub.ts:

#!/bin/bash
rg -n 'restoreParentOrReleasePermit' src/__tests__/helpers/provider-stub.ts src/__tests__/provider-delegation.spec.ts
🤖 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/__tests__/provider-delegation.spec.ts` around lines 9 - 26, Remove the
local bindRestoreParentOrReleasePermit implementation from
provider-delegation.spec.ts and reuse a shared binder exported from
makeProviderStub’s provider-stub helper, or construct these providers through
makeProviderStub. Ensure restoreParentOrReleasePermit binding and its signature
have a single centralized implementation.
apps/vscode-e2e/src/suite/subtasks.test.ts (1)

78-97: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Aimock journal is read/matched without per-test scoping.

readAimockJournal fetches the full server-side journal on every poll and waitForAimockRequest/assertAimockRequest match against it without filtering to the current test's requests. Since the aimock server likely persists its journal across the whole suite run, this risks matching stale entries from an earlier test if predicates aren't sufficiently unique (here they happen to rely on the newly-added entry.body.model, which is unique to this test, so the risk is currently mitigated in practice, but the helper itself provides no structural guarantee against pulling in older requests).
As per coding guidelines, "For fetch-interceptor test suites, reset in-memory request/event capture in setup() or allocate a fresh per-test buffer instead of reusing shared mutable state; scope request-shape assertions to the current probe or test tag only; do not pull in older requests."

🤖 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 `@apps/vscode-e2e/src/suite/subtasks.test.ts` around lines 78 - 97, Scope
Aimock journal assertions to the current test in waitForAimockRequest and
assertAimockRequest rather than matching the full persisted journal. Reset or
establish a fresh per-test request buffer during setup, and update
readAimockJournal or the matching flow to exclude entries from earlier tests
while preserving polling behavior.

Source: Coding guidelines

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

Inline comments:
In `@apps/vscode-e2e/src/suite/subtasks.test.ts`:
- Around line 255-256: Validate the results of both upsertProfile calls for the
parent and child profiles before using them to populate modeApiConfigs. Fail
immediately with a clear error if either result is undefined, and remove the
non-null assertions while preserving the existing successful-profile flow.

In `@src/core/task/__tests__/delegation-concurrent.spec.ts`:
- Around line 148-152: Bound both scheduler polling loops in
delegation-concurrent.spec.ts: lines 148-152 waiting for scheduler.available > 0
and lines 316-318 waiting for scheduler.available > 1. Use the bounded polling
pattern demonstrated by the childStarted loop, preserving the existing await
behavior while ensuring a regression exits the loop and allows the test
assertion to fail.

In `@src/core/task/Task.ts`:
- Around line 3786-3795: The task-local values are only introduced in the shown
scope but shared-state mode and apiConfiguration are still used elsewhere in
attemptApiRequest(). Replace every provider-state read passed to
buildNativeToolsArrayWithRestrictions, metadata.mode, and the Gemini
allowedFunctionNames path with the local await this.getTaskMode() and
this.apiConfiguration values, ensuring delegated requests consistently use the
current Task instance’s configuration.

---

Nitpick comments:
In `@apps/vscode-e2e/src/suite/subtasks.test.ts`:
- Around line 78-97: Scope Aimock journal assertions to the current test in
waitForAimockRequest and assertAimockRequest rather than matching the full
persisted journal. Reset or establish a fresh per-test request buffer during
setup, and update readAimockJournal or the matching flow to exclude entries from
earlier tests while preserving polling behavior.

In `@src/__tests__/provider-delegation.spec.ts`:
- Around line 9-26: Remove the local bindRestoreParentOrReleasePermit
implementation from provider-delegation.spec.ts and reuse a shared binder
exported from makeProviderStub’s provider-stub helper, or construct these
providers through makeProviderStub. Ensure restoreParentOrReleasePermit binding
and its signature have a single centralized implementation.

In `@src/core/task/__tests__/delegation-concurrent.spec.ts`:
- Around line 251-272: Restore the vscode.window.showErrorMessage spy created in
the test after its assertions complete, ensuring cleanup runs even when the test
fails. Update the test around showErrorMessage and preserve the existing error
and call-count assertions.

In `@src/core/webview/ClineProvider.ts`:
- Around line 213-236: Update enqueueProviderProfileMutation and
withProviderProfileMutationTimeout so a timeout emits a diagnostic log
identifying the hung provider profile mutation, and also log when the underlying
operation eventually settles after timing out. Preserve the existing caller
rejection, queue chaining, and timeout cleanup behavior.
- Around line 3645-3679: Refactor the non-fan-out branch of
restoreParentOrReleasePermit into a small bounded retry loop around
getTaskWithId and createTaskWithHistoryItem, preserving the initial-attempt and
single-retry behavior. Keep distinct logging for the first failure and retry
failure, and retain the existing showErrorMessage handling after both attempts
fail; leave the fan-out permit release path unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844dfb75-f9db-47b2-b90f-4708230169b0

📥 Commits

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

📒 Files selected for processing (15)
  • apps/vscode-e2e/src/fixtures/subtasks.ts
  • apps/vscode-e2e/src/suite/subtasks.test.ts
  • docs/formal/task-delegation/README.md
  • docs/formal/task-delegation/TaskDelegation.cfg
  • docs/formal/task-delegation/TaskDelegation.tla
  • src/__tests__/helpers/provider-stub.ts
  • src/__tests__/provider-delegation.spec.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/core/task/__tests__/delegation-concurrent.spec.ts
  • src/core/task/__tests__/delegation-state-machine.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts
  • src/eslint-suppressions.json
  • src/extension/api.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/eslint-suppressions.json

Comment thread apps/vscode-e2e/src/suite/subtasks.test.ts
Comment thread src/core/task/__tests__/delegation-concurrent.spec.ts
Comment thread src/core/task/Task.ts

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/core/task/__tests__/Task.spec.ts (1)

462-481: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert task-local API configuration in this prompt test.

This test only distinguishes and asserts mode; it still passes if getSystemPrompt() regresses to provider-state apiConfiguration. Make a prompt option such as todoListEnabled differ and assert the SYSTEM_PROMPT options argument uses the task value.

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js} must use package-local unit tests for state transitions and request construction.

🤖 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__/Task.spec.ts` around lines 462 - 481, The
getSystemPrompt test currently verifies only task-local mode and does not
protect task-local apiConfiguration. Update the test around
getTaskTestAccess(cline).getSystemPrompt() to give the task a distinct
apiConfiguration option such as todoListEnabled, then assert the SYSTEM_PROMPT
options argument uses that task value rather than mockProvider state; keep the
existing mode assertions intact.

Source: Coding guidelines

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

Inline comments:
In `@src/core/task/Task.ts`:
- Around line 4030-4033: Scope ProviderProfileChanged handling in
Task.setupProviderProfileChangeListener to the task affected by the profile
change, preventing updates from overwriting unrelated live tasks’
configurations; the request construction around getTaskMode and
this.apiConfiguration should then retain each task’s original metadata. In
src/core/task/Task.ts lines 4030-4033, ensure the request uses the task-isolated
configuration. In src/core/task/__tests__/Task.spec.ts lines 602-651, add a
package-local unit test that creates tasks with distinct configurations, emits a
profile-change event, and verifies the parent request metadata remains
unchanged.

---

Outside diff comments:
In `@src/core/task/__tests__/Task.spec.ts`:
- Around line 462-481: The getSystemPrompt test currently verifies only
task-local mode and does not protect task-local apiConfiguration. Update the
test around getTaskTestAccess(cline).getSystemPrompt() to give the task a
distinct apiConfiguration option such as todoListEnabled, then assert the
SYSTEM_PROMPT options argument uses that task value rather than mockProvider
state; keep the existing mode assertions intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fae2acc2-f12d-465b-9003-e1e5bfd14434

📥 Commits

Reviewing files that changed from the base of the PR and between 5e899f3 and 17109ab.

📒 Files selected for processing (7)
  • apps/vscode-e2e/src/suite/subtasks.test.ts
  • src/__tests__/helpers/provider-stub.ts
  • src/__tests__/provider-delegation.spec.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/core/task/__tests__/delegation-concurrent.spec.ts
  • src/core/webview/__tests__/ClineProvider.lockApiConfig.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/vscode-e2e/src/suite/subtasks.test.ts

Comment thread src/core/task/Task.ts Outdated
@edelauna
edelauna force-pushed the issue/369 branch 2 times, most recently from 9a810ed to 7cde55d Compare August 8, 2026 19:46
@edelauna
edelauna marked this pull request as ready for review August 9, 2026 02:25

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/core/webview/ClineProvider.ts (1)

3714-3748: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicated restore attempt into a loop.

The restore body is written twice, once for the first attempt and once for the retry. A short loop removes the duplication and keeps the log messages distinct by attempt index.

♻️ Proposed refactor
 		if (!fanOut) {
-			try {
-				const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
-				await this.createTaskWithHistoryItem(parentHistory)
-			} catch (firstRollbackError) {
-				this.log(
-					`[delegateParentAndOpenChild] Failed to restore parent ${parentTaskId} during rollback, retrying once: ${
-						(firstRollbackError as Error)?.message ?? String(firstRollbackError)
-					}`,
-				)
-				try {
-					const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
-					await this.createTaskWithHistoryItem(parentHistory)
-				} catch (rollbackError) {
-					this.log(
-						`[delegateParentAndOpenChild] Failed to restore parent ${parentTaskId} during rollback retry: ${
-							(rollbackError as Error)?.message ?? String(rollbackError)
-						}`,
-					)
-					vscode.window.showErrorMessage(
-						"Failed to restore the parent task after subtask creation failed. Reopen the task from history to continue.",
-					)
-				}
-			}
+			const maxAttempts = 2
+			for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+				try {
+					const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
+					await this.createTaskWithHistoryItem(parentHistory)
+					return
+				} catch (rollbackError) {
+					this.log(
+						`[delegateParentAndOpenChild] Failed to restore parent ${parentTaskId} during rollback (attempt ${attempt}/${maxAttempts}): ${
+							(rollbackError as Error)?.message ?? String(rollbackError)
+						}`,
+					)
+				}
+			}
+			vscode.window.showErrorMessage(
+				"Failed to restore the parent task after subtask creation failed. Reopen the task from history to continue.",
+			)
 		} else {
🤖 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 3714 - 3748, Refactor the
non-fan-out branch of restoreParentOrReleasePermit to perform the parent
restoration in a short two-attempt loop instead of duplicating the
getTaskWithId/createTaskWithHistoryItem body. Keep attempt-specific failure
logging, retry only once, and preserve the final error message and
showErrorMessage behavior after both attempts fail.
🤖 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.

Inline comments:
In `@apps/vscode-e2e/src/fixtures/subtasks.ts`:
- Around line 177-193: Both fan-out child fixtures incorrectly use flat latency,
delaying every response chunk; export a shared SUBTASK_FANOUT_CHILD_DELAY_MS
constant next to SUBTASK_API_HANG_RESPONSE_LATENCY_MS, then replace latency:
15_000 with streamingProfile.ttft using that constant in
apps/vscode-e2e/src/fixtures/subtasks.ts lines 177-193 and 234-250.

In `@src/core/task/Task.ts`:
- Around line 567-572: Update the startupSnapshot branch in Task initialization
so an undefined startupSnapshot.apiConfigName falls back to the existing default
API configuration name before resolving taskApiConfigReady. Preserve explicitly
provided API configuration names and keep behavior aligned with the non-snapshot
initialization path.

---

Nitpick comments:
In `@src/core/webview/ClineProvider.ts`:
- Around line 3714-3748: Refactor the non-fan-out branch of
restoreParentOrReleasePermit to perform the parent restoration in a short
two-attempt loop instead of duplicating the
getTaskWithId/createTaskWithHistoryItem body. Keep attempt-specific failure
logging, retry only once, and preserve the final error message and
showErrorMessage behavior after both attempts fail.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d95e62e-7390-451b-b293-27aea6ce92ff

📥 Commits

Reviewing files that changed from the base of the PR and between b2d2156 and a95c46a.

📒 Files selected for processing (14)
  • apps/vscode-e2e/src/fixtures/subtasks.ts
  • apps/vscode-e2e/src/suite/subtasks.test.ts
  • src/__tests__/helpers/provider-stub.ts
  • src/__tests__/provider-delegation.spec.ts
  • src/core/task/Task.ts
  • src/core/task/TaskScheduler.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/core/task/__tests__/TaskScheduler.spec.ts
  • src/core/task/__tests__/delegation-concurrent.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • src/eslint-suppressions.json
  • src/extension/__tests__/api-task-conversation-history-length.spec.ts
  • src/utils/__tests__/TaskSemaphore.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/vscode-e2e/src/suite/subtasks.test.ts
  • src/core/task/TaskScheduler.ts
  • src/core/task/tests/delegation-concurrent.spec.ts

Comment thread apps/vscode-e2e/src/fixtures/subtasks.ts
Comment thread src/core/task/Task.ts
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 9, 2026
@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 9, 2026
@edelauna edelauna changed the title feat(TaskScheduler): fan-out — parent stays active while child runs feat(task-scheduler): enable concurrent delegated task fan-out Aug 9, 2026
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 9, 2026
@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant