Skip to content

feat(mattermost): seed prior thread posts into first-turn context - #64270

Open
wernerhp wants to merge 7 commits into
NousResearch:mainfrom
wernerhp:feat/37695-mattermost-thread-context
Open

feat(mattermost): seed prior thread posts into first-turn context#64270
wernerhp wants to merge 7 commits into
NousResearch:mainfrom
wernerhp:feat/37695-mattermost-thread-context

Conversation

@wernerhp

Copy link
Copy Markdown
Contributor

What does this PR do?

When Hermes is drawn into an existing Mattermost thread for the first time (a reply carrying root_id, in a thread it has no session for yet), it had no context of the conversation it just joined. The receive path in plugins/platforms/mattermost/adapter.py read root_id for session keying and reply routing only; it never called GET /api/v4/posts/{root_id}/thread, so the LLM saw just the triggering message.

Slack already solves this with _fetch_thread_context(); Mattermost was the remaining gap tracked by #37695. This adds first-turn thread-history seeding for the Mattermost adapter, mirroring the Slack approach and reusing existing infrastructure.

Design notes:

  • Seeded history is attached through MessageEvent.channel_context, not prepended into the message text. The gateway (gateway/run.py) applies the triggering sender's prefix to text only, then prepends channel_context ahead of a [New message] marker. Routing history through channel_context keeps each prior post's own [username] attribution instead of letting the trigger sender's prefix swallow the whole block.
  • Seeding is guarded to the first turn only by _has_active_session_for_thread(), which uses build_session_key() as the single source of truth so the group_sessions_per_user / thread_sessions_per_user isolation settings are honoured. Once a session exists the thread history is already carried in it, so no refetch and no duplication.
  • Senders not on the allowlist are tagged [unverified] (via the base _is_sender_authorized() hook) so the model treats their content as background reference rather than authoritative input. This preserves context without dropping it, and the header switches to a security-aware variant that tells the model not to act on unverified content. Without this, injected channel_context would bypass the triggering-author authorization check.
  • The bot's own prior replies (circular context) and slash-command posts are excluded. Command-text normalization already runs before the message-type gate on main, so a leading-space /new is correctly classified as a command and never receives injected context.
  • Policy is exposed via config.yaml mattermost.thread_context (on default / off), bridged to MATTERMOST_THREAD_CONTEXT through the existing _apply_yaml_config hook — no new bespoke .env-only setting.

Related Issue

Fixes #37695

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • ✅ Tests (adding or improving test coverage)
  • 📝 Documentation update

Changes Made

  • plugins/platforms/mattermost/adapter.py: add _fetch_thread_context(), _has_active_session_for_thread(), and _resolve_thread_user_name(); wire first-turn seeding into _handle_ws_event() via MessageEvent.channel_context; add per-root TTL cache and user-name cache; bridge thread_context in _apply_yaml_config().
  • hermes_cli/config.py: register MATTERMOST_THREAD_CONTEXT as a user-facing optional messaging setting.
  • website/docs/user-guide/messaging/mattermost.md: document the thread-context behaviour and the thread_context / MATTERMOST_THREAD_CONTEXT knob.
  • cli-config.yaml.example: add a mattermost: block including thread_context.
  • tests/gateway/test_mattermost.py: 10 tests covering formatting/attribution, bot-and-command exclusion, [unverified] tagging, empty-root and cache behaviour, first-turn seeding via _handle_ws_event, no-seed on root posts, off disabling, and the YAML->env bridge.

How to Test

  1. Set up a Mattermost bot (see website/docs/user-guide/messaging/mattermost.md). In a channel, have two users exchange a few posts in a thread without mentioning the bot.
  2. @mention the bot in a reply to that thread. The bot's first response reflects awareness of the prior posts; posts from non-allowlisted users appear as background tagged [unverified].
  3. Reply again in the same thread — no refetch occurs (history is now in the session).
  4. Automated: pytest tests/gateway/test_mattermost.py -q — 69 passed.
  5. Revert-to-fail proof: git stash push -- plugins/platforms/mattermost/adapter.py then rerun the thread-context tests — 7 behaviour-pinning tests fail (the remaining negative-guard tests trivially hold with the feature absent); git stash pop restores green.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(scope):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this feature (no unrelated commits)
  • I've run pytest tests/gateway/test_mattermost.py -q and all tests pass (69 passed)
  • I've added tests for my changes
  • I've tested on my platform: Ubuntu 24.04

Documentation & Housekeeping

  • I've updated relevant documentation (docs/, docstrings)
  • I've updated cli-config.yaml.example for the new config key
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact — N/A (no OS-specific code paths)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Copilot AI review requested due to automatic review settings July 14, 2026 07:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-turn Mattermost thread-history seeding so when Hermes is mentioned in an existing Mattermost thread for the first time, prior thread posts are fetched (GET /api/v4/posts/{root_id}/thread) and injected via MessageEvent.channel_context to provide the LLM with conversation context.

Changes:

  • Add Mattermost-side thread context fetching, formatting, allowlist-aware [unverified] tagging, and TTL caching, wired into the WS receive path for first-turn-only seeding.
  • Expose a config/ENV toggle (mattermost.thread_contextMATTERMOST_THREAD_CONTEXT) and document the behavior.
  • Add Mattermost gateway tests covering formatting, exclusions, caching, first-turn-only behavior, and toggle disabling.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
plugins/platforms/mattermost/adapter.py Fetch and inject prior thread posts into channel_context, with caching and authorization tagging, plus YAML→env bridging.
tests/gateway/test_mattermost.py Adds tests for thread-context seeding behavior and configuration bridging.
website/docs/user-guide/messaging/mattermost.md Documents the new thread context behavior and its configuration knob.
hermes_cli/config.py Registers MATTERMOST_THREAD_CONTEXT as a user-facing optional messaging setting.
cli-config.yaml.example Adds an example mattermost.thread_context config block.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/platforms/mattermost/adapter.py Outdated
Comment thread plugins/platforms/mattermost/adapter.py Outdated
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels Jul 14, 2026
@wernerhp

wernerhp commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 1c46fae.

YAML 1.1 parses thread_context: off as boolean False, so str(False).lower() produced 'false' and the literal-'off' check left the feature enabled. Added _normalize_onoff() to coerce the falsy family (off/false/0/no/none/empty and Python bool False) to canonical 'on'/'off', applied in the adapter constructor and the _apply_yaml_config bridge.

pytest tests/gateway/test_mattermost.py -q
# 87 passed

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment (prior COMMENT exists)

This PR seeds prior thread posts into first-turn context for Mattermost (5 files, 584 additions).

Observations

  • Has prior COMMENT review.
  • Thread history seeding is valuable for context quality.

Suggestion

Consider storage implications for large threads; ensure truncation/sampling.


Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for carrying the Mattermost thread-context path through channel_context; current main still lacks this behavior (plugins/platforms/mattermost/adapter.py:867-960), so the feature remains useful. The YAML boolean follow-up in 1c46fae addresses the earlier review concern.

Problems

  • plugins/platforms/mattermost/adapter.py:274 fetches posts/{root_id}/thread without applying limit; only after materializing and sorting the whole response does line 286 slice the tail. Line 296 also passes each complete post body into the prompt. This does not provide the bounded fetch or per-post cap described in the discussion.
  • plugins/platforms/mattermost/adapter.py:1452 adds MATTERMOST_STRICT_MENTION, but current main has no Mattermost runtime reader for that variable. Please keep this unrelated dead bridge out of the change.

Suggested changes

  • Bound retrieval before materializing posts, cap per-post and total rendered context, and test both limits.
  • Remove the strict-mention bridge unless it is completed as a separate feature.

Automated hermes-sweeper review.

Comment thread plugins/platforms/mattermost/adapter.py Outdated
Comment thread plugins/platforms/mattermost/adapter.py
Comment thread plugins/platforms/mattermost/adapter.py Outdated
@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 16, 2026
@wernerhp
wernerhp force-pushed the feat/37695-mattermost-thread-context branch from a10f0b5 to 7603dee Compare July 25, 2026 15:12
@wernerhp
wernerhp force-pushed the feat/37695-mattermost-thread-context branch from 47b141c to 2525f04 Compare July 30, 2026 06:04
When Hermes is drawn into an existing Mattermost thread for the first
time (a reply carrying root_id, with no session yet), it had no context
of the conversation it just joined: the receive path never called
GET /api/v4/posts/{root_id}/thread, so the LLM saw only the triggering
message. Slack already has this via _fetch_thread_context(); Mattermost
was the gap tracked by NousResearch#37695.

This adds first-turn thread-history seeding for the Mattermost adapter:

- _fetch_thread_context() pulls prior thread posts and formats them as
  [username]: message, attached through MessageEvent.channel_context so
  the gateway keeps per-author attribution and frames the trigger as
  [New message] (rather than prepending into text, which would let the
  triggering sender's prefix swallow the whole block).
- _has_active_session_for_thread() guards seeding to the first turn only,
  using build_session_key() as the single source of truth so isolation
  settings are honoured; once a session exists the history is already
  carried and is not refetched.
- Senders not on the allowlist are tagged [unverified] via the base
  _is_sender_authorized() hook so their content is treated as background
  reference, not authoritative input, instead of being dropped.
- Command posts and the bot's own prior replies are excluded; a short
  per-root TTL cache avoids refetching on bursts.
- Policy via config.yaml mattermost.thread_context (on/off), bridged to
  MATTERMOST_THREAD_CONTEXT through the existing _apply_yaml_config hook.

Fixes NousResearch#37695
Copilot review (PR NousResearch#64270) caught that YAML 1.1 parses a bare
`thread_context: off` as Python bool False, so `str(False).lower()`
became 'false' — which the literal-'off' check did not treat as
disabled, silently keeping the feature on when a user tried to turn it
off via off/false/0/no.

Add _normalize_onoff() to coerce the whole falsy family (off/false/0/
no/none/empty, plus Python bool False) to canonical 'on'/'off', and use
it in both the adapter constructor and the _apply_yaml_config bridge.
Tests cover the YAML-boolean cases for the helper, the config bridge,
and the adapter constructor.
…ead strict_mention bridge

Addresses review on NousResearch#64270:
- Bound retrieval at the request (perPage + direction=up) so a long
  thread is never fully materialized before slicing; keep the defensive
  in-memory tail slice as a fallback.
- Add explicit prompt-input caps independent of MAX_POST_LENGTH (which
  governs outbound sends only): per-post truncation
  (MAX_THREAD_CONTEXT_POST_CHARS) and a total-context budget
  (MAX_THREAD_CONTEXT_TOTAL_CHARS) that trims oldest posts first.
- Remove the unrelated MATTERMOST_STRICT_MENTION YAML->env bridge; no
  Mattermost runtime reader exists for it on main (Slack-only feature).
- Tests: request-bound assertion, per-post cap, total cap keeping newest,
  and strict_mention non-bridging. 91 passed.
@wernerhp
wernerhp force-pushed the feat/37695-mattermost-thread-context branch from d8e0008 to 9f95811 Compare August 4, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(mattermost): add _fetch_thread_context() to seed prior thread messages into LLM context

5 participants