@@ -583,6 +583,134 @@ describe("getEffectiveApiHistory", () => {
583583 } )
584584} )
585585
586+ describe ( "getEffectiveApiHistory - orphan tool_result filtering after truncation (no summary)" , ( ) => {
587+ it ( "should filter orphan tool_result blocks when truncation removes assistant tool_use messages" , ( ) => {
588+ const truncationId = "trunc-1"
589+ const messages : ApiMessage [ ] = [
590+ // Truncation marker
591+ {
592+ role : "user" ,
593+ content : [ { type : "text" , text : "[Previous context truncated]" } ] ,
594+ isTruncationMarker : true ,
595+ truncationId,
596+ } ,
597+ // Assistant message with tool_use, hidden by truncation
598+ {
599+ role : "assistant" ,
600+ content : [ { type : "tool_use" , id : "tool-1" , name : "read_file" , input : { path : "test.ts" } } ] ,
601+ truncationParent : truncationId ,
602+ } ,
603+ // User message with tool_result referencing truncated tool_use
604+ {
605+ role : "user" ,
606+ content : [ { type : "tool_result" , tool_use_id : "tool-1" , content : "file contents" } ] ,
607+ truncationParent : truncationId ,
608+ } ,
609+ // Visible assistant message with tool_use
610+ {
611+ role : "assistant" ,
612+ content : [
613+ { type : "tool_use" , id : "tool-2" , name : "write_file" , input : { path : "out.ts" , content : "code" } } ,
614+ ] ,
615+ } ,
616+ // Visible user message with tool_result for tool-2
617+ {
618+ role : "user" ,
619+ content : [ { type : "tool_result" , tool_use_id : "tool-2" , content : "file written" } ] ,
620+ } ,
621+ ]
622+
623+ const result = getEffectiveApiHistory ( messages )
624+
625+ // Should have: truncation marker, visible assistant, visible user (3 messages)
626+ // Truncated assistant and user are filtered by truncationParent
627+ expect ( result ) . toHaveLength ( 3 )
628+ expect ( result [ 0 ] . isTruncationMarker ) . toBe ( true )
629+ expect ( result [ 1 ] . role ) . toBe ( "assistant" )
630+ expect ( result [ 2 ] . role ) . toBe ( "user" )
631+ const userContent = result [ 2 ] . content as any [ ]
632+ expect ( userContent [ 0 ] . tool_use_id ) . toBe ( "tool-2" )
633+ } )
634+
635+ it ( "should filter orphan tool_result when user message survives truncation but referenced assistant is truncated" , ( ) => {
636+ const truncationId = "trunc-1"
637+ const messages : ApiMessage [ ] = [
638+ // Truncation marker
639+ {
640+ role : "user" ,
641+ content : [ { type : "text" , text : "[Previous context truncated]" } ] ,
642+ isTruncationMarker : true ,
643+ truncationId,
644+ } ,
645+ // Assistant message with tool_use, hidden by truncation
646+ {
647+ role : "assistant" ,
648+ content : [ { type : "tool_use" , id : "tool-orphan" , name : "read_file" , input : { path : "test.ts" } } ] ,
649+ truncationParent : truncationId ,
650+ } ,
651+ // User message with orphan tool_result - NOT tagged with truncationParent
652+ // This is the bug scenario: truncation removed the assistant but not the user message
653+ {
654+ role : "user" ,
655+ content : [ { type : "tool_result" , tool_use_id : "tool-orphan" , content : "file contents" } ] ,
656+ } ,
657+ // Visible conversation continues
658+ {
659+ role : "assistant" ,
660+ content : "Here is the result." ,
661+ } ,
662+ ]
663+
664+ const result = getEffectiveApiHistory ( messages )
665+
666+ // The orphan tool_result user message should be removed entirely
667+ // Result: truncation marker, assistant text (2 messages)
668+ expect ( result ) . toHaveLength ( 2 )
669+ expect ( result [ 0 ] . isTruncationMarker ) . toBe ( true )
670+ expect ( result [ 1 ] . role ) . toBe ( "assistant" )
671+ expect ( result [ 1 ] . content ) . toBe ( "Here is the result." )
672+ } )
673+
674+ it ( "should keep non-orphan content in mixed user message after truncation" , ( ) => {
675+ const truncationId = "trunc-1"
676+ const messages : ApiMessage [ ] = [
677+ {
678+ role : "user" ,
679+ content : [ { type : "text" , text : "[Previous context truncated]" } ] ,
680+ isTruncationMarker : true ,
681+ truncationId,
682+ } ,
683+ {
684+ role : "assistant" ,
685+ content : [ { type : "tool_use" , id : "tool-orphan" , name : "read_file" , input : { path : "test.ts" } } ] ,
686+ truncationParent : truncationId ,
687+ } ,
688+ // User message with both orphan tool_result AND text content
689+ {
690+ role : "user" ,
691+ content : [
692+ { type : "text" , text : "Here's some context" } ,
693+ { type : "tool_result" , tool_use_id : "tool-orphan" , content : "file contents" } ,
694+ ] ,
695+ } ,
696+ {
697+ role : "assistant" ,
698+ content : "Got it." ,
699+ } ,
700+ ]
701+
702+ const result = getEffectiveApiHistory ( messages )
703+
704+ // Should keep the user message but strip the orphan tool_result
705+ expect ( result ) . toHaveLength ( 3 )
706+ expect ( result [ 0 ] . isTruncationMarker ) . toBe ( true )
707+ const userContent = result [ 1 ] . content as any [ ]
708+ expect ( userContent ) . toHaveLength ( 1 )
709+ expect ( userContent [ 0 ] . type ) . toBe ( "text" )
710+ expect ( userContent [ 0 ] . text ) . toBe ( "Here's some context" )
711+ } )
712+ } )
713+
586714describe ( "cleanupAfterTruncation" , ( ) => {
587715 it ( "should clear orphaned condenseParent references" , ( ) => {
588716 const orphanedCondenseId = "deleted-summary"
0 commit comments