Enabling a delay on a rule with delay-unsafe conditions called onEventChange, which
resets conditions AND actions to defaults, silently replacing the user's configured
actions with assign_agent. Reset only the conditions (getDefaultConditions) and leave
the actions intact.
- process-job specs make rows due before running (the sweep only enqueues due rows).
- claim! guard: a row pushed into the future isn't claimed.
- reschedule_paused resets overdue rows; re-enabling the account flag enqueues the
resume job.
- factory derives a current episode key so rows are episode_current.
- claimable? now requires the row to still be due, so a reply-chase reschedule that
pushes due_at forward after the sweep enqueued the row no longer fires early.
- Re-enabling delayed_automations on an account reschedules its overdue pending rows
(past DUE_WINDOW) via a job enqueued ahead of the next sweep, so a pause longer than
the expiry window resumes those rows instead of expiring them.
Event-narrowing wrongly hid valid delayed triggers like conversation_created
(new conversation idle 10m -> assign), forcing the event to conversation_updated.
The real guardrail is the Status/Inbox condition narrowing, which stays; the event
list no longer needs restricting since every event supports a delayed rule with
the right conditions.
inbox_id never changes after a conversation is created, so filtering a delayed
conversation-level rule by inbox is safe: the status episode key still tracks the
only mutable dimension, and inbox rides along as a static fire-time re-check. Allow
status + inbox_id in the validation and the narrowed condition dropdown.
When a delay is on, the condition attribute dropdown for conversation-level events
offers only Status, and attribute_changed is dropped from the operator list for any
delayed rule. Toggling the delay on resets conditions the delayed rule can't use.
This makes unsupported delayed rules unconstructable, so the inline restriction
warning (and its i18n strings) are removed.
When a delay is on, the condition attribute dropdown for conversation-level events
offers only Status, and attribute_changed is dropped from the operator list for any
delayed rule. Toggling the delay on resets conditions the delayed rule can't use.
This makes unsupported delayed rules unconstructable, so the inline restriction
warning (and its i18n strings) are removed.
Delayed execution is now a toggle chosen first (above Event). Turning it on limits
the Event dropdown to the events where a delayed rule is meaningful (Conversation
Updated for status waits, Message Created for awaiting-agent/reply-chase) and snaps
an unsupported event to a supported one, so users can't build a delayed rule on an
event the engine can't handle.
Now that a flag-off account's due rows stay pending (paused), order(:due_at).limit
would keep re-selecting that backlog every sweep, starving enabled accounts with
later due_at. Filter the sweep to accounts with delayed_automations enabled via a
for_enabled_accounts scope, so paused rows sit out of the limit until re-enabled.
Marking a due row skipped/flag_disabled was terminal, so re-enabling the account
flag could never resume it (sweepable only sees pending/processing) — the delayed
action was lost, contradicting the banner that says rules resume when re-enabled.
Check the flag before claiming and return, leaving the row pending like the
instance kill switch does, so it fires once the feature is turned back on.
The edit dialog's delay radios/value were synced once in open() from the automation
model, whose prop only settles a tick later — so the first edit click showed the
previous rule's delay (or immediate). Thread the clicked rule's execution_delay
directly into open() so the delay controls hydrate correctly on the first click.
The guard was dead code: should_index? is only true when searchkick is loaded
(both gated on advanced_search_allowed?), so #reindex is always defined when the
callback fires. It only masked a flaky develop-owned message_spec and contradicts
the fail-loudly-on-impossible-state guideline. Also trim verbose comments in
automation_rule.rb.
Same float64 imprecision as the awaiting-agent key: a status episode armed from an
in-memory status_changed_at (Time.current, nanosecond) could recompute to a slightly
different float once the worker reloads the DB-rounded value, skipping the row as
episode_moved. Use the shared microsecond_stamp helper so both sides agree.
searchkick (which defines #reindex) is only mixed into Message at class-load time
when advanced_search_allowed? (enterprise + OPENSEARCH_URL) is true at boot. A spec
that stubs advanced_search_allowed? true without a loaded index makes should_index?
true and fires the reindex callback, raising NoMethodError. Guard on respond_to?
so it no-ops when no index is available (real deployments without search index
already have should_index? false, so behavior is unchanged).
- Match the microsecond-integer (strftime %s%6N) awaiting-agent key format,
fixing the CI failure from float epoch imprecision.
- Assert exactly one ProcessPendingExecutionJob is enqueued and the future row
is not, so an early-fire regression is caught.
- Count the reply-chase follow-up as exactly one so a double-send regression fails.
- Stub the ActionService instance's perform (not new) to raise, exercising the
job's error/retry path rather than only constructor failures.
Float epoch seconds carry ~16 significant digits, exceeding float64 precision,
and the arm path compares an in-memory created_at against the DB-stored
waiting_since. A float rounds differently on each side, so the armed key never
matched at fire time and the awaiting-agent automation was always skipped.
Use strftime('%s%6N') (integer microseconds) on both paths.
clone duplicates the rule via dup (including execution_delay) with no param for
ensure_execution_delay_allowed to inspect, so it could create new delayed rules
while the feature is disabled. Drop the delay on the clone unless the account has
delayed_automations enabled, matching create/update.
- 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.
The earlier waiting_since fallback in episode_key_for also matched at fire time,
so an agent reply (which clears waiting_since) no longer ended the episode and the
rule could fire anyway. Apply the created_at fallback only at arm time
(arm_episode_key_for); keep episode_key_for strict so a nil waiting_since at fire
time still means the agent replied and the episode is over.
- episode_key_for falls back to the incoming message's created_at when
waiting_since is still nil (it is written after MESSAGE_CREATED dispatches),
so awaiting-agent episodes no longer arm as awaiting_agent:0 and get skipped
as episode_moved on the first customer message after an agent reply.
- Editing a rule's trigger, conditions, or actions (not just the delay) now
discards its armed pending rows so they can't fire against a definition they
were never armed under; deleting rather than skipping frees the episode slot
so the new definition re-arms on the next matching event.
- The rule form only offers a delay when the config supports it: the delayed
option is disabled with an explanation for attribute_changed conditions and
for non-status conditions on conversation events, and the backend's specific
error is surfaced on save instead of a generic message.