Skip to content

Commit c1d07b0

Browse files
committed
refactor(discord): extract route resolution helpers
1 parent 269cc22 commit c1d07b0

File tree

4 files changed

+221
-54
lines changed

4 files changed

+221
-54
lines changed

src/discord/monitor/message-handler.preflight.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
2929
import { logDebug } from "../../logger.js";
3030
import { getChildLogger } from "../../logging.js";
3131
import { buildPairingReply } from "../../pairing/pairing-messages.js";
32-
import { resolveAgentRoute } from "../../routing/resolve-route.js";
33-
import { DEFAULT_ACCOUNT_ID, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
32+
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
3433
import { fetchPluralKitMessageInfo } from "../pluralkit.js";
3534
import { sendMessageDiscord } from "../send.js";
3635
import {
@@ -60,6 +59,11 @@ import {
6059
resolveDiscordMessageText,
6160
} from "./message-utils.js";
6261
import { resolveDiscordPreflightAudioMentionContext } from "./preflight-audio.js";
62+
import {
63+
buildDiscordRoutePeer,
64+
resolveDiscordConversationRoute,
65+
resolveDiscordEffectiveRoute,
66+
} from "./route-resolution.js";
6367
import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js";
6468
import { resolveDiscordSystemEvent } from "./system-events.js";
6569
import { isRecentlyUnboundThreadWebhookMessage } from "./thread-bindings.js";
@@ -333,18 +337,18 @@ export async function preflightDiscordMessage(
333337
? params.data.rawMember.roles.map((roleId: string) => String(roleId))
334338
: [];
335339
const freshCfg = loadConfig();
336-
const route = resolveAgentRoute({
340+
const route = resolveDiscordConversationRoute({
337341
cfg: freshCfg,
338-
channel: "discord",
339342
accountId: params.accountId,
340343
guildId: params.data.guild_id ?? undefined,
341344
memberRoleIds,
342-
peer: {
343-
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
344-
id: isDirectMessage ? author.id : messageChannelId,
345-
},
346-
// Pass parent peer for thread binding inheritance
347-
parentPeer: earlyThreadParentId ? { kind: "channel", id: earlyThreadParentId } : undefined,
345+
peer: buildDiscordRoutePeer({
346+
isDirectMessage,
347+
isGroupDm,
348+
directUserId: author.id,
349+
conversationId: messageChannelId,
350+
}),
351+
parentConversationId: earlyThreadParentId,
348352
});
349353
let threadBinding: SessionBindingRecord | undefined;
350354
threadBinding =
@@ -381,15 +385,13 @@ export async function preflightDiscordMessage(
381385
return null;
382386
}
383387
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
384-
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
385-
const effectiveRoute = boundSessionKey
386-
? {
387-
...route,
388-
sessionKey: boundSessionKey,
389-
agentId: boundAgentId ?? route.agentId,
390-
matchedBy: "binding.channel" as const,
391-
}
392-
: (configuredRoute?.route ?? route);
388+
const effectiveRoute = resolveDiscordEffectiveRoute({
389+
route,
390+
boundSessionKey,
391+
configuredRoute,
392+
matchedBy: "binding.channel",
393+
});
394+
const boundAgentId = boundSessionKey ? effectiveRoute.agentId : undefined;
393395
const isBoundThreadSession = Boolean(boundSessionKey && earlyThreadChannel);
394396
if (
395397
isBoundThreadBotSystemMessage({

src/discord/monitor/native-command.ts

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
5252
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
5353
import { buildPairingReply } from "../../pairing/pairing-messages.js";
5454
import { executePluginCommand, matchPluginCommand } from "../../plugins/commands.js";
55-
import { resolveAgentRoute } from "../../routing/resolve-route.js";
56-
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
55+
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
5756
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
5857
import { chunkItems } from "../../utils/chunk-items.js";
5958
import { withTimeout } from "../../utils/with-timeout.js";
@@ -86,6 +85,11 @@ import {
8685
toDiscordModelPickerMessagePayload,
8786
type DiscordModelPickerCommandContext,
8887
} from "./model-picker.js";
88+
import {
89+
buildDiscordRoutePeer,
90+
resolveDiscordConversationRoute,
91+
resolveDiscordEffectiveRoute,
92+
} from "./route-resolution.js";
8993
import { resolveDiscordSenderIdentity } from "./sender-identity.js";
9094
import type { ThreadBindingManager } from "./thread-bindings.js";
9195
import { resolveDiscordThreadParentInfo } from "./threading.js";
@@ -448,36 +452,32 @@ async function resolveDiscordModelPickerRoute(params: {
448452
threadParentId = parentInfo.id;
449453
}
450454

451-
const route = resolveAgentRoute({
455+
const route = resolveDiscordConversationRoute({
452456
cfg,
453-
channel: "discord",
454457
accountId,
455458
guildId: interaction.guild?.id ?? undefined,
456459
memberRoleIds,
457-
peer: {
458-
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
459-
id: isDirectMessage ? (interaction.user?.id ?? rawChannelId) : rawChannelId,
460-
},
461-
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
460+
peer: buildDiscordRoutePeer({
461+
isDirectMessage,
462+
isGroupDm,
463+
directUserId: interaction.user?.id ?? rawChannelId,
464+
conversationId: rawChannelId,
465+
}),
466+
parentConversationId: threadParentId,
462467
});
463468

464469
const threadBinding = isThreadChannel
465470
? params.threadBindings.getByThreadId(rawChannelId)
466471
: undefined;
467-
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
468-
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
469-
return boundSessionKey
470-
? {
471-
...route,
472-
sessionKey: boundSessionKey,
473-
agentId: boundAgentId ?? route.agentId,
474-
}
475-
: route;
472+
return resolveDiscordEffectiveRoute({
473+
route,
474+
boundSessionKey: threadBinding?.targetSessionKey,
475+
});
476476
}
477477

478478
function resolveDiscordModelPickerCurrentModel(params: {
479479
cfg: ReturnType<typeof loadConfig>;
480-
route: ReturnType<typeof resolveAgentRoute>;
480+
route: ResolvedAgentRoute;
481481
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
482482
}): string {
483483
const fallback = buildDiscordModelPickerCurrentModel(
@@ -1606,17 +1606,18 @@ async function dispatchDiscordCommandInteraction(params: {
16061606
const isGuild = Boolean(interaction.guild);
16071607
const channelId = rawChannelId || "unknown";
16081608
const interactionId = interaction.rawData.id;
1609-
const route = resolveAgentRoute({
1609+
const route = resolveDiscordConversationRoute({
16101610
cfg,
1611-
channel: "discord",
16121611
accountId,
16131612
guildId: interaction.guild?.id ?? undefined,
16141613
memberRoleIds,
1615-
peer: {
1616-
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
1617-
id: isDirectMessage ? user.id : channelId,
1618-
},
1619-
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
1614+
peer: buildDiscordRoutePeer({
1615+
isDirectMessage,
1616+
isGroupDm,
1617+
directUserId: user.id,
1618+
conversationId: channelId,
1619+
}),
1620+
parentConversationId: threadParentId,
16201621
});
16211622
const threadBinding = isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined;
16221623
const configuredRoute =
@@ -1646,15 +1647,12 @@ async function dispatchDiscordCommandInteraction(params: {
16461647
}
16471648
const configuredBoundSessionKey = configuredRoute?.boundSessionKey?.trim() || undefined;
16481649
const boundSessionKey = threadBinding?.targetSessionKey?.trim() || configuredBoundSessionKey;
1649-
const boundAgentId = boundSessionKey ? resolveAgentIdFromSessionKey(boundSessionKey) : undefined;
1650-
const effectiveRoute = boundSessionKey
1651-
? {
1652-
...route,
1653-
sessionKey: boundSessionKey,
1654-
agentId: boundAgentId ?? route.agentId,
1655-
...(configuredBinding ? { matchedBy: "binding.channel" as const } : {}),
1656-
}
1657-
: (configuredRoute?.route ?? route);
1650+
const effectiveRoute = resolveDiscordEffectiveRoute({
1651+
route,
1652+
boundSessionKey,
1653+
configuredRoute,
1654+
matchedBy: configuredBinding ? "binding.channel" : undefined,
1655+
});
16581656
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
16591657
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
16601658
channelConfig,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { OpenClawConfig } from "../../config/config.js";
3+
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
4+
import {
5+
buildDiscordRoutePeer,
6+
resolveDiscordConversationRoute,
7+
resolveDiscordEffectiveRoute,
8+
} from "./route-resolution.js";
9+
10+
describe("discord route resolution helpers", () => {
11+
it("builds a direct peer from DM metadata", () => {
12+
expect(
13+
buildDiscordRoutePeer({
14+
isDirectMessage: true,
15+
isGroupDm: false,
16+
directUserId: "user-1",
17+
conversationId: "channel-1",
18+
}),
19+
).toEqual({
20+
kind: "direct",
21+
id: "user-1",
22+
});
23+
});
24+
25+
it("resolves bound session keys on top of the routed session", () => {
26+
const route: ResolvedAgentRoute = {
27+
agentId: "main",
28+
channel: "discord",
29+
accountId: "default",
30+
sessionKey: "agent:main:discord:channel:c1",
31+
mainSessionKey: "agent:main:main",
32+
matchedBy: "default",
33+
};
34+
35+
expect(
36+
resolveDiscordEffectiveRoute({
37+
route,
38+
boundSessionKey: "agent:worker:discord:channel:c1",
39+
matchedBy: "binding.channel",
40+
}),
41+
).toEqual({
42+
...route,
43+
agentId: "worker",
44+
sessionKey: "agent:worker:discord:channel:c1",
45+
matchedBy: "binding.channel",
46+
});
47+
});
48+
49+
it("falls back to configured route when no bound session exists", () => {
50+
const route: ResolvedAgentRoute = {
51+
agentId: "main",
52+
channel: "discord",
53+
accountId: "default",
54+
sessionKey: "agent:main:discord:channel:c1",
55+
mainSessionKey: "agent:main:main",
56+
matchedBy: "default",
57+
};
58+
const configuredRoute = {
59+
route: {
60+
...route,
61+
agentId: "worker",
62+
sessionKey: "agent:worker:discord:channel:c1",
63+
mainSessionKey: "agent:worker:main",
64+
matchedBy: "binding.peer" as const,
65+
},
66+
};
67+
68+
expect(
69+
resolveDiscordEffectiveRoute({
70+
route,
71+
configuredRoute,
72+
}),
73+
).toEqual(configuredRoute.route);
74+
});
75+
76+
it("resolves the same route shape as the inline Discord route inputs", () => {
77+
const cfg: OpenClawConfig = {
78+
agents: {
79+
list: [{ id: "worker" }],
80+
},
81+
bindings: [
82+
{
83+
agentId: "worker",
84+
match: {
85+
channel: "discord",
86+
accountId: "default",
87+
peer: { kind: "channel", id: "c1" },
88+
},
89+
},
90+
],
91+
};
92+
93+
expect(
94+
resolveDiscordConversationRoute({
95+
cfg,
96+
accountId: "default",
97+
guildId: "g1",
98+
memberRoleIds: [],
99+
peer: { kind: "channel", id: "c1" },
100+
}),
101+
).toMatchObject({
102+
agentId: "worker",
103+
sessionKey: "agent:worker:discord:channel:c1",
104+
matchedBy: "binding.peer",
105+
});
106+
});
107+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { OpenClawConfig } from "../../config/config.js";
2+
import {
3+
resolveAgentRoute,
4+
type ResolvedAgentRoute,
5+
type RoutePeer,
6+
} from "../../routing/resolve-route.js";
7+
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
8+
9+
export function buildDiscordRoutePeer(params: {
10+
isDirectMessage: boolean;
11+
isGroupDm: boolean;
12+
directUserId?: string | null;
13+
conversationId: string;
14+
}): RoutePeer {
15+
return {
16+
kind: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
17+
id: params.isDirectMessage
18+
? params.directUserId?.trim() || params.conversationId
19+
: params.conversationId,
20+
};
21+
}
22+
23+
export function resolveDiscordConversationRoute(params: {
24+
cfg: OpenClawConfig;
25+
accountId?: string | null;
26+
guildId?: string | null;
27+
memberRoleIds?: string[];
28+
peer: RoutePeer;
29+
parentConversationId?: string | null;
30+
}): ResolvedAgentRoute {
31+
return resolveAgentRoute({
32+
cfg: params.cfg,
33+
channel: "discord",
34+
accountId: params.accountId,
35+
guildId: params.guildId ?? undefined,
36+
memberRoleIds: params.memberRoleIds,
37+
peer: params.peer,
38+
parentPeer: params.parentConversationId
39+
? { kind: "channel", id: params.parentConversationId }
40+
: undefined,
41+
});
42+
}
43+
44+
export function resolveDiscordEffectiveRoute(params: {
45+
route: ResolvedAgentRoute;
46+
boundSessionKey?: string | null;
47+
configuredRoute?: { route: ResolvedAgentRoute } | null;
48+
matchedBy?: ResolvedAgentRoute["matchedBy"];
49+
}): ResolvedAgentRoute {
50+
const boundSessionKey = params.boundSessionKey?.trim();
51+
if (!boundSessionKey) {
52+
return params.configuredRoute?.route ?? params.route;
53+
}
54+
return {
55+
...params.route,
56+
sessionKey: boundSessionKey,
57+
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
58+
...(params.matchedBy ? { matchedBy: params.matchedBy } : {}),
59+
};
60+
}

0 commit comments

Comments
 (0)