fix(mattermost): prefer metadata thread root over stale reply_to - #12299
fix(mattermost): prefer metadata thread root over stale reply_to#12299imapotato123 wants to merge 1 commit into
Conversation
|
✓ Automated scan: no security concerns. Mattermost threading fix. |
877fe2c to
408e701
Compare
|
Are there any merger plans? |
|
Heads-up — the original PR included three fixes; the |
408e701 to
5ffb964
Compare
|
Rebased onto current main (clean merge). Also cleaned up the commit message to accurately reflect the two fixes in the diff. This is a small change (+19/−5) fixing two thread-handling bugs in the Mattermost platform — happy to address any feedback. Would appreciate a review when someone has a chance. 🙏 |
5ffb964 to
95bae59
Compare
|
Rebased onto current main. This is a small 2-fix PR for Mattermost threading (root_id resolution + per-thread session keying). Would appreciate a review when you get a chance. cc @teknium1 |
cc0f3d7 to
72a05e7
Compare
72a05e7 to
0a1feb9
Compare
|
Rebased/refreshed this PR onto current Changes in the refreshed branch:
Validation:
I also tried the broader |
0a1feb9 to
e54003a
Compare
|
Refreshed this PR onto current Validation from the refreshed branch:
|
Independent confirmation from a real deploymentJust hit this bug on a self-hosted Mattermost install (LXC running Hermes off Repro before the patch (
|
Top-level Mattermost posts in reply_mode=thread were keyed by user_id while thread replies were keyed by thread_id, so the same user in the same channel landed in two separate sessions and the bot lost context across turns. Prefer an explicit thread root from metadata on send, and key top-level posts in thread mode by their own post id on receive. Carries upstream PR NousResearch#12299 (still open). Runtime fix only; upstream PR test hunks omitted (refactored test file diverged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified: diff confirms the two-fix mechanism ( Competing/related cluster on the same session-isolation mechanism: #18280 and #29993 (both OPEN, same root/session-keying approach), #12283 (superseded). This PR (#12299) is the earliest open one and looks canonical (smallest focused diff, composes with upstream's |
|
Thanks for the focused Mattermost investigation. Current main already contains the inbound channel-thread isolation, but the remaining outbound metadata-precedence case needs a narrow port. Problems
Suggested changes
Automated hermes-sweeper review. |
Outbound Mattermost deliveries resolve the thread root from `reply_to` first, falling back to `metadata["thread_id"]` only when `reply_to` is absent. When both are present and disagree — delayed or progress deliveries where `reply_to` points at a post in another thread — the message lands in the wrong thread. `metadata["thread_id"]` is the thread root recorded when the inbound post was received, so treat it as authoritative and fall back to `reply_to` only when metadata carries no root. Inbound channel-thread isolation and the DM guard already landed on main in 5a0e0d3, so this ports only the remaining outbound case into `_thread_root_for_send()`. Tests: conflicting `reply_to` + `metadata["thread_id"]`, and the `reply_to`-only fallback. Existing DM regression coverage unchanged.
e54003a to
6d8e242
Compare
|
@teknium1 — reworked to the scope you outlined. Rebased onto Addressing each point:
Description updated to match the new scope, and the title now reflects the outbound fix rather than the original session-isolation framing. |
|
Small correction on the triage note above, since it affects how these two get compared. The note says #20874 "resolves metadata_root_id = None
if isinstance(metadata, dict):
metadata_root_id = metadata.get("thread_id") or metadata.get("root_id")
if metadata_root_id:
return str(metadata_root_id)
if not reply_to:
return NoneThat's the same precedence this PR adopts, so the two aren't in policy conflict. The note may have been written against an earlier revision. Given that, I don't think "not a duplicate" holds for the reason stated. #20874 is a superset — it adds configurable auto-threading, channel-type caching, One behavioral difference does survive, and it runs the other way: #20874 returns Happy to close this as superseded and land that hardening as a one-line change on #20874 instead — that's likely the faster path to a fix in main. Deferring to whichever the maintainers prefer. |
|
Closing this as superseded by #20874. As covered above, #20874 adopts the same metadata-over- The one exception was the Thanks @alt-glitch and @teknium1 for the triage and review, and @johnkattenhorn for reproducing it on a real deployment — that's what moved it from "probably real" to actionable. |
Reworked per the hermes-sweeper review to the narrow salvageable scope.
What changed
Rebased onto current
main(d83e85850) and reduced to the single outbound fix. The adapter path migration toplugins/platforms/mattermost/adapter.py(af973e407) is picked up, so the branch no longer conflicts.Dropped — inbound channel-thread isolation, which landed on main in
5a0e0d35bwith coverage attests/gateway/test_mattermost.py:1029-1085. The DM guard atadapter.py:869-876is untouched, so DM sessions keep their existing behavior and thetest_mattermost_dm_post_does_not_seed_thread_rootregression still passes.Kept — the outbound metadata-precedence case in
_thread_root_for_send()(adapter.py:181-194), where main still prefersreply_toover metadata.The remaining bug
_thread_root_for_send()readsreply_tofirst and consultsmetadata["thread_id"]only whenreply_tois empty. When both are present and disagree — delayed or progress deliveries wherereply_topoints at a post in a different thread — the message is posted under the wrong root.metadata["thread_id"]is the thread root recorded when the inbound post was received, so this makes it authoritative and falls back toreply_toonly when metadata carries no root. The fallback path is unchanged for ordinary replies.Tests
Two added, per the review request:
test_send_prefers_metadata_thread_id_over_conflicting_reply_to— conflictingreply_to+metadata["thread_id"]. Also asserts the stalereply_tonever reaches_resolve_root_id(), which drops an API round-trip on this path.test_send_falls_back_to_reply_to_without_metadata_thread_id— guards the fallback.Verified the first test fails on unpatched
mainwithassert 'other_thread_post_999' == 'root_post_123', and passes with the change.Also clean:
python3 -m py_compileon both files,git diff --check.Diff is +49/−3 across 2 files. The previous branch tip is preserved at
imapotato123:backup/pr12299-pre-reworkif any of the dropped work is wanted later.