Skip to content

feat(mattermost): carry thread context (replyToId) to non-inbound-triggered deliveries #39759

@teconomix

Description

@teconomix

Problem

When a Mattermost session is started by a channel message with replyToMode: "all", the agent correctly replies in-thread for direct responses to that message. However, two delivery paths fail to carry the thread context:

Case 1 — TUI/WebUI messages to an active Mattermost session:
If the user sends a follow-up message via TUI or WebUI (not Mattermost), the agent's response is correctly routed back to Mattermost, but it arrives in the channel root instead of the session's thread.

Case 2 — Agent-initiated messages:
When the agent sends proactively (e.g. tool call callbacks, subagent responses, message tool calls within a session), the same problem occurs — the message arrives in the channel instead of the thread.

Root Cause

effectiveReplyToId is computed as a closure variable when processing an inbound Mattermost message and correctly used in the deliver callback for direct replies. However, when responses are triggered from other surfaces (TUI, WebUI) or agent-initiated, they go through the channel plugin's sendText/sendMedia path in channel.ts. This path receives replyToId from payload.replyToId, which is not populated for non-inbound-Mattermost-triggered deliveries.

Inbound Mattermost message:
  effectiveReplyToId = threadRootId ?? post.id  ← computed here
  ctxPayload.ReplyToId = effectiveReplyToId      ← stored in session ctx
  deliver() callback → uses effectiveReplyToId  ← WORKS ✅

TUI/WebUI or agent-initiated:
  channel.ts sendText({ replyToId })             ← replyToId = undefined ❌
  → message goes to channel root

Proposed Solution

Maintain a Map<sessionKey, string> (thread context cache) in the monitor:

  1. Write: when processing an inbound Mattermost message, store threadContextMap.set(sessionKey, effectiveReplyToId)
  2. Read: in the outbound delivery path (sendText/sendMedia in channel.ts, or via a hook in monitor.ts), look up the cached replyToId as fallback when payload.replyToId is not set
  3. Cleanup: entries can be evicted after a configurable TTL or LRU limit

Alternatively: if the core already forwards ctxPayload.ReplyToId as payload.replyToId for all session deliveries (including TUI-triggered ones), it may be sufficient to ensure the Mattermost channel plugin uses payload.replyToId as its primary thread reference.

How to Reproduce

  1. Configure replyToMode: "all" on a Mattermost account
  2. Send a message to the bot in a channel — it correctly creates a thread and replies in it
  3. Send a follow-up message to the same session via TUI (openclaw tui) or WebUI
  4. Observe: agent reply arrives in channel root, not in the thread
  5. For Case 2: trigger a subagent from within the session and have it report back — response arrives in channel root

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions