fix(mattermost): key private channels as group on outbound routing#96645
fix(mattermost): key private channels as group on outbound routing#96645iloveleon19 wants to merge 4 commits into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 12:41 PM ET / 16:41 UTC. Summary PR surface: Source +96, Tests +172. Total +268 across 8 files. Reproducibility: yes. at source level: current main maps Mattermost Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Keep this PR as the likely canonical Mattermost fix path, but first avoid caching non-authoritative channel-kind fallbacks and add inspectable redacted session-store or log proof for the private-channel cron delivery path. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main maps Mattermost Is this the best way to solve the issue? No, not as submitted: the owner-boundary fix direction is right, but the target resolver must not cache a non-authoritative Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against cc147c6a4060. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +96, Tests +172. Total +268 across 8 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
a4a3df2 to
1d0e840
Compare
|
Updated with the complete fix + live proof. Expanded scope to the path that actually splits the thread. The earlier revision fixed Deliberately no channel-kind cache. The sibling PR #96521 added a 30-minute channel-kind cache and its author documented two correctness issues with it: a cold-restart regression (empty cache → Live Mattermost proof (private channel, type
Both delivered into the thread. The same split is visible in untouched production data (existing private-channel threads carry both @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
0ba2a22 to
7207b3f
Compare
|
This pull request has been automatically marked as stale due to inactivity. |
7207b3f to
fb2a31a
Compare
A Mattermost private channel (server type `P`) is authoritatively chat_type `group`, but it is addressed as the delivery target `channel:<id>` — the same prefix as a public channel. Inbound classified it correctly as `group`, while outbound/session reconstruction re-derived `channel` from the target string, so one conversation was keyed under two session namespaces (`...:mattermost:group:<id>:thread` inbound vs a phantom `...:channel:<id>:thread` on delivery). Threaded/scheduled deliveries bound to one then failed to match the other (fail-closed delivery, or a conversation split across two session keys). The Mattermost outbound path could not represent `group` at all: resolveMattermostOutboundSessionRoute only produced direct/channel, and resolveMattermostOpaqueTarget only classified user/channel. - session-route: key a conversation as `group` from an authoritative signal — the resolved target kind, an explicit `group:` prefix, or the inbound currentSessionKey peer kind — so outbound shares the inbound `group:<id>` namespace instead of forking `channel:<id>`. - target-resolution: classify a bare channel id by its real channel type (P/G -> group, O -> channel), cached per id. The wire target stays `channel:<id>` (Mattermost posts to the channel id either way; parseMattermostTarget only accepts channel:/user:) — the group distinction lives in the session key. Adds unit coverage for both paths. Resolves openclaw#95646. Co-Authored-By: Claude Opus 4.8 <[email protected]>
An isolated cron run executes under an ephemeral agentSessionKey that does not carry the source conversation's namespace. resolveDirectCronDeliverySessionKey resolved the outbound delivery route from that isolated key, so for a job bound to a real conversation thread (e.g. the gitlab-pipeline-watch recheck bound to agent:...:mattermost:group:<id>:thread:<root>) the lossy channel:<id> target was re-derived as `channel`, forking a phantom channel:<id> session and splitting the private-channel thread across two namespaces (openclaw#95646). Prefer the job's bound conversation identity as the currentSessionKey used to resolve the route (new selectCronRouteCurrentSessionKey helper), so the existing currentSessionKey-based namespace resolution keeps group:<id>. No channel-type cache is introduced — which is what made the cache-based attempts brittle on cold restart (a sibling PR documented exactly that failure mode). Falls back to the isolated key for unbound jobs and cron-namespace bindings. Adds unit coverage. Refs openclaw#95646. Co-Authored-By: Claude Opus 4.8 <[email protected]>
listMattermostDirectoryGroups labeled every joined channel — public `O` and private `P` — as kind `group`. A name-resolved public channel could then be keyed as `mattermost:group:<id>` on outbound routing, forking a phantom group session and splitting the transcript from the inbound `channel:<id>` one. Derive the kind from the authoritative Mattermost channel type (`O` -> channel, `P`/`G` -> group) and add a regression test. This closes the public-channel regression path flagged in review for openclaw#95646 while keeping private channels keyed as `group`.
fb2a31a to
ead7ab6
Compare
|
ClawSweeper status: review started. I am starting a fresh review of this pull request: fix(mattermost): key private channels as group on outbound routing This is item 1/1 in the current shard. Shard 0/1. This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking. Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted. |
What Problem This Solves
Resolves #95646.
Mattermost private channels are authoritatively classified from the channel type as
P/G→groupon inbound paths, but they are addressed on the wire aschannel:<id>, just like publicOchannels. Outbound route reconstruction couldtherefore key one private-channel thread under both:
agent:…:mattermost:group:<id>:thread:<root>(inbound)agent:…:mattermost:channel:<id>:thread:<root>(outbound/cron)The split breaks thread/session continuity, especially when an isolated cron run
reconstructs a route without the bound conversation identity.
Why This Change Was Made
The target string alone is lossy:
channel:<id>cannot distinguish public fromprivate Mattermost channels. This change preserves authoritative information at
each boundary instead of treating the prefix as the chat type:
user lookup returns 404.
groupinformation from theresolved target or the exact canonical current Mattermost session key.
reconstruction, while non-Mattermost providers retain the existing isolated-run
behavior.
Ochannels aschanneland privateP/groupGchannels asgroup.The wire target remains
channel:<id>for all non-DM Mattermost posts.User Impact
group:<id>namespace instead of creating a phantomchannel:<id>namespace.channel:<id>.There is no migration or destructive rewrite. Existing duplicate session entries
remain readable; new affected deliveries stop creating the wrong namespace.
Implementation Notes
Authoritative target classification
The existing opaque-target cache is retained but hardened:
channelfor compatibility but are notcached, so a later request retries the authoritative lookup.
This directly addresses the P1 review finding that a transient channel lookup
failure could pin a private channel as
channelfor the process lifetime.Session-route and cron scope
currentSessionKeyis accepted as an authoritative fallback only when the wholekey matches a canonical Mattermost direct/group/channel session for the same peer.
An unrelated or malformed key cannot leak a
groupclassification.The cron change is intentionally provider-specific. A bound Telegram or other
provider session is not substituted into its adapter's route resolution.
Related work
namespace split.
uses a short, authoritative-only cache.
Evidence
Automated tests
Local verification after rebasing onto
1fc81c2054:Coverage includes:
O→channel,P→group, and the unchangedchannel:<id>wire target;The local host has Node 24.13.0 while the current repository requires Node
24.15.0+, so the package-manager preinstall guard reports that environment
mismatch. The checked-in tool binaries still completed the focused tests, type
checks, lint, and formatting above; hosted CI is the authoritative supported-Node
run.
Redacted deployed behavior
An equivalent deployed bundle patch was exercised against a real private
Mattermost channel. No credentials, message bodies, channel IDs, post IDs, or
session IDs are included below.
Before the cron fix, one redacted thread root had both namespaces:
{"label":"unpatched","matchingEntries":2,"kinds":["channel","group"],"allThreadScoped":true}After the cron fix, the patched proof thread retained only the authoritative
namespace:
{"label":"patched","matchingEntries":1,"kinds":["group"],"allThreadScoped":true}These values were regenerated from the current persisted session store using a
read-only script that emits only counts, enum kinds, and a thread-scope boolean.
The raw keys and entry contents were never printed. The deployed gateway was later
rebuilt/restarted and the persisted result remains the same.
The previously attached visual proof shows the scheduled reply landing in the
private-channel thread:
A new live post was not created solely for testing, per repository guidance.
Rollback
Revert the four commits in this PR. No data migration or cleanup is required;
rollback restores the previous route reconstruction behavior.