Telegram: fix named-account DM topic session keys (#48773)

This commit is contained in:
Vincent Koc
2026-03-17 00:41:44 -07:00
committed by GitHub
parent 9053f551cb
commit 1eb810a5e3
6 changed files with 137 additions and 29 deletions

View File

@@ -64,7 +64,10 @@ import {
resolveTelegramGroupAllowFromContext,
} from "./bot/helpers.js";
import type { TelegramContext } from "./bot/types.js";
import { resolveTelegramConversationRoute } from "./conversation-route.js";
import {
resolveTelegramConversationBaseSessionKey,
resolveTelegramConversationRoute,
} from "./conversation-route.js";
import { enforceTelegramDmAccess } from "./dm-access.js";
import {
isTelegramExecApprovalApprover,
@@ -331,7 +334,13 @@ export const registerTelegramHandlers = ({
senderId: params.senderId,
topicAgentId: topicConfig?.agentId,
});
const baseSessionKey = route.sessionKey;
const baseSessionKey = resolveTelegramConversationBaseSessionKey({
cfg,
route,
chatId: params.chatId,
isGroup: params.isGroup,
senderId: params.senderId,
});
const threadKeys =
dmThreadId != null
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` })

View File

@@ -6,9 +6,13 @@ import {
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
const recordInboundSessionMock = vi.fn().mockResolvedValue(undefined);
vi.mock("../../../src/channels/session.js", () => ({
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
}));
vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
return {
...actual,
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
};
});
describe("buildTelegramMessageContext named-account DM fallback", () => {
const baseCfg = {

View File

@@ -9,7 +9,7 @@ import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin-sdk/config-runtime";
import { ensureConfiguredAcpRouteReady } from "openclaw/plugin-sdk/conversation-runtime";
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
import { buildAgentSessionKey, deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { withTelegramApiErrorLogging } from "./api-logging.js";
@@ -17,12 +17,11 @@ import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from
import { resolveTelegramInboundBody } from "./bot-message-context.body.js";
import { buildTelegramInboundContextPayload } from "./bot-message-context.session.js";
import type { BuildTelegramMessageContextParams } from "./bot-message-context.types.js";
import { buildTypingThreadParams, resolveTelegramThreadSpec } from "./bot/helpers.js";
import {
buildTypingThreadParams,
resolveTelegramDirectPeerId,
resolveTelegramThreadSpec,
} from "./bot/helpers.js";
import { resolveTelegramConversationRoute } from "./conversation-route.js";
resolveTelegramConversationBaseSessionKey,
resolveTelegramConversationRoute,
} from "./conversation-route.js";
import { enforceTelegramDmAccess } from "./dm-access.js";
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
import {
@@ -224,22 +223,13 @@ export const buildTelegramMessageContext = async ({
return false;
};
const baseSessionKey = isNamedAccountFallback
? buildAgentSessionKey({
agentId: route.agentId,
channel: "telegram",
accountId: route.accountId,
peer: {
kind: "direct",
id: resolveTelegramDirectPeerId({
chatId,
senderId,
}),
},
dmScope: "per-account-channel-peer",
identityLinks: freshCfg.session?.identityLinks,
}).toLowerCase()
: route.sessionKey;
const baseSessionKey = resolveTelegramConversationBaseSessionKey({
cfg: freshCfg,
route,
chatId,
isGroup,
senderId,
});
// DMs: use thread suffix for session isolation (works regardless of dmScope)
const threadKeys =
dmThreadId != null

View File

@@ -63,7 +63,10 @@ import {
resolveTelegramThreadSpec,
} from "./bot/helpers.js";
import type { TelegramContext } from "./bot/types.js";
import { resolveTelegramConversationRoute } from "./conversation-route.js";
import {
resolveTelegramConversationBaseSessionKey,
resolveTelegramConversationRoute,
} from "./conversation-route.js";
import { shouldSuppressLocalTelegramExecApprovalPrompt } from "./exec-approvals.js";
import type { TelegramTransport } from "./fetch.js";
import {
@@ -650,7 +653,13 @@ export const registerTelegramNativeCommands = ({
});
return;
}
const baseSessionKey = route.sessionKey;
const baseSessionKey = resolveTelegramConversationBaseSessionKey({
cfg,
route,
chatId,
isGroup,
senderId,
});
// DMs: use raw messageThreadId for thread sessions (not resolvedThreadId which is for forums)
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
const threadKeys =

View File

@@ -0,0 +1,64 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import { describe, expect, it } from "vitest";
import { resolveTelegramConversationBaseSessionKey } from "./conversation-route.js";
describe("resolveTelegramConversationBaseSessionKey", () => {
const cfg: OpenClawConfig = {};
it("keeps the routed session key for the default account", () => {
expect(
resolveTelegramConversationBaseSessionKey({
cfg,
route: {
agentId: "main",
accountId: "default",
matchedBy: "default",
sessionKey: "agent:main:main",
},
chatId: 12345,
isGroup: false,
senderId: 12345,
}),
).toBe("agent:main:main");
});
it("uses the per-account fallback key for named-account DMs without an explicit binding", () => {
expect(
resolveTelegramConversationBaseSessionKey({
cfg,
route: {
agentId: "main",
accountId: "personal",
matchedBy: "default",
sessionKey: "agent:main:main",
},
chatId: 12345,
isGroup: false,
senderId: 12345,
}),
).toBe("agent:main:telegram:personal:direct:12345");
});
it("keeps DM topic isolation on the named-account fallback key", () => {
const baseSessionKey = resolveTelegramConversationBaseSessionKey({
cfg,
route: {
agentId: "main",
accountId: "personal",
matchedBy: "default",
sessionKey: "agent:main:main",
},
chatId: 12345,
isGroup: false,
senderId: 12345,
});
expect(
resolveThreadSessionKeys({
baseSessionKey,
threadId: "12345:99",
}).sessionKey,
).toBe("agent:main:telegram:personal:direct:12345:thread:12345:99");
});
});

View File

@@ -9,6 +9,7 @@ import {
} from "openclaw/plugin-sdk/routing";
import {
buildAgentMainSessionKey,
DEFAULT_ACCOUNT_ID,
resolveAgentIdFromSessionKey,
sanitizeAgentId,
} from "openclaw/plugin-sdk/routing";
@@ -148,3 +149,34 @@ export function resolveTelegramConversationRoute(params: {
configuredBindingSessionKey,
};
}
export function resolveTelegramConversationBaseSessionKey(params: {
cfg: OpenClawConfig;
route: Pick<
ReturnType<typeof resolveTelegramConversationRoute>["route"],
"agentId" | "accountId" | "matchedBy" | "sessionKey"
>;
chatId: number | string;
isGroup: boolean;
senderId?: string | number | null;
}): string {
const isNamedAccountFallback =
params.route.accountId !== DEFAULT_ACCOUNT_ID && params.route.matchedBy === "default";
if (!isNamedAccountFallback || params.isGroup) {
return params.route.sessionKey;
}
return buildAgentSessionKey({
agentId: params.route.agentId,
channel: "telegram",
accountId: params.route.accountId,
peer: {
kind: "direct",
id: resolveTelegramDirectPeerId({
chatId: params.chatId,
senderId: params.senderId,
}),
},
dmScope: "per-account-channel-peer",
identityLinks: params.cfg.session?.identityLinks,
}).toLowerCase();
}