feat(mattermost): add ambient session ingestion mode - #26663
Closed
And1rew132 wants to merge 9 commits into
Closed
And1rew132 wants to merge 9 commits into
And1rew132 wants to merge 9 commits into
Conversation
… thread
Two bugs surfaced when MATTERMOST_REPLY_MODE=thread was enabled:
1. Replies inside an existing thread failed with HTTP 400 from
POST /api/v4/posts:
api.post.create_post.root_id.app_error
"Invalid RootId parameter."
The adapter was passing reply_to (= the user's message id) straight
through as root_id, but Mattermost requires root_id to reference the
thread *root* — a post that has no root_id of its own. When the user
replied inside an existing thread, their post id was a reply, so the
server rejected it. Reproduced with a direct API call: a root_id
pointing at a reply returns 400 with the same error code; pointing at
a top-level post succeeds.
Fix: add a small _resolve_thread_root helper that does one cached
GET /posts/{id} lookup and returns the post's root_id when present,
otherwise the id itself. The three send sites (text, single media,
multi-image) all route reply_to through the helper, so root_id always
points at a real thread root. Missing/deleted posts fall back to the
original id so callers see the same error they would have seen
pre-fix instead of a silent rewrite.
2. The typing indicator showed up in the main channel even when the
user was conversing inside a thread. The dispatcher already passes
metadata={"thread_id": event.source.thread_id} into send_typing, but
the adapter ignored it and only sent {"channel_id": chat_id}.
Fix: when metadata carries a thread_id, forward it as Mattermost's
parent_id field so "hermes is typing…" appears inside the thread
the user just messaged from.
Tests: existing test_send_with_thread_reply updated to mock the new
_api_get lookup (still asserts that a root post is used unchanged), plus
new coverage for resolution-from-reply, lookup caching, missing-post
fallback, and parent_id behaviour in send_typing.
…hread
Even after threading was wired up for the main reply, tool-call previews,
reasoning chunks, background-task notifications, and other intermediate
sends still landed in the main channel.
Cause: the dispatcher fires those sends as
adapter.send(chat_id, content, metadata={"thread_id": ...})
— with no `reply_to` — but the Mattermost adapter was only computing
`root_id` from `reply_to`. With no `reply_to`, no `root_id` got attached,
so the post defaulted to the main channel. Media helpers had a related
bug: `send_image` / `send_image_file` / `send_document` / `send_voice`
/ `send_video` accept `metadata` but were dropping it on the way to the
internal `_send_url_as_file` / `_send_local_file` helpers, and
`send_multiple_images` never set `root_id` at all.
Fix: factor a `_root_id_for_payload(reply_to, metadata)` helper that
prefers an explicit `reply_to` (resolved to the thread root) and falls
back to `metadata['thread_id']` (already a root post id from
`event.source.thread_id`). Use it at every outbound-post site:
- `send` (text)
- `_send_url_as_file` (image URLs)
- `_send_local_file` (local image / file / audio / video)
- `send_multiple_images` (Mattermost-native multi-image posts)
Plumb `metadata` through the public media methods to the internal helpers.
Reply mode "off" continues to suppress threading entirely; the helper
returns `None` in that case.
Tests: covers the metadata-only fallback (root_id from thread_id),
explicit `reply_to` winning over metadata, reply_mode "off" suppressing
both, and `metadata` being forwarded through every public media method to
its internal helper.
Thread mode now treats handled top-level channel posts as the root of the bot's reply thread, so progress messages, streaming chunks, media sends, and follow-up replies share the same thread metadata and Hermes session key. DMs stay unthreaded unless Mattermost provides a real root_id so their stable DM session behavior is preserved. Also avoid caching failed post lookups in _resolve_thread_root, since _api_get returns an empty dict for transient API/network failures as well as missing posts. When reply_to resolution fails, prefer trusted dispatcher thread metadata before falling back to the original reply id. Typing indicators now honor reply_mode too: parent_id is only sent when thread mode is enabled, matching the actual response routing. Tests cover top-level channel thread metadata, DM preservation, lookup failure caching, metadata fallback, and reply_mode-gated typing.
Introduce MATTERMOST_AMBIENT_CHANNELS (alias: MATTERMOST_SILENT_SESSION_CHANNELS) env vars to support passive context accumulation in Mattermost channels without triggering an LLM response on every message. Behavior: - Normal channel: message ignored unless @mentioned - Free-response channel (MATTERMOST_FREE_RESPONSE_CHANNELS): immediate LLM run - Ambient channel: message stored silently in session history; LLM only runs on explicit @mention, slash command, webhook, or other external trigger Implementation: - Add `trigger_llm: bool = True` field to `MessageEvent` in base.py - `BasePlatformAdapter.handle_message()` short-circuits when trigger_llm=False: calls session_store.get_or_create_session() + append_to_transcript() then returns without invoking _message_handler or spawning background tasks - `MattermostAdapter._handle_ws_event()` detects ambient channels, sets trigger_llm=False when no @mention is present, trigger_llm=True on @mention - Config also reads `ambient_channels` key from config.yaml extra block Env vars: MATTERMOST_AMBIENT_CHANNELS comma-separated channel IDs (primary) MATTERMOST_SILENT_SESSION_CHANNELS alias for the above
Ambient messages now stored as '[username]: text' so the agent has full sender context when it is eventually triggered via @mention. sender_id and sender_name are also stored as transcript metadata.
…ity/hermes-agent Triggers on push to feature/mattermost-ambient-session-ingestion. Pushes :latest and :<sha> tags to ghcr.io/coding-reality/hermes-agent. Requires CR_PAT secret on the fork with write:packages access.
Author
|
Superseded by #26901 — moved to Coding-Reality/hermes-agent fork. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add support for ambient session ingestion in the Mattermost connector — a new channel mode where messages are silently stored into Hermes session history without triggering an LLM response.
Problem
The existing
MATTERMOST_FREE_RESPONSE_CHANNELSbypasses mention gating and immediately invokeshandle_message()for every message. In multi-agent and large-team environments this causes:Solution
Introduce two new env vars:
Behavior matrix:
Implementation
gateway/platforms/base.pytrigger_llm: bool = Truefield toMessageEventBasePlatformAdapter.handle_message()short-circuits whentrigger_llm=False:session_store.get_or_create_session()+append_to_transcript()_message_handleror spawning background tasksgateway/platforms/mattermost.pyMATTERMOST_AMBIENT_CHANNELS/MATTERMOST_SILENT_SESSION_CHANNELS(also readable fromconfig.yamlviaambient_channelskey)_handle_ws_event(): detect ambient channels, settrigger_llm=Falsewhen no @mention present,trigger_llm=Trueon @mentiontrigger_llmtoMessageEventconstructorUse cases