fix(telegram): scope DM topic thread keys by chat id (#31064)

* fix(telegram): scope DM topic thread keys by chat id

* test(telegram): update dm topic session-key expectation

* fix(telegram): parse scoped dm thread ids in outbound recovery

* chore(telegram): format accounts config merge block

* test(nodes): simplify mocked exports for ts tuple spreads
This commit is contained in:
Brian Le
2026-03-01 21:54:45 -05:00
committed by GitHub
parent bbab94c1fe
commit f64d25bd3e
11 changed files with 74 additions and 19 deletions

View File

@@ -84,8 +84,11 @@ function resolveAccountConfig(
}
function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig {
const { accounts: _ignored, groups: channelGroups, ...base } = (cfg.channels?.telegram ??
{}) as TelegramAccountConfig & { accounts?: unknown };
const {
accounts: _ignored,
groups: channelGroups,
...base
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { accounts?: unknown };
const account = resolveAccountConfig(cfg, accountId) ?? {};
// In multi-account setups, channel-level `groups` must NOT be inherited by

View File

@@ -290,7 +290,7 @@ export const registerTelegramHandlers = ({
const dmThreadId = !params.isGroup ? params.messageThreadId : undefined;
const threadKeys =
dmThreadId != null
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` })
: null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });

View File

@@ -19,7 +19,7 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
expect(ctx).not.toBeNull();
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:42");
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:1234:42");
});
it("keeps legacy dm session key when no thread id", async () => {

View File

@@ -204,7 +204,7 @@ export const buildTelegramMessageContext = async ({
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
const threadKeys =
dmThreadId != null
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
: null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);

View File

@@ -551,7 +551,7 @@ export const registerTelegramNativeCommands = ({
dmThreadId != null
? resolveThreadSessionKeys({
baseSessionKey,
threadId: String(dmThreadId),
threadId: `${chatId}:${dmThreadId}`,
})
: null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;

View File

@@ -928,7 +928,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:99");
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:12345:99");
});
it("allows native DM commands for paired users", async () => {

View File

@@ -6,6 +6,14 @@ export function parseTelegramReplyToMessageId(replyToId?: string | null): number
return Number.isFinite(parsed) ? parsed : undefined;
}
function parseIntegerId(value: string): number | undefined {
if (!/^-?\d+$/.test(value)) {
return undefined;
}
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : undefined;
}
export function parseTelegramThreadId(threadId?: string | number | null): number | undefined {
if (threadId == null) {
return undefined;
@@ -17,6 +25,8 @@ export function parseTelegramThreadId(threadId?: string | number | null): number
if (!trimmed) {
return undefined;
}
const parsed = Number.parseInt(trimmed, 10);
return Number.isFinite(parsed) ? parsed : undefined;
// DM topic session keys may scope thread ids as "<chatId>:<threadId>".
const scopedMatch = /^-?\d+:(-?\d+)$/.exec(trimmed);
const rawThreadId = scopedMatch ? scopedMatch[1] : trimmed;
return parseIntegerId(rawThreadId);
}