From 412b72db7c629d1eb3ca431314ad34d9b0552091 Mon Sep 17 00:00:00 2001 From: Alexander Udovichenko Date: Fri, 13 Mar 2026 12:30:17 +0300 Subject: [PATCH 1/4] fix: Delete double hmac check (#12464) ## Description When hmac identity check is enabled according to [this](https://www.chatwoot.com/hc/user-guide/articles/1677587479-how-to-enable-identity-validation-in-chatwoot) I found out, that it checked twice. If `should_verify_hmac? -> true` then hmac checked in `before_action` and we don't need to do it again later. This perfomance related and PR fixes this. --- app/controllers/api/v1/widget/contacts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index 5138fe675..6c595ab59 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController contact = @contact end - @contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac? + @contact_inbox.update(hmac_verified: true) if should_verify_hmac? identify_contact(contact) end From a90ffe6264bd2bc614a8a8949ffb05ae8596fe00 Mon Sep 17 00:00:00 2001 From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:34:58 +0530 Subject: [PATCH 2/4] feat: Add force legacy auto-resolve flag (#13804) # Pull Request Template ## Description Add account setting and store_accessor for `captain_force_legacy_auto_resolve`. Enterprise job now skips LLM evaluation when this flag is true and falls back to legacy time-based resolution. Add spec to cover the fallback. ## Type of change We recently rolled out Captain deciding if a conversation is resolved or not. While it is an improvement for majority of customers, some still prefer the old way of auto-resolving based on inactivity. This PR adds a check. ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. legacy_auto_resolve = true CleanShot 2026-03-13 at 19 55 55@2x legacy_auto_resolve = false CleanShot 2026-03-13 at 20 00 50@2x ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --- app/models/account.rb | 5 ++- .../concerns/account_captain_auto_resolve.rb | 21 ++++++++++ ...ox_pending_conversations_resolution_job.rb | 8 +++- .../conversations_resolution_scheduler_job.rb | 2 +- .../tools/resolve_conversation_tool.rb | 2 +- ...nding_conversations_resolution_job_spec.rb | 22 ++++++++++- ...ersations_resolution_scheduler_job_spec.rb | 20 +++++++++- .../tools/resolve_conversation_tool_spec.rb | 13 ++++++- spec/models/account_spec.rb | 38 +++++++++++++++++++ 9 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 app/models/concerns/account_captain_auto_resolve.rb diff --git a/app/models/account.rb b/app/models/account.rb index eabaa5c26..087226710 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -41,7 +41,7 @@ class Account < ApplicationRecord 'audio_transcriptions': { 'type': %w[boolean null] }, 'auto_resolve_label': { 'type': %w[string null] }, 'keep_pending_on_bot_failure': { 'type': %w[boolean null] }, - 'captain_disable_auto_resolve': { 'type': %w[boolean null] }, + 'captain_auto_resolve_mode': { 'type': %w[string null], 'enum': ['evaluated', 'legacy', 'disabled', nil] }, 'conversation_required_attributes': { 'type': %w[array null], 'items': { 'type': 'string' } @@ -91,7 +91,8 @@ 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 - store_accessor :settings, :captain_disable_auto_resolve + store_accessor :settings, :captain_auto_resolve_mode + include AccountCaptainAutoResolve has_many :account_users, dependent: :destroy_async has_many :agent_bot_inboxes, dependent: :destroy_async diff --git a/app/models/concerns/account_captain_auto_resolve.rb b/app/models/concerns/account_captain_auto_resolve.rb new file mode 100644 index 000000000..5d17e92f9 --- /dev/null +++ b/app/models/concerns/account_captain_auto_resolve.rb @@ -0,0 +1,21 @@ +module AccountCaptainAutoResolve + extend ActiveSupport::Concern + + VALID_CAPTAIN_AUTO_RESOLVE_MODES = %w[evaluated legacy disabled].freeze + + included do + VALID_CAPTAIN_AUTO_RESOLVE_MODES.each do |mode| + define_method("captain_auto_resolve_#{mode}?") do + captain_auto_resolve_mode == mode + end + end + end + + def captain_auto_resolve_mode + mode = settings&.[]('captain_auto_resolve_mode') + return mode if VALID_CAPTAIN_AUTO_RESOLVE_MODES.include?(mode) + return 'disabled' if settings&.[]('captain_disable_auto_resolve') == true + + feature_enabled?('captain_tasks') ? 'evaluated' : 'legacy' + end +end diff --git a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb index 538c03d22..0be179f04 100644 --- a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb +++ b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb @@ -5,9 +5,9 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob queue_as :low def perform(inbox) - return if inbox.account.captain_disable_auto_resolve + return if inbox.account.captain_auto_resolve_disabled? - if inbox.account.feature_enabled?('captain_tasks') + if evaluate_conversation_completion?(inbox.account) perform_with_evaluation(inbox) else perform_time_based(inbox) @@ -18,6 +18,10 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob private + def evaluate_conversation_completion?(account) + account.feature_enabled?('captain_tasks') && account.captain_auto_resolve_evaluated? + end + def perform_time_based(inbox) Current.executed_by = inbox.captain_assistant diff --git a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb index 8b6527c93..7ccebf72f 100644 --- a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb +++ b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb @@ -12,7 +12,7 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob inbox = captain_inbox.inbox next if inbox.email? - next if inbox.account.captain_disable_auto_resolve + next if inbox.account.captain_auto_resolve_disabled? Captain::InboxPendingConversationsResolutionJob.perform_later( inbox diff --git a/enterprise/lib/captain/tools/resolve_conversation_tool.rb b/enterprise/lib/captain/tools/resolve_conversation_tool.rb index 2b71098c9..eeffc3f06 100644 --- a/enterprise/lib/captain/tools/resolve_conversation_tool.rb +++ b/enterprise/lib/captain/tools/resolve_conversation_tool.rb @@ -6,7 +6,7 @@ class Captain::Tools::ResolveConversationTool < Captain::Tools::BasePublicTool conversation = find_conversation(tool_context.state) return 'Conversation not found' unless conversation return "Conversation ##{conversation.display_id} is already resolved" if conversation.resolved? - return 'Auto-resolve is disabled for this account' if conversation.account.captain_disable_auto_resolve + return 'Auto-resolve is disabled for this account' if conversation.account.captain_auto_resolve_disabled? log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason }) diff --git a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb index 63b8c1bca..f432aae62 100644 --- a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb +++ b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb @@ -84,6 +84,16 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do expect(resolvable_pending_conversation.reload.status).to eq('pending') expect(resolvable_pending_conversation.messages.outgoing).to be_empty end + + it 'falls back to legacy time-based resolve when legacy auto-resolve is forced' do + inbox.account.update!(captain_auto_resolve_mode: 'legacy') + allow(Captain::ConversationCompletionService).to receive(:new) + + described_class.perform_now(inbox) + + expect(Captain::ConversationCompletionService).not_to have_received(:new) + expect(resolvable_pending_conversation.reload.status).to eq('resolved') + end end context 'when LLM evaluation returns complete' do @@ -322,7 +332,7 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do end it 'does not resolve conversations when auto-resolve is disabled at execution time' do - inbox.account.update!(captain_disable_auto_resolve: true) + inbox.account.update!(captain_auto_resolve_mode: 'disabled') expect do described_class.perform_now(inbox) @@ -331,4 +341,14 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do expect(resolvable_pending_conversation.reload.status).to eq('pending') expect(resolvable_pending_conversation.messages.outgoing).to be_empty end + + it 'falls back to disabled mode from legacy settings key' do + inbox.account.update!(settings: inbox.account.settings.merge('captain_disable_auto_resolve' => true)) + + expect do + described_class.perform_now(inbox) + end.not_to(change { resolvable_pending_conversation.reload.status }) + + expect(resolvable_pending_conversation.reload.status).to eq('pending') + end end diff --git a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb index 343100a50..1988d346c 100644 --- a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb +++ b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb @@ -30,12 +30,28 @@ RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do end end - context 'when account has captain_disable_auto_resolve enabled' do + context 'when account has captain auto resolve disabled' do let!(:regular_inbox) { create(:inbox, account: account) } before do create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox) - account.update!(captain_disable_auto_resolve: true) + account.update!(captain_auto_resolve_mode: 'disabled') + end + + it 'does not enqueue resolution jobs' do + expect do + described_class.perform_now + end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob) + .with(regular_inbox) + end + end + + context 'when account uses legacy disabled settings key' do + let!(:regular_inbox) { create(:inbox, account: account) } + + before do + create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox) + account.update!(settings: account.settings.merge('captain_disable_auto_resolve' => true)) end it 'does not enqueue resolution jobs' do diff --git a/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb b/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb index f676b2974..2eae2905a 100644 --- a/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb +++ b/spec/enterprise/lib/captain/tools/resolve_conversation_tool_spec.rb @@ -41,7 +41,18 @@ RSpec.describe Captain::Tools::ResolveConversationTool do end describe 'when auto-resolve is disabled for the account' do - before { account.update!(captain_disable_auto_resolve: true) } + before { account.update!(captain_auto_resolve_mode: 'disabled') } + + it 'does not resolve and returns a disabled message' do + result = tool.perform(tool_context, reason: 'Possible spam') + + expect(result).to eq('Auto-resolve is disabled for this account') + expect(conversation.reload).not_to be_resolved + end + end + + describe 'when auto-resolve is disabled via legacy settings key' do + before { account.update!(settings: account.settings.merge('captain_disable_auto_resolve' => true)) } it 'does not resolve and returns a disabled message' do result = tool.perform(tool_context, reason: 'Possible spam') diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 5ccff6517..166b4c34b 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -198,6 +198,44 @@ RSpec.describe Account do expect(account.settings['auto_resolve_message']).to eq(message) end + it 'defaults captain_auto_resolve_mode to legacy when captain_tasks is disabled' do + allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(false) + + expect(account.captain_auto_resolve_mode).to eq('legacy') + expect(account).to be_captain_auto_resolve_legacy + end + + it 'defaults captain_auto_resolve_mode to evaluated when captain_tasks is enabled' do + allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(true) + + expect(account.captain_auto_resolve_mode).to eq('evaluated') + expect(account).to be_captain_auto_resolve_evaluated + end + + it 'correctly gets and sets captain_auto_resolve_mode' do + account.captain_auto_resolve_mode = 'legacy' + + expect(account.captain_auto_resolve_mode).to eq('legacy') + expect(account.settings['captain_auto_resolve_mode']).to eq('legacy') + expect(account).to be_captain_auto_resolve_legacy + end + + it 'allows clearing captain_auto_resolve_mode to fall back to feature defaults' do + allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(false) + account.captain_auto_resolve_mode = nil + + expect(account).to be_valid + expect(account.captain_auto_resolve_mode).to eq('legacy') + expect(account.settings['captain_auto_resolve_mode']).to be_nil + end + + it 'falls back to disabled mode from legacy settings key' do + account.settings = { 'captain_disable_auto_resolve' => true } + + expect(account.captain_auto_resolve_mode).to eq('disabled') + expect(account).to be_captain_auto_resolve_disabled + end + it 'handles nil values correctly' do account.auto_resolve_after = nil account.auto_resolve_message = nil From 73a90f284119330511ed87f38e63c12b42d85985 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 16 Mar 2026 11:04:27 +0530 Subject: [PATCH 3/4] feat: update bunny video support in HC (#13815) Bunny Video has added a new URL player.mediadelivery.net, this PR adds support for the new URL --- .../EmptyState/Portal/PortalEmptyState.vue | 1 + config/markdown_embeds.yml | 4 ++-- spec/config/markdown_embeds_spec.rb | 6 +++++- spec/lib/custom_markdown_renderer_spec.rb | 16 ++++++++++++++-- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/javascript/dashboard/components-next/HelpCenter/EmptyState/Portal/PortalEmptyState.vue b/app/javascript/dashboard/components-next/HelpCenter/EmptyState/Portal/PortalEmptyState.vue index b7a986254..192e1d5ee 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/EmptyState/Portal/PortalEmptyState.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/EmptyState/Portal/PortalEmptyState.vue @@ -26,6 +26,7 @@ const onPortalCreate = ({ slug: portalSlug, locale }) => {