Profiled the prod alloc with pprof. Three things stood out.
SLA evaluation was refetching about 234k pending rows every 10 minutes and the
count only ever grew. A policy can leave the resolution time blank, which
stores a NULL resolution deadline. The evaluator needs a deadline to compare
against, so it could never set met_at or breached_at, and the row stayed
pending forever. The pending query now skips a metric with no deadline, and a
new set based query closes any pending row whose configured metrics have all
settled. It replaces the old per row status update, so there is one status
writer and the backlog drains itself on the first tick. No migration is needed
because nothing outside the sla package reads that status.
Static assets were copied into fasthttp's pooled per connection response
buffer on every request. That buffer keeps its grown size, so serving the
frontend bundle held about 165MB. SetBodyRaw points at the bytes instead of
copying them.
API key auth ran a full bcrypt compare on every request, which was 62 percent
of cpu samples. API secrets are 64 char random tokens, so bcrypt's work factor
buys nothing there. They are now hashed with sha256 and compared in constant
time. Old bcrypt hashes still verify and get upgraded in place on first use,
so no key needs reissuing. Password login still uses bcrypt.
This also fixes an unrelated bug the work turned up. apply-sla deleted pending
rows by status alone, so re-applying an SLA threw away a first response breach
that was already recorded. It now deletes only rows with nothing recorded and
closes the rest to their terminal status first.
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.
Address CodeRabbit review on PR #426.
Log the error when deleting a contact's avatar file fails so a
leftover avatar (personal data) does not go unnoticed.
Add availability_status, last_active_at, last_login_at, and enabled
to the contact data export since these are personal data under GDPR
right to access.
First part of GDPR support (#244): the right to erasure and the right
to access.
- DELETE /api/v1/contacts/{id} permanently deletes a contact or
visitor. Conversations, messages, notes, and participants go with it
via DB cascades. The avatar file is removed too.
- GET /api/v1/contacts/{id}/export downloads a JSON file with
everything stored about the contact: profile, custom attributes, and
all conversations with their messages. Private notes stay internal.
- Both actions are gated by new permissions (contacts:delete,
contacts:export), granted to Admin in migration v2.6.0, and recorded
in the activity log with actor and IP.
- Contact page gets Export data and Delete contact buttons, with a
confirm dialog for delete.
- The unlinked media cleaner now also removes attachment files whose
message no longer exists. Before this, deleting a conversation left
its attachments on disk forever.
Replies from the AI agent are now converted from markdown to HTML with
goldmark before queueing, so bold, links, and lists render properly in the
widget, agent app, and email. The prompt now allows simple markdown. Raw
HTML in model output is escaped by goldmark, and both frontends sanitize
on render anyway.
Other fixes bundled in:
- validate avatar type and size before creating or updating an assistant,
and roll back the assistant if the avatar upload fails after create
- return 404 from agent update and API key endpoints for AI assistant
identity users, and hide assistants from mention and SLA user pickers
- reserve the autonomous assistant's built-in tool names so custom tools
cannot shadow them
- apply resolve after the reply is posted so the CSAT survey follows the
answer instead of preceding it
- unassign the assistant on handoff even when the fallback team is the
same team
- count reopens by status category instead of status name, and exclude
CSAT messages from the turn cap
- split oversized wrapper divs into child blocks when chunking KB HTML
instead of truncating them
- return 404 when soft-deleting an already-deleted agent, and keep AI
assistants (which have no email) visible in the compact users list
Medium (title + short body):
add WIP autonomous AI agent
New internal/aiagent package runs AI assistants that reply to customers
on conversations assigned to them, grounded on a knowledge base. Also mines
resolved conversations for FAQ suggestions. Adds admin UI and the v2.7.0
schema. Still work in progress.
CreateContact treated any SetExternalUserID failure as "ext_id taken" and
fell through to the upsert, so a transient DB error created a second
contact with the same email. Now only a unique violation or a contact
deleted mid-flight falls through, other errors are returned.
SetExternalUserID also reports whether a row was actually updated, and
dbutil error checks use errors.As so wrapped errors match. Widget JWTs
with no email now store NULL instead of an empty string.
Re-derived for the v2.x contact model. insert-contact-with-external-id
already updates names on conflict; this adds the same to
insert-contact-without-external-id, preserving existing non-empty names
via COALESCE(NULLIF(EXCLUDED.first_name, ''), users.first_name).
Without this, names from the email worker and Gravity Forms never update
existing contacts.
[Used Claude Code 🤖]
Reject disabled agents in FilterAuthorizedListUUIDs, kick role members on
permission removal or role delete, and re-sub the list and open conv on
ws reconnect using local state. Also plug the app logger into the ws
package so kicks no longer log when no connections exist.
Convs and messages now push the full list-row payload to perm-authorized agents on create/assign/message, so livechat returning customers bubble up instantly without waiting for the timer refresh. Background list refresh bumped 30s -> 60s as a drift safety net.
Targets broadcasts via per-tab list and open subs (validated at subscribe time)
instead of broadcasting message uuid to all agents. Adds frontend list typing, throttled sound,
and many perf improvements to reduce cpu memory & database queries.
Other fixes: Enforce message-to-conversation binding in handleGetMessage
Increase max page size to 500
Allow large out of bound page sizes by overriding to maximum set.
Reset websocket reconnect attempts when widget is back online
- Continuity email layout fixes and improvments.
- Replace haikunator with email local part for visitor names, fallback to "Visitor"
- Add optional website_url config field for "go to chat" link in continuity email footer
- Remove unused / unncessary i18n keys.