# Design: Matty — forkless' annoying Mattermost sidekick > **Status:** v0.1.18 — Implemented and deployed > **Last updated:** 2026-06-17 > **Bot username:** @matty > **Default model:** dolphin-llama3:latest (configurable in System Console) --- ## 1. Purpose & Scope ### 1.1 What It Is Matty is a Mattermost (Community Edition) plugin that lets users chat with remote LLMs through an Ollama backend. Users address the bot by **@-mentioning** it in any channel (e.g. `@matty write a poem`). The bot responds **only when addressed** and disengages naturally — it never reads every message in the room. Once addressed, the user enters an **active session** where follow-up messages continue the conversation without needing to re-@mention. Sessions end on stop words ("thanks", "bye"), explicit `@matty stop`, or 5 minutes of inactivity. A slash command (`/ai`) remains available as an alternative entry point. ### 1.2 What It Is Not (Explicit Out-of-Scope) | Out of scope | Rationale | |---|---| | Hosting its own LLM inference | Ollama is the sole backend; the plugin is a thin proxy | | Multi-backend abstraction (OpenAI, Anthropic, etc.) | If needed later, abstract behind an `LLMProvider` interface — not now | | Training / fine-tuning | Ollama handles that | | **Channel whisper mode** (replying to every message) | The agent only activates on @-mention or active session. It never reads unprompted messages. | | Bot account management | Plugin creates the bot account automatically via `API.CreateBot()` | | Webapp UI | No frontend — @-mention + `/ai` covers all interaction | ### 1.3 Target Audience - Teams self-hosting Mattermost who want AI assistant access without data leaving their infrastructure - Users comfortable with @-mentioning a bot the same way they @-mention a human teammate --- ## 2. Architecture Overview ### 2.1 Component Diagram (text) ``` ┌──────────────────────────────────────────────────────────────┐ │ Mattermost Server (Community Edition) │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Matty Plugin │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌────────────────────────┐ │ │ │ │ │ MessageHasBeen │──▶│ Plugin (Go) │ │ │ │ │ │ Posted (primary) │ │ │ │ │ │ │ └──────────────────┘ │ ┌──────────────────┐ │ │ │ │ │ ┌──────────────────┐ │ │ OllamaClient │──┼───┼───┼──▶ Ollama │ │ │ SlashCommand │──▶│ └──────────────────┘ │ │ │ (remote) │ │ │ /ai (alt.) │ │ ┌──────────────────┐ │ │ │ │ │ └──────────────────┘ │ │ ConversationStore│ │ │ │ │ │ ┌──────────────────┐ │ └──────────────────┘ │ │ │ │ │ │ EngagementEngine │ │ ┌──────────────────┐ │ │ │ │ │ │ sleep / active │──▶│ │ RateLimiter │ │ │ │ │ │ └──────────────────┘ │ └──────────────────┘ │ │ │ │ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │ │ │ │ PersonaLibrary │ │ │ Config (KV) │ │ │ │ │ │ │ resolvePersona() │ │ └──────────────────┘ │ │ │ │ │ └──────────────────┘ │ │ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ │ │ Bot Account │ │ │ │ │ │ │ │ (auto-created) │ │ │ │ │ │ │ └──────────────────┘ │ │ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ │ │ Webapp (none) │ │ │ │ │ │ │ └──────────────────┘ │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ KV Store (per-plugin) │ │ │ │ • ctx_{user}_{channel} — conversation history │ │ │ │ • persona_{user}_{ch} — persona override │ │ │ └──────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ ``` ### 2.2 Request Flow (happy path — @-mention) ``` User types "@matty write a release note for v2.1" │ ▼ MessageHasBeenPosted fires │ ▼ EngagementEngine.Wake(userId, channelId) → mark session Active │ ▼ @-mention detected → strip "@matty " prefix → "write a release note for v2.1" │ ▼ RateLimiter.Allow(userId) → 200 │ ▼ Check stop words → no match │ ▼ Check persona override (KV store) → resolvePersona() → full description │ ▼ ConversationStore.BuildMessages(userId, channelId, prompt) → up to 10 turns │ ▼ Trim context based on Brevity (e.g. brevity=1 → keep only current message) │ ▼ Prepend system prompt: "You are {persona}. Respond in character. ... Reply in {N} sentences." │ ▼ Calculate token cap: maxTokens = auto from brevity, or manual override │ ▼ OllamaClient.ChatCompletionStream(model, messages, options) │ ▼ Ollama returns ndjson stream → accumulate all tokens │ ▼ Create single post with full response (not a thread reply, not live-updated) ``` ### 2.3 Engagement Session Flow (follow-up without @-mention) ``` User: "@matty hello" → bot responds, session starts User: "tell me a joke" (no @) → IsActive() → true → bot responds User: "what about weather?" (no @) → IsActive() → true → bot responds User: "thanks" → stopWordMatch → Sleep() → bot signs off User: "hello again" (no @, 10 min later)→ IsActive() → false → ignored User: "@matty hello again" → Wake() → bot responds, new session ``` ### 2.4 Error Flow | Failure point | Behaviour | |---|---| | Ollama unreachable | Post: "Matty is asleep. @matty when you need me." | | Rate limit hit | Post: "Rate limit exceeded. Please wait before sending another request." | | Invalid persona | Ephemeral: "Unknown persona. Available: ..." | | Timeout (>30 s) | Post truncated with "(Matty is asleep...)" | --- ## 3. Plugin Manifest & Identity ### 3.1 `plugin.json` Current settings (v0.1.18): | Key | Type | Default | Purpose | |---|---|---|---| | `OllamaURL` | text | `http://localhost:11434` | Ollama server endpoint | | `DefaultModel` | dropdown | `dolphin-llama3:latest` | Model for chat | | `Persona` | dropdown | `assistant` | Character for the bot | | `RepeatPenalty` | number | `11` (1.1) | Penalise repeated tokens | | `FrequencyPenalty` | number | `1` (0.1) | Penalise frequent phrases | | `Brevity` | dropdown | `3` | Max sentences | | `DebugLogging` | bool | `false` | Enable verbose logging | | `CustomPersona` | text | `""` | Override dropdown persona | | `MaxTokens` | number | `0` | Manual token cap (0 = auto from Brevity) | | `RateLimitPerMinute` | number | `0` | Requests/min/user (0 = unlimited) | | `AllowedUserIDs` | text | `""` | Restrict to specific users | | `StopWords` | text | `thanks, bye, goodbye, done, stop, quit, end` | Disengagement triggers | ### 3.2 Versioning Follow `release-workflow` skill: `v0.1.x` for small iterations. Bump PATCH for bug fixes, MINOR for features. No MAJOR bump until stable release. --- ## 4. Server-Side Components ### 4.1 Plugin Lifecycle Hooks | Hook | Purpose | |---|---| | `OnActivate()` | Register `/ai` command, create bot account via `ensureBot()`, initialise `OllamaClient`, `ConversationStore`, `RateLimiter`, `EngagementEngine` | | `OnConfigurationChange()` | Re-read System Console settings, validate OllamaURL, sync debug logging flag to OllamaClient | | `MessageHasBeenPosted()` | **Primary entry point** — detect @-mentions and active sessions, delegate to EngagementEngine | | `ExecuteCommand()` | **Alternative entry point** — intercept `/ai` and subcommands (`/ai persona`, `/ai new`, `/ai help`) | | `Implemented()` | Declares handled hooks: `OnActivate`, `OnDeactivate`, `OnConfigurationChange`, `ExecuteCommand`, `MessageHasBeenPosted` | ### 4.2 Interaction Model #### 4.2.1 @-mention (primary) User types `@matty ` in any channel. The plugin: 1. Detects the @-mention via `MessageHasBeenPosted` → string match on `@matty` 2. Strips the bot username from the prompt 3. Checks stop words, rate limit, authorization 4. Looks up persona override from KV store (set via `/ai persona `) 5. Builds conversation context from `ConversationStore` (up to 10 turns) 6. Trims context based on Brevity setting (e.g. brevity 1 = only current message) 7. Prepends persona system prompt with general instructions + brevity 8. Calculates token cap (auto from Brevity or manual MaxTokens override) 9. Wakes the `EngagementEngine` for this user+channel 10. Sends to Ollama via `ChatCompletionStream` 11. Accumulates all tokens, creates a single channel post with the full response **When the bot does NOT respond:** - Messages without `@matty` while the EngagementEngine is Sleeping - Messages during the Disengaging → Sleeping transition - Messages from other bot accounts (infinite-loop guard) #### 4.2.2 Engagement Session (continuation) Once a user is in an Active session (via @-mention or `/ai`), their subsequent non-@-mention messages in the same channel are treated as continuations: ``` User enters Active → subsequent messages auto-continue → stop word → Sleep ``` The session expires after 5 minutes of inactivity. #### 4.2.3 Slash command (alternative) `/ai ` works as a fallback. Behaviour is identical. **Full syntax:** ``` /ai — Chat with default model /ai persona — Show current active persona /ai persona list — List all available personas with descriptions /ai persona — Switch persona (persisted) /ai new — Reset conversation context + end session /ai help — Show help ``` ### 4.3 OllamaClient Located in `server/ollama.go` (package main, flat file). Key methods: | Method | Purpose | |---|---| | `ChatCompletion(req)` | Non-streaming call (not used by main flow) | | `ChatCompletionStream(req)` | **Primary** — returns `< -chan StreamEvent`, reads ndjson | | `ListModels()` | Returns available model names | | `Ping()` | Health check | The client accepts a `logFunc` callback for debug logging (hooked to `p.API.LogInfo` when `DebugLogging` is enabled). ### 4.4 ConversationStore Stored in KV store per `ctx_{userID}_{channelID}`. Contains the last 10 messages (5 user + 5 assistant). Context is trimmed further based on Brevity setting before sending to Ollama. | Brevity | Messages kept | |---|---| | 1 | 1 (current only) | | 2 | 2 (last exchange) | | 3 | 4 (last 2 exchanges) | | 5 | 6 | | 10 | 10 | ### 4.5 RateLimiter Token-bucket per user. Configurable from System Console. Default: 0 (unlimited). Resets on config change. Stored in memory. ### 4.6 EngagementEngine Tracks active conversations per user+channel. Two states: | State | Behaviour | Entered when | |---|---|---| | **Sleeping** | Ignores all messages | Plugin start, stop word, `/ai new`, 5 min timeout | | **Active** | Responds to @-mentions and follow-ups | @-mention or `/ai` | The timeout is 5 minutes of inactivity. On expiry, the session is silently removed — next @-mention starts fresh. ### 4.7 Persona System Located in `server/persona.go`. A library of 19 personas mapped by stable short IDs: ``` assistant a helpful assistant. Be concise, 2-3 sentences max... canadian an overly polite Canadian who apologises excessively... carlin George Carlin. Acerbic, observational, profane comedy... dnd a Dungeons & Dragons dungeon master... infomercial a 1980s TV infomercial host. Over-enthusiastic, loud... jester a medieval court jester who mocks everyone... mime a French mime. Respond with dramatic pauses... motivational a motivational speaker who turns everything into a life lesson... noir a noir detective who answers every question with another... philosopher a philosopher who overcomplicates everything... pirate a pirate captain. Use pirate slang... pirro a drunk Jeanine Pirro. Slur your words... ramsay Gordon Ramsay but for code reviews. Aggressive chef... scammer a tech support scammer from 2008. Alarmist, pushy... shakespeare William Shakespeare himself. Respond in iambic pentameter... surfer a surfer dude explaining complex topics badly... sysadmin a grumpy sysadmin who thinks every question is stupid... tiredparent a tired parent who's given up on life... trump Donald Trump. Boastful, hyperbolic, third-person... ``` Priority: **KV override** (`/ai persona `) → **CustomPersona** (System Console text field) → **Persona** dropdown. `resolvePersona(id)` looks up the short ID in the library. Returns empty for unknown IDs (so `/ai persona shakespear` is properly rejected). ### 4.8 Debug Logging When `DebugLogging` is enabled in System Console, the plugin logs: | Log message | Source | What it contains | |---|---|---| | `"Config loaded"` | `configuration.go` | All settings on save | | `"Chat request"` | `hooks.go` | 13 fields: model, url, persona, brevity, tokens, repeat, freq, rate limit, allowed users, stop words, full system prompt | | `"Ollama API call"` | `ollama.go` | Full request body sent to Ollama (model, messages, options) | --- ## 5. Ollama Integration ### 5.1 API Mapping | Plugin Action | Ollama Endpoint | Method | Stream | |---|---|---|---| | Chat completion | `/api/chat` | POST | Yes | | List models | `/api/tags` | GET | No | | Health check | `/api/tags` | GET | No | ### 5.2 Streaming Strategy 1. Send POST to `/api/chat` with `"stream": true`. 2. Read ndjson response body line by line. 3. Accumulate all tokens in a buffer (no live post updates — avoids "Edited" spam). 4. On `done:true`, create a single channel post with the full content. ### 5.3 Token Auto-Calculation Brevity auto-calculates the token cap. Formula: `100 + brevity × 75`. | Brevity | Auto tokens | Override with | |---|---|---| | 1 | 175 | MaxTokens > 175 | | 2 | 250 | MaxTokens > 250 | | 3 | 325 | MaxTokens > 325 | | 5 | 475 | MaxTokens > 475 | | 10 | 850 | MaxTokens > 850 | | Unlimited (0) | Falls back to MaxTokens (0 = Ollama default) | MaxTokens > 0 | ### 5.4 Penalties RepeatPenalty and FrequencyPenalty use whole numbers divided by 10: | UI value | Sent to Ollama | |---|---| | 10 | 1.0 (none) | | 11 | 1.1 (mild) | | 12 | 1.2 (strong) | | UI value | Sent to Ollama | |---|---| | 0 | 0.0 (none) | | 1 | 0.1 (mild) | | 5 | 0.5 (strong) | --- ## 6. Configuration All configuration is done via **System Console → Plugins → Matty**. The 12 settings are described in §3.1. `OnConfigurationChange()` reloads all settings when saved. Plugins do not have a `SavePluginConfiguration` API in Mattermost v11.x, so `/ai persona` uses the KV store to persist overrides instead. --- ## 7. Data Flow — Detailed ### 7.1 MessageHasBeenPosted (full lifecycle) ``` Step User / System Component ──── ────────────────────────────── ────────────────── 1 User types "@matty explain TCP" 2 MessageHasBeenPosted fires 3 Check: not bot's own post Loop guard 4 Check: not from OAuth bot Loop guard 5 Check: has @matty? yes Mention detection 6 Check: EngagementEngine.IsActive? EngagementEngine 7 Strip @matty → "explain TCP" 8 Check stop words StopWordSet 9 Check rate limit RateLimiter 10 Check authorization AllowedUserIDs 11 EngagementEngine.Wake() EngagementEngine 12 GetPersonaOverride → resolvePersona KV Store / personaLibrary 13 BuildMessages → conversation history ConversationStore 14 Trim context by Brevity hooks.go 15 Build system prompt + brevity hooks.go 16 Calculate token cap hooks.go 17 Build Ollama options (penalties) hooks.go 18 Log Chat request (if debug) hooks.go 19 ChatCompletionStream() OllamaClient 20 Accumulate tokens hooks.go 21 CreatePost with full response postReply() ``` --- ## 8. Security Model ### 8.1 Input Sanitization - Stop words checked before sending to Ollama (saves round-trip) - Rate limiting per user (configurable, default: unlimited) ### 8.2 Network Security - `OllamaURL` validated as http/https at config load - TLS enabled by default ### 8.3 Authorization - `AllowedUserIDs` restricts by Mattermost user ID - Empty = all authenticated users - Bot account created by plugin, managed automatically ### 8.4 Infinite-Loop Guard The plugin never processes messages from: - Its own bot account (`post.UserId == botUserID`) - Other OAuth bots (`post.IsFromOAuthBot()`) --- ## 9. Observability ### 9.1 Logging When Debug Logging is enabled: | Level | When | |---|---| | `INFO` | Chat request (all 13 settings), Config loaded, Ollama API call body | | `ERROR` | Ollama unreachable, config validation failure, KV store error | | `WARN` | Rate limit hit (with debug on) | ### 9.2 Error Reporting - Ollama unreachable → "Matty is asleep. @matty when you need me." - Rate limit → "Rate limit exceeded..." - Unknown persona → "Unknown persona. Available: ..." - Stream error → appended as "(Matty is asleep...)" in the response --- ## 10. Resolved & Open Design Decisions ### Resolved | # | Question | Decision | |---|---|---| | D1 | Reply style | **Direct channel posts** (not ephemeral, not threads) | | D4 | Webapp UI | **None** — @-mention + `/ai` + `/ai persona` covers everything | | D6 | Thread replies | **EngagementEngine** — active sessions track follow-ups without threads | | D11 | Engagement scope | **Per-user+channel** — separate sessions per channel | | D12 | Thread timeout | **5 minutes** (not configurable yet) | ### Still Open | # | Question | Proposed | |---|---|---| | D2 | Context scope | Per-channel per-user (implemented) | | D3 | Context turns | Currently 10 messages; Brevity trims further | | D5 | Model governance | All models available via System Console dropdown | | D7 | `num_ctx` per request | Not implemented; Brevity controls token cap | | D8 | Auto-pull models | No (returns error with list) | | D9 | Prompt injection guard | Deferred — document risk | | D10 | Metrics endpoint | Deferred | --- ## 11. Deployment & Lifecycle ### 11.1 Build ```bash make dist ``` Produces `dist/com.forkless.mattermore-{version}.tar.gz` containing the plugin manifest and platform binaries. ### 11.2 Upload Via Mattermost System Console → Plugins → Upload Plugin, or via API. ### 11.3 Upgrade Upload new `.tar.gz` — Mattermost replaces the plugin binary. Settings are preserved via KV store. Bot account is re-fetched (not re-created). ### 11.4 Local Testing ```bash docker compose -f dev/docker-compose.yml up -d ``` Spins up Mattermost 11.6.1 + PostgreSQL 16 at http://localhost:8065. Auto-creates admin user (admin/password). Test rig is disposable: `docker compose down -v` wipes everything. --- ## 12. File Structure ``` mattermore/ ├── plugin.json — Plugin manifest ├── Makefile — Build, dist, clean ├── .golangci.yml — Linter config ├── DESIGN.md — This document ├── PROJECT.md ├── CHANGES.md ├── DECISIONS.md ├── README.md ├── dev/ │ ├── docker-compose.yml — Local Mattermost + PostgreSQL │ └── init.sh — Admin auto-creation script ├── server/ │ ├── main.go — plugin.ClientMain │ ├── plugin.go — Plugin struct │ ├── hooks.go — OnActivate, MessageHasBeenPosted, streaming │ ├── command.go — /ai and subcommands, persona override │ ├── configuration.go — Config struct, validation, OnConfigurationChange │ ├── ollama.go — OllamaClient (streaming + blocking) │ ├── store.go — ConversationStore (KV-backed) │ ├── rate.go — RateLimiter (token bucket) │ ├── engagement.go — EngagementEngine (sleep/active) │ └── persona.go — PersonaLibrary with 19 personas └── dist/ └── com.forkless.mattermore-*.tar.gz ``` --- ## 13. Future Considerations (v0.2+) - **Multi-modal inputs** — Ollama supports images via `/api/chat` with `images` field. - **Tool calling** — Ollama supports tool definitions for external APIs. - **Conversation export** — Allow users to export AI threads as markdown. - **Streaming into message attachments** — Render structured data (tables, JSON, code blocks) with better formatting.