From fb6673664ac3f8799135ed626ed38ed67d041b07 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sun, 19 Apr 2026 01:01:28 -0300 Subject: [PATCH] fix(captain-memory): isolate per-account failures in SilenceDetectorJob + fix typo --- .../contact_memories/silence_detector_job.rb | 19 +++++++++++---- .../silence_detector_job_spec.rb | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb b/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb index 98617f988..d9c13f060 100644 --- a/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb +++ b/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb @@ -5,15 +5,26 @@ class Captain::ContactMemories::SilenceDetectorJob < ApplicationJob def perform Account.where("custom_attributes->>'captain_contact_memory_extraction_enabled' = 'true'").find_each do |account| - elegible_conversation_ids(account).each do |conv_id| - Captain::ContactMemories::ExtractFromConversationJob.perform_later(conv_id) - end + process_account(account) + rescue StandardError => e + Rails.logger.error("[SilenceDetectorJob] account=#{account.id} failed: #{e.class}: #{e.message}") + ChatwootExceptionTracker.new(e, account: account).capture_exception end end private - def elegible_conversation_ids(account) + def process_account(account) + eligible_conversation_ids(account).each do |conv_id| + Captain::ContactMemories::ExtractFromConversationJob.perform_later(conv_id) + end + end + + # Intentionally no `.where('messages.created_at < ?', SILENCE_THRESHOLD.ago)` pre-filter here: + # that would let a conversation with ANY old message + a recent one be incorrectly + # classified as silent. The HAVING MAX(...) clause alone is the correct semantics. + # If this full-join becomes a perf issue at scale, rewrite as NOT EXISTS subquery. + def eligible_conversation_ids(account) Conversation .where(account_id: account.id) .joins(:messages) diff --git a/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb b/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb index 436d360a1..e34c62b20 100644 --- a/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb +++ b/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb @@ -49,4 +49,27 @@ RSpec.describe Captain::ContactMemories::SilenceDetectorJob do expect { described_class.perform_now } .not_to have_enqueued_job(Captain::ContactMemories::ExtractFromConversationJob) end + + it 'continues processing other accounts when one account raises' do + bad_account = create(:account, custom_attributes: { 'captain_contact_memory_extraction_enabled' => true }) + bad_contact = create(:contact, account: bad_account) + create(:conversation, account: bad_account, contact: bad_contact) + + good_account = create(:account, custom_attributes: { 'captain_contact_memory_extraction_enabled' => true }) + good_contact = create(:contact, account: good_account) + good_conv = create(:conversation, account: good_account, contact: good_contact) + create(:message, conversation: good_conv, account: good_account) + age_all_messages(good_conv, 35.minutes.ago) + + original = described_class.instance_method(:eligible_conversation_ids) + allow_any_instance_of(described_class).to receive(:eligible_conversation_ids) do |instance, acc| # rubocop:disable RSpec/AnyInstance + raise StandardError, 'boom' if acc.id == bad_account.id + + original.bind_call(instance, acc) + end + allow(ChatwootExceptionTracker).to receive(:new).and_return(instance_double(ChatwootExceptionTracker, capture_exception: true)) + + expect { described_class.perform_now } + .to have_enqueued_job(Captain::ContactMemories::ExtractFromConversationJob).with(good_conv.id) + end end