Skip to content

Commit 75406a7

Browse files
committed
fix: implement comprehensive gray state recovery for subtask completion failures
- Add error recovery in Task.resumePausedTask() to handle provider disconnections gracefully - Enhance Task.recursivelyMakeClineRequests() with recovery mechanisms for API failures - Add ClineProvider.recoverFromGrayState() method with multiple recovery strategies - Enhance ClineProvider.finishSubTask() with comprehensive error handling - Add UI state validation in ChatView.tsx to detect and recover from gray state - Include comprehensive test suite for gray state recovery mechanisms Fixes #5892: Gray state issue where Orchestrator subtask completion with provider disconnection leaves parent task unusable
1 parent 38d8edf commit 75406a7

File tree

4 files changed

+436
-7
lines changed

4 files changed

+436
-7
lines changed

src/core/task/Task.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,16 @@ export class Task extends EventEmitter<ClineEvents> {
780780
.deref()
781781
?.log(`Error failed to add reply from subtask into conversation of parent task, error: ${error}`)
782782

783-
throw error
783+
// Don't throw the error - instead try to recover gracefully
784+
// This prevents the task from getting stuck in a gray state
785+
console.warn(`[Task.resumePausedTask] Recovered from error during subtask completion: ${error}`)
786+
787+
// Ensure the task can continue by adding a fallback message
788+
try {
789+
await this.say("subtask_result", `Subtask completed with recovery: ${lastMessage}`)
790+
} catch (fallbackError) {
791+
console.error(`[Task.resumePausedTask] Failed to add fallback message: ${fallbackError}`)
792+
}
784793
}
785794
}
786795

@@ -1457,7 +1466,19 @@ export class Task extends EventEmitter<ClineEvents> {
14571466
const history = await provider?.getTaskWithId(this.taskId)
14581467

14591468
if (history) {
1460-
await provider?.initClineWithHistoryItem(history.historyItem)
1469+
try {
1470+
await provider?.initClineWithHistoryItem(history.historyItem)
1471+
} catch (recoveryError) {
1472+
// If recovery fails, ensure we don't leave the task in a gray state
1473+
console.error(`[Task.recursivelyMakeClineRequests] Recovery failed: ${recoveryError}`)
1474+
1475+
// Force a clean state by clearing the task if recovery fails
1476+
try {
1477+
await provider?.clearTask()
1478+
} catch (clearError) {
1479+
console.error(`[Task.recursivelyMakeClineRequests] Failed to clear task during recovery: ${clearError}`)
1480+
}
1481+
}
14611482
}
14621483
}
14631484
} finally {
@@ -1731,7 +1752,7 @@ export class Task extends EventEmitter<ClineEvents> {
17311752
const contextWindow = modelInfo.contextWindow
17321753

17331754
const currentProfileId =
1734-
state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ??
1755+
state?.listApiConfigMeta.find((profile: any) => profile.name === state?.currentApiConfigName)?.id ??
17351756
"default"
17361757

17371758
const truncateResult = await truncateConversationIfNeeded({

src/core/webview/ClineProvider.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,60 @@ export class ClineProvider
228228
// this is used when a sub task is finished and the parent task needs to be resumed
229229
async finishSubTask(lastMessage: string) {
230230
console.log(`[subtasks] finishing subtask ${lastMessage}`)
231-
// remove the last cline instance from the stack (this is the finished sub task)
232-
await this.removeClineFromStack()
233-
// resume the last cline instance in the stack (if it exists - this is the 'parent' calling task)
234-
await this.getCurrentCline()?.resumePausedTask(lastMessage)
231+
232+
try {
233+
// remove the last cline instance from the stack (this is the finished sub task)
234+
await this.removeClineFromStack()
235+
// resume the last cline instance in the stack (if it exists - this is the 'parent' calling task)
236+
await this.getCurrentCline()?.resumePausedTask(lastMessage)
237+
} catch (error) {
238+
console.error(`[ClineProvider.finishSubTask] Error during subtask completion: ${error}`)
239+
240+
// Attempt to recover from gray state by ensuring we have a valid task state
241+
await this.recoverFromGrayState(lastMessage)
242+
}
243+
}
244+
245+
/**
246+
* Attempts to recover from a gray state where the task is stuck with no valid UI state
247+
*/
248+
private async recoverFromGrayState(lastMessage: string) {
249+
console.log(`[ClineProvider.recoverFromGrayState] Attempting recovery with message: ${lastMessage}`)
250+
251+
try {
252+
const currentTask = this.getCurrentCline()
253+
254+
if (currentTask) {
255+
// If we have a current task, try to force it into a valid state
256+
console.log(`[ClineProvider.recoverFromGrayState] Found current task ${currentTask.taskId}, attempting to resume`)
257+
258+
// Force the task to be unpaused and try to resume
259+
currentTask.isPaused = false
260+
261+
// Try to add a recovery message to the task
262+
try {
263+
await currentTask.say("subtask_result", `Recovery: ${lastMessage}`)
264+
} catch (sayError) {
265+
console.warn(`[ClineProvider.recoverFromGrayState] Failed to add recovery message: ${sayError}`)
266+
}
267+
268+
// Post state to webview to refresh UI
269+
await this.postStateToWebview()
270+
} else {
271+
// No current task - this is a more severe gray state
272+
console.log(`[ClineProvider.recoverFromGrayState] No current task found, clearing task state`)
273+
await this.clearTask()
274+
}
275+
} catch (recoveryError) {
276+
console.error(`[ClineProvider.recoverFromGrayState] Recovery failed: ${recoveryError}`)
277+
278+
// Last resort: clear the task entirely
279+
try {
280+
await this.clearTask()
281+
} catch (clearError) {
282+
console.error(`[ClineProvider.recoverFromGrayState] Failed to clear task during recovery: ${clearError}`)
283+
}
284+
}
235285
}
236286

237287
// Clear the current task without treating it as a subtask

0 commit comments

Comments
 (0)