fix(mattermost): converge the Agents bot against the store the plugin reads

Agents 2.5.1 migrates config->'bots' into its own agents_useragents table on
startup and logs "Migrated legacy config bots to self-service agents table".
The wiring still treated the legacy location as the source of truth, which
broke in three ways.

set-ai-service.sql.j2 re-inserted the bot into config->'bots' unconditionally,
so both stores held a bot named `agent`. EnsureBots aborts on that with
`duplicate bot name: agent`, leaves its in-memory bot list empty, and
handleMessages then returns without logging: no reply, no gateway request,
no error. Only write the legacy entry while no live agent row owns the name.

set-ai-agent.sql.j2 retargets the migrated row, because once
legacy_config_bots_migrated is set, writing config->'bots' is a permanent
no-op and the role could never change its model or service again.

select/ai/service.sql.j2 asserts against agents_useragents and additionally
reports stale when a legacy entry collides with a live agent row; the old
check reported converged over a plugin whose bot list had failed to load.

ai_gateway.yml fixes two orderings: the retarget now runs before the plugin
cycle, because the plugin reads its agents into memory at startup, and the
cycle is unconditional, because a converged-looking database was preventing
the very reload that repairs a drifted plugin.

Verified live: legacy bots 0, no duplicate bot name, the bot posting real
replies, and test-litellm-agent.js passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-28 12:10:16 +02:00
1 parent 8f6e869791
commit 43f6ff62e6
4 files changed
+40 -21

No files matched your search

@@ -31,16 +31,6 @@
delegate_to: "{{ STACK_HOST }}"
run_once: true
- name: "🔎 Mattermost | Count the Agents bot accounts the plugin has provisioned"
database_query:
config: "{{ lookup('database', application_id) }}"
query: "{{ lookup('template', role_path ~ '/templates/sql/select/ai/bot-user.sql.j2') }}" # nocheck: database-query-raw the bot username is templated into the predicate
expect_rows: true
register: mattermost_ai_bot_state
changed_when: false
delegate_to: "{{ STACK_HOST }}"
run_once: true
- name: "🤖 Mattermost | Point the Agents bot at the platform gateway"
when: (mattermost_ai_state.rows[0][0] | trim) != 'converged'
database_query:
@@ -51,11 +41,17 @@
delegate_to: "{{ STACK_HOST }}"
run_once: true
- name: "🤖 Mattermost | Retarget the migrated agent row at the gateway"
database_query:
config: "{{ lookup('database', application_id) }}"
query: "{{ lookup('template', role_path ~ '/templates/sql/set-ai-agent.sql.j2') }}" # nocheck: database-query-raw the desired bot is rendered from role vars
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: true
delegate_to: "{{ STACK_HOST }}"
run_once: true
- name: "🔁 Mattermost | Cycle the Agents plugin so it adopts the merged config"
ansible.builtin.include_tasks: "{{ role_path }}/tasks/utils/agents/reload.yml"
when: >-
(mattermost_ai_state.rows[0][0] | trim) != 'converged'
or (mattermost_ai_bot_state.rows[0][0] | int) == 0
vars:
mattermost_agents_cid: "{{ MATTERMOST_AI_LOCAL_CID }}"
mattermost_agents_node: "{{ MATTERMOST_AI_HOST_NODE }}"
@@ -6,13 +6,22 @@ SELECT CASE
ELSE '[]'::jsonb END
) AS s
WHERE s ->> 'id' = d.service ->> 'id') = d.service
AND (SELECT b
FROM jsonb_array_elements(
CASE WHEN jsonb_typeof(a.config::jsonb -> 'bots') = 'array'
THEN a.config::jsonb -> 'bots'
ELSE '[]'::jsonb END
) AS b
WHERE b ->> 'id' = d.bot ->> 'id') = d.bot
AND EXISTS (SELECT 1
FROM agents_useragents u
WHERE u.username = d.bot ->> 'name'
AND u.serviceid = d.bot ->> 'serviceID'
AND u.model = d.bot ->> 'model'
AND u.deleteat = 0
AND u.botuserid <> '')
AND NOT EXISTS (SELECT 1
FROM jsonb_array_elements(
CASE WHEN jsonb_typeof(a.config::jsonb -> 'bots') = 'array'
THEN a.config::jsonb -> 'bots'
ELSE '[]'::jsonb END
) AS legacy
JOIN agents_useragents u2
ON u2.username = legacy ->> 'name'
AND u2.deleteat = 0)
THEN 'converged'
ELSE 'stale'
END
@@ -0,0 +1,9 @@
UPDATE agents_useragents u
SET serviceid = d.bot ->> 'serviceID',
model = d.bot ->> 'model',
updateat = (extract(epoch from now()) * 1000)::bigint
FROM (SELECT $infinito${{ MATTERMOST_LITELLM_BOT | to_json }}$infinito$::jsonb AS bot) AS d
WHERE u.username = d.bot ->> 'name'
AND u.deleteat = 0
AND (u.serviceid IS DISTINCT FROM d.bot ->> 'serviceID'
OR u.model IS DISTINCT FROM d.bot ->> 'model');
@@ -23,7 +23,12 @@ UPDATE agents_confighistory a
ELSE '[]'::jsonb END
) AS b
WHERE b ->> 'id' <> d.bot ->> 'id'
), '[]'::jsonb) || jsonb_build_array(d.bot)
), '[]'::jsonb) || CASE WHEN EXISTS (SELECT 1
FROM agents_useragents u
WHERE u.username = d.bot ->> 'name'
AND u.deleteat = 0)
THEN '[]'::jsonb
ELSE jsonb_build_array(d.bot) END
)
|| jsonb_build_object('defaultBotName', d.bot ->> 'name')
)::text