fix(automations): sub-second awaiting-agent keys and discard stale processing rows on edit
- awaiting_agent episode keys use sub-second (.to_f) precision like status keys, so a reply then re-wait within the same second is a distinct episode and the original armed row no longer matches the later waiting period. - discard_stale_pending_executions now deletes armed rows (pending and stale processing), since the sweep reclaims stale processing rows and would otherwise fire them against the edited rule definition.
This commit is contained in:
@@ -133,7 +133,8 @@ class AutomationRule < ApplicationRecord
|
||||
end
|
||||
|
||||
def discard_stale_pending_executions
|
||||
pending_executions.pending.delete_all
|
||||
# Includes stale processing rows: the sweep would otherwise reclaim and fire them.
|
||||
pending_executions.armed.delete_all
|
||||
end
|
||||
|
||||
def validate_single_condition(condition)
|
||||
|
||||
@@ -42,6 +42,9 @@ class AutomationRulePendingExecution < ApplicationRecord
|
||||
pending.where(due_at: ..Time.current).or(processing.where(updated_at: ...STALE_PROCESSING_TIMEOUT.ago))
|
||||
}
|
||||
|
||||
# Non-terminal rows still bound to fire (a stale processing row is reclaimed by the sweep).
|
||||
scope :armed, -> { where(status: [statuses[:pending], statuses[:processing]]) }
|
||||
|
||||
def self.schedule(rule:, conversation:, message: nil)
|
||||
key = arm_episode_key_for(conversation, message)
|
||||
create!(
|
||||
@@ -63,7 +66,7 @@ class AutomationRulePendingExecution < ApplicationRecord
|
||||
def self.arm_episode_key_for(conversation, message)
|
||||
return episode_key_for(conversation, message) unless message&.incoming? && conversation.waiting_since.blank?
|
||||
|
||||
"awaiting_agent:#{message.created_at.to_i}"
|
||||
"awaiting_agent:#{message.created_at.to_f}"
|
||||
end
|
||||
|
||||
# Episode keys identify one qualifying stretch of conversation state; when the recomputed
|
||||
@@ -73,9 +76,10 @@ class AutomationRulePendingExecution < ApplicationRecord
|
||||
# Sub-second precision so a resolve→reopen inside one second still ends the episode.
|
||||
"status:#{(conversation.status_changed_at.presence || conversation.created_at).to_f}"
|
||||
elsif message.incoming?
|
||||
# waiting_since is cleared on agent/bot reply, so a reply invalidates this episode.
|
||||
# Strict here: at fire time a nil waiting_since means the agent replied (episode ended).
|
||||
"awaiting_agent:#{conversation.waiting_since.to_i}"
|
||||
# waiting_since is cleared on agent/bot reply, so a reply invalidates this episode. Strict
|
||||
# here: at fire time a nil waiting_since means the agent replied (episode ended). Sub-second
|
||||
# precision so a reply→re-wait inside one second is still a distinct episode.
|
||||
"awaiting_agent:#{conversation.waiting_since.to_f}"
|
||||
else
|
||||
# A new customer message changes the max incoming id, invalidating this episode.
|
||||
"reply_chase:#{conversation.messages.incoming.maximum(:id) || 0}"
|
||||
|
||||
Reference in New Issue
Block a user