Skip to content

Commit c8911ca

Browse files
committed
fix(orchestrator): always restore parent mode after subtask completion
Previously, when an orchestrated workflow dispatched a subtask (e.g., in code or architect mode) and the subtask completed, the parent orchestrator task would resume execution but remain in the child mode instead of reverting to orchestrator mode. This caused confusion and improper workflows, as documented in issues #4896, #5478, and #5747. The fix ensures that, upon subtask completion (including cancellations or API aborts), the parent task's mode is explicitly restored to the `pausedModeSlug` saved before launching the subtask. All possible subtask completion and stack handling code paths are covered, making regressions unlikely. Also adds a focused test to verify mode restoration logic. Rationale: Correct orchestrator handoff is critical to multi-mode workflows. This change ensures state isn't leaked between parent/child, and is robust against all completion/cancellation scenarios.
1 parent 2c8c140 commit c8911ca

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

src/core/task/Task.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16231623
this.isPaused = false
16241624
this.childTaskId = undefined
16251625

1626+
const provider = this.providerRef.deref()
1627+
if (provider) {
1628+
await provider.handleModeSwitch(this.pausedModeSlug)
1629+
}
1630+
16261631
this.emit(RooCodeEventName.TaskUnpaused, this.taskId)
16271632

16281633
// Fake an answer from the subtask that it has completed running and
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { Task } from "../Task"
3+
import { ClineProvider } from "../../webview/ClineProvider"
4+
import { TodoItem } from "@roo-code/types"
5+
6+
describe("Task subtask mode restoration", () => {
7+
let parentTask: Task
8+
let mockProvider: any
9+
10+
beforeEach(() => {
11+
mockProvider = {
12+
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
13+
log: vi.fn(),
14+
deref: vi.fn().mockReturnValue({
15+
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
16+
}),
17+
}
18+
})
19+
20+
it("should restore parent task mode when subtask completes", async () => {
21+
// Create parent task with orchestrator mode
22+
parentTask = new Task({
23+
provider: mockProvider as any,
24+
apiConfiguration: {} as any,
25+
task: "Parent task",
26+
})
27+
28+
// Set parent task to orchestrator mode
29+
parentTask.pausedModeSlug = "orchestrator"
30+
31+
// Mock the provider reference
32+
parentTask.providerRef = {
33+
deref: () => mockProvider.deref(),
34+
} as any
35+
36+
// Complete the subtask
37+
await parentTask.completeSubtask("Subtask completed")
38+
39+
// Verify handleModeSwitch was called with the pausedModeSlug
40+
expect(mockProvider.deref().handleModeSwitch).toHaveBeenCalledWith("orchestrator")
41+
42+
// Verify task is unpaused
43+
expect(parentTask.isPaused).toBe(false)
44+
45+
// Verify childTaskId is cleared
46+
expect(parentTask.childTaskId).toBeUndefined()
47+
})
48+
49+
it("should handle missing provider gracefully", async () => {
50+
// Create parent task
51+
parentTask = new Task({
52+
provider: mockProvider as any,
53+
apiConfiguration: {} as any,
54+
task: "Parent task",
55+
})
56+
57+
// Set parent task to orchestrator mode
58+
parentTask.pausedModeSlug = "orchestrator"
59+
60+
// Mock provider as unavailable
61+
parentTask.providerRef = {
62+
deref: () => undefined,
63+
} as any
64+
65+
// Complete the subtask - should not throw
66+
await expect(parentTask.completeSubtask("Subtask completed")).resolves.not.toThrow()
67+
68+
// Verify task is still unpaused
69+
expect(parentTask.isPaused).toBe(false)
70+
})
71+
})

0 commit comments

Comments
 (0)