Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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
41 changes: 41 additions & 0 deletions src/__tests__/removeClineFromStack-delegation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 MockTask = Pick<Task, "taskId" | "instanceId"> &
Partial<Pick<Task, "parentTaskId" | "abort" | "abandoned">> & {
Expand Down Expand Up @@ -208,6 +209,46 @@ 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[] = []
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)
})

it("keeps the task active after an unrelated history resume failure", async () => {
const cleanupListener = vi.fn()
const task = {
Expand Down
39 changes: 36 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,23 @@ 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