Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cf5abe6
fix(task): stage-independent saveClineMessages + finalize open partia…
easonLiangWorldedtech Oct 5, 2026
d172c95
feat(write-to-file): per-task partial stream state + cleanup primitives
easonLiangWorldedtech Oct 5, 2026
52699c6
test(write-to-file): cover partial-state cleanup primitives directly
easonLiangWorldedtech Oct 5, 2026
4b23b6a
fix(write-to-file): capture streaming failure once, report it once
easonLiangWorldedtech Oct 5, 2026
e3c1040
feat(tools): onParameterParseFailure teardown boundary
easonLiangWorldedtech Oct 5, 2026
9b93a6f
fix(write-to-file): run diff cleanup when handleError rejects
easonLiangWorldedtech Oct 5, 2026
820a765
fix(tools): do not report a completed teardown when the diff revert f…
Oct 6, 2026
5030d11
test(tools): cover rollback failure with a retained stream error, and…
Oct 6, 2026
c5eb9ec
fix(tools): report the rollback hazard from every write_to_file clean…
Oct 6, 2026
8a9a484
test(tools): drop the as-any casts from the write_to_file harness
Oct 6, 2026
8b11960
chore: trigger a fresh review pass at this head
Oct 7, 2026
1dfd76f
fix(tools): release the stream state on every execute() exit
Oct 8, 2026
6667085
fix(write-to-file): release the partial stream state when the preview…
Oct 9, 2026
c6b77fb
Merge org main (036245c5e, U1 #1927) into p1066/u6-execute-error-path…
Oct 9, 2026
443993e
fix(write-to-file): re-check the stream state after every await, and …
easonLiangWorldedtech Oct 9, 2026
19e8512
fix(write-to-file): cover the pre-streaming window and discard withou…
easonLiangWorldedtech Oct 10, 2026
cda6aea
style: satisfy the repository prettier gate on the files this PR changes
easonLiangWorldedtech Oct 10, 2026
694bed4
fix(diff-view): close tabs on a clean buffer and stand the rollback d…
easonLiangWorldedtech Oct 10, 2026
94bd3ba
chore(tools): drop Stryker suppression comments per maintainer directive
easonliang28 Oct 14, 2026
71b2f98
test: clear unused bindings exposed by re-enabled unused-variable lint
easonliang28 Oct 14, 2026
6ff54c0
chore(tools): mark unused hook params in onParameterParseFailure default
easonliang28 Oct 14, 2026
eca7960
fix(editor): skip the clean save when the pre-stream restore is refused
easonliang28 Oct 14, 2026
9ecc224
fix(editor): stop the rollback instead of closing over an unconfirmed…
easonliang28 Oct 15, 2026
13f5658
fix(editor): stop stale sessions from deleting approved content after…
easonliang28 Oct 15, 2026
a3f9977
Merge remote-tracking branch 'upstream/main' into HEAD
easonLiangWorldedtech Oct 15, 2026
fb2f189
fix(editor): finish the rollback teardown when the modify editor neve…
easonliang28 Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/__tests__/removeClineFromStack-delegation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// npx vitest run __tests__/removeClineFromStack-delegation.spec.ts

import { describe, it, expect, vi } from "vitest"
import { RooCodeEventName } from "@roo-code/types"
import { ClineProvider } from "../core/webview/ClineProvider"
import { TaskRegistry } from "../core/task/TaskRegistry"
import { PendingActionSettlementError, type Task } from "../core/task/Task"
import { makeProviderStub } from "./helpers/provider-stub"
import { writeToFileTool } from "../core/tools/WriteToFileTool"

type PrivateClineProviderMethods = {
removeClineFromStack: (this: ClineProvider) => ReturnType<ClineProvider["removeClineFromStack"]>
Expand Down Expand Up @@ -202,6 +204,52 @@ describe("ClineProvider failed history restoration cleanup", () => {
expect(task.dispose).toHaveBeenCalledOnce()
})

it("releases the tool's per-task state before a directly disposed task loses its listeners", async () => {
const key = "failed-history-task.inst-1"
const order: string[] = []
// The listener identity matters: the release must detach THIS function, so the
// fixture binds it to a variable instead of an anonymous inline no-op.
const abortCleanup = vi.fn()
const task = {
taskId: "failed-history-task",
instanceId: "inst-1",
emit: vi.fn(),
once: vi.fn(),
off: vi.fn(),
dispose: vi.fn().mockImplementation(() => {
// Dispose removes every listener, so this is the last moment the abort
// cleanup could still have run; the entry must already be gone here.
order.push(writeToFileTool["taskPartialStreamState"].has(key) ? "state-retained" : "state-cleared")
order.push("dispose")
return Promise.resolve()
}),
} as unknown as Task
// Fixture: the state entry the tool would have created during a partial stream.
writeToFileTool["taskPartialStreamState"].set(key, {
lastSeenPartialPath: undefined,
streamFailed: false,
streamError: undefined,
task,
abortCleanup,
})
const taskEventListeners = new Map([[task, [vi.fn()]]])
const taskRegistry = new TaskRegistry()
taskRegistry.push(task)
const provider = { taskRegistry, taskEventListeners, log: vi.fn() } as unknown as ClineProvider

await privateClineProvider.cleanupFailedHistoryTask.call(
provider,
task,
new PendingActionSettlementError("settlement failed"),
)

expect(order).toEqual(["state-cleared", "dispose"])
expect(writeToFileTool["taskPartialStreamState"].size).toBe(0)
// Dropping the map entry is not enough: the registered TaskAborted listener must
// be detached with the same function reference it was registered with.
expect(task.off).toHaveBeenCalledWith(RooCodeEventName.TaskAborted, abortCleanup)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("keeps the task active after an unrelated history resume failure", async () => {
const cleanupListener = vi.fn()
const task = {
Expand Down
43 changes: 40 additions & 3 deletions src/core/tools/BaseTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,23 @@ export abstract class BaseTool<TName extends ToolName> {
throw new Error("Tool call is missing native arguments (nativeArgs).")
}
} catch (error) {
console.error(`Error parsing parameters:`, error)
const errorMessage = `Failed to parse ${this.name} parameters: ${error instanceof Error ? error.message : String(error)}`
await callbacks.handleError(`parsing ${this.name} args`, new Error(errorMessage))
const parseError = error instanceof Error ? error : new Error(String(error))
console.error(`Error parsing parameters:`, parseError)
// Final args could not be parsed (e.g. the model's tool call was truncated
// mid-JSON by the output token limit), so execute() will never run. If a
// streaming delta already opened a partial "tool" ask (partial: true),
// finalize it here or the webview spinner stays stuck indefinitely.
await task.finalizePartialToolAsk().catch((finalizeError) => {
console.error(`Error finalizing ${this.name} partial tool ask:`, finalizeError)
})
// execute() never runs on this path, so tools that keep per-task state
// outside execute() (streaming failure marks, abort listeners) get their
// one remaining teardown boundary here.
const reportedStreamingFailure = await this.onParameterParseFailure(task, callbacks, parseError)
if (!reportedStreamingFailure) {
const errorMessage = `Failed to parse ${this.name} parameters: ${parseError.message}`
await callbacks.handleError(`parsing ${this.name} args`, new Error(errorMessage))
}
// Note: handleError already emits a tool_result via formatResponse.toolError in the caller.
// Do NOT call pushToolResult here to avoid duplicate tool_result payloads.
return
Expand All @@ -166,4 +180,27 @@ export abstract class BaseTool<TName extends ToolName> {
// Execute with typed parameters
await this.execute(params, task, callbacks)
}

/**
* Teardown boundary for the native-argument parse-failure path in handle().
*
* When nativeArgs are missing or malformed, execute() never runs, so per-task
* state a tool registered outside execute() (streaming failure marks, abort
* listeners) is never torn down there. Streaming tools override this to tear
* that state down and, when a streaming delta already failed, to report the
* captured streaming error instead of the generic parse error.
*
* @param _task - Task instance
* @param _callbacks - Tool execution callbacks
* @param _parseError - The native-argument parse error
* @returns true when the override already reported the failure to the user,
* so handle() suppresses the generic parse error
*/
protected async onParameterParseFailure(
_task: Task,
_callbacks: ToolCallbacks,
_parseError: Error,
): Promise<boolean> {
return false
}
}
Loading
Loading