@@ -728,4 +728,125 @@ describe("MessageManager", () => {
728728 expect ( apiCall [ 0 ] . role ) . toBe ( "system" )
729729 } )
730730 } )
731+
732+ describe ( "Race condition handling" , ( ) => {
733+ it ( "should preserve assistant message when clineMessage timestamp is earlier due to async execution" , async ( ) => {
734+ // This test reproduces the bug where deleting a user_feedback clineMessage
735+ // incorrectly removes an assistant API message that was added AFTER the
736+ // clineMessage (due to async tool execution during streaming).
737+ //
738+ // Timeline (race condition scenario):
739+ // - T1 (100): clineMessage "user_feedback" created during tool execution
740+ // - T2 (200): assistant API message added when stream completes
741+ // - T3 (300): user API message (tool_result) added after pWaitFor
742+ //
743+ // When deleting the clineMessage at T1, we should:
744+ // - Keep the assistant message at T2
745+ // - Remove the user message at T3
746+
747+ mockTask . clineMessages = [
748+ { ts : 50 , say : "user" , text : "Initial request" } ,
749+ { ts : 100 , say : "user_feedback" , text : "tell me a joke 3" } , // Race: created BEFORE assistant API msg
750+ ]
751+
752+ mockTask . apiConversationHistory = [
753+ { ts : 50 , role : "user" , content : [ { type : "text" , text : "Initial request" } ] } ,
754+ {
755+ ts : 200 , // Race: added AFTER clineMessage at ts=100
756+ role : "assistant" ,
757+ content : [ { type : "tool_use" , id : "tool_1" , name : "attempt_completion" , input : { } } ] ,
758+ } ,
759+ {
760+ ts : 300 ,
761+ role : "user" ,
762+ content : [ { type : "tool_result" , tool_use_id : "tool_1" , content : "tell me a joke 3" } ] ,
763+ } ,
764+ ]
765+
766+ // Delete the user_feedback clineMessage at ts=100
767+ await manager . rewindToTimestamp ( 100 )
768+
769+ // The fix ensures we find the first API user message at or after cutoff (ts=300)
770+ // and use that as the actual cutoff, preserving the assistant message (ts=200)
771+ const apiCall = mockTask . overwriteApiConversationHistory . mock . calls [ 0 ] [ 0 ]
772+ expect ( apiCall ) . toHaveLength ( 2 )
773+ expect ( apiCall [ 0 ] . ts ) . toBe ( 50 ) // Initial user message preserved
774+ expect ( apiCall [ 1 ] . ts ) . toBe ( 200 ) // Assistant message preserved (was incorrectly removed before fix)
775+ expect ( apiCall [ 1 ] . role ) . toBe ( "assistant" )
776+ } )
777+
778+ it ( "should handle normal case where timestamps are properly ordered" , async ( ) => {
779+ // Normal case: clineMessage timestamp aligns with API message timestamp
780+ mockTask . clineMessages = [
781+ { ts : 100 , say : "user" , text : "First" } ,
782+ { ts : 200 , say : "user_feedback" , text : "Feedback" } ,
783+ ]
784+
785+ mockTask . apiConversationHistory = [
786+ { ts : 100 , role : "user" , content : [ { type : "text" , text : "First" } ] } ,
787+ { ts : 150 , role : "assistant" , content : [ { type : "text" , text : "Response" } ] } ,
788+ { ts : 200 , role : "user" , content : [ { type : "text" , text : "Feedback" } ] } ,
789+ ]
790+
791+ await manager . rewindToTimestamp ( 200 )
792+
793+ // Should keep messages before the user message at ts=200
794+ const apiCall = mockTask . overwriteApiConversationHistory . mock . calls [ 0 ] [ 0 ]
795+ expect ( apiCall ) . toHaveLength ( 2 )
796+ expect ( apiCall [ 0 ] . ts ) . toBe ( 100 )
797+ expect ( apiCall [ 1 ] . ts ) . toBe ( 150 )
798+ } )
799+
800+ it ( "should fall back to original cutoff when no user message found at or after cutoff" , async ( ) => {
801+ // Edge case: no API user message at or after the cutoff timestamp
802+ mockTask . clineMessages = [
803+ { ts : 100 , say : "user" , text : "First" } ,
804+ { ts : 200 , say : "assistant" , text : "Response" } ,
805+ { ts : 300 , say : "assistant" , text : "Another response" } ,
806+ ]
807+
808+ mockTask . apiConversationHistory = [
809+ { ts : 100 , role : "user" , content : [ { type : "text" , text : "First" } ] } ,
810+ { ts : 150 , role : "assistant" , content : [ { type : "text" , text : "Response" } ] } ,
811+ { ts : 250 , role : "assistant" , content : [ { type : "text" , text : "Another response" } ] } ,
812+ // No user message at or after ts=200
813+ ]
814+
815+ await manager . rewindToTimestamp ( 200 )
816+
817+ // Falls back to original behavior: keep messages with ts < 200
818+ // This removes the assistant message at ts=250
819+ const apiCall = mockTask . overwriteApiConversationHistory . mock . calls [ 0 ] [ 0 ]
820+ expect ( apiCall ) . toHaveLength ( 2 )
821+ expect ( apiCall [ 0 ] . ts ) . toBe ( 100 )
822+ expect ( apiCall [ 1 ] . ts ) . toBe ( 150 )
823+ } )
824+
825+ it ( "should handle multiple assistant messages before the user message in race condition" , async ( ) => {
826+ // Complex race scenario: multiple assistant messages added before user message
827+ mockTask . clineMessages = [
828+ { ts : 50 , say : "user" , text : "Initial" } ,
829+ { ts : 100 , say : "user_feedback" , text : "Feedback" } , // Race condition
830+ ]
831+
832+ mockTask . apiConversationHistory = [
833+ { ts : 50 , role : "user" , content : [ { type : "text" , text : "Initial" } ] } ,
834+ { ts : 150 , role : "assistant" , content : [ { type : "text" , text : "First assistant msg" } ] } , // After clineMessage
835+ { ts : 200 , role : "assistant" , content : [ { type : "text" , text : "Second assistant msg" } ] } , // After clineMessage
836+ { ts : 250 , role : "user" , content : [ { type : "text" , text : "Feedback" } ] } ,
837+ ]
838+
839+ await manager . rewindToTimestamp ( 100 )
840+
841+ // Should preserve both assistant messages (ts=150, ts=200) because the first
842+ // user message at or after cutoff is at ts=250
843+ const apiCall = mockTask . overwriteApiConversationHistory . mock . calls [ 0 ] [ 0 ]
844+ expect ( apiCall ) . toHaveLength ( 3 )
845+ expect ( apiCall [ 0 ] . ts ) . toBe ( 50 )
846+ expect ( apiCall [ 1 ] . ts ) . toBe ( 150 )
847+ expect ( apiCall [ 1 ] . role ) . toBe ( "assistant" )
848+ expect ( apiCall [ 2 ] . ts ) . toBe ( 200 )
849+ expect ( apiCall [ 2 ] . role ) . toBe ( "assistant" )
850+ } )
851+ } )
731852} )
0 commit comments