Skip to content

fix(mattermost): persist threadId in delivery context for all session types#52236

Draft
teconomix wants to merge 1 commit intoopenclaw:mainfrom
teconomix:fix/mattermost-cron-thread-context
Draft

fix(mattermost): persist threadId in delivery context for all session types#52236
teconomix wants to merge 1 commit intoopenclaw:mainfrom
teconomix:fix/mattermost-cron-thread-context

Conversation

@teconomix
Copy link
Copy Markdown
Contributor

@teconomix teconomix commented Mar 22, 2026

Problem

When a cron job or heartbeat wakes a thread-scoped Mattermost session, the reply lands at the channel root instead of in the thread.

Root Cause

updateLastRoute in monitor.ts was guarded by if (kind === "direct"), so delivery context (including threadId) was only persisted for DM sessions. For group/channel sessions with threads, deliveryContext.threadId was never stored in the session store.

When a cron fires into a thread-scoped session, dispatch-from-config.ts recovers the thread ID via parseSessionThreadInfo(runSessionKey) — which correctly extracts the thread ID from the session key string. However, without threadId stored in deliveryContext, the delivery has no thread context when looking up the session entry.

Fix

Remove the kind === "direct" guard from updateLastRoute so delivery context is persisted for all session types. This ensures threadId is stored whenever an inbound message is processed in a thread-scoped session.

How to test

  1. Configure Mattermost with replyToMode: "all"
  2. Start a conversation in a channel thread — this creates a thread-scoped session
  3. Have the agent set a cron job targeting the thread session key using --session main --session-key <thread-session-key>
  4. Before: reply lands at channel root
  5. After: reply should land in the thread

Related: #45082 (case 2), companion to #52120 (case 1 — message tool)

Closes #45082

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

greptile-apps Bot commented Mar 22, 2026

Greptile Summary

This PR fixes a thread-routing regression for cron/heartbeat wakeups in Mattermost channel sessions. The root cause was that updateLastRoute was guarded behind if (kind === "direct"), so deliveryContext.threadId was never persisted for group/channel sessions. When a cron job fired into a thread-scoped session it had no stored thread context and replied to the channel root instead of the thread.

The fix is minimal and correct:

  • Drops the kind === "direct" guard so updateLastRoute runs for all session types
  • Adds threadId: effectiveReplyToId to the delivery context object so the thread ID is stored when a thread-scoped message arrives
  • The store.ts clearThreadFromFallback logic already handles the threadId: undefined case correctly (non-thread messages clear any previously stored thread ID, reflecting the most-recent interaction context) — this mirrors the pre-existing behaviour for DM sessions
  • The new preflight CI workflow adds scoped format/lint/test/build checks for this fork's Mattermost patches

Confidence Score: 5/5

  • Safe to merge — the change is a minimal two-line surgical fix with no regressions identified.
  • The fix is focused: one guard removed, one field added. The clearThreadFromFallback path in store.ts correctly handles threadId: undefined (non-thread messages) by clearing stale thread context from the fallback, exactly as it already did for DMs. Thread-scoped messages correctly store their effectiveReplyToId. Live-tested result confirms the fix works. No unrelated code is touched and the accompanying CI workflow is clean.
  • No files require special attention.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'fork/main'..." | Re-trigger Greptile

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

https://github.com/openclaw/openclaw/blob/5504b8172942a361a7fcb7d063d9723dbc96c745/extensions/mattermost/src/mattermost/monitor.ts#L1354-L1357
P1 Badge Persist Mattermost thread routes on the thread session key

Now that non-DM turns enter this block, the write always targets route.mainSessionKey instead of the active thread session. resolveSessionDeliveryTarget later reuses deliveryContext.threadId from whatever entry is selected (src/infra/outbound/targets.ts:109-119,165-168), so after any threaded Mattermost turn a plain main-session cron/manual wake can inherit that thread and post there instead of the channel root. Other inbound paths avoid this by choosing the last-route session key from the current route (src/routing/resolve-route.ts:70-75), so persisting thread-scoped routing on the main session here is a regression.

ℹ️ 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 +1347 to +1349
// Persist delivery context for all session types (including threads).
// Without threadId here, cron/heartbeat wakeups into a thread-scoped session
// lose thread context and deliver to channel root (issue #45082 case 2).
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 Don't claim heartbeat routing is fixed by saving threadId here

Saving threadId in deliveryContext does not change the heartbeat path: resolveHeartbeatDeliveryTarget always calls resolveSessionDeliveryTarget(..., mode: "heartbeat"), and that helper deliberately drops session-derived thread IDs when mode === "heartbeat" (src/infra/outbound/targets.ts:280-285 and src/infra/outbound/targets.ts:165-166). As written, thread-scoped Mattermost heartbeats will still route to the channel root, so the new comment and fix are incomplete for the heartbeat case.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are correct. The heartbeat delivery path calls resolveSessionDeliveryTarget with mode: \"heartbeat\", which explicitly sets threadId = undefined at targets.ts:165-166 to prevent stale thread context from leaking. Storing threadId in deliveryContext via this change alone does not fix the heartbeat case.

The cron wake path that does benefit from this change is the --session main --session-key <thread-key> path: dispatch-from-config.ts recovers routeThreadId via parseSessionThreadInfo(acpDispatchSessionKey).threadId — which reads from the session key string directly, not from deliveryContext. For this to work, ctx.SessionKey must be the thread-scoped key, which happens when the cron job is set with --session main --session-key <thread-session-key>.

The PR description has been updated to remove live-test claims. This fix is a necessary prerequisite for the cron thread-delivery path, but a complete fix for heartbeat thread routing would require changes to resolveHeartbeatDeliveryTarget to preserve thread context when explicitly targeting a thread-scoped session.

@teconomix teconomix force-pushed the fix/mattermost-cron-thread-context branch 2 times, most recently from 29ac697 to 3658428 Compare March 22, 2026 12:31
@teconomix teconomix marked this pull request as draft March 22, 2026 12:35
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

https://github.com/openclaw/openclaw/blob/3658428580aad57541d4979dd578e66f6447b4e9/extensions/mattermost/src/mattermost/monitor.ts#L1356-L1357
P1 Badge Persist Mattermost thread routes on the thread session entry

This still writes the route only to route.mainSessionKey, so thread-scoped Mattermost sessions remain indistinguishable in the store. resolveDeliveryTarget() explicitly checks store[job.sessionKey] first and falls back to the main entry when that thread key has no record (src/cron/isolated-agent/delivery-target.ts:60-64, exercised by src/cron/isolated-agent.delivery-target-thread-session.test.ts:83-100). In the current Mattermost flow this handler never records the actual sessionKey, so if thread A receives a cron after thread B was the latest inbound message, the job for A will inherit B's threadId from the shared main entry and post into the wrong thread.


https://github.com/openclaw/openclaw/blob/3658428580aad57541d4979dd578e66f6447b4e9/extensions/mattermost/src/mattermost/monitor.ts#L1356-L1357
P1 Badge Avoid redirecting main-session heartbeats to public Mattermost threads

Updating route.mainSessionKey for every non-DM Mattermost message changes the shared main-session route, not just the thread-scoped session that woke up. When heartbeat.session is unset, the heartbeat path uses that main entry by default (src/infra/heartbeat-runner.ts:218-225) and resolves delivery from its lastChannel/lastTo (src/infra/outbound/targets.ts:280-315), so a single mention in a channel or thread will make later heartbeats or other main-session deliveries jump from the user's DM into that public Mattermost conversation. If the goal is only to fix thread-session wakeups, this needs a per-session write rather than overwriting the shared main route.

ℹ️ 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".

@teconomix
Copy link
Copy Markdown
Contributor Author

Leaving as draft pending a more complete fix. The monitor.ts change (storing threadId in deliveryContext for all session types) is a necessary prerequisite but not sufficient on its own.

Two remaining issues:

  1. Heartbeat delivery drops threadId: resolveHeartbeatDeliveryTarget calls resolveSessionDeliveryTarget with mode: "heartbeat", which explicitly sets threadId = undefined (targets.ts:165-166) to prevent stale thread context. A complete fix needs to preserve thread context when explicitly targeting a thread-scoped session key.

  2. Cron tool gap: The cron tool schema has no way to set sessionTarget to the current thread session with payload.kind: "systemEvent". sessionTarget: "current" requires agentTurn (isolated), and sessionTarget: "main" targets the main session. This means agents cannot schedule cron wakeups in their own thread via the tool — only via the CLI directly.

… types

Previously updateLastRoute was only called for DM (kind === direct) sessions.
For group/channel sessions with threads, the deliveryContext never stored
threadId, so cron/heartbeat wakeups targeting a thread-scoped session had no
thread context and delivered replies to the channel root.

Remove the kind === direct guard so delivery context is persisted for all
session types, including thread-scoped sessions.

Fixes openclaw#45082 case 2: cron/heartbeat wake replies landing at channel root.
@teconomix teconomix force-pushed the fix/mattermost-cron-thread-context branch from 3658428 to 30ae81e Compare March 31, 2026 04:51
@teconomix teconomix closed this Apr 1, 2026
@teconomix
Copy link
Copy Markdown
Contributor Author

teconomix commented Apr 1, 2026

abandoning — we have migrated from Mattermost to Matrix and are no longer maintaining Mattermost patches. Thanks for the reviews. maybe someone wants to take it from here.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 29, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

Codex review: needs maintainer review before merge.

Summary
Review failed before ClawSweeper could summarize the requested change.

Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path.

Next step before merge
Review did not complete, so no work-lane recommendation was made.

Review details

Best possible solution:

Retry the Codex review after fixing the execution failure.

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

Unclear. The review failed before ClawSweeper could establish a reproduction path.

Is this the best way to solve the issue?

Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction.

What I checked:

  • failure reason: codex execution failed.
  • codex failure detail: Codex review failed for this PR with exit 1.
  • codex stdout: Per-item Codex failure; continuing with the rest of the shard.

Likely related people:

  • unknown: Codex failed before it could trace repository history. (role: review did not complete; confidence: low)

Remaining risk / open question:

  • No close action taken because the review did not complete.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 8c7ec5d1f9ff.

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

Labels

channel: mattermost Channel integration: mattermost size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(mattermost): proactive message tool sends ignore thread context

1 participant