Skip to content

fix(mattermost): route thread replies correctly when replyToMode is off#57565

Open
dave wants to merge 12 commits intoopenclaw:mainfrom
dave:fix/mattermost-thread-id-on-replyto-off
Open

fix(mattermost): route thread replies correctly when replyToMode is off#57565
dave wants to merge 12 commits intoopenclaw:mainfrom
dave:fix/mattermost-thread-id-on-replyto-off

Conversation

@dave
Copy link
Copy Markdown

@dave dave commented Mar 30, 2026

Depends on #57970 (fix/mattermost-unknown-chat-type). This branch is rebased on that PR. After #57970 merges, this PR should be rebased onto main.

With replyToMode: "off", resolveMattermostEffectiveReplyToId returns undefined
to prevent thread session forking. However, this also broke two delivery behaviours
that depended on effectiveReplyToId being set:

  1. MessageThreadId was always undefined in the inbound context, so tools and
    plugin hooks had no way to know which thread a message came from.

  2. Agent replies went to the main channel instead of the originating thread, because
    all three deliver call sites passed effectiveReplyToId (undefined) as the
    threadRootId to resolveMattermostReplyRootId. Plugin command responses share
    the same deliver path and are affected by the same bug.

Fix:

  • resolveMattermostThreadSessionContext now computes messageThreadId separately
    as post.root_id ?? effectiveReplyToId, reflecting the actual thread root
    regardless of replyToMode.
  • MessageThreadId in the inbound ctxPayload is now set from messageThreadId
    at all three dispatch sites (interaction, model-picker/sendMessage, main handler).
  • All three resolveMattermostReplyRootId deliver call sites now pass
    messageThreadId as threadRootId instead of effectiveReplyToId, so replies
    are delivered to the correct thread.
  • All three sendTypingIndicator call sites now pass messageThreadId so typing
    indicators appear in the same thread as the eventual reply.
  • Added buildToolContext to the Mattermost threading config so that
    currentThreadTs is populated from MessageThreadId, giving agent tools
    visibility into the active thread.
  • Fixed resolveThreadFlag in src/config/sessions/reset.ts to not classify a
    session as thread-scoped when messageThreadId is set but the session key
    explicitly identifies a group/channel session. messageThreadId is a delivery
    detail; the session key is authoritative for session type.
  • Removed the channelTypeKnown workaround from handlePost — now dead code
    because the kind === "unknown" early-exit guard from fix(mattermost): return 'unknown' ChatType when channel type is not resolvable #57970 prevents reaching
    the threading logic with an unresolved channel type.

Summary

  • Problem: With replyToMode: "off", any message posted inside a Mattermost
    thread caused effectiveReplyToId to be undefined. All three deliver call sites
    used effectiveReplyToId as the thread root for outgoing replies, so agent replies
    posted to the main channel instead of the originating thread. Plugin command
    responses share the same deliver path and are affected identically. Additionally,
    ctx.MessageThreadId was always undefined, so tools had no visibility into the
    active thread.
  • Why it matters: Users with replyToMode: "off" (the typical setup for bots
    that should not fork per-thread sessions) could not use Mattermost threads at all
    — every agent reply landed in the main channel instead of the thread it came from.
  • What changed: resolveMattermostThreadSessionContext now returns a separate
    messageThreadId field (post.root_id ?? effectiveReplyToId) that carries the
    actual thread root independent of replyToMode. All three deliver call sites,
    all three typing indicator call sites, and all three ctxPayload construction
    sites now use messageThreadId instead of effectiveReplyToId.
    buildToolContext added to the Mattermost threading config so currentThreadTs
    is populated. resolveThreadFlag updated to treat messageThreadId as a
    delivery detail rather than a session-type signal when the session key is
    explicitly a group/channel session. Removed the channelTypeKnown workaround
    (dead code after fix(mattermost): return 'unknown' ChatType when channel type is not resolvable #57970).
  • What did NOT change: Session routing is unaffected — effectiveReplyToId and
    resolveThreadSessionKeys are unchanged, so replyToMode: "off" still prevents
    thread session forking exactly as intended.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: effectiveReplyToId was serving two distinct purposes — session
    routing (which session key to use) and delivery routing (where to post the reply).
    Commit aaba1ae653 correctly set it to undefined for session routing when
    replyToMode: "off", but inadvertently broke delivery routing as a side effect.
  • Missing detection / guardrail: Tests for resolveMattermostThreadSessionContext
    only asserted on effectiveReplyToId and sessionKey. No test covered the
    end-to-end delivery path (deliver callback → resolveMattermostReplyRootId
    actual post target) for the replyToMode: "off" + threaded message scenario.
  • Prior context: replyToMode support was added in 2026.3.12 (171d2df9e0,
    PR feat(mattermost): add replyToMode support (off | first | all) #29587). The regression was introduced in 2026.3.22 by aaba1ae653.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/mattermost/src/mattermost/monitor.test.ts,
    resolveMattermostThreadSessionContext describe block
  • Scenario the test should lock in: When replyToMode: "off" and a non-zero
    threadRootId is present, messageThreadId equals threadRootId and
    effectiveReplyToId is still undefined.
  • Existing test that already covers this: Added
    "still reports messageThreadId when replyToMode is off" in this PR; also updated
    the existing "keeps threaded messages top-level when replyToMode is off"
    assertion to include messageThreadId: "root-456".

User-visible / Behavior Changes

With replyToMode: "off", agent replies now go to the originating thread instead
of the main channel. Typing indicators appear in the same thread. Plugin command
responses are also fixed as they share the same delivery path. No change in
behaviour for replyToMode: "all" or "first".

Diagram (if applicable)

Before (replyToMode: "off", user posts inside thread T1):
  post.root_id="T1"
    -> effectiveReplyToId=undefined
    -> resolveMattermostReplyRootId(threadRootId=undefined, replyToId=undefined)
    -> reply posts to main channel  ✗

After:
  post.root_id="T1"
    -> messageThreadId="T1"   effectiveReplyToId=undefined (unchanged)
    -> resolveMattermostReplyRootId(threadRootId="T1")
    -> reply posts to thread T1  ✓
    -> session key unchanged (no forking)  ✓
    -> resolveThreadFlag: session key is ":channel:" → isThread=false  ✓

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • Integration/channel: Mattermost, account with replyToMode: "off"
  • Relevant config: channels.mattermost.replyToMode: "off" (or per-account equivalent)

Steps

  1. Configure a Mattermost account with replyToMode: "off"
  2. Post a message inside a channel thread
  3. Observe where the agent reply appears

Expected

  • Agent reply appears inside the thread

Actual (before fix)

  • Agent reply appears in the main channel

Human Verification (required)

  • Verified on unpatched 2026.3.28: with replyToMode: "off", agent replies land in
    the main channel instead of the originating thread. Confirmed fixed with this PR.
  • Typing indicators also appear in-thread after fix.
  • Edge cases checked: top-level messages with replyToMode: "off" unaffected;
    replyToMode: "all" behaviour unchanged.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: messageThreadId used where effectiveReplyToId was previously undefined
    for replyToMode: "all"/"first" top-level posts — could change delivery target.
    • Mitigation: messageThreadId = threadRootId ?? effectiveReplyToId, so for
      non-"off" modes the value is identical to the old effectiveReplyToId. No
      behaviour change for those modes.
  • Risk: setting MessageThreadId could misclassify channel sessions as thread
    sessions in resolveThreadFlag, changing reset policy for users with separate
    session.resetByType.thread config.
    • Mitigation: fixed in src/config/sessions/reset.tsresolveThreadFlag now
      defers to the session key when it explicitly identifies a group/channel session,
      treating messageThreadId as a delivery detail rather than a session-type signal.

@openclaw-barnacle openclaw-barnacle Bot added channel: mattermost Channel integration: mattermost size: S labels Mar 30, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 30, 2026

Greptile Summary

This PR correctly separates two previously conflated concerns in the Mattermost integration: session routing (which session key to use, controlled by replyToMode) and delivery routing (where to post replies and what MessageThreadId to report). It introduces a new messageThreadId field computed as threadRootId?.trim() || effectiveReplyToId that always reflects the actual thread the inbound message came from, regardless of replyToMode. All three deliver call sites and all three ctxPayload construction sites are updated to use this value, and buildToolContext is added so agent tools get correct thread visibility.

Key changes:

  • resolveMattermostThreadSessionContext now returns messageThreadId in addition to effectiveReplyToId; the two fields serve different purposes and effectiveReplyToId is left untouched (session routing is unchanged)
  • All three resolveMattermostReplyRootId call sites now pass messageThreadId as threadRootId, fixing reply delivery into threads when replyToMode: "off"
  • MessageThreadId in ctxPayload now uses messageThreadId at all three dispatch sites, so tools always see the correct thread root
  • buildToolContext added to the Mattermost threading config in channel.ts so currentThreadTs is populated from MessageThreadId
  • Two minor follow-up items: typing indicator call sites still pass effectiveReplyToId (causing a typing-in-channel / reply-in-thread mismatch with replyToMode: "off"), and the MessageThreadId fallback ?? params.effectiveReplyToId at line 681 is redundant given how messageThreadId is computed

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, all three affected call sites are updated consistently, and new tests lock in the corrected behaviour at the exact regression seam.

All remaining findings are P2: the typing-indicator inconsistency is cosmetic and pre-existing behaviour is not made worse, and the redundant fallback is harmless. No P0 or P1 issues found. Session routing is provably unchanged.

No files require special attention beyond the optional P2 typing-indicator cleanup in monitor.ts.

Important Files Changed

Filename Overview
extensions/mattermost/src/mattermost/monitor.ts Adds messageThreadId to resolveMattermostThreadSessionContext return value and updates all three deliver call sites and ctxPayload construction sites to use it instead of effectiveReplyToId for routing; fixes thread delivery with replyToMode: "off". Typing indicator call sites still use effectiveReplyToId (minor inconsistency).
extensions/mattermost/src/mattermost/monitor.test.ts Adds messageThreadId assertions to all existing resolveMattermostThreadSessionContext test cases and adds a new dedicated test for the replyToMode: "off" + threaded message scenario; coverage is comprehensive.
extensions/mattermost/src/channel.ts Adds buildToolContext to the Mattermost threading config, populating currentThreadTs from context.MessageThreadId so agent tools get correct thread visibility regardless of replyToMode.

Comments Outside Diff (1)

  1. extensions/mattermost/src/mattermost/monitor.ts, line 517 (link)

    P2 Typing indicators still target channel root with replyToMode: "off"

    All three sendTypingIndicator call sites (lines 517, 711, 1436) still pass effectiveReplyToId, which is undefined when replyToMode: "off" and the inbound message is in a thread. Since this PR correctly routes the reply to the thread via messageThreadId, the two are now out of sync: the typing indicator appears at the channel root while the response arrives inside the thread.

    Before this PR both were at channel root (consistently wrong). After this PR the reply lands in the right place, but the typing indicator still targets channel root — a subtle but user-visible inconsistency.

    Consider passing messageThreadId (or threadContext.messageThreadId) in all three call sites to keep the typing indicator co-located with the eventual reply:

    Same change is needed at line 711 (params.messageThreadId) and line 1436 (messageThreadId).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/mattermost/src/mattermost/monitor.ts
    Line: 517
    
    Comment:
    **Typing indicators still target channel root with `replyToMode: "off"`**
    
    All three `sendTypingIndicator` call sites (lines 517, 711, 1436) still pass `effectiveReplyToId`, which is `undefined` when `replyToMode: "off"` and the inbound message is in a thread. Since this PR correctly routes the *reply* to the thread via `messageThreadId`, the two are now out of sync: the typing indicator appears at the channel root while the response arrives inside the thread.
    
    Before this PR both were at channel root (consistently wrong). After this PR the reply lands in the right place, but the typing indicator still targets channel root — a subtle but user-visible inconsistency.
    
    Consider passing `messageThreadId` (or `threadContext.messageThreadId`) in all three call sites to keep the typing indicator co-located with the eventual reply:
    
    
    
    Same change is needed at line 711 (`params.messageThreadId`) and line 1436 (`messageThreadId`).
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/monitor.ts
Line: 517

Comment:
**Typing indicators still target channel root with `replyToMode: "off"`**

All three `sendTypingIndicator` call sites (lines 517, 711, 1436) still pass `effectiveReplyToId`, which is `undefined` when `replyToMode: "off"` and the inbound message is in a thread. Since this PR correctly routes the *reply* to the thread via `messageThreadId`, the two are now out of sync: the typing indicator appears at the channel root while the response arrives inside the thread.

Before this PR both were at channel root (consistently wrong). After this PR the reply lands in the right place, but the typing indicator still targets channel root — a subtle but user-visible inconsistency.

Consider passing `messageThreadId` (or `threadContext.messageThreadId`) in all three call sites to keep the typing indicator co-located with the eventual reply:

```suggestion
            start: () => sendTypingIndicator(opts.channelId, threadContext.messageThreadId),
```

Same change is needed at line 711 (`params.messageThreadId`) and line 1436 (`messageThreadId`).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/monitor.ts
Line: 681

Comment:
**Redundant fallback in `MessageThreadId` assignment**

`messageThreadId` is already computed in `resolveMattermostThreadSessionContext` as `threadRootId?.trim() || effectiveReplyToId`, so it already incorporates `effectiveReplyToId` when `threadRootId` is absent. The `?? params.effectiveReplyToId` fallback here can never add new information — if `params.messageThreadId` is `undefined` it means both `threadRootId` and `effectiveReplyToId` were `undefined`.

The two other `MessageThreadId` assignments (lines 493 and 1386) use the value directly without a fallback. Keeping this consistent avoids confusion about whether the fallback ever fires:

```suggestion
      MessageThreadId: params.messageThreadId,
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(mattermost): route thread replies co..." | Re-trigger Greptile

Comment thread extensions/mattermost/src/mattermost/monitor.ts Outdated
@dave dave force-pushed the fix/mattermost-thread-id-on-replyto-off branch from 795b3a7 to 472530b Compare March 30, 2026 07:59
@dave
Copy link
Copy Markdown
Author

dave commented Mar 30, 2026

Greptile suggestions done ✅

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 472530b794

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/mattermost/src/mattermost/monitor.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 413f157aea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/mattermost/src/mattermost/monitor.ts Outdated
@dave dave force-pushed the fix/mattermost-thread-id-on-replyto-off branch from 16d7192 to bd5528b Compare March 30, 2026 09:07
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd5528be65

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/mattermost/src/mattermost/monitor.ts
@dave dave force-pushed the fix/mattermost-thread-id-on-replyto-off branch 2 times, most recently from f0445d5 to 236d330 Compare March 30, 2026 09:58
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 236d330c14

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/mattermost/src/mattermost/monitor.ts Outdated
@dave dave force-pushed the fix/mattermost-thread-id-on-replyto-off branch from a1650a9 to fd377f0 Compare March 30, 2026 12:12
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd377f0b6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 547 to 549
replyToId: resolveMattermostReplyRootId({
threadRootId: threadContext.effectiveReplyToId,
threadRootId: threadContext.messageThreadId,
replyToId: payload.replyToId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard threaded delivery on unknown interaction channel types

This delivery path now prefers threadContext.messageThreadId, but interaction handlers still infer kind only from resolveChannelInfo; when that lookup fails, mapMattermostChannelTypeToChatType(undefined) falls back to "channel" and messageThreadId is populated from post.root_id even for DM interactions. Because resolveChannelInfo caches null lookups for 5 minutes, a transient lookup failure can make DM button/model-picker replies with replyToMode: "off" get posted into a thread instead of the main DM window, violating the direct-message non-threaded contract. Fresh evidence in this commit is that delivery switched from effectiveReplyToId to messageThreadId at these interaction call sites without a matching channel-type-known guard.

Useful? React with 👍 / 👎.

@dave
Copy link
Copy Markdown
Author

dave commented Mar 30, 2026

Thanks for the continued review. The interaction-handler DM threading edge case (failed channelInfo lookup causing mapMattermostChannelTypeToChatType(undefined) to return "channel" and setting messageThreadId from post.root_id) is a real issue but it's the same root cause we keep patching around: ChatType has no "unknown" value, so a failed lookup silently masquerades as a channel.

We're going to fix this properly in a separate PR by adding "unknown" to ChatType and returning it from mapMattermostChannelTypeToChatType when the input is null/undefined. That makes the failure explicit and addressable at each call site rather than playing whack-a-mole per-handler. The channelTypeKnown guard in the main handler also becomes unnecessary.

Will leave this PR as-is and rebase once the ChatType change merges.

@dave
Copy link
Copy Markdown
Author

dave commented Mar 30, 2026

Quick update on scope: this was first observed with plugin command responses going to the main channel instead of the originating thread. We've now tested generic agent replies on unpatched 2026.3.28 and confirmed they're broken in exactly the same way — replies land in the main channel regardless of whether it's a command response or a normal agent reply. All three deliver call sites are shared, so the impact is the same. Updated the PR description to make agent replies the primary framing.

…solvable

Previously, mapMattermostChannelTypeToChatType returned "channel" for
null/undefined input, masking failed channel lookups as valid routing state.
All dispatch paths (handlePost, dispatchButtonClick, handleReactionEvent,
resolveSessionKey) now early-exit on "unknown" instead of silently routing
to the wrong session. monitor-auth's unknown-channel deny path now correctly
sets kind/chatType to "unknown". SlashInvocationAuth updated to discriminated
union; resolveSlackReplyToMode guards against "unknown" indexing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dave and others added 11 commits March 31, 2026 09:38
…anch

Addresses Greptile review comment: the local channel-lookup-failure path in
authorizeSlashInvocation was still returning kind: "channel", inconsistent
with the discriminated union refactor and monitor-auth's unknown-channel deny.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses contributor review suggestion: add a comment explaining why we
return the base session key early when kind is 'unknown', and why this is
safe (handlePost drops the post before the session key is used for routing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eturning coerced key

Addresses Codex P1 review: returning a coerced 'channel' session key caused
the system event to be enqueued into the wrong session before dispatchButtonClick's
guard ever ran. Throwing instead causes the caller's try/catch in interactions.ts
to skip enqueueSystemEvent entirely, truly failing closed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onKey

The throw for unknown kind was placed after resolveAgentRoute, doing
unnecessary work. Move it earlier to also eliminate the kind coercion
in the peer object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With replyToMode "off", resolveMattermostEffectiveReplyToId returns undefined
to prevent thread session forking. However, this also broke two delivery
behaviours that depended on effectiveReplyToId being set:

1. MessageThreadId was always undefined in the inbound context, so tools and
   bootstrap hooks had no way to know which thread a message came from.

2. Agent and plugin-command replies went to the main channel instead of the
   originating thread, because all three deliver call sites passed
   effectiveReplyToId (undefined) as the threadRootId to
   resolveMattermostReplyRootId.

Fix:
- resolveMattermostThreadSessionContext now computes messageThreadId separately
  as post.root_id ?? effectiveReplyToId, reflecting the actual thread root
  regardless of replyToMode.
- MessageThreadId in the inbound ctxPayload is now set from messageThreadId
  at all three dispatch sites (interaction, model-picker/sendMessage, main handler).
- All three resolveMattermostReplyRootId deliver call sites now pass
  messageThreadId as threadRootId instead of effectiveReplyToId, so replies are
  delivered to the correct thread.
- Added buildToolContext to the Mattermost threading config so that
  currentThreadTs is populated from MessageThreadId, giving agent tools
  visibility into the active thread.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sageThreadId

- All three sendTypingIndicator call sites now pass messageThreadId instead
  of effectiveReplyToId, keeping typing indicators co-located with the reply
  when replyToMode is "off" and the message is inside a thread.
- Removed redundant ?? effectiveReplyToId fallback in the sendMessage
  MessageThreadId assignment; messageThreadId already incorporates
  effectiveReplyToId by construction, matching the other two call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n messageThreadId is set for routing

resolveThreadFlag returned true whenever messageThreadId was non-null, which
misclassified sessions as thread-scoped when the session key was explicitly a
group/channel session. Channels like Mattermost with replyToMode: "off" set
messageThreadId for delivery routing (so replies go to the correct thread)
while intentionally keeping all messages in a single shared channel session.

The session key (e.g. containing ":channel:") is the authoritative signal for
session type. messageThreadId is a delivery detail that should not override it.
When the session key is explicitly a group/channel session, messageThreadId is
now ignored for classification purposes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ct messages

Direct messages are never threaded by contract — resolveMattermostReplyToMode
hardcodes replyToMode: "off" for DMs. A DM can carry post.root_id (Mattermost
supports DM threads) but replies should always go to the main DM window, not
into the thread.

Before this fix, messageThreadId was computed from threadRootId unconditionally,
so a DM that was a reply in a thread would cause the bot to respond into that
thread instead of the main DM. For non-direct chat types, the behaviour is
unchanged: messageThreadId still comes from threadRootId when set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When resolveChannelInfo returns null (network/lookup error),
mapMattermostChannelTypeToChatType(undefined) silently falls back to
"channel", bypassing the DM guard in resolveMattermostReplyToMode. In
that scenario, setting threadRootId from post.root_id could cause a DM
to be treated as a threaded channel session.

Guard all three threadRootId assignments: resolveSessionKey, button-click
dispatchButtonClick, and the main handler. For the main handler, also
accept a raw channel_type from the websocket event payload as a signal
that the channel kind is known.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sionKey and dispatchButtonClick

The channelInfo != null guard was added to prevent DM threading when
channel lookup fails and kind defaults to "channel". However, when
channelInfo is null, both the `to` address and session key are already
misrouted (kind defaults to "channel" instead of "direct"), so the
threadRootId guard only protects against a secondary symptom of a more
fundamental failure.

The cost is too high: resolveChannelInfo caches null for 5 minutes on
any network error, so a single transient failure would de-thread all
interaction/session-key replies in real channel threads for the entire
cache window.

The main handler already has a more precise guard using the websocket
event's channel_type as a fallback (channelTypeKnown = channelInfo !=
null || payload.data?.channel_type != null). The interaction and
resolveSessionKey callbacks have no equivalent event-level fallback, so
the null guard there is strictly lossy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The early-exit guard for kind === "unknown" in handlePost (from the
unknown-chat-type PR) means we never reach the threading logic with an
unresolved channel type. The channelTypeKnown conditional was the old
workaround for this exact scenario and is now unreachable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dave dave force-pushed the fix/mattermost-thread-id-on-replyto-off branch from fd377f0 to 02de778 Compare March 31, 2026 02:42
@openclaw-barnacle openclaw-barnacle Bot added the channel: slack Channel integration: slack label Mar 31, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02de7789bc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/mattermost/src/mattermost/monitor.ts
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs changes before merge.

Summary
The PR separates Mattermost delivery/tool thread context from session routing, adds unknown-channel-type handling, and adjusts session reset classification for non-forked threaded delivery.

Reproducibility: yes. A source-level reproduction exists on current main: channel/group posts with post.root_id and replyToMode: off produce effectiveReplyToId: undefined, and all outbound roots still use that undefined value.

Next step before merge
The bug is valid and narrow, the source branch is dirty, and the contributor explicitly stepped away, so automation can attempt a current-main replacement.

Security
Cleared: The diff changes Mattermost routing/type logic and tests without adding CI, dependency, install, release, secret-handling, or third-party code-execution surface.

Review details

Best possible solution:

Land a fresh current-main implementation that keeps effectiveReplyToId for session forking, adds a separate Mattermost delivery/tool-context thread id for channel and group thread replies when replyToMode is off, preserves DM non-threading, and folds or sequences the unknown-channel-type handling with focused tests and a changelog entry.

Do we have a high-confidence way to reproduce the issue?

Yes. A source-level reproduction exists on current main: channel/group posts with post.root_id and replyToMode: off produce effectiveReplyToId: undefined, and all outbound roots still use that undefined value.

Is this the best way to solve the issue?

Yes conceptually, but not as a direct merge. Separating session routing from delivery routing is the narrow maintainable fix; the current branch should be replaced or rebased onto main while resolving #57970 and current Mattermost drift.

Acceptance criteria:

  • pnpm test extensions/mattermost/src/mattermost/monitor.test.ts extensions/mattermost/src/mattermost/monitor-gating.test.ts extensions/mattermost/src/mattermost/monitor.channel-kind.test.ts extensions/mattermost/src/mattermost/monitor-auth.test.ts extensions/mattermost/src/mattermost/slash-http.test.ts extensions/mattermost/src/channel.test.ts
  • pnpm test src/config/sessions
  • pnpm exec oxfmt --check --threads=1 extensions/mattermost/src/channel.ts extensions/mattermost/src/mattermost/monitor.ts extensions/mattermost/src/mattermost/monitor.test.ts extensions/mattermost/src/mattermost/monitor-gating.ts extensions/mattermost/src/mattermost/monitor-gating.test.ts extensions/mattermost/src/mattermost/monitor.channel-kind.test.ts extensions/mattermost/src/mattermost/monitor-auth.ts extensions/mattermost/src/mattermost/slash-http.ts extensions/mattermost/src/types.ts src/channels/chat-type.ts src/config/sessions/reset.ts src/plugin-sdk/core.ts CHANGELOG.md
  • pnpm check:changed

What I checked:

Likely related people:

Remaining risk / open question:

  • The branch mixes the thread-routing fix with the open unknown-chat-type dependency, so a direct rebase needs careful conflict resolution rather than blind application.
  • Open fix(mattermost): inherit thread context in message tool send action #52120 also changes Mattermost threading.buildToolContext; a replacement should account for that overlap to avoid dueling tool-context behavior.
  • This review used source-level reproduction and API metadata only; no live Mattermost server verification was run in the read-only checkout.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 9fff2b779159.

@dave
Copy link
Copy Markdown
Author

dave commented Apr 28, 2026

Quick update from me, it kind of felt like I was playing whack-a-mole with these bugs and going round and round in circles. I'm not really familiar with any of this code, so it might be better if someone who knows the system a little bit better took over this. I don't actually have any more time to devote to it, unfortunately, so I'm going to have to leave this hanging. Sorry.

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

Labels

channel: mattermost Channel integration: mattermost channel: slack Channel integration: slack size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant