@@ -304,6 +304,127 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
304304 } )
305305 } )
306306
307+ describe ( "didRejectTool cleanup path" , ( ) => {
308+ it ( "merges a pending native protocol guide into the rejection tool_result" , async ( ) => {
309+ const state = getTaskErrorState ( mockTask )
310+ state . setPendingNativeProtocolGuide ( "[XML_NATIVE_DUAL_PROTOCOL occurrence=1] guide-to-merge" )
311+
312+ mockTask . didRejectTool = true
313+ const toolCallId = "call_rejected_with_guide"
314+ mockTask . assistantMessageContent = [
315+ {
316+ type : "tool_use" ,
317+ id : toolCallId ,
318+ name : "read_file" ,
319+ params : { path : "x.txt" } ,
320+ partial : false ,
321+ } ,
322+ ]
323+
324+ await presentAssistantMessage ( mockTask )
325+
326+ const toolResult = mockTask . userMessageContent . find (
327+ ( item : any ) => item . type === "tool_result" && item . tool_use_id === toolCallId ,
328+ )
329+ expect ( toolResult ) . toBeDefined ( )
330+ expect ( toolResult . is_error ) . toBe ( true )
331+ expect ( String ( toolResult . content ) ) . toContain ( "Skipping tool" )
332+ expect ( String ( toolResult . content ) ) . toContain ( "guide-to-merge" )
333+ // Guide must be consumed so it cannot leak into later turns.
334+ expect ( state . consumePendingNativeProtocolGuide ( ) ) . toBeUndefined ( )
335+ } )
336+ } )
337+
338+ describe ( "missing nativeArgs (malformed native call)" , ( ) => {
339+ it ( "pushes a structured tool_result and does not set didAlreadyUseTool" , async ( ) => {
340+ const toolCallId = "call_missing_native_args"
341+ mockTask . assistantMessageContent = [
342+ {
343+ type : "tool_use" ,
344+ id : toolCallId ,
345+ name : "read_file" ,
346+ params : { path : "x.txt" } ,
347+ // nativeArgs intentionally omitted: parser could not finalize arguments.
348+ partial : false ,
349+ } ,
350+ ]
351+
352+ await presentAssistantMessage ( mockTask )
353+
354+ const toolResult = mockTask . userMessageContent . find (
355+ ( item : any ) => item . type === "tool_result" && item . tool_use_id === toolCallId ,
356+ )
357+ expect ( toolResult ) . toBeDefined ( )
358+ expect ( toolResult . is_error ) . toBe ( true )
359+ // The interceptor transforms the missing-nativeArgs signal into a
360+ // structured guided payload (PARAM_MISSING category).
361+ expect ( String ( toolResult . content ) ) . toContain ( "guided_tool_error" )
362+ expect ( String ( toolResult . content ) ) . toContain ( "PARAM_MISSING" )
363+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
364+ expect ( mockTask . recordToolError ) . toHaveBeenCalledWith (
365+ "read_file" ,
366+ expect . stringContaining ( "missing nativeArgs" ) ,
367+ )
368+ expect ( mockTask . didAlreadyUseTool ) . toBe ( false )
369+ } )
370+ } )
371+
372+ describe ( "structural fingerprint change" , ( ) => {
373+ it ( "resets the PARAM_TYPE_MISMATCH circuit when the failure shape changes" , async ( ) => {
374+ const state = getTaskErrorState ( mockTask )
375+
376+ // First failure: CWD_OBJECT_MISUSE on execute_command.
377+ mockTask . assistantMessageContent = [
378+ {
379+ type : "tool_use" ,
380+ id : "call_shape_a" ,
381+ name : "execute_command" ,
382+ params : { command : "ls" } ,
383+ nativeArgs : { command : "ls" , cwd : { nested : "object" } } ,
384+ partial : false ,
385+ } ,
386+ ]
387+ await presentAssistantMessage ( mockTask )
388+ const fingerprintA = state . getFingerprint ( "PARAM_TYPE_MISMATCH" )
389+ expect ( fingerprintA ) . toContain ( "CWD_OBJECT_MISUSE" )
390+
391+ // Second failure on the SAME task with a different structural shape
392+ // (NESTED_PARAM_OVERFLOW on read_file) must reset the circuit and
393+ // report the new variant instead of inheriting the previous one.
394+ mockTask . assistantMessageContent = [
395+ {
396+ type : "tool_use" ,
397+ id : "call_shape_b" ,
398+ name : "read_file" ,
399+ params : { } ,
400+ nativeArgs : {
401+ path : "a.txt" ,
402+ extra : { name : "read_file" , arguments : { path : "b.txt" } } ,
403+ } ,
404+ partial : false ,
405+ } ,
406+ ]
407+ mockTask . currentStreamingContentIndex = 0
408+ mockTask . didAlreadyUseTool = false
409+ mockTask . userMessageContent = [ ]
410+ mockTask . consecutiveMistakeCount = 0
411+
412+ await presentAssistantMessage ( mockTask )
413+
414+ const fingerprintB = state . getFingerprint ( "PARAM_TYPE_MISMATCH" )
415+ expect ( fingerprintB ) . toContain ( "NESTED_PARAM_OVERFLOW" )
416+ expect ( fingerprintB ) . not . toBe ( fingerprintA )
417+ // After a shape change the circuit restarts, so occurrence is 1
418+ // and the message is the first-occurrence NESTED_PARAM_OVERFLOW
419+ // guidance rather than a repeat/stuck-loop escalation.
420+ expect ( mockTask . recordToolError ) . toHaveBeenLastCalledWith (
421+ "read_file" ,
422+ expect . stringContaining ( "NESTED_PARAM_OVERFLOW" ) ,
423+ )
424+ expect ( state . isOpen ( "PARAM_TYPE_MISMATCH" ) ) . toBe ( false )
425+ } )
426+ } )
427+
307428 describe ( "missing tool_use.id (legacy XML call)" , ( ) => {
308429 it ( "transforms the error through interceptor and pushes guided text" , async ( ) => {
309430 mockTask . assistantMessageContent = [
0 commit comments