From e381ab630e2466798f1c3d00b3c0378b890e8def Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 01:26:11 +0000 Subject: [PATCH] refactor(channels): share native command session targets --- .../native-command-session-targets.test.ts | 48 +++++++++++++++++++ .../native-command-session-targets.ts | 19 ++++++++ .../native-command-session-targets.test.ts | 37 -------------- .../monitor/native-command-session-targets.ts | 22 --------- src/discord/monitor/native-command.ts | 9 ++-- src/slack/monitor/slash.ts | 13 +++-- src/telegram/bot-native-commands.ts | 12 ++++- 7 files changed, 92 insertions(+), 68 deletions(-) create mode 100644 src/channels/native-command-session-targets.test.ts create mode 100644 src/channels/native-command-session-targets.ts delete mode 100644 src/discord/monitor/native-command-session-targets.test.ts delete mode 100644 src/discord/monitor/native-command-session-targets.ts diff --git a/src/channels/native-command-session-targets.test.ts b/src/channels/native-command-session-targets.test.ts new file mode 100644 index 00000000000..08bf41d7f4f --- /dev/null +++ b/src/channels/native-command-session-targets.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { resolveNativeCommandSessionTargets } from "./native-command-session-targets.js"; + +describe("resolveNativeCommandSessionTargets", () => { + it("uses the bound session for both targets when present", () => { + expect( + resolveNativeCommandSessionTargets({ + agentId: "codex", + sessionPrefix: "discord:slash", + userId: "user-1", + targetSessionKey: "agent:codex:discord:channel:chan-1", + boundSessionKey: "agent:codex:acp:binding:discord:default:seed", + }), + ).toEqual({ + sessionKey: "agent:codex:acp:binding:discord:default:seed", + commandTargetSessionKey: "agent:codex:acp:binding:discord:default:seed", + }); + }); + + it("falls back to the routed session target when unbound", () => { + expect( + resolveNativeCommandSessionTargets({ + agentId: "qwen", + sessionPrefix: "telegram:slash", + userId: "user-1", + targetSessionKey: "agent:qwen:telegram:direct:user-1", + }), + ).toEqual({ + sessionKey: "agent:qwen:telegram:slash:user-1", + commandTargetSessionKey: "agent:qwen:telegram:direct:user-1", + }); + }); + + it("supports lowercase session keys for providers that already normalize", () => { + expect( + resolveNativeCommandSessionTargets({ + agentId: "Qwen", + sessionPrefix: "Slack:Slash", + userId: "U123", + targetSessionKey: "agent:qwen:slack:channel:c1", + lowercaseSessionKey: true, + }), + ).toEqual({ + sessionKey: "agent:qwen:slack:slash:u123", + commandTargetSessionKey: "agent:qwen:slack:channel:c1", + }); + }); +}); diff --git a/src/channels/native-command-session-targets.ts b/src/channels/native-command-session-targets.ts new file mode 100644 index 00000000000..8d50029843b --- /dev/null +++ b/src/channels/native-command-session-targets.ts @@ -0,0 +1,19 @@ +export type ResolveNativeCommandSessionTargetsParams = { + agentId: string; + sessionPrefix: string; + userId: string; + targetSessionKey: string; + boundSessionKey?: string; + lowercaseSessionKey?: boolean; +}; + +export function resolveNativeCommandSessionTargets( + params: ResolveNativeCommandSessionTargetsParams, +) { + const rawSessionKey = + params.boundSessionKey ?? `agent:${params.agentId}:${params.sessionPrefix}:${params.userId}`; + return { + sessionKey: params.lowercaseSessionKey ? rawSessionKey.toLowerCase() : rawSessionKey, + commandTargetSessionKey: params.boundSessionKey ?? params.targetSessionKey, + }; +} diff --git a/src/discord/monitor/native-command-session-targets.test.ts b/src/discord/monitor/native-command-session-targets.test.ts deleted file mode 100644 index 08e3dd9a395..00000000000 --- a/src/discord/monitor/native-command-session-targets.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveDiscordNativeCommandSessionTargets } from "./native-command-session-targets.js"; - -describe("resolveDiscordNativeCommandSessionTargets", () => { - it("uses the bound session for both targets when present", () => { - expect( - resolveDiscordNativeCommandSessionTargets({ - boundSessionKey: "agent:codex:acp:binding:discord:default:seed", - effectiveRoute: { - agentId: "codex", - sessionKey: "agent:codex:discord:channel:chan-1", - }, - sessionPrefix: "discord:slash", - userId: "user-1", - }), - ).toEqual({ - sessionKey: "agent:codex:acp:binding:discord:default:seed", - commandTargetSessionKey: "agent:codex:acp:binding:discord:default:seed", - }); - }); - - it("falls back to the routed slash and command target session keys", () => { - expect( - resolveDiscordNativeCommandSessionTargets({ - effectiveRoute: { - agentId: "qwen", - sessionKey: "agent:qwen:discord:channel:chan-1", - }, - sessionPrefix: "discord:slash", - userId: "user-1", - }), - ).toEqual({ - sessionKey: "agent:qwen:discord:slash:user-1", - commandTargetSessionKey: "agent:qwen:discord:channel:chan-1", - }); - }); -}); diff --git a/src/discord/monitor/native-command-session-targets.ts b/src/discord/monitor/native-command-session-targets.ts deleted file mode 100644 index 4430a2140d8..00000000000 --- a/src/discord/monitor/native-command-session-targets.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type ResolveDiscordNativeCommandSessionTargetsParams = { - boundSessionKey?: string; - effectiveRoute: { - agentId: string; - sessionKey: string; - }; - sessionPrefix: string; - userId: string; -}; - -export function resolveDiscordNativeCommandSessionTargets( - params: ResolveDiscordNativeCommandSessionTargetsParams, -) { - const sessionKey = - params.boundSessionKey ?? - `agent:${params.effectiveRoute.agentId}:${params.sessionPrefix}:${params.userId}`; - const commandTargetSessionKey = params.boundSessionKey ?? params.effectiveRoute.sessionKey; - return { - sessionKey, - commandTargetSessionKey, - }; -} diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 7e8a6a07e4f..23b5bcd4c9d 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -41,6 +41,7 @@ import { resolveStoredModelOverride } from "../../auto-reply/reply/model-selecti import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; +import { resolveNativeCommandSessionTargets } from "../../channels/native-command-session-targets.js"; import { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; import type { OpenClawConfig, loadConfig } from "../../config/config.js"; import { isDangerousNameMatchingEnabled } from "../../config/dangerous-name-matching.js"; @@ -83,7 +84,6 @@ import { type DiscordModelPickerCommandContext, } from "./model-picker.js"; import { buildDiscordNativeCommandContext } from "./native-command-context.js"; -import { resolveDiscordNativeCommandSessionTargets } from "./native-command-session-targets.js"; import { resolveDiscordBoundConversationRoute, resolveDiscordEffectiveRoute, @@ -1645,11 +1645,12 @@ async function dispatchDiscordCommandInteraction(params: { configuredRoute, matchedBy: configuredBinding ? "binding.channel" : undefined, }); - const { sessionKey, commandTargetSessionKey } = resolveDiscordNativeCommandSessionTargets({ - boundSessionKey, - effectiveRoute, + const { sessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({ + agentId: effectiveRoute.agentId, sessionPrefix, userId: user.id, + targetSessionKey: effectiveRoute.sessionKey, + boundSessionKey, }); const ctxPayload = buildDiscordNativeCommandContext({ prompt, diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index a8df6900153..ffb8ef6f6e5 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -5,6 +5,7 @@ import { } from "../../auto-reply/commands-registry.js"; import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; +import { resolveNativeCommandSessionTargets } from "../../channels/native-command-session-targets.js"; import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js"; import { danger, logVerbose } from "../../globals.js"; import { chunkItems } from "../../utils/chunk-items.js"; @@ -546,6 +547,13 @@ export async function registerSlackMonitorSlashCommands(params: { channelConfig, }); + const { sessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({ + agentId: route.agentId, + sessionPrefix: slashCommand.sessionPrefix, + userId: command.user_id, + targetSessionKey: route.sessionKey, + lowercaseSessionKey: true, + }); const ctxPayload = finalizeInboundContext({ Body: prompt, BodyForAgent: prompt, @@ -580,9 +588,8 @@ export async function registerSlackMonitorSlashCommands(params: { WasMentioned: true, MessageSid: command.trigger_id, Timestamp: Date.now(), - SessionKey: - `agent:${route.agentId}:${slashCommand.sessionPrefix}:${command.user_id}`.toLowerCase(), - CommandTargetSessionKey: route.sessionKey, + SessionKey: sessionKey, + CommandTargetSessionKey: commandTargetSessionKey, AccountId: route.accountId, CommandSource: "native" as const, CommandAuthorized: commandAuthorized, diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index bb171287c0d..0aec74b969e 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -14,6 +14,7 @@ import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js"; +import { resolveNativeCommandSessionTargets } from "../channels/native-command-session-targets.js"; import { createReplyPrefixOptions } from "../channels/reply-prefix.js"; import { recordInboundSessionMetaSafe } from "../channels/session-meta.js"; import type { OpenClawConfig } from "../config/config.js"; @@ -638,6 +639,13 @@ export const registerTelegramNativeCommands = ({ groupConfig, topicConfig, }); + const { sessionKey: commandSessionKey, commandTargetSessionKey } = + resolveNativeCommandSessionTargets({ + agentId: route.agentId, + sessionPrefix: "telegram:slash", + userId: String(senderId || chatId), + targetSessionKey: sessionKey, + }); const conversationLabel = isGroup ? msg.chat.title ? `${msg.chat.title} id:${chatId}` @@ -665,9 +673,9 @@ export const registerTelegramNativeCommands = ({ WasMentioned: true, CommandAuthorized: commandAuthorized, CommandSource: "native" as const, - SessionKey: `agent:${route.agentId}:telegram:slash:${senderId || chatId}`, + SessionKey: commandSessionKey, AccountId: route.accountId, - CommandTargetSessionKey: sessionKey, + CommandTargetSessionKey: commandTargetSessionKey, MessageThreadId: threadSpec.id, IsForum: isForum, // Originating context for sub-agent announce routing