diff --git a/DECISIONS.md b/DECISIONS.md index 8735d31..a88af43 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -71,4 +71,84 @@ not final until v0.1.0 implementation. --- +--- + +## Why @-mention + Engagement Lifecycle over Slash-Command-Only + +**Context:** How does the user interact with the AI agent? Slash commands +(`/ai`) are explicit but unnatural — users must learn a command syntax. +Bot @-mentions match how users already talk to each other in Mattermost. + +**Alternatives considered:** +1. **Slash command only** (`/ai `) — Explicit, discoverable via + `/ai help`. But every interaction requires the prefix, and thread + continuation is ambiguous. +2. **@-mention only** (`@mattermore `) — Natural, familiar UX. + Users already @-mention teammates. Thread replies continue the + conversation without re-mentioning. Risk of accidental triggers. +3. **Channel whisper (reply to everything)** — Simplest for the user but + noisy and invasive. The user explicitly rejected this. +4. **Hybrid: @-mention primary + /ai fallback** — Both work identically. + +**Outcome:** Hybrid (#4) chosen because: +- @-mention is the primary, most natural interface +- `/ai` is available for users who prefer it or when @-mention is + inconvenient (e.g. automated scripts, mobile) +- Both paths go through the same `EngagementEngine` — identical behaviour +- Thread replies auto-continue without re-mentioning (D6 resolved — core + feature, not deferred) + +**Trade-off:** @-mention requires a bot account (`API.CreateBot()`). This +adds a one-time setup step and a DB row, but the plugin handles creation +and deactivation automatically. The bot account is not a separate service — +it's an identity within the plugin process, so there's no extra deployment +artifact or OAuth management. + +## Why Thread Posts Instead of Ephemeral Messages + +**Context:** Where does the bot's response appear? + +**Alternatives considered:** +1. **Ephemeral post** — Only the requesting user sees it. Clean but + invisible to the rest of the channel/reviewers. +2. **Channel-wide post** — Visible to everyone. Can be noisy if the bot + posts long responses. +3. **Thread post** — Visible to everyone but collapsed into a thread. + Channel stays clean, conversation history is easy to follow. Thread + replies naturally continue the conversation. + +**Outcome:** Thread post (#3) chosen because: +- Threads keep the channel tidy +- Community can see and follow AI interactions +- Thread replies map 1:1 to conversation continuation +- Users can collapse/hide threads they don't care about + +**Trade-off:** Threads require the user to click into them to see the full +response on some clients. Mitigated by showing a preview snippet in the +channel. + +## Why No Webapp UI + +**Context:** Should the plugin ship a webapp component (channel header +button, right-hand side panel, custom settings page)? + +**Alternatives considered:** +1. **No webapp** — Pure server-side plugin. @-mention + `/ai` + thread + replies covers everything. Simplest to build and maintain. +2. **Channel header button** — Button to open an AI chat panel. Adds + discoverability but duplicates what @-mention already does. +3. **Custom settings page** — Duplicates System Console settings in the + main UI. Adds complexity for marginal benefit. + +**Outcome:** No webapp (#1) chosen because: +- @-mention is already discoverable (Mattermost autocompletes) +- Thread replies provide a natural conversation UI +- No frontend to build, bundle, or version +- All configuration stays in System Console where admins expect it + +**Trade-off:** Users must know the bot exists to @-mention it. Mitigated by +mentioning it in channel header, onboarding posts, or `/ai help`. + +--- + *This file follows the template from the `project-docs` skill.* diff --git a/DESIGN.md b/DESIGN.md index 8aec200..ce4a02d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -13,9 +13,11 @@ ### 1.1 What It Is Mattermore is a Mattermost (Community Edition) plugin that lets users chat -with remote LLMs through an Ollama backend. Users invoke the plugin via a -slash command (e.g. `/ai`) and receive responses — either ephemeral or as -thread replies — streamed directly into the channel. +with remote LLMs through an Ollama backend. Users address the bot by +**@-mentioning** it in any channel (e.g. `@mattermore write a poem`). The +bot responds **only when addressed** and disengages naturally — it never +reads every message in the room. A slash command (`/ai`) remains available +as an alternative entry point. ### 1.2 What It Is Not (Explicit Out-of-Scope) @@ -24,14 +26,14 @@ thread replies — streamed directly into the channel. | 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 | -| Native webapp UI beyond slash commands | `TODO(design):` decide if a channel header button or RHS panel adds value | -| Bot accounts | Pure plugin hooks — no separate bot user to manage | +| **Channel whisper mode** — replying to every message in a channel | The agent only activates on @-mention or thread reply. It never reads unprompted messages. | ### 1.3 Target Audience - Teams self-hosting Mattermost who want AI assistant access without data leaving their infrastructure -- Users comfortable with slash commands +- Users comfortable with @-mentioning a bot the same way they @-mention a + human teammate --- @@ -40,56 +42,87 @@ thread replies — streamed directly into the channel. ### 2.1 Component Diagram (text) ``` -┌─────────────────────────────────────────────────────────┐ -│ Mattermost Server (Community Edition) │ -│ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ Mattermore Plugin │ │ -│ │ │ │ -│ │ ┌─────────────┐ ┌─────────────────────────┐ │ │ -│ │ │ SlashCommand │──▶│ Plugin (Go) │ │ │ -│ │ │ /ai │ │ │ │ │ -│ │ └─────────────┘ │ ┌───────────────────┐ │ │ │ -│ │ │ │ OllamaClient │──┼───┼───┼──▶ Ollama API -│ │ ┌─────────────┐ │ └───────────────────┘ │ │ │ (remote) -│ │ │ Config (KV) │ │ ┌───────────────────┐ │ │ │ -│ │ └─────────────┘ │ │ ConversationStore │ │ │ │ -│ │ │ └───────────────────┘ │ │ │ -│ │ ┌─────────────┐ │ ┌───────────────────┐ │ │ │ -│ │ │ Webapp (?) │ │ │ RateLimiter │ │ │ │ -│ │ └─────────────┘ │ └───────────────────┘ │ │ │ -│ └──────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────┐ +│ Mattermost Server (Community Edition) │ +│ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Mattermore Plugin │ │ +│ │ │ │ +│ │ ┌──────────────────┐ ┌────────────────────────┐ │ │ +│ │ │ MessageHasBeen │──▶│ Plugin (Go) │ │ │ +│ │ │ Posted (primary) │ │ │ │ │ +│ │ └──────────────────┘ │ ┌──────────────────┐ │ │ │ +│ │ ┌──────────────────┐ │ │ OllamaClient │──┼───┼───┼──▶ Ollama +│ │ │ SlashCommand │──▶│ └──────────────────┘ │ │ │ (remote) +│ │ │ /ai (alt.) │ │ ┌──────────────────┐ │ │ │ +│ │ └──────────────────┘ │ │ ConversationStore│ │ │ │ +│ │ ┌──────────────────┐ │ └──────────────────┘ │ │ │ +│ │ │ EngagementEngine │ │ ┌──────────────────┐ │ │ │ +│ │ │ wake / sleep │──▶│ │ RateLimiter │ │ │ │ +│ │ └──────────────────┘ │ └──────────────────┘ │ │ │ +│ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │ │ +│ │ │ Bot Account │ │ │ Config (KV) │ │ │ │ +│ │ │ (auto-created) │ │ └──────────────────┘ │ │ │ +│ │ └──────────────────┘ │ │ │ │ +│ │ ┌──────────────────┐ │ │ │ │ +│ │ │ Webapp (none) │ │ │ │ │ +│ │ └──────────────────┘ │ │ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ ``` ### 2.2 Request Flow (happy path) ``` -User types "/ai write a release note for v2.1" +User types "@mattermore write a release note for v2.1" │ ▼ -Mattermost parses slash command → routes to Mattermore plugin +Mattermore MessageHasBeenPosted fires │ ▼ -Plugin.OnConfigurationChange() → reads config (Ollama URL, model, system prompt) +EngagementEngine.Wake(userId, channelId) → mark session Active │ ▼ -Plugin.ExecuteCommand() → parses args, strips command prefix +@-mention detected → strip "@mattermore " prefix from prompt │ ▼ RateLimiter.Allow(userId) → 200 or 429 │ ▼ -ConversationStore.GetSession(userId, channelId) → previous messages for context +ConversationStore.GetSession(userId, channelId, threadId) → previous messages for context │ ▼ OllamaClient.ChatCompletion(messages, stream=true, model=defaultModel) │ ▼ -Ollama returns ndjson stream → plugin writes ephemeral post with live updates +Ollama returns ndjson stream → plugin creates root post with live-updating thread │ ▼ -Post created → user sees response in channel (ephemeral or thread reply) +Post created in channel → user sees response in thread + │ + ▼ +ConversationStore.Append(userId, channelId, threadId, userMsg, assistantMsg) +``` + +#### Thread Reply Flow (continues the conversation) + +``` +User replies in thread → "Can you make it more formal?" + │ + ▼ +Mattermore MessageHasBeenPosted fires (same thread as bot post) + │ + ▼ +EngagementEngine.IsActive(userId, threadId) → true + │ + ▼ +Not an @-mention, but active thread → treat as continuation + │ + ▼ +ConversationStore.GetSession(userId, channelId, threadId) → full context so far + │ + ▼ +OllamaClient.ChatCompletion(...) → stream response into same thread │ ▼ ConversationStore.Append(userId, channelId, userMsg, assistantMsg) @@ -103,7 +136,7 @@ ConversationStore.Append(userId, channelId, userMsg, assistantMsg) | Rate limit hit | Ephemeral post: "Rate limit exceeded. Try again in N seconds." | | Invalid model name | Ephemeral post: "Model 'xyz' not found on Ollama server." | | Timeout (>30 s) | Abort stream, post partial response + "Response truncated (timeout)." | -| Input too long | Reject with context window limit message; suggest `/ai new` to reset. | +| Input too long | Reject with context window limit message; suggest `@mattermore new` to reset. | --- @@ -141,7 +174,7 @@ ConversationStore.Append(userId, channelId, userMsg, assistantMsg) "key": "DefaultModel", "display_name": "Default Model", "type": "text", - "help_text": "Model name to use when none is specified in the command.", + "help_text": "Model name to use when none is specified.", "placeholder": "llama3.2", "default": "llama3.2" }, @@ -173,6 +206,14 @@ ConversationStore.Append(userId, channelId, userMsg, assistantMsg) "type": "text", "help_text": "Restrict to specific user IDs. Empty = all users.", "default": "" + }, + { + "key": "StopWords", + "display_name": "Stop Words (comma-separated)", + "type": "text", + "help_text": "When the user says one of these, the bot responds briefly and disengages.", + "placeholder": "thanks, thank you, bye, goodbye, that's all, done", + "default": "thanks, thank you, bye, goodbye, see you, that's all, that's it, done, all done, stop, quit, end" } ] } @@ -195,14 +236,40 @@ bugs, MINOR for features. MAJOR after 1.0 for breaking config schema changes. | Hook | Purpose | |---|---| -| `OnConfigurationChange()` | Re-read System Console settings, validate OllamaURL, rebuild clients | -| `ExecuteCommand()` | Intercept `/ai` and subcommands (`/ai new`, `/ai model ...`) | -| `OnActivate()` | Register slash command in `OnConfigurationChange()` — idempotent | -| `OnDeactivate()` | Clean up goroutines, close idle HTTP connections | +| `OnActivate()` | Register slash command, create bot account via `API.CreateBot()`, initialise `EngagementEngine` | +| `OnConfigurationChange()` | Re-read System Console settings, validate OllamaURL, rebuild clients, rebuild stop-word trie | +| `MessageHasBeenPosted()` | **Primary entry point** — detect @-mentions and thread continuations, delegate to `EngagementEngine` | +| `ExecuteCommand()` | **Alternative entry point** — intercept `/ai` and subcommands | +| `OnDeactivate()` | Clean up goroutines, deactivate bot account, close idle HTTP connections | -### 4.2 Slash Command: `/ai` +### 4.2 Interaction Model -**Syntax:** +The plugin has two entry points, but **@-mention is the primary UX**. + +#### 4.2.1 @-mention (primary) + +User types `@mattermore ` in any channel. The plugin: + +1. Detects the @-mention via `MessageHasBeenPosted` → `model.Post.Mentions` +2. Strips the bot username from the prompt +3. Wakes the `EngagementEngine` for this user+thread +4. Creates a **thread root post** with the streamed response +5. Monitors thread replies for continuation — as long as the user keeps + replying in the thread, the bot auto-responds (no @-mention needed for + follow-ups in the same thread) + +**When the bot does NOT respond:** +- Messages without `@mattermore` while the EngagementEngine is Sleeping +- Messages in threads the bot didn't start +- Messages from other bot accounts (infinite-loop guard) + +#### 4.2.2 Slash command (alternative) + +`/ai ` works as a fallback for users who prefer it or when +@-mention is inconvenient. Behaviour is identical — creates a thread, +activates the EngagementEngine, continues on thread replies. + +**Full syntax:** ``` /ai @@ -212,15 +279,11 @@ bugs, MINOR for features. MAJOR after 1.0 for breaking config schema changes. /ai new — Reset conversation context for the current user+channel. /ai model list - — List available models from the Ollama server (ephemeral). + — List available models from the Ollama server. /ai help — Show usage help. ``` -`TODO(design):` Should `/ai` reply ephemerally (only the caller sees it) or -as a thread post (visible to the channel)? **Proposed:** ephemeral for -privacy, with an opt-in `--public` flag to post to thread. - ### 4.3 OllamaClient Interface (in `server/ollama/client.go`): @@ -271,6 +334,74 @@ Store session context in Mattermost's KV store (plugin's `API.KVSet` / Token-bucket per user. Configurable requests/minute from System Console. Resets on config change. Stored in memory, not KV (ephemeral state). +### 4.6 Engagement Lifecycle + +The **EngagementEngine** is the core mechanism that prevents the bot from +replying to everything in the channel. It manages whether the bot should +respond to a given message. + +#### States + +| State | Behaviour | Entered when | +|---|---|---| +| **Sleeping** | Ignores all messages | Agent started, disengage triggered, timeout expired | +| **Active** | Responds to @-mentions and thread replies in the active session | User @-mentions bot or sends `/ai` | +| **Disengaging** | Responds to current message with a closing note, then sleeps | Stop word detected or user sends stop command | + +``` + ┌─────────────────┐ + │ Sleeping │ + └────────┬────────┘ + │ @-mention or /ai + ▼ + ┌─────────────────┐ + ┌──────│ Active │◄──── thread reply continues + │ └────────┬────────┘ + │ │ stop word / stop command / timeout + │ ▼ + │ ┌─────────────────┐ + │ │ Disengaging │── responds once, then... + │ └────────┬────────┘ + │ │ + └───────────────┘ +``` + +#### Disengagement Triggers + +| Trigger | Example | Behaviour | +|---|---|---| +| Stop word | "thanks", "thank you", "bye", "goodbye", "that's all", "that's it" | Bot responds with a brief closing message, then sleeps | +| Stop command | `@mattermore stop` | Bot sleeps immediately, no closing message | +| Thread timeout | No reply in thread for 15 minutes | Bot sleeps silently — next @-mention starts a fresh session | +| Model EOS signal | Model outputs `[EOS]` naturally | Same as stop word — clean disengagement from the AI's side | + +#### Stop word matching + +Stop words are configurable via System Console (`StopWords`). The plugin +builds a case-insensitive trie at config load. Matching is done on the +**user's raw prompt** (after stripping the @-mention), before sending to +Ollama — this avoids the latency and cost of a round-trip just to detect +a goodbye. + +The system prompt also instructs the model to append `[EOS]` when it +believes the conversation is naturally complete. The EngagementEngine +watches the response stream for this token as an additional trigger. + +#### Thread timeout + +15 minutes of inactivity in the thread → bot sleeps. The `ConversationStore` +TTL naturally handles cleanup. On wake, a new thread is created rather than +resuming the old one. + +`TODO(design):` Should the timeout be configurable via System Console? +**Proposed:** yes — `EngagementTimeoutMinutes` setting, default 15. + +#### Infinite-loop guard + +The plugin never processes messages from: +- Its own bot account (checked via `post.UserId == botUserID`) +- Any other bot account (`post.UserId` cross-referenced against `model.Bot`) + --- ## 5. Ollama Integration @@ -292,10 +423,10 @@ available models instead. 1. Send POST to `/api/chat` with `"stream": true`. 2. Read ndjson response body line by line (each line is `{"message":{"role":"assistant","content":"..."},"done":false}`). -3. On each `done:false` event, update an ephemeral post via - `API.UpdateEphemeralPost()` with accumulated content. -4. On `done:true`, create the final ephemeral (or thread) post with full - content, usage stats (token count, duration). +3. On each `done:false` event, update the thread root post via + `API.UpdatePost()` with accumulated content. +4. On `done:true`, finalise the post with full content, usage stats + (token count, duration). `TODO(design):` Post-update on every token may be too chatty. **Proposed:** batch updates every ~200 ms or every 3 tokens, whichever comes first. @@ -309,11 +440,11 @@ batch updates every ~200 ms or every 3 tokens, whichever comes first. - Display a warning: "Context window trimmed (oldest messages removed)." `TODO(design):` Should the user be able to set `num_ctx` per-request? -`/ai --context 8192 ...` +`@mattermore --context 8192 ...` ### 5.4 Model Selection Precedence -1. Per-request: `/ai model gemma3:12b write a poem` +1. Per-request: `@mattermore model gemma3:12b write a poem` 2. Session override: `/ai model gemma3:12b` (stored in KV) 3. Default from System Console: `DefaultModel` @@ -333,7 +464,8 @@ provides the UI; the plugin reads via `OnConfigurationChange()`. 2. Validate `OllamaURL` (parse as URL, reject non-HTTP(S) schemes). 3. If URL changed, create a new `http.Client` (with configurable timeout). 4. If `RateLimitPerMinute` changed, rebuild the token buckets. -5. Log the change at debug level. +5. If `StopWords` changed, rebuild the stop-word trie. +6. Log the change at debug level. --- @@ -344,29 +476,50 @@ provides the UI; the plugin reads via `OnConfigurationChange()`. ``` Step User / System Component ──── ────────────────────────────── ────────────────── - 1 User types "/ai explain TCP" Mattermost webapp - 2 Mattermost routes to plugin mm-server - 3 Plugin.ExecuteCommand() parses args Mattermore plugin - 3a If "/ai help" → show help text - 3b If "/ai new" → KVStore.Delete(ctx) - 3c If "/ai model …" → override model KVStore.Set(model) - 3d If "/ai model list" → OllamaClient.ListModels() - 4 RateLimiter.Allow(userId) RateLimiter - 5 ConversationStore.GetSession(...) ConversationStore - 6 Build []ChatMessage (system + history + new prompt) - 7 OllamaClient.ChatCompletion(...) OllamaClient - 8 Ollama streams ndjson tokens Ollama (remote) - 9 Plugin accumulates tokens, updates Mattermore plugin - ephemeral post every ~200ms - 10 On done:true → create final post Mattermore plugin - 11 ConversationStore.Append(...) ConversationStore + 1 User types "@mattermore explain TCP" Mattermost webapp + 2 MessageHasBeenPosted fires Mattermore plugin + 3 EngagementEngine.Wake() EngagementEngine + 3a Check not a bot post (loop guard) + 3b Check @-mention or active thread + 3c Transition to Active + 4 Strip "@mattermore " → "explain TCP" + 5 Check stop-word trie (no match) + 6 RateLimiter.Allow(userId) RateLimiter + 7 ConversationStore.GetSession(...) ConversationStore + 8 Build []ChatMessage (system + history + new prompt) + 9 OllamaClient.ChatCompletion(...) OllamaClient + 10 Ollama streams ndjson tokens Ollama (remote) + 11 Plugin accumulates tokens, updates Mattermore plugin + thread post every ~200ms + 12 On done:true → finalise post Mattermore plugin + 13 Check response for [EOS] signal EngagementEngine + 14 ConversationStore.Append(...) ConversationStore ``` -### 7.2 Thread Participation +### 7.2 Disengagement Flow -`TODO(design):` If the user replies in a thread where the bot posted, should -the plugin pick it up? This requires a `MessageHasBeenPosted` hook and -thread detection — adds complexity. **Scope:** defer to v0.2+. +``` +Step User / System Component +──── ────────────────────────────── ────────────────── + 1 User replies "thanks that's all" + 2 MessageHasBeenPosted fires + 3 EngagementEngine.IsActive → true + 4 Strip @-mention if present + 5 Stop-word trie matches "that's all" StopWordTrie + 6 EngagementEngine.Transition(Disengaging) + 7 OllamaClient.ChatCompletion(lastMsg) (brief closing response) + 8 Post closing message in thread + 9 EngagementEngine.Transition(Sleeping) +``` + +### 7.3 Thread Participation + +Thread replies continue the conversation without requiring a new @-mention. +The EngagementEngine tracks which threads it owns (via `ownerThread_{threadID}` +in KV). For posts in owned threads: +- If the EngagementEngine is Active for this user+thread → respond +- If Sleeping → respond once to wake, then continue +- If Disengaging → respond with closing note, then sleep --- @@ -418,7 +571,7 @@ thread detection — adds complexity. **Scope:** defer to v0.2+. | `ERROR` | Ollama unreachable, config validation failure, KV store error | | `WARN` | Rate limit hit, context window trimmed, TLS skipped | | `INFO` | Plugin activated/deactivated, config changed, first request | -| `DEBUG` | Every request/response round-trip (with userID, model, token count, latency) | +| `DEBUG` | Every request/response round-trip (with userID, model, token count, latency, engagement state) | ### 9.2 Metrics (future) @@ -442,16 +595,18 @@ These are the unresolved questions that need answers before v0.1.0 ships. | # | Question | Proposed | Alternatives | Decided? | |---|---|---|---|---| -| D1 | Reply style | Ephemeral by default, `--public` flag for thread | Always ephemeral, always thread, always channel | ❌ | -| D2 | Context scope | Per-channel per-user | Per-user global, per-channel only, no context | ❌ | +| D1 | Reply style | **Thread posts** (not ephemeral) — community sees the interaction | Ephemeral, channel-wide | ✅ → DECISIONS.md | +| D2 | Context scope | Per-channel per-user — each channel gets its own conversation | Per-user global, per-channel only, no context | ❌ | | D3 | Context turns | Last 5 exchanges (10 messages) | Last N tokens, everything in TTL, sliding window | ❌ | -| D4 | Webapp UI | None (slash commands only) | Channel header button, RHS panel, settings page | ❌ | +| D4 | Webapp UI | **None** — @-mention + thread replies cover all interaction | Channel header button, RHS panel | ✅ → DECISIONS.md | | D5 | Model governance | All models available | Admin-restricted model list, least-privilege Ollama API key | ❌ | -| D6 | Thread replies | Defer to v0.2 | Auto-reply in thread when user replies to bot post | ❌ | +| D6 | Thread replies | **Core feature** — the EngagementEngine auto-follows thread replies | Slash-command-only, ephemeral | ✅ → DECISIONS.md | | D7 | `num_ctx` per request | Via `--context N` flag | System Console default only | ❌ | | D8 | Auto-pull models | No (error + list available) | Yes (convenient but slow) | ❌ | | D9 | Prompt injection guard | Defer (document risk) | Regex blocklist, LLM-as-judge pre-filter | ❌ | | D10 | Metrics endpoint | Defer | `/metrics` via plugin HTTP handler | ❌ | +| D11 | Engagement scope | Per-user+channel | Per-user global, per-channel only | ❌ | +| D12 | Thread timeout | 15 minutes, configurable | Fixed 30min, no timeout | ❌ | Each decision should be recorded in `DECISIONS.md` once resolved. @@ -482,25 +637,25 @@ curl -X POST $MM_URL/api/v4/plugins \ 1. Upload new `.tar.gz` — Mattermost replaces the plugin binaries. 2. Plugin's `OnConfigurationChange()` fires (config preserved). -3. If the KV schema changed, a migration in `OnActivate()` handles it. +3. Bot account is re-fetched (not re-created) — idempotent. +4. If the KV schema changed, a migration in `OnActivate()` handles it. ### 11.4 Rollback Re-upload the previous version's `.tar.gz`. No data migration needed if the -KV key schema hasn't changed. If it has, restore from backup or -`DECISIONS.md` records the migration path. +KV key schema hasn't changed. Bot account survives rollback. If it has, +restore from backup or `DECISIONS.md` records the migration path. --- ## 12. Future Considerations (v0.2+) - **Multi-modal inputs** — Ollama supports images via `/api/chat` with - `images` field. Could allow `/ai` with attached image. + `images` field. Could allow image attachment in thread. - **Multi-model routing** — Different models for different channel categories (e.g. code-review channel uses codellama). - **Tool calling** — Ollama supports tool definitions. Could let the plugin fetch data from external APIs (weather, Jira, etc.). -- **Bot account mode** — A separate `mattermost-bot` skill / mode for - always-on presence in channels vs. on-demand slash commands. - **Streaming into message attachments** — Render structured data (tables, JSON, code blocks) with better formatting. +- **Conversation export** — Allow users to export an AI thread as markdown.