Skip to content

Commit 6635091

Browse files
committed
fix(session): honor per-binding session scope for outbound base session keys
buildOutboundBaseSessionKey now resolves the matching binding's dmScope/groupScope override (via a shared findMatchingBinding extracted from resolveAgentRoute) instead of using only the global session config, so inbound and outbound resolve to the same session for peers/groups with a per-binding override. Addresses PR review.
1 parent 326ac5a commit 6635091

3 files changed

Lines changed: 346 additions & 93 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Verifies outbound base-session keys honor per-binding session-scope
2+
// overrides so outbound-only sends resolve to the same session as inbound.
3+
import { describe, expect, it } from "vitest";
4+
import type { OpenClawConfig } from "../../config/config.js";
5+
import { buildOutboundBaseSessionKey } from "./base-session-key.js";
6+
7+
describe("buildOutboundBaseSessionKey per-binding session scope", () => {
8+
it("folds a group into main via a per-binding groupScope override against a per-group default", () => {
9+
const cfg: OpenClawConfig = {
10+
session: { groupScope: "per-group" },
11+
bindings: [
12+
{
13+
type: "route",
14+
agentId: "main",
15+
match: {
16+
channel: "telegram",
17+
accountId: "default",
18+
peer: { kind: "group", id: "-100folded" },
19+
},
20+
session: { groupScope: "main" },
21+
},
22+
],
23+
};
24+
25+
// Bound group folds into main per its override.
26+
expect(
27+
buildOutboundBaseSessionKey({
28+
cfg,
29+
agentId: "main",
30+
channel: "telegram",
31+
accountId: "default",
32+
peer: { kind: "group", id: "-100folded" },
33+
}),
34+
).toBe("agent:main:main");
35+
36+
// Other groups keep their own key under the per-group default.
37+
expect(
38+
buildOutboundBaseSessionKey({
39+
cfg,
40+
agentId: "main",
41+
channel: "telegram",
42+
accountId: "default",
43+
peer: { kind: "group", id: "-100other" },
44+
}),
45+
).toBe("agent:main:telegram:group:-100other");
46+
});
47+
48+
it("keeps a group on its own key via a per-binding groupScope override against a main default", () => {
49+
const cfg: OpenClawConfig = {
50+
session: { groupScope: "main" },
51+
bindings: [
52+
{
53+
type: "route",
54+
agentId: "main",
55+
match: {
56+
channel: "telegram",
57+
accountId: "default",
58+
peer: { kind: "group", id: "-100separate" },
59+
},
60+
session: { groupScope: "per-group" },
61+
},
62+
],
63+
};
64+
65+
// Bound group stays on its own key per its override.
66+
expect(
67+
buildOutboundBaseSessionKey({
68+
cfg,
69+
agentId: "main",
70+
channel: "telegram",
71+
accountId: "default",
72+
peer: { kind: "group", id: "-100separate" },
73+
}),
74+
).toBe("agent:main:telegram:group:-100separate");
75+
76+
// Other groups fold into main under the global default.
77+
expect(
78+
buildOutboundBaseSessionKey({
79+
cfg,
80+
agentId: "main",
81+
channel: "telegram",
82+
accountId: "default",
83+
peer: { kind: "group", id: "-100other" },
84+
}),
85+
).toBe("agent:main:main");
86+
});
87+
88+
it("honors a per-binding dmScope override for a direct peer", () => {
89+
const cfg: OpenClawConfig = {
90+
session: { dmScope: "main" },
91+
bindings: [
92+
{
93+
type: "route",
94+
agentId: "main",
95+
match: {
96+
channel: "telegram",
97+
accountId: "default",
98+
peer: { kind: "direct", id: "123" },
99+
},
100+
session: { dmScope: "per-channel-peer" },
101+
},
102+
],
103+
};
104+
105+
// Bound DM peer keeps its own per-channel-peer key per its override.
106+
expect(
107+
buildOutboundBaseSessionKey({
108+
cfg,
109+
agentId: "main",
110+
channel: "telegram",
111+
accountId: "default",
112+
peer: { kind: "direct", id: "123" },
113+
}),
114+
).toBe("agent:main:telegram:direct:123");
115+
116+
// Other DM peers fold into main under the global default.
117+
expect(
118+
buildOutboundBaseSessionKey({
119+
cfg,
120+
agentId: "main",
121+
channel: "telegram",
122+
accountId: "default",
123+
peer: { kind: "direct", id: "999" },
124+
}),
125+
).toBe("agent:main:main");
126+
});
127+
128+
it("falls back to global session scope when no binding matches", () => {
129+
const perGroupCfg: OpenClawConfig = { session: { groupScope: "per-group" } };
130+
expect(
131+
buildOutboundBaseSessionKey({
132+
cfg: perGroupCfg,
133+
agentId: "main",
134+
channel: "telegram",
135+
accountId: "default",
136+
peer: { kind: "group", id: "-100plain" },
137+
}),
138+
).toBe("agent:main:telegram:group:-100plain");
139+
140+
const mainCfg: OpenClawConfig = { session: { groupScope: "main" } };
141+
expect(
142+
buildOutboundBaseSessionKey({
143+
cfg: mainCfg,
144+
agentId: "main",
145+
channel: "telegram",
146+
accountId: "default",
147+
peer: { kind: "group", id: "-100plain" },
148+
}),
149+
).toBe("agent:main:main");
150+
151+
// Preserves the caller's explicit agentId rather than the binding's agent.
152+
expect(
153+
buildOutboundBaseSessionKey({
154+
cfg: perGroupCfg,
155+
agentId: "scribe",
156+
channel: "telegram",
157+
accountId: "default",
158+
peer: { kind: "group", id: "-100plain" },
159+
}),
160+
).toBe("agent:scribe:telegram:group:-100plain");
161+
});
162+
});
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
// Base session-key helper keeps outbound-only delivery aligned with route
22
// resolution session-scope rules.
33
import type { OpenClawConfig } from "../../config/types.openclaw.js";
4-
import { buildAgentSessionKey, type RoutePeer } from "../../routing/resolve-route.js";
4+
import {
5+
buildAgentSessionKey,
6+
resolveOutboundBindingSessionScope,
7+
type RoutePeer,
8+
} from "../../routing/resolve-route.js";
59

610
/**
711
* Builds the canonical outbound base-session key for a resolved route peer.
812
*
913
* Mirrors the routing layer's session-scope rules so outbound-only sends and
10-
* inbound route resolution keep the same `dmScope` and identity-link behavior.
14+
* inbound route resolution keep the same `dmScope`, `groupScope`, and
15+
* identity-link behavior. The session scope is resolved from the matching
16+
* binding's per-binding `session` override (when present) falling back to the
17+
* global `session` config, so a peer/group with a per-binding override routes
18+
* to the same session inbound and outbound.
1119
*/
1220
export function buildOutboundBaseSessionKey(params: {
1321
cfg: OpenClawConfig;
@@ -16,13 +24,19 @@ export function buildOutboundBaseSessionKey(params: {
1624
accountId?: string | null;
1725
peer: RoutePeer;
1826
}): string {
27+
const override = resolveOutboundBindingSessionScope(
28+
params.cfg,
29+
params.channel,
30+
params.accountId,
31+
params.peer,
32+
);
1933
return buildAgentSessionKey({
2034
agentId: params.agentId,
2135
channel: params.channel,
2236
accountId: params.accountId,
2337
peer: params.peer,
24-
dmScope: params.cfg.session?.dmScope ?? "main",
25-
groupScope: params.cfg.session?.groupScope,
38+
dmScope: override?.dmScope ?? params.cfg.session?.dmScope ?? "main",
39+
groupScope: override?.groupScope ?? params.cfg.session?.groupScope,
2640
identityLinks: params.cfg.session?.identityLinks,
2741
});
2842
}

0 commit comments

Comments
 (0)