Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 974e207

Browse files
ctehannesrudolph
authored andcommitted
Handle cancel/resume abort races without crashing (#11422)
1 parent 8575297 commit 974e207

2 files changed

Lines changed: 113 additions & 3 deletions

File tree

src/core/task/Task.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -731,15 +731,49 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
731731
if (startTask) {
732732
this._started = true
733733
if (task || images) {
734-
this.startTask(task, images)
734+
this.runLifecycleTaskInBackground(this.startTask(task, images), "startTask")
735735
} else if (historyItem) {
736-
this.resumeTaskFromHistory()
736+
this.runLifecycleTaskInBackground(this.resumeTaskFromHistory(), "resumeTaskFromHistory")
737737
} else {
738738
throw new Error("Either historyItem or task/images must be provided")
739739
}
740740
}
741741
}
742742

743+
private runLifecycleTaskInBackground(taskPromise: Promise<void>, operation: "startTask" | "resumeTaskFromHistory") {
744+
void taskPromise.catch((error) => {
745+
if (this.shouldIgnoreBackgroundLifecycleError(error)) {
746+
return
747+
}
748+
749+
console.error(
750+
`[Task#${operation}] task ${this.taskId}.${this.instanceId} failed: ${
751+
error instanceof Error ? error.message : String(error)
752+
}`,
753+
)
754+
})
755+
}
756+
757+
private shouldIgnoreBackgroundLifecycleError(error: unknown): boolean {
758+
if (error instanceof AskIgnoredError) {
759+
return true
760+
}
761+
762+
if (this.abandoned === true || this.abort === true || this.abortReason === "user_cancelled") {
763+
return true
764+
}
765+
766+
if (!(error instanceof Error)) {
767+
return false
768+
}
769+
770+
const abortedByCurrentTask =
771+
error.message.includes(`[RooCode#ask] task ${this.taskId}.${this.instanceId} aborted`) ||
772+
error.message.includes(`[RooCode#say] task ${this.taskId}.${this.instanceId} aborted`)
773+
774+
return abortedByCurrentTask
775+
}
776+
743777
/**
744778
* Initialize the task mode from the provider state.
745779
* This method handles async initialization with proper error handling.
@@ -2089,7 +2123,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
20892123
const { task, images } = this.metadata
20902124

20912125
if (task || images) {
2092-
return this.startTask(task ?? undefined, images ?? undefined)
2126+
this.runLifecycleTaskInBackground(this.startTask(task ?? undefined, images ?? undefined), "startTask")
20932127
}
20942128
}
20952129

src/core/task/__tests__/Task.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,82 @@ describe("Cline", () => {
394394
new Task({ provider: mockProvider, apiConfiguration: mockApiConfig })
395395
}).toThrow("Either historyItem or task/images must be provided")
396396
})
397+
398+
it("should ignore cancelled background resumeTaskFromHistory errors", async () => {
399+
const resumeSpy = vi
400+
.spyOn(Task.prototype as any, "resumeTaskFromHistory")
401+
.mockImplementationOnce(async function (this: Task) {
402+
this.abort = true
403+
throw new Error("resume aborted")
404+
})
405+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
406+
407+
new Task({
408+
provider: mockProvider,
409+
apiConfiguration: mockApiConfig,
410+
historyItem: {
411+
id: "history-task-id",
412+
number: 1,
413+
ts: Date.now(),
414+
task: "historical task",
415+
tokensIn: 0,
416+
tokensOut: 0,
417+
cacheWrites: 0,
418+
cacheReads: 0,
419+
totalCost: 0,
420+
} as any,
421+
startTask: true,
422+
})
423+
424+
await Promise.resolve()
425+
await Promise.resolve()
426+
427+
const lifecycleErrors = consoleErrorSpy.mock.calls.filter(
428+
([message]) => typeof message === "string" && message.includes("[Task#resumeTaskFromHistory]"),
429+
)
430+
expect(lifecycleErrors).toHaveLength(0)
431+
432+
resumeSpy.mockRestore()
433+
consoleErrorSpy.mockRestore()
434+
})
435+
436+
it("should log unexpected background resumeTaskFromHistory errors", async () => {
437+
const resumeSpy = vi
438+
.spyOn(Task.prototype as any, "resumeTaskFromHistory")
439+
.mockRejectedValueOnce(new Error("unexpected resume failure"))
440+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
441+
442+
new Task({
443+
provider: mockProvider,
444+
apiConfiguration: mockApiConfig,
445+
historyItem: {
446+
id: "history-task-id",
447+
number: 1,
448+
ts: Date.now(),
449+
task: "historical task",
450+
tokensIn: 0,
451+
tokensOut: 0,
452+
cacheWrites: 0,
453+
cacheReads: 0,
454+
totalCost: 0,
455+
} as any,
456+
startTask: true,
457+
})
458+
459+
await Promise.resolve()
460+
await Promise.resolve()
461+
462+
const lifecycleErrors = consoleErrorSpy.mock.calls.filter(
463+
([message]) =>
464+
typeof message === "string" &&
465+
message.includes("[Task#resumeTaskFromHistory]") &&
466+
message.includes("unexpected resume failure"),
467+
)
468+
expect(lifecycleErrors).toHaveLength(1)
469+
470+
resumeSpy.mockRestore()
471+
consoleErrorSpy.mockRestore()
472+
})
397473
})
398474

399475
describe("getEnvironmentDetails", () => {

0 commit comments

Comments
 (0)