From 8f95fafff44d2a5393c0ab187541ded95b655f70 Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 10 Feb 2026 17:27:42 -0800 Subject: [PATCH] feat: Add a setting to keep conversations pending on bot failures (#13512) Adds an account-level setting `keep_pending_on_bot_failure` to control whether conversations should move from pending to open when agent bot webhooks fail. Some users experience occasional message drops and don't want conversations to automatically reopen due to transient bot failures. This setting gives accounts control over that behavior. This is a temporary setting which will be removed in future once a proper fix for it is done, so it is not added in the UI. --- app/models/account.rb | 2 ++ lib/webhooks/trigger.rb | 15 ++++++--- spec/lib/webhooks/trigger_spec.rb | 56 +++++++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index fead5f0f7..4816494fb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -40,6 +40,7 @@ class Account < ApplicationRecord 'auto_resolve_ignore_waiting': { 'type': %w[boolean null] }, 'audio_transcriptions': { 'type': %w[boolean null] }, 'auto_resolve_label': { 'type': %w[string null] }, + 'keep_pending_on_bot_failure': { 'type': %w[boolean null] }, 'conversation_required_attributes': { 'type': %w[array null], 'items': { 'type': 'string' } @@ -88,6 +89,7 @@ class Account < ApplicationRecord store_accessor :settings, :audio_transcriptions, :auto_resolve_label store_accessor :settings, :captain_models, :captain_features + store_accessor :settings, :keep_pending_on_bot_failure has_many :account_users, dependent: :destroy_async has_many :agent_bot_inboxes, dependent: :destroy_async diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index 54bd7499d..ef3410b78 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -36,16 +36,21 @@ class Webhooks::Trigger case @webhook_type when :agent_bot_webhook - conversation = message.conversation - return unless conversation&.pending? - - conversation.open! - create_agent_bot_error_activity(conversation) + update_conversation_status(message) when :api_inbox_webhook update_message_status(error) end end + def update_conversation_status(message) + conversation = message.conversation + return unless conversation&.pending? + return if conversation&.account&.keep_pending_on_bot_failure + + conversation.open! + create_agent_bot_error_activity(conversation) + end + def create_agent_bot_error_activity(conversation) content = I18n.t('conversations.activity.agent_bot.error_moved_to_open') Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(conversation, content)) diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb index 78bf361c4..79cf92150 100644 --- a/spec/lib/webhooks/trigger_spec.rb +++ b/spec/lib/webhooks/trigger_spec.rb @@ -74,10 +74,11 @@ describe Webhooks::Trigger do context 'when webhook type is agent bot' do let(:webhook_type) { :agent_bot_webhook } + let!(:pending_conversation) { create(:conversation, inbox: inbox, status: :pending, account: account) } + let!(:pending_message) { create(:message, account: account, inbox: inbox, conversation: pending_conversation) } it 'reopens conversation and enqueues activity message if pending' do - conversation.update(status: :pending) - payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id } + payload = { event: 'message_created', id: pending_message.id } expect(RestClient::Request).to receive(:execute) .with( @@ -92,11 +93,11 @@ describe Webhooks::Trigger do perform_enqueued_jobs do trigger.execute(url, payload, webhook_type) end - end.not_to(change { message.reload.status }) + end.not_to(change { pending_message.reload.status }) - expect(conversation.reload.status).to eq('open') + expect(pending_conversation.reload.status).to eq('open') - activity_message = conversation.reload.messages.order(:created_at).last + activity_message = pending_conversation.reload.messages.order(:created_at).last expect(activity_message.message_type).to eq('activity') expect(activity_message.content).to eq(agent_bot_error_content) end @@ -118,9 +119,52 @@ describe Webhooks::Trigger do end.not_to(change { message.reload.status }) expect(Conversations::ActivityMessageJob).not_to have_been_enqueued - expect(conversation.reload.status).to eq('open') end + + it 'keeps conversation pending when keep_pending_on_bot_failure setting is enabled' do + account.update(keep_pending_on_bot_failure: true) + payload = { event: 'message_created', id: pending_message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: webhook_timeout + ).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once + + trigger.execute(url, payload, webhook_type) + + expect(Conversations::ActivityMessageJob).not_to have_been_enqueued + expect(pending_conversation.reload.status).to eq('pending') + end + + it 'reopens conversation when keep_pending_on_bot_failure setting is disabled' do + account.update(keep_pending_on_bot_failure: false) + payload = { event: 'message_created', id: pending_message.id } + + expect(RestClient::Request).to receive(:execute) + .with( + method: :post, + url: url, + payload: payload.to_json, + headers: { content_type: :json, accept: :json }, + timeout: webhook_timeout + ).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once + expect do + perform_enqueued_jobs do + trigger.execute(url, payload, webhook_type) + end + end.not_to(change { pending_message.reload.status }) + + expect(pending_conversation.reload.status).to eq('open') + + activity_message = pending_conversation.reload.messages.order(:created_at).last + expect(activity_message.message_type).to eq('activity') + expect(activity_message.content).to eq(agent_bot_error_content) + end end end