@@ -5,6 +5,7 @@ import { ClineProvider } from "../core/webview/ClineProvider"
55import { TaskRegistry } from "../core/task/TaskRegistry"
66import { PendingActionSettlementError , type Task } from "../core/task/Task"
77import { makeProviderStub } from "./helpers/provider-stub"
8+ import { writeToFileTool } from "../core/tools/WriteToFileTool"
89
910type PrivateClineProviderMethods = {
1011 removeClineFromStack : ( this : ClineProvider ) => ReturnType < ClineProvider [ "removeClineFromStack" ] >
@@ -202,6 +203,47 @@ describe("ClineProvider failed history restoration cleanup", () => {
202203 expect ( task . dispose ) . toHaveBeenCalledOnce ( )
203204 } )
204205
206+ it ( "releases the tool's per-task state before a directly disposed task loses its listeners" , async ( ) => {
207+ const key = "failed-history-task.inst-1"
208+ const order : string [ ] = [ ]
209+ const task = {
210+ taskId : "failed-history-task" ,
211+ instanceId : "inst-1" ,
212+ emit : vi . fn ( ) ,
213+ once : vi . fn ( ) ,
214+ off : vi . fn ( ) ,
215+ dispose : vi . fn ( ) . mockImplementation ( ( ) => {
216+ // Dispose removes every listener, so this is the last moment the abort
217+ // cleanup could still have run; the entry must already be gone here.
218+ order . push ( writeToFileTool [ "taskPartialStreamState" ] . has ( key ) ? "state-retained" : "state-cleared" )
219+ order . push ( "dispose" )
220+ return Promise . resolve ( )
221+ } ) ,
222+ } as unknown as Task
223+ // Fixture: the state entry the tool would have created during a partial stream.
224+ writeToFileTool [ "taskPartialStreamState" ] . set ( key , {
225+ lastSeenPartialPath : undefined ,
226+ streamFailed : false ,
227+ streamError : undefined ,
228+ rollbackFailure : undefined ,
229+ task,
230+ abortCleanup : ( ) => { } ,
231+ } )
232+ const taskEventListeners = new Map ( [ [ task , [ vi . fn ( ) ] ] ] )
233+ const taskRegistry = new TaskRegistry ( )
234+ taskRegistry . push ( task )
235+ const provider = { taskRegistry, taskEventListeners, log : vi . fn ( ) } as unknown as ClineProvider
236+
237+ await privateClineProvider . cleanupFailedHistoryTask . call (
238+ provider ,
239+ task ,
240+ new PendingActionSettlementError ( "settlement failed" ) ,
241+ )
242+
243+ expect ( order ) . toEqual ( [ "state-cleared" , "dispose" ] )
244+ expect ( writeToFileTool [ "taskPartialStreamState" ] . size ) . toBe ( 0 )
245+ } )
246+
205247 it ( "keeps the task active after an unrelated history resume failure" , async ( ) => {
206248 const cleanupListener = vi . fn ( )
207249 const task = {
0 commit comments