fix(mattermost): resolve private-channel chat_type from the real channel kind, not the lossy target prefix#96521
Conversation
…nel kind, not the lossy target prefix
|
Codex review: needs real behavior proof before merge. Reviewed June 27, 2026, 11:10 AM ET / 15:10 UTC. Summary PR surface: Source +75, Tests +219. Total +294 across 11 files. Reproducibility: yes. for source-level reproduction: current main maps Mattermost private channel types Review metrics: 2 noteworthy metrics.
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: Land one canonical Mattermost-owned fix for #95646 that derives private-channel session identity from authoritative channel type or current route/session metadata, preserves Do we have a high-confidence way to reproduce the issue? Yes for source-level reproduction: current main maps Mattermost private channel types Is this the best way to solve the issue? No as submitted. The cache-based path is a plausible mitigation, but the best fix should use authoritative route/session evidence without a longer-lived side cache and should be proven in real Mattermost private and public channels. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4010b81a77f4. Label changesLabel justifications:
Evidence reviewedPR surface: Source +75, Tests +219. Total +294 across 11 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
|
Added tests for resolving group targets and ensuring correct namespaces.
…hatType group Fix missing newline at end of session-route.test.ts
Behavior proof — P1 fix: route resolver now propagates
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Add caching mechanism for Mattermost channel kind resolution
Add cache size limit for channel-kind entries.
|
[P2] channel-kind-store.ts (commit: "Implement maximum cache size") — @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Added cache expiration logic for channel kinds.
|
Behavior proof — 25/25 assertions passed (Node.js self-contained routing simulation, @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Updated resolvedTarget structure in session-route tests.
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Fixed check-test-types CI (f5a649c): removed excess |
…ssion-route tests
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Closing this PR. The root-cause analysis is correct — Mattermost private channels get split across `group:` (inbound) and `channel:` (outbound) session namespaces — but the fix has two unresolved correctness issues and a proof blocker I can't clear without a live Mattermost environment:
For whoever picks this up: the cache-based approach in `session-route.ts` is on the right track — use `currentSessionKey` identity as a secondary signal before the cache fallback, align cache TTL with `resolveChannelInfo`, and validate with a real Mattermost workspace. Closes #95646 remains open. |
Fixes #95646
Related: #95645
What Problem This Solves
The
chat_typeOpenClaw reports for a Mattermost private channel is non-deterministic across code paths: it isgroupwhen derived from the channel's real type (the authoritative inbound path), butchannelwhen derived from the delivery target string (the lossy outbound/heartbeat path). Mattermost addresses private channels aschannel:<id>— the same prefix as a public channel — so the lossy path can't tell them apart. The same conversation can end up keyed under two different session namespaces (group:<id>:thread:<root>from inbound,channel:<id>:thread:<root>from a delivery-side session), causing threaded/scheduled deliveries bound to one namespace to fail to match the other.Why This Change Was Made
OpenClaw already has a per-plugin extension point for exactly this —
messaging.inferTargetChatType({ to })— implemented by Telegram, Discord, Signal, iMessage, and ClickClack, but not Mattermost. Two problems needed fixing together:D/O/P/G) is only known on the inbound path, resolved asynchronously viaresolveChannelInfo(inmonitor-resources.ts) against the Mattermost API.inferTargetChatTypeis a synchronous, best-effort hook ((params: { to: string }) => ChatType | undefined) — it can't make a network call. Newextensions/mattermost/src/mattermost/channel-kind-store.tsadds a tiny process-wide cache (rememberMattermostChannelKind/peekMattermostChannelKind) thatresolveChannelInfonow populates as a side effect of its existing async resolution, whenever it has a fresh answer. The newinferTargetChatTypehook on Mattermost'schannel.tsreads from this cache synchronously — correct once the channel has been seen at least once (the common case), falling back toundefined(today's lossy default) otherwise, never worse than before.src/infra/outbound/targets.ts(inferChatTypeFromTarget) andsrc/agents/subagent-announce-delivery.ts(inferDeliveryTargetChatType) returned the hardcoded"channel"for anychannel:/thread:-prefixed target before checkingplugin.messaging.inferTargetChatType, so even a correctly implemented hook would never be reached for that shape. Both now consult the plugin hook first and only fall back to"channel"when the hook has no answer.User Impact
Mattermost private-channel conversations get a consistent, correct
groupchat type instead of being split across two session namespaces depending on whether the path was inbound or outbound/heartbeat delivery. Public channels (O) are unaffected — they correctly remainchannel. Other channels are unaffected: the generic dispatcher change is purely additive (consult the hook first, same fallback as before when the hook returns nothing), and no other current plugin'sinferTargetChatTypeimplementation returns a different answer forchannel:/thread:-shaped targets than the existing default.Evidence
Behavior addressed: Mattermost private channels (
P) and true multi-user groups (G) reportchat_type: "channel"on the outbound/heartbeat/delivery path instead of the authoritative"group", because (a) Mattermost has noinferTargetChatTypehook and (b) the generic dispatcher never reaches a plugin hook forchannel:-prefixed targets regardless.Real environment tested: macOS arm64 (M4), Darwin 25.5.0, Node v25.9.0, branch
fix/mattermost-private-channel-chat-type, commit3cb04b1c4e.Exact steps or command run after this patch:
Evidence after fix:
Observed result after fix: added 7 new regression tests covering: (1)
resolveChannelInforemembers a private channel's kind as"group"in the new store, (2) Mattermost'sinferTargetChatTypereturns the remembered kind for achannel:target andundefinedwhen never seen, (3)inferChatTypeFromTarget(viaresolveHeartbeatDeliveryTargetWithSessionRoute) now returns the plugin's override instead of the hardcoded"channel"for achannel:target, with a paired test confirming the"channel"fallback still applies when the plugin has no hook, (4) the same two cases forinferDeliveryTargetChatTypeinsubagent-announce-delivery.ts(exposed via the existingtestingnamespace for direct unit coverage, since its only current caller —isDirectMessageDeliveryTarget— only checks for"direct"and doesn't otherwise observe the group/channel distinction). Confirmed all new tests fail against the pre-fix code (by reverting each source file individually while keeping its test) and pass after restoring the fix.What was not tested: did not exercise this against a live Mattermost server (no credentials/test workspace in this environment) — this is a classifier/cache-wiring unit fix verified against the exact derivation mismatch described in the issue, not a live end-to-end private-channel round trip. The
inferDeliveryTargetChatTypefix insubagent-announce-delivery.tscurrently has no externally observable effect through its only call site (isDirectMessageDeliveryTargetonly distinguishes direct vs. non-direct) — it's included for derivation consistency with the documented bug pattern and to be correct for any future caller that does care about the group/channel distinction.