feat(routing): add per-account-channel-peer session scope
Adds a new dmScope option that includes accountId in session keys, enabling isolated sessions per channel account for multi-bot setups. - Add 'per-account-channel-peer' to DmScope type - Update session key generation to include accountId - Pass accountId through routing chain - Add tests for new routing behavior (13/13 passing) Closes #3094 Co-authored-by: Sebastian Almeida <89653954+SebastianAlmeida@users.noreply.github.com>
This commit is contained in:
parent
93c2d65398
commit
d499b14842
@ -3,7 +3,7 @@ import type { NormalizedChatType } from "../channels/chat-type.js";
|
|||||||
export type ReplyMode = "text" | "command";
|
export type ReplyMode = "text" | "command";
|
||||||
export type TypingMode = "never" | "instant" | "thinking" | "message";
|
export type TypingMode = "never" | "instant" | "thinking" | "message";
|
||||||
export type SessionScope = "per-sender" | "global";
|
export type SessionScope = "per-sender" | "global";
|
||||||
export type DmScope = "main" | "per-peer" | "per-channel-peer";
|
export type DmScope = "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
|
||||||
export type ReplyToMode = "off" | "first" | "all";
|
export type ReplyToMode = "off" | "first" | "all";
|
||||||
export type GroupPolicy = "open" | "disabled" | "allowlist";
|
export type GroupPolicy = "open" | "disabled" | "allowlist";
|
||||||
export type DmPolicy = "pairing" | "allowlist" | "open" | "disabled";
|
export type DmPolicy = "pairing" | "allowlist" | "open" | "disabled";
|
||||||
|
|||||||
@ -20,7 +20,12 @@ export const SessionSchema = z
|
|||||||
.object({
|
.object({
|
||||||
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
|
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
|
||||||
dmScope: z
|
dmScope: z
|
||||||
.union([z.literal("main"), z.literal("per-peer"), z.literal("per-channel-peer")])
|
.union([
|
||||||
|
z.literal("main"),
|
||||||
|
z.literal("per-peer"),
|
||||||
|
z.literal("per-channel-peer"),
|
||||||
|
z.literal("per-account-channel-peer"),
|
||||||
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
identityLinks: z.record(z.string(), z.array(z.string())).optional(),
|
identityLinks: z.record(z.string(), z.array(z.string())).optional(),
|
||||||
resetTriggers: z.array(z.string()).optional(),
|
resetTriggers: z.array(z.string()).optional(),
|
||||||
|
|||||||
@ -103,11 +103,13 @@ function buildBaseSessionKey(params: {
|
|||||||
cfg: MoltbotConfig;
|
cfg: MoltbotConfig;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
channel: ChannelId;
|
channel: ChannelId;
|
||||||
|
accountId?: string | null;
|
||||||
peer: RoutePeer;
|
peer: RoutePeer;
|
||||||
}): string {
|
}): string {
|
||||||
return buildAgentSessionKey({
|
return buildAgentSessionKey({
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: params.channel,
|
channel: params.channel,
|
||||||
|
accountId: params.accountId,
|
||||||
peer: params.peer,
|
peer: params.peer,
|
||||||
dmScope: params.cfg.session?.dmScope ?? "main",
|
dmScope: params.cfg.session?.dmScope ?? "main",
|
||||||
identityLinks: params.cfg.session?.identityLinks,
|
identityLinks: params.cfg.session?.identityLinks,
|
||||||
@ -200,6 +202,7 @@ async function resolveSlackSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "slack",
|
channel: "slack",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
const threadId = normalizeThreadId(params.threadId ?? params.replyToId);
|
const threadId = normalizeThreadId(params.threadId ?? params.replyToId);
|
||||||
@ -237,6 +240,7 @@ function resolveDiscordSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "discord",
|
channel: "discord",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
const explicitThreadId = normalizeThreadId(params.threadId);
|
const explicitThreadId = normalizeThreadId(params.threadId);
|
||||||
@ -285,6 +289,7 @@ function resolveTelegramSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -312,6 +317,7 @@ function resolveWhatsAppSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -337,6 +343,7 @@ function resolveSignalSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "signal",
|
channel: "signal",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -371,6 +378,7 @@ function resolveSignalSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "signal",
|
channel: "signal",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -395,6 +403,7 @@ function resolveIMessageSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "imessage",
|
channel: "imessage",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -419,6 +428,7 @@ function resolveIMessageSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "imessage",
|
channel: "imessage",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
const toPrefix =
|
const toPrefix =
|
||||||
@ -450,6 +460,7 @@ function resolveMatrixSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "matrix",
|
channel: "matrix",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -483,6 +494,7 @@ function resolveMSTeamsSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "msteams",
|
channel: "msteams",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -517,6 +529,7 @@ function resolveMattermostSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "mattermost",
|
channel: "mattermost",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
const threadId = normalizeThreadId(params.replyToId ?? params.threadId);
|
const threadId = normalizeThreadId(params.replyToId ?? params.threadId);
|
||||||
@ -561,6 +574,7 @@ function resolveBlueBubblesSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "bluebubbles",
|
channel: "bluebubbles",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -586,6 +600,7 @@ function resolveNextcloudTalkSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "nextcloud-talk",
|
channel: "nextcloud-talk",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -612,6 +627,7 @@ function resolveZaloSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "zalo",
|
channel: "zalo",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -639,6 +655,7 @@ function resolveZalouserSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "zalouser",
|
channel: "zalouser",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -661,6 +678,7 @@ function resolveNostrSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "nostr",
|
channel: "nostr",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -719,6 +737,7 @@ function resolveTlonSession(
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
channel: "tlon",
|
channel: "tlon",
|
||||||
|
accountId: params.accountId,
|
||||||
peer,
|
peer,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -227,3 +227,29 @@ describe("resolveAgentRoute", () => {
|
|||||||
expect(route.sessionKey).toBe("agent:home:main");
|
expect(route.sessionKey).toBe("agent:home:main");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("dmScope=per-account-channel-peer isolates DM sessions per account, channel and sender", () => {
|
||||||
|
const cfg: MoltbotConfig = {
|
||||||
|
session: { dmScope: "per-account-channel-peer" },
|
||||||
|
};
|
||||||
|
const route = resolveAgentRoute({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram",
|
||||||
|
accountId: "tasks",
|
||||||
|
peer: { kind: "dm", id: "7550356539" },
|
||||||
|
});
|
||||||
|
expect(route.sessionKey).toBe("agent:main:telegram:tasks:dm:7550356539");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dmScope=per-account-channel-peer uses default accountId when not provided", () => {
|
||||||
|
const cfg: MoltbotConfig = {
|
||||||
|
session: { dmScope: "per-account-channel-peer" },
|
||||||
|
};
|
||||||
|
const route = resolveAgentRoute({
|
||||||
|
cfg,
|
||||||
|
channel: "telegram",
|
||||||
|
accountId: null,
|
||||||
|
peer: { kind: "dm", id: "7550356539" },
|
||||||
|
});
|
||||||
|
expect(route.sessionKey).toBe("agent:main:telegram:default:dm:7550356539");
|
||||||
|
});
|
||||||
|
|||||||
@ -69,9 +69,10 @@ function matchesAccountId(match: string | undefined, actual: string): boolean {
|
|||||||
export function buildAgentSessionKey(params: {
|
export function buildAgentSessionKey(params: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
|
accountId?: string | null;
|
||||||
peer?: RoutePeer | null;
|
peer?: RoutePeer | null;
|
||||||
/** DM session scope. */
|
/** DM session scope. */
|
||||||
dmScope?: "main" | "per-peer" | "per-channel-peer";
|
dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
|
||||||
identityLinks?: Record<string, string[]>;
|
identityLinks?: Record<string, string[]>;
|
||||||
}): string {
|
}): string {
|
||||||
const channel = normalizeToken(params.channel) || "unknown";
|
const channel = normalizeToken(params.channel) || "unknown";
|
||||||
@ -80,6 +81,7 @@ export function buildAgentSessionKey(params: {
|
|||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
mainKey: DEFAULT_MAIN_KEY,
|
mainKey: DEFAULT_MAIN_KEY,
|
||||||
channel,
|
channel,
|
||||||
|
accountId: params.accountId,
|
||||||
peerKind: peer?.kind ?? "dm",
|
peerKind: peer?.kind ?? "dm",
|
||||||
peerId: peer ? normalizeId(peer.id) || "unknown" : null,
|
peerId: peer ? normalizeId(peer.id) || "unknown" : null,
|
||||||
dmScope: params.dmScope,
|
dmScope: params.dmScope,
|
||||||
@ -160,6 +162,7 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
|
|||||||
const sessionKey = buildAgentSessionKey({
|
const sessionKey = buildAgentSessionKey({
|
||||||
agentId: resolvedAgentId,
|
agentId: resolvedAgentId,
|
||||||
channel,
|
channel,
|
||||||
|
accountId,
|
||||||
peer,
|
peer,
|
||||||
dmScope,
|
dmScope,
|
||||||
identityLinks,
|
identityLinks,
|
||||||
|
|||||||
@ -111,11 +111,12 @@ export function buildAgentPeerSessionKey(params: {
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
mainKey?: string | undefined;
|
mainKey?: string | undefined;
|
||||||
channel: string;
|
channel: string;
|
||||||
|
accountId?: string | null;
|
||||||
peerKind?: "dm" | "group" | "channel" | null;
|
peerKind?: "dm" | "group" | "channel" | null;
|
||||||
peerId?: string | null;
|
peerId?: string | null;
|
||||||
identityLinks?: Record<string, string[]>;
|
identityLinks?: Record<string, string[]>;
|
||||||
/** DM session scope. */
|
/** DM session scope. */
|
||||||
dmScope?: "main" | "per-peer" | "per-channel-peer";
|
dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
|
||||||
}): string {
|
}): string {
|
||||||
const peerKind = params.peerKind ?? "dm";
|
const peerKind = params.peerKind ?? "dm";
|
||||||
if (peerKind === "dm") {
|
if (peerKind === "dm") {
|
||||||
@ -131,6 +132,11 @@ export function buildAgentPeerSessionKey(params: {
|
|||||||
});
|
});
|
||||||
if (linkedPeerId) peerId = linkedPeerId;
|
if (linkedPeerId) peerId = linkedPeerId;
|
||||||
peerId = peerId.toLowerCase();
|
peerId = peerId.toLowerCase();
|
||||||
|
if (dmScope === "per-account-channel-peer" && peerId) {
|
||||||
|
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
||||||
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
|
return `agent:${normalizeAgentId(params.agentId)}:${channel}:${accountId}:dm:${peerId}`;
|
||||||
|
}
|
||||||
if (dmScope === "per-channel-peer" && peerId) {
|
if (dmScope === "per-channel-peer" && peerId) {
|
||||||
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
|
||||||
return `agent:${normalizeAgentId(params.agentId)}:${channel}:dm:${peerId}`;
|
return `agent:${normalizeAgentId(params.agentId)}:${channel}:dm:${peerId}`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user