Two contact resolution fixes in create conversation:
Under the reuse policy, an unknown external_user_id went straight to
insert. If the email belonged to an existing contact, that inserted a
duplicate contact sharing the email, since the unique email index only
covers rows without an ext_id. The ext_id lookup now falls back to the
email match before inserting. Regression test added.
The new conversation form sends name fields as a snapshot from contact
search, so a contacts:write agent submitting the form could overwrite a
contact that was renamed after the search. The form now sends
reuse_contact: true and the backend downgrades to the reuse policy when
it is set. API callers like an external CRM omit the flag and keep the
full sync/upsert behavior. The flag can only downgrade, never escalate.
Also from review: hoist the repeated password generation block into
newContactPassword, drop testutil.NewLogger and the duplicate
newTestI18n in transcript_test.go in favor of testutil.NewI18n, and
clean up stale comments.
The verification tools and OTP prompt block loaded for every unverified
customer, so an assistant with no account-acting tools still asked people
for an email code it had no use for. The worker now checks the assistant's
tools first and skips both unless one is flagged requires_verification. A
lookup error fails open and keeps the flow.
The send tool's result did not say which address it mailed, so the model
filled one in from chat history. After a sibling conversation rebound the
contact's email, it named the old address while the mail went to the new
one. The result now names the address and mentions the spam folder.
The prompt did not say to check every code the customer sends, so after one
rejection the model declared the next code wrong without calling the tool
and handed off to a human.
Automation update events now carry the actor. A suppressed conversation drops only system-user events (the engine's own in-flight actions), so an agent updating the conversation during that window is evaluated instead of silently dropped. Autoassigner events still trigger automations since they arrive outside the suppress window.
Macros with no actions never hit the apply endpoint, so their usage count
stayed at zero. The frontend now calls apply whenever a macro was picked, and
drafts store the macro id so a macro survives switching conversations.
The notify action only created a fixed in-app notification and recipients
were typed as a raw team:<id> / user:<id> DSL where typos silently notified
nobody. Now:
- recipients are picked by name (assignee, assigned team, any team or agent)
- admin writes the subject and message; both show in the bell notification
and go out as an email (message plus conversation link, wrapped in the
default outgoing email template, so nothing is hardcoded in English)
- the action serializes as typed fields {subject, message, recipients}
instead of a positional value array
Also: previous_* condition fields are hidden for time triggers and populated
on message events (previous = current) so those rules can actually match,
action row widths now follow the RuleBox pattern (fixed type select, value
widgets fill the row), and three new i18n keys merged into existing globals.
strconv.Atoi accepts "0" and negative values, and the delivery worker treats a
non-positive WebhookID as a fan out to every subscriber of the event. So a
malformed target value sent the conversation payload to unintended webhooks.
Reject it in the action and drop it in TriggerWebhook as well.
An automation rule that listens on an event and then writes the same field
re-fires its own event, so the rule matches again and loops forever. Status
already returned early on a no-op write, but priority and user assignment did
not, so those two could spin a worker doing a DB write per lap. Add the same
unchanged-value guard to both.
Suppression while the engine applies actions was a plain flag in a sync.Map,
so with more than one worker on the same conversation the first one to finish
deleted the key while the other was still applying actions. Make it a refcount
so each worker releases only its own claim.
The starts with operator is only implemented by the automation evaluator, not
the SQL filter builder, so give automation text fields their own operator list
instead of adding it to the shared one.
Also switch the notify action to its own recipients field type, fix the snooze
duration hint since the backend only takes Go duration units, trim and
lowercase notify recipient entries, and treat a missing previous value as no
match rather than an empty string.