refactor(telegram): keep topic thread mapping plugin-local

* refactor(telegram): keep topic thread mapping plugin-local

* fix(telegram): preserve native topic ids for username targets
This commit is contained in:
Peter Steinberger
2026-05-31 10:25:54 +01:00
committed by GitHub
parent 9e2bd8b2f7
commit 70c8abdca1
2 changed files with 85 additions and 8 deletions

View File

@@ -553,28 +553,60 @@ function resolveTelegramOutboundSessionRoute(params: {
if (isGroup) {
return baseRoute;
}
const outboundSessionThreadId =
resolvedThreadId !== undefined ? `${chatId}:${resolvedThreadId}` : undefined;
const canonicalThreadId =
resolvedThreadId !== undefined
? buildTelegramCanonicalTopicThreadId({ chatId, topicId: resolvedThreadId })
: undefined;
const route = buildThreadAwareOutboundSessionRoute({
route: baseRoute,
threadId: outboundSessionThreadId,
threadId: canonicalThreadId,
currentSessionKey: params.currentSessionKey,
precedence: ["threadId", "currentSession"],
canRecoverCurrentThread: ({ route }) =>
route.chatType !== "direct" || (params.cfg.session?.dmScope ?? "main") !== "main",
});
const deliveryThreadId =
resolvedThreadId ?? parseTelegramThreadId(route.threadId) ?? route.threadId;
const routeThreadId = resolveTelegramNativeTopicThreadId(route.threadId, resolvedThreadId);
return {
...route,
...(deliveryThreadId !== undefined ? { threadId: deliveryThreadId } : {}),
...(routeThreadId !== undefined ? { threadId: routeThreadId } : {}),
from:
deliveryThreadId !== undefined
? `telegram:${chatId}:topic:${deliveryThreadId}`
routeThreadId !== undefined
? `telegram:${chatId}:topic:${routeThreadId}`
: `telegram:${chatId}`,
};
}
function buildTelegramCanonicalTopicThreadId(params: { chatId: string; topicId: number }): string {
// Core session routing sees one canonical thread id. Telegram topic ids are
// chat-scoped, so direct-topic sessions include the chat id to avoid collisions.
return `${params.chatId}:${params.topicId}`;
}
function resolveTelegramNativeTopicThreadId(
threadId?: string | number,
nativeTopicId?: number,
): string | number | undefined {
if (nativeTopicId !== undefined) {
return nativeTopicId;
}
// Keep the chat-scoped canonical id inside OpenClaw state; translate it back
// only when returning Telegram route metadata used by send/typing paths.
if (threadId === undefined) {
return undefined;
}
const parsedThreadId = parseTelegramThreadId(threadId);
if (parsedThreadId !== undefined) {
return parsedThreadId;
}
if (typeof threadId === "string") {
const canonicalMatch = /:(\d+)$/.exec(threadId.trim());
if (canonicalMatch?.[1]) {
return Number(canonicalMatch[1]);
}
}
return threadId;
}
async function resolveTelegramTargets(params: {
cfg: OpenClawConfig;
accountId?: string | null;

View File

@@ -14,6 +14,37 @@ describe("telegram session route", () => {
expect(route?.threadId).toBe(99);
});
it("keeps same direct topic ids distinct across chats", async () => {
const first = await telegramPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: {},
agentId: "main",
target: "12345:topic:99",
});
const second = await telegramPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: {},
agentId: "main",
target: "67890:topic:99",
});
expect(first?.sessionKey).toBe("agent:main:main:thread:12345:99");
expect(second?.sessionKey).toBe("agent:main:main:thread:67890:99");
expect(first?.threadId).toBe(99);
expect(second?.threadId).toBe(99);
});
it("returns native topic ids for username direct topic targets", async () => {
const route = await telegramPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: {},
agentId: "main",
target: "@alice:topic:99",
});
expect(route?.sessionKey).toBe("agent:main:main:thread:@alice:99");
expect(route?.baseSessionKey).toBe("agent:main:main");
expect(route?.threadId).toBe(99);
expect(route?.from).toBe("telegram:@alice:topic:99");
});
it("aligns isolated direct topic sessions with inbound reply routing", async () => {
const route = await telegramPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: { session: { dmScope: "per-account-channel-peer" } },
@@ -44,6 +75,20 @@ describe("telegram session route", () => {
expect(route?.from).toBe("telegram:12345:topic:99");
});
it("recovers username direct topic thread routes from currentSessionKey", async () => {
const route = await telegramPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: { session: { dmScope: "per-channel-peer" } },
agentId: "main",
target: "@alice",
currentSessionKey: "agent:main:telegram:direct:@alice:thread:@alice:99",
});
expect(route?.sessionKey).toBe("agent:main:telegram:direct:@alice:thread:@alice:99");
expect(route?.baseSessionKey).toBe("agent:main:telegram:direct:@alice");
expect(route?.threadId).toBe(99);
expect(route?.from).toBe("telegram:@alice:topic:99");
});
it('does not recover currentSessionKey threads for shared dmScope "main" DMs', async () => {
const route = await telegramPlugin.messaging?.resolveOutboundSessionRoute?.({
cfg: {},