fix(mattermost): persist threadId in delivery context for all session types#52236
fix(mattermost): persist threadId in delivery context for all session types#52236teconomix wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a thread-routing regression for cron/heartbeat wakeups in Mattermost channel sessions. The root cause was that The fix is minimal and correct:
Confidence Score: 5/5
Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'fork/main'..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
https://github.com/openclaw/openclaw/blob/5504b8172942a361a7fcb7d063d9723dbc96c745/extensions/mattermost/src/mattermost/monitor.ts#L1354-L1357
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".
| // 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). |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
29ac697 to
3658428
Compare
There was a problem hiding this comment.
💡 Codex Review
https://github.com/openclaw/openclaw/blob/3658428580aad57541d4979dd578e66f6447b4e9/extensions/mattermost/src/mattermost/monitor.ts#L1356-L1357
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
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".
|
Leaving as draft pending a more complete fix. The Two remaining issues:
|
… 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.
3658428 to
30ae81e
Compare
|
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. |
|
This pull request has been automatically marked as stale due to inactivity. |
|
Codex review: needs maintainer review before merge. Summary Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path. Next step before merge Review detailsBest 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:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 8c7ec5d1f9ff. |
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
updateLastRouteinmonitor.tswas guarded byif (kind === "direct"), so delivery context (includingthreadId) was only persisted for DM sessions. For group/channel sessions with threads,deliveryContext.threadIdwas never stored in the session store.When a cron fires into a thread-scoped session,
dispatch-from-config.tsrecovers the thread ID viaparseSessionThreadInfo(runSessionKey)— which correctly extracts the thread ID from the session key string. However, withoutthreadIdstored indeliveryContext, the delivery has no thread context when looking up the session entry.Fix
Remove the
kind === "direct"guard fromupdateLastRouteso delivery context is persisted for all session types. This ensuresthreadIdis stored whenever an inbound message is processed in a thread-scoped session.How to test
replyToMode: "all"--session main --session-key <thread-session-key>Related: #45082 (case 2), companion to #52120 (case 1 — message tool)
Closes #45082