Skip to content

Commit b8c0a1e

Browse files
committed
fix(telegram): keep dm reply threads on main session
1 parent 2b92de6 commit b8c0a1e

14 files changed

Lines changed: 141 additions & 29 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
4343
- Control UI/Talk: allow the OpenAI Realtime WebRTC offer endpoint through the Control UI CSP, configure browser sessions with explicit VAD/transcription input settings, and surface OpenAI realtime error/lifecycle events instead of leaving Talk stuck as live with no diagnostic. Fixes #73427.
4444
- Plugins: clarify config-selected duplicate plugin override diagnostics and document manifest schema updates for bundled-plugin forks. Fixes #8582. Thanks @sachah.
4545
- CLI backends/Claude: make live-session JSONL turn caps bounded and configurable via `reliability.outputLimits`, raising the default guard for tool-heavy Claude CLI turns while preserving memory limits. Fixes #75838. Thanks @hcordoba840.
46+
- Telegram/DMs: keep incidental `message_thread_id` reply-with-quote metadata on the flat DM session by default while preserving opt-in DM topic isolation for configured topics. Fixes #75975. Thanks @ProjectEvolutionEVE.
4647
- Providers/OpenAI: resolve `keychain:<service>:<account>` `OPENAI_API_KEY` refs before creating OpenAI Realtime browser sessions or voice bridges, with a bounded cached Keychain lookup. Fixes #72120. Thanks @ctbritt.
4748
- Discord/gateway: reconnect when the gateway socket closes while waiting for the shared IDENTIFY concurrency window, instead of silently skipping IDENTIFY and leaving the bot online but unresponsive. Fixes #74617. Thanks @zeeskdr-ai.
4849
- Voice Call: add `sessionScope: "per-call"` for fresh per-call agent memory while preserving the default per-phone caller history. Fixes #45280. Thanks @pondcountry.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
366770fd037ace1092595b351fbd83473ee1ecce188bceb0ab4510a5579a9073 config-baseline.json
2-
2d132b4c2e3b0e0f2524fc1cc889d3be658ad0e40c970b2d367bf27348883658 config-baseline.core.json
3-
f42329d45c095881bd226bdb192c235980658fd250606d0c0badc2b12f12f5d3 config-baseline.channel.json
1+
ba41e5775c361dba63fec0441f943106d8cd0cb0f10c10fee36becc1555d5059 config-baseline.json
2+
7b1716d578d22e5b4388f56140b50d326f61327b760f8c580bdd9b971335fb85 config-baseline.core.json
3+
74632b512b6470a155652c7d15b9e430738a05df3b5a85dca16cc4d84dcea764 config-baseline.channel.json
44
fffe0e74eab92a88c3c57952a70bc932438ce3a7f5f9982688437f2cdaee0bcb config-baseline.plugin.json
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
84befa4ad71bee22d9ea91a6ff689532deb3783143af7488a98a7341d5ce5f25 plugin-sdk-api-baseline.json
2-
046bb0c9bc40bfb2f8a323bf658c45eeeb486571301757abc5472018db7d2189 plugin-sdk-api-baseline.jsonl
1+
1b91ea9cadcedacd0c7e7cf9ca2e48739bd8f99a107cb59ba8b0798d0729b374 plugin-sdk-api-baseline.json
2+
f323d1b6e71b9e65555c13e22dcdad0cd9c9db24243dad4c7da27855d2b69888 plugin-sdk-api-baseline.jsonl

docs/channels/telegram.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
260260
- Routing is deterministic: Telegram inbound replies back to Telegram (the model does not pick channels).
261261
- Inbound messages normalize into the shared channel envelope with reply metadata and media placeholders.
262262
- Group sessions are isolated by group ID. Forum topics append `:topic:<threadId>` to keep topics isolated.
263-
- DM messages can carry `message_thread_id`; OpenClaw routes them with thread-aware session keys and preserves thread ID for replies.
263+
- DM messages can carry `message_thread_id`; OpenClaw preserves the thread ID for replies but keeps DMs on the flat session by default. Configure `channels.telegram.direct.<chatId>.threadReplies: "inbound"` or `requireTopic: true` when you intentionally want DM topic session isolation.
264264
- Long polling uses grammY runner with per-chat/per-thread sequencing. Overall runner sink concurrency uses `agents.defaults.maxConcurrent`.
265265
- Long polling is guarded inside each gateway process so only one active poller can use a bot token at a time. If you still see `getUpdates` 409 conflicts, another OpenClaw gateway, script, or external poller is likely using the same token.
266266
- Long-polling watchdog restarts trigger after 120 seconds without completed `getUpdates` liveness by default. Increase `channels.telegram.pollingStallThresholdMs` only if your deployment still sees false polling-stall restarts during long-running work. The value is in milliseconds and is allowed from `30000` to `600000`; per-account overrides are supported.
@@ -542,7 +542,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
542542

543543
**Thread-bound ACP spawn from chat**: `/acp spawn <agent> --thread here|auto` binds the current topic to a new ACP session; follow-ups route there directly. OpenClaw pins the spawn confirmation in-topic. Requires `channels.telegram.threadBindings.spawnSessions` to remain enabled (default: `true`).
544544

545-
Template context exposes `MessageThreadId` and `IsForum`. DM chats with `message_thread_id` keep DM routing but use thread-aware session keys.
545+
Template context exposes `MessageThreadId` and `IsForum`. DM chats with `message_thread_id` keep DM routing and reply metadata; they only use thread-aware session keys when the DM is configured with `threadReplies: "inbound"`, `threadReplies: "always"`, `requireTopic: true`, or a matching topic config.
546546

547547
</Accordion>
548548

extensions/telegram/src/bot-handlers.runtime.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
import { buildCommandsMessagePaginated } from "openclaw/plugin-sdk/command-status";
1515
import { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
1616
import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
17-
import type { TelegramGroupConfig, TelegramTopicConfig } from "openclaw/plugin-sdk/config-types";
17+
import type {
18+
TelegramDirectConfig,
19+
TelegramGroupConfig,
20+
TelegramTopicConfig,
21+
} from "openclaw/plugin-sdk/config-types";
1822
import {
1923
buildPluginBindingResolvedText,
2024
parsePluginBindingApprovalCustomId,
@@ -72,6 +76,7 @@ import {
7276
resolveTelegramForumFlag,
7377
resolveTelegramForumThreadId,
7478
resolveTelegramGroupAllowFromContext,
79+
shouldUseTelegramDmThreadSession,
7580
withResolvedTelegramForumFlag,
7681
} from "./bot/helpers.js";
7782
import type { TelegramContext, TelegramGetChat } from "./bot/types.js";
@@ -320,7 +325,10 @@ export const registerTelegramHandlers = ({
320325
});
321326
const dmThreadId = !params.isGroup ? params.messageThreadId : undefined;
322327
const topicThreadId = resolvedThreadId ?? dmThreadId;
323-
const { topicConfig } = resolveTelegramGroupConfig(params.chatId, topicThreadId);
328+
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(params.chatId, topicThreadId);
329+
const directConfig = !params.isGroup
330+
? (groupConfig as TelegramDirectConfig | undefined)
331+
: undefined;
324332
const { route } = resolveTelegramConversationRoute({
325333
cfg: runtimeCfg,
326334
accountId,
@@ -338,10 +346,9 @@ export const registerTelegramHandlers = ({
338346
isGroup: params.isGroup,
339347
senderId: params.senderId,
340348
});
341-
const threadKeys =
342-
dmThreadId != null
343-
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` })
344-
: null;
349+
const threadKeys = shouldUseTelegramDmThreadSession({ dmThreadId, directConfig, topicConfig })
350+
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` })
351+
: null;
345352
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
346353
const storePath = telegramDeps.resolveStorePath(runtimeCfg.session?.store, {
347354
agentId: route.agentId,

extensions/telegram/src/bot-message-context.dm-threads.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,19 @@ afterEach(() => {
5959
});
6060

6161
describe("buildTelegramMessageContext dm thread sessions", () => {
62-
const buildContext = async (message: Record<string, unknown>) =>
62+
const buildContext = async (
63+
message: Record<string, unknown>,
64+
params?: Pick<
65+
Parameters<typeof buildTelegramMessageContextForTest>[0],
66+
"resolveTelegramGroupConfig"
67+
>,
68+
) =>
6369
await buildTelegramMessageContextForTest({
6470
message,
71+
...params,
6572
});
6673

67-
it("uses thread session key for dm topics", async () => {
74+
it("keeps incidental dm message_thread_id on the main session by default", async () => {
6875
const ctx = await buildContext({
6976
message_id: 1,
7077
chat: { id: 1234, type: "private" },
@@ -74,6 +81,29 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
7481
from: { id: 42, first_name: "Alice" },
7582
});
7683

84+
expect(ctx).not.toBeNull();
85+
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
86+
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main");
87+
});
88+
89+
it("uses thread session key for configured dm topics", async () => {
90+
const ctx = await buildContext(
91+
{
92+
message_id: 3,
93+
chat: { id: 1234, type: "private" },
94+
date: 1700000002,
95+
text: "hello",
96+
message_thread_id: 42,
97+
from: { id: 42, first_name: "Alice" },
98+
},
99+
{
100+
resolveTelegramGroupConfig: () => ({
101+
groupConfig: { requireTopic: true },
102+
topicConfig: undefined,
103+
}),
104+
},
105+
);
106+
77107
expect(ctx).not.toBeNull();
78108
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
79109
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:1234:42");

extensions/telegram/src/bot-message-context.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
extractTelegramForumFlag,
2323
resolveTelegramForumFlag,
2424
resolveTelegramThreadSpec,
25+
shouldUseTelegramDmThreadSession,
2526
} from "./bot/helpers.js";
2627
import type { TelegramGetChat } from "./bot/types.js";
2728
import {
@@ -381,11 +382,14 @@ export const buildTelegramMessageContext = async ({
381382
isGroup,
382383
senderId,
383384
});
384-
// DMs: use thread suffix for session isolation (works regardless of dmScope)
385-
const threadKeys =
386-
dmThreadId != null
387-
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
388-
: null;
385+
const useDmThreadSession = shouldUseTelegramDmThreadSession({
386+
dmThreadId,
387+
directConfig,
388+
topicConfig,
389+
});
390+
const threadKeys = useDmThreadSession
391+
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
392+
: null;
389393
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
390394
route = {
391395
...route,

extensions/telegram/src/bot-native-commands.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
resolveTelegramForumFlag,
6868
resolveTelegramGroupAllowFromContext,
6969
resolveTelegramThreadSpec,
70+
shouldUseTelegramDmThreadSession,
7071
} from "./bot/helpers.js";
7172
import type { TelegramContext, TelegramGetChat } from "./bot/types.js";
7273
import type { TelegramInlineButtons } from "./button-types.js";
@@ -887,13 +888,16 @@ export const registerTelegramNativeCommands = ({
887888
senderId,
888889
});
889890
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
890-
const threadKeys =
891-
dmThreadId != null
892-
? (await resolveNativeCommandRuntime()).resolveThreadSessionKeys({
893-
baseSessionKey,
894-
threadId: `${chatId}:${dmThreadId}`,
895-
})
896-
: null;
891+
const threadKeys = shouldUseTelegramDmThreadSession({
892+
dmThreadId,
893+
directConfig: !isGroup ? (groupConfig as TelegramDirectConfig | undefined) : undefined,
894+
topicConfig,
895+
})
896+
? (await resolveNativeCommandRuntime()).resolveThreadSessionKeys({
897+
baseSessionKey,
898+
threadId: `${chatId}:${dmThreadId}`,
899+
})
900+
: null;
897901
cachedTargetSessionKey = threadKeys?.sessionKey ?? baseSessionKey;
898902
return cachedTargetSessionKey;
899903
};

extensions/telegram/src/bot.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,7 +2240,7 @@ describe("createTelegramBot", () => {
22402240
undefined,
22412241
);
22422242
});
2243-
it("sets command target session key for dm topic commands", async () => {
2243+
it("keeps unconfigured dm topic commands on the flat dm session", async () => {
22442244
onSpy.mockClear();
22452245
sendMessageSpy.mockClear();
22462246
commandSpy.mockClear();
@@ -2279,7 +2279,7 @@ describe("createTelegramBot", () => {
22792279

22802280
expect(replySpy).toHaveBeenCalledTimes(1);
22812281
const payload = replySpy.mock.calls[0][0];
2282-
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:12345:99");
2282+
expect(payload.CommandTargetSessionKey).toBe("agent:main:main");
22832283
});
22842284

22852285
it("allows native DM commands for paired users", async () => {

extensions/telegram/src/bot/helpers.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
resolveTelegramForumFlag,
1414
resolveTelegramForumThreadId,
1515
resetTelegramForumFlagCacheForTest,
16+
shouldUseTelegramDmThreadSession,
1617
} from "./helpers.js";
1718

1819
describe("resolveTelegramForumThreadId", () => {
@@ -125,6 +126,33 @@ describe("buildTelegramThreadParams", () => {
125126
});
126127
});
127128

129+
describe("shouldUseTelegramDmThreadSession", () => {
130+
it("keeps incidental DM thread ids flat by default", () => {
131+
expect(shouldUseTelegramDmThreadSession({ dmThreadId: 42 })).toBe(false);
132+
});
133+
134+
it("uses DM thread sessions for explicit or topic-required configs", () => {
135+
expect(
136+
shouldUseTelegramDmThreadSession({
137+
dmThreadId: 42,
138+
directConfig: { threadReplies: "inbound" },
139+
}),
140+
).toBe(true);
141+
expect(
142+
shouldUseTelegramDmThreadSession({
143+
dmThreadId: 42,
144+
directConfig: { requireTopic: true },
145+
}),
146+
).toBe(true);
147+
expect(
148+
shouldUseTelegramDmThreadSession({
149+
dmThreadId: 42,
150+
topicConfig: { agentId: "support" },
151+
}),
152+
).toBe(true);
153+
});
154+
});
155+
128156
describe("buildTelegramRoutingTarget", () => {
129157
it.each([
130158
{

0 commit comments

Comments
 (0)