mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Handle cancel/resume abort races without crashing (#11422)
This commit is contained in:
parent
b51af98278
commit
77b76a891f
2 changed files with 113 additions and 3 deletions
|
|
@ -749,15 +749,49 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
if (startTask) {
|
||||
this._started = true
|
||||
if (task || images) {
|
||||
this.startTask(task, images)
|
||||
this.runLifecycleTaskInBackground(this.startTask(task, images), "startTask")
|
||||
} else if (historyItem) {
|
||||
this.resumeTaskFromHistory()
|
||||
this.runLifecycleTaskInBackground(this.resumeTaskFromHistory(), "resumeTaskFromHistory")
|
||||
} else {
|
||||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private runLifecycleTaskInBackground(taskPromise: Promise<void>, operation: "startTask" | "resumeTaskFromHistory") {
|
||||
void taskPromise.catch((error) => {
|
||||
if (this.shouldIgnoreBackgroundLifecycleError(error)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[Task#${operation}] task ${this.taskId}.${this.instanceId} failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private shouldIgnoreBackgroundLifecycleError(error: unknown): boolean {
|
||||
if (error instanceof AskIgnoredError) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.abandoned === true || this.abort === true || this.abortReason === "user_cancelled") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const abortedByCurrentTask =
|
||||
error.message.includes(`[RooCode#ask] task ${this.taskId}.${this.instanceId} aborted`) ||
|
||||
error.message.includes(`[RooCode#say] task ${this.taskId}.${this.instanceId} aborted`)
|
||||
|
||||
return abortedByCurrentTask
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the task mode from the provider state.
|
||||
* This method handles async initialization with proper error handling.
|
||||
|
|
@ -2067,7 +2101,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
const { task, images } = this.metadata
|
||||
|
||||
if (task || images) {
|
||||
this.startTask(task ?? undefined, images ?? undefined)
|
||||
this.runLifecycleTaskInBackground(this.startTask(task ?? undefined, images ?? undefined), "startTask")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -394,6 +394,82 @@ describe("Cline", () => {
|
|||
new Task({ provider: mockProvider, apiConfiguration: mockApiConfig })
|
||||
}).toThrow("Either historyItem or task/images must be provided")
|
||||
})
|
||||
|
||||
it("should ignore cancelled background resumeTaskFromHistory errors", async () => {
|
||||
const resumeSpy = vi
|
||||
.spyOn(Task.prototype as any, "resumeTaskFromHistory")
|
||||
.mockImplementationOnce(async function (this: Task) {
|
||||
this.abort = true
|
||||
throw new Error("resume aborted")
|
||||
})
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
historyItem: {
|
||||
id: "history-task-id",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "historical task",
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
totalCost: 0,
|
||||
} as any,
|
||||
startTask: true,
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const lifecycleErrors = consoleErrorSpy.mock.calls.filter(
|
||||
([message]) => typeof message === "string" && message.includes("[Task#resumeTaskFromHistory]"),
|
||||
)
|
||||
expect(lifecycleErrors).toHaveLength(0)
|
||||
|
||||
resumeSpy.mockRestore()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should log unexpected background resumeTaskFromHistory errors", async () => {
|
||||
const resumeSpy = vi
|
||||
.spyOn(Task.prototype as any, "resumeTaskFromHistory")
|
||||
.mockRejectedValueOnce(new Error("unexpected resume failure"))
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
new Task({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
historyItem: {
|
||||
id: "history-task-id",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "historical task",
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
totalCost: 0,
|
||||
} as any,
|
||||
startTask: true,
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const lifecycleErrors = consoleErrorSpy.mock.calls.filter(
|
||||
([message]) =>
|
||||
typeof message === "string" &&
|
||||
message.includes("[Task#resumeTaskFromHistory]") &&
|
||||
message.includes("unexpected resume failure"),
|
||||
)
|
||||
expect(lifecycleErrors).toHaveLength(1)
|
||||
|
||||
resumeSpy.mockRestore()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getEnvironmentDetails", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue