Providing the lines for different transactions are never interleaved you can do it using an aggregate filter and a class variable. Note that you must set --pipeline.workers 1 to use an aggregate. Your use case matches example 1 in the aggregate documentation, except you are missing the taskid on some lines. So use a class variable to add it.
if [message] =~ /TRANSACTION START/ {
grok { match => { "message" => "TRANSACTION START \[%{NUMBER:taskid}\]" } }
ruby { code => '@@taskid = event.get("taskid") ' }
} else {
ruby { code => 'event.set("taskid", @@taskid)' }
}
Then do the aggregate using task_id => "%{taskid}"