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.
This commit is contained in:
Pranav
2026-02-10 17:27:42 -08:00
committed by GitHub
parent 0ad47d87f4
commit 8f95fafff4
3 changed files with 62 additions and 11 deletions

View File

@@ -40,6 +40,7 @@ class Account < ApplicationRecord
'auto_resolve_ignore_waiting': { 'type': %w[boolean null] }, 'auto_resolve_ignore_waiting': { 'type': %w[boolean null] },
'audio_transcriptions': { 'type': %w[boolean null] }, 'audio_transcriptions': { 'type': %w[boolean null] },
'auto_resolve_label': { 'type': %w[string null] }, 'auto_resolve_label': { 'type': %w[string null] },
'keep_pending_on_bot_failure': { 'type': %w[boolean null] },
'conversation_required_attributes': { 'conversation_required_attributes': {
'type': %w[array null], 'type': %w[array null],
'items': { 'type': 'string' } 'items': { 'type': 'string' }
@@ -88,6 +89,7 @@ class Account < ApplicationRecord
store_accessor :settings, :audio_transcriptions, :auto_resolve_label store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :settings, :captain_models, :captain_features store_accessor :settings, :captain_models, :captain_features
store_accessor :settings, :keep_pending_on_bot_failure
has_many :account_users, dependent: :destroy_async has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async has_many :agent_bot_inboxes, dependent: :destroy_async

View File

@@ -36,16 +36,21 @@ class Webhooks::Trigger
case @webhook_type case @webhook_type
when :agent_bot_webhook when :agent_bot_webhook
conversation = message.conversation update_conversation_status(message)
return unless conversation&.pending?
conversation.open!
create_agent_bot_error_activity(conversation)
when :api_inbox_webhook when :api_inbox_webhook
update_message_status(error) update_message_status(error)
end end
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) def create_agent_bot_error_activity(conversation)
content = I18n.t('conversations.activity.agent_bot.error_moved_to_open') content = I18n.t('conversations.activity.agent_bot.error_moved_to_open')
Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(conversation, content)) Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(conversation, content))

View File

@@ -74,10 +74,11 @@ describe Webhooks::Trigger do
context 'when webhook type is agent bot' do context 'when webhook type is agent bot' do
let(:webhook_type) { :agent_bot_webhook } 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 it 'reopens conversation and enqueues activity message if pending' do
conversation.update(status: :pending) payload = { event: 'message_created', id: pending_message.id }
payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute) expect(RestClient::Request).to receive(:execute)
.with( .with(
@@ -92,11 +93,11 @@ describe Webhooks::Trigger do
perform_enqueued_jobs do perform_enqueued_jobs do
trigger.execute(url, payload, webhook_type) trigger.execute(url, payload, webhook_type)
end 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.message_type).to eq('activity')
expect(activity_message.content).to eq(agent_bot_error_content) expect(activity_message.content).to eq(agent_bot_error_content)
end end
@@ -118,9 +119,52 @@ describe Webhooks::Trigger do
end.not_to(change { message.reload.status }) end.not_to(change { message.reload.status })
expect(Conversations::ActivityMessageJob).not_to have_been_enqueued expect(Conversations::ActivityMessageJob).not_to have_been_enqueued
expect(conversation.reload.status).to eq('open') expect(conversation.reload.status).to eq('open')
end 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
end end