Skip to content

Commit cab9efc

Browse files
committed
fix(mattermost): key resolved group targets under group: namespace (#95646)
A private channel / group DM is classified group by the inbound path (mapMattermostChannelKind: P/G -> group) but the outbound session route flattened every non-user target to channel, splitting the same conversation across group:<id> and a phantom channel:<id> session key. Honor an authoritative resolved group kind so delivery keys match inbound. A bare channel:<id> target with no resolved kind still resolves to channel since the real channel type is not knowable from the target string alone; full coverage of that path needs live Mattermost repro.
1 parent a594d2c commit cab9efc

2 files changed

Lines changed: 33 additions & 3 deletions

File tree

extensions/mattermost/src/session-route.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ describe("mattermost session route", () => {
4343
expect(channelRoute.sessionKey).toContain("thread456");
4444
});
4545

46+
it("keys a resolved group target under group:, matching inbound classification (#95646)", () => {
47+
const route = resolveMattermostOutboundSessionRoute({
48+
cfg: {},
49+
agentId: "main",
50+
accountId: "acct-1",
51+
target: "mattermost:channel:priv123",
52+
resolvedTarget: { kind: "group", to: "channel:priv123" },
53+
});
54+
55+
const groupRoute = expectRoute(route);
56+
expect(groupRoute.peer.kind).toBe("group");
57+
expect(groupRoute.peer.id).toBe("priv123");
58+
expect(groupRoute.chatType).toBe("group");
59+
expect(groupRoute.from).toBe("mattermost:group:priv123");
60+
// Delivery target string stays channel: — Mattermost addresses all channels
61+
// (including private) by id; only the session namespace is type-aware.
62+
expect(groupRoute.to).toBe("channel:priv123");
63+
expect(groupRoute.baseSessionKey).toContain(":group:priv123");
64+
expect(groupRoute.baseSessionKey).not.toContain(":channel:priv123");
65+
});
66+
4667
it("recovers channel thread routes from currentSessionKey", () => {
4768
const route = resolveMattermostOutboundSessionRoute({
4869
cfg: {},

extensions/mattermost/src/session-route.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,33 @@ export function resolveMattermostOutboundSessionRoute(params: ChannelOutboundSes
2020
(resolvedKind !== "channel" &&
2121
resolvedKind !== "group" &&
2222
(lower.startsWith("user:") || trimmed.startsWith("@")));
23+
// Honor an authoritative `group` kind (private channel / group DM, from the
24+
// directory resolver) instead of flattening every non-user target to
25+
// `channel`. The inbound path keys these under `group:<id>` via
26+
// mapMattermostChannelKind (P/G -> group); a `channel:<id>` outbound route
27+
// would split the same conversation across two session namespaces (#95646).
28+
// A bare `channel:<id>` with no resolved kind stays `channel` since the real
29+
// channel type is not knowable from the target string alone.
30+
const isGroup = !isUser && resolvedKind === "group";
2331
if (trimmed.startsWith("@")) {
2432
trimmed = trimmed.slice(1).trim();
2533
}
2634
const rawId = stripTargetKindPrefix(trimmed);
2735
if (!rawId) {
2836
return null;
2937
}
38+
const kind = isUser ? "direct" : isGroup ? "group" : "channel";
3039
const baseRoute = buildChannelOutboundSessionRoute({
3140
cfg: params.cfg,
3241
agentId: params.agentId,
3342
channel: "mattermost",
3443
accountId: params.accountId,
3544
peer: {
36-
kind: isUser ? "direct" : "channel",
45+
kind,
3746
id: rawId,
3847
},
39-
chatType: isUser ? "direct" : "channel",
40-
from: isUser ? `mattermost:${rawId}` : `mattermost:channel:${rawId}`,
48+
chatType: kind,
49+
from: isUser ? `mattermost:${rawId}` : `mattermost:${kind}:${rawId}`,
4150
to: isUser ? `user:${rawId}` : `channel:${rawId}`,
4251
});
4352
return buildThreadAwareOutboundSessionRoute({

0 commit comments

Comments
 (0)