Files
openclaw/extensions
iloveleon19 c193ffd554 fix(mattermost): key private channels as group on outbound routing (#96645)
* fix(mattermost): key private channels as group on outbound routing

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 #95646.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cron): route bound-session cron deliveries under the bound namespace

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>🧵<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 (#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 #95646.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(mattermost): key public channels as channel in directory listing

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 #95646 while keeping private channels keyed as `group`.

* fix(mattermost): harden private channel routing

* test: expose cron route selection through production module

* fix. scope cron session reuse to Mattermost delivery

* fix(cron): validate bound delivery peer and channel authority

* fix(cron): capture validated delivery destination peer

---------

Co-authored-by: leon <leon@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-29 02:43:21 -04:00
..