Skip to content

feat(mattermost): add ambient session ingestion mode - #26663

Closed
And1rew132 wants to merge 9 commits into
NousResearch:mainfrom
And1rew132:feature/mattermost-ambient-session-ingestion
Closed

And1rew132 wants to merge 9 commits into
NousResearch:mainfrom
And1rew132:feature/mattermost-ambient-session-ingestion

Conversation

@And1rew132

Copy link
Copy Markdown

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_CHANNELS bypasses mention gating and immediately invokes handle_message() for every message. In multi-agent and large-team environments this causes:

  • Excessive token usage
  • Response loops between agents
  • Noisy behavior in busy channels
  • No way to build passive/ambient memory

Solution

Introduce two new env vars:

MATTERMOST_AMBIENT_CHANNELS=channel_id_1,channel_id_2
MATTERMOST_SILENT_SESSION_CHANNELS=channel_id_1,channel_id_2  # alias

Behavior matrix:

Channel type Message Result
Normal no mention ignored
Normal @mention LLM runs
Free-response any LLM runs immediately
Ambient no mention stored silently, no LLM
Ambient @mention LLM runs with accumulated context

Implementation

gateway/platforms/base.py

  • Add trigger_llm: bool = True field to MessageEvent
  • BasePlatformAdapter.handle_message() short-circuits when trigger_llm=False:
    • Calls session_store.get_or_create_session() + append_to_transcript()
    • Returns without invoking _message_handler or spawning background tasks

gateway/platforms/mattermost.py

  • Parse MATTERMOST_AMBIENT_CHANNELS / MATTERMOST_SILENT_SESSION_CHANNELS (also readable from config.yaml via ambient_channels key)
  • In _handle_ws_event(): detect ambient channels, set trigger_llm=False when no @mention present, trigger_llm=True on @mention
  • Pass trigger_llm to MessageEvent constructor

Use cases

  • Multi-agent orchestration (prevent response loops)
  • Passive memory accumulation
  • Background context awareness
  • Enterprise chat environments
  • Mattermost-based operational workflows

potatosalad and others added 7 commits May 6, 2026 09:17
… 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.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery labels May 16, 2026
…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.
@And1rew132

Copy link
Copy Markdown
Author

Superseded by #26901 — moved to Coding-Reality/hermes-agent fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants