@@ -27,22 +27,47 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
2727 readonly name = "write_to_file" as const
2828
2929 /**
30- * Set when a filesystem error aborts diff-view streaming during handlePartial for the
31- * current tool invocation. Subsequent streaming deltas for the same block then skip the
32- * doomed open()/update() retry, which would otherwise create a fresh "Zoo wants to edit
33- * this file" message on every delta. Cleared by resetPartialState() between invocations.
30+ * Tracks filesystem failures from diff-view streaming by task id. Tool instances are
31+ * singletons, so this state must be keyed per task to avoid one task's failing partial
32+ * stream suppressing another task's streaming deltas.
3433 */
35- private partialStreamFailed = false
34+ private partialStreamFailuresByTaskId = new Set < string > ( )
35+
36+ /**
37+ * Tracks partial path stabilization by task id. The tool is a singleton, so using the
38+ * BaseTool singleton path state lets concurrent tasks incorrectly stabilize each other.
39+ */
40+ private lastSeenPartialPathByTaskId = new Map < string , string | undefined > ( )
41+
42+ private getPartialStreamFailureKey ( task : Task ) : string {
43+ return `${ task . taskId } .${ task . instanceId } `
44+ }
45+
46+ private hasPathStabilizedForTask ( task : Task , partialPath : string | undefined ) : boolean {
47+ const key = this . getPartialStreamFailureKey ( task )
48+ const lastSeenPath = this . lastSeenPartialPathByTaskId . get ( key )
49+ const pathHasStabilized = lastSeenPath !== undefined && lastSeenPath === partialPath
50+ this . lastSeenPartialPathByTaskId . set ( key , partialPath )
51+ return pathHasStabilized && ! ! partialPath
52+ }
53+
54+ private resetTaskPartialState ( task : Task ) : void {
55+ const key = this . getPartialStreamFailureKey ( task )
56+ this . lastSeenPartialPathByTaskId . delete ( key )
57+ this . partialStreamFailuresByTaskId . delete ( key )
58+ }
3659
3760 override resetPartialState ( ) : void {
3861 super . resetPartialState ( )
39- this . partialStreamFailed = false
62+ this . partialStreamFailuresByTaskId . clear ( )
63+ this . lastSeenPartialPathByTaskId . clear ( )
4064 }
4165
4266 async execute ( params : WriteToFileParams , task : Task , callbacks : ToolCallbacks ) : Promise < void > {
4367 const { pushToolResult, handleError, askApproval } = callbacks
4468 const relPath = params . path
4569 let newContent = params . content
70+ const partialStreamFailureKey = this . getPartialStreamFailureKey ( task )
4671
4772 if ( ! relPath ) {
4873 task . consecutiveMistakeCount ++
@@ -104,15 +129,15 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
104129 }
105130
106131 try {
107- task . consecutiveMistakeCount = 0
108-
109132 // Create parent directories for new files inside the try block so filesystem
110133 // errors (EROFS, EACCES, etc.) route through handleError with proper cleanup
111134 // and consecutive-mistake counting, rather than escaping unhandled.
112135 if ( ! fileExists ) {
113136 await createDirectoriesForFile ( absolutePath )
114137 }
115138
139+ task . consecutiveMistakeCount = 0
140+
116141 const provider = task . providerRef . deref ( )
117142 const state = await provider ?. getState ( )
118143 const diagnosticsEnabled = state ?. diagnosticsEnabled ?? true
@@ -194,7 +219,7 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
194219 pushToolResult ( message )
195220
196221 await task . diffViewProvider . reset ( )
197- this . resetPartialState ( )
222+ this . resetTaskPartialState ( task )
198223
199224 task . processQueuedMessages ( )
200225
@@ -207,7 +232,7 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
207232 await task . finalizePartialToolAsk ( )
208233 await handleError ( "writing file" , error as Error )
209234 await task . diffViewProvider . reset ( )
210- this . resetPartialState ( )
235+ this . resetTaskPartialState ( task )
211236 return
212237 }
213238 }
@@ -216,15 +241,17 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
216241 const relPath : string | undefined = block . params . path
217242 const newContent : string | undefined = block . params . content
218243
219- // A prior streaming delta for this invocation already hit a fatal filesystem error.
244+ const partialStreamFailureKey = this . getPartialStreamFailureKey ( task )
245+
246+ // A prior streaming delta for this task already hit a fatal filesystem error.
220247 // Skip further streaming work so we don't create a new partial tool message on every
221248 // subsequent delta. execute() will report the error once when the block completes.
222- if ( this . partialStreamFailed ) {
249+ if ( this . partialStreamFailuresByTaskId . has ( partialStreamFailureKey ) ) {
223250 return
224251 }
225252
226253 // Wait for path to stabilize before showing UI (prevents truncated paths)
227- if ( ! this . hasPathStabilized ( relPath ) || newContent === undefined ) {
254+ if ( ! this . hasPathStabilizedForTask ( task , relPath ) || newContent === undefined ) {
228255 return
229256 }
230257
@@ -286,9 +313,11 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
286313 console . error ( `Error streaming write_to_file diff view:` , error )
287314 // Mark the stream as failed so later deltas don't re-attempt and spawn a new
288315 // partial tool message each time.
289- this . partialStreamFailed = true
290- await task . finalizePartialToolAsk ( )
291- await task . diffViewProvider . reset ( )
316+ this . partialStreamFailuresByTaskId . add ( partialStreamFailureKey )
317+ await task . finalizePartialToolAsk ( partialMessage )
318+ await task . diffViewProvider . reset ( ) . catch ( ( resetError ) => {
319+ console . error ( "Error resetting write_to_file diff view after partial failure:" , resetError )
320+ } )
292321 }
293322 }
294323 }
0 commit comments