Skip to content

fix(telegram): preserve DM topic thread id for outbound media#7261

Open
ViffyGwaanl wants to merge 5 commits intoopenclaw:mainfrom
ViffyGwaanl:fix/telegram-dm-topic-media-threadid
Open

fix(telegram): preserve DM topic thread id for outbound media#7261
ViffyGwaanl wants to merge 5 commits intoopenclaw:mainfrom
ViffyGwaanl:fix/telegram-dm-topic-media-threadid

Conversation

@ViffyGwaanl
Copy link

@ViffyGwaanl ViffyGwaanl commented Feb 2, 2026

Problem

In Telegram private chats with topics (Bot API 9.3+), async followup sends (routeReply/outbound → sendMessageTelegram) could drop message_thread_id when the DM thread id is 1, because sendMessageTelegram always treated thread ids as forum scope.

This causes outbound media/documents to land in the main DM window instead of the correct DM topic.

Fix

  • Detect private vs forum chat ids (positive chat id = private chat).
  • Build threadSpec with scope dm for private chats and forum otherwise, so buildTelegramThreadParams only omits id=1 for forum General topic, but keeps id=1 for DM topics.

Tests

  • Added tests covering DM message_thread_id=1 for media/doc sends and stickers.
  • Added tests ensuring forum General topic id=1 still omits message_thread_id.

This should fix delayed media/file replies in DM topics while preserving existing forum behavior.

Greptile Overview

Greptile Summary

This PR updates Telegram outbound sending to preserve message_thread_id=1 when sending to DM topics (Bot API 9.3+), while continuing to omit message_thread_id=1 for forum “General” topic sends. It does this by inferring whether the destination is a private chat vs a forum chat and passing a corresponding scope into buildTelegramThreadParams, plus adds unit tests covering DM topic id=1 for documents/media and stickers and forum general-topic omission.

Confidence Score: 3/5

  • This PR is likely safe to merge, but the DM-vs-forum detection and one of the new tests could allow regressions to slip through.
  • Core change is small and leverages existing buildTelegramThreadParams behavior, but it duplicates chat-id parsing logic in two places and can misclassify non-numeric chat IDs (e.g., @username). Additionally, the new “omit message_thread_id” test does not robustly assert absence of the field, reducing confidence that the intended regression is covered.
  • src/telegram/send.ts and src/telegram/send.returns-undefined-empty-input.test.ts

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

@openclaw-barnacle openclaw-barnacle bot added the channel: telegram Channel integration: telegram label Feb 2, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 225 to 227
// Detect DM vs forum: positive chatId = DM, negative (especially -100...) = forum
const isPrivateChat = /^-?\d+$/.test(chatId) && Number.parseInt(chatId, 10) > 0;
const scope = isPrivateChat ? "dm" : "forum";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid re-parsing chatId here; use target info or existing helpers.

normalizeChatId() already ensures numeric IDs match /^-?\d+$/ and parseTelegramTarget(to) typically contains enough structure to determine DM vs group/forum. Re-checking with RegExp + parseInt adds duplication and can misclassify non-numeric chat identifiers (e.g. @username) as forum even if they’re private/user chats; if messageThreadId is present in that case, we’ll send it with scope: "forum" and may incorrectly drop id=1.

Also appears in src/telegram/send.ts:705-708.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/send.ts
Line: 225:227

Comment:
[P2] Avoid re-parsing `chatId` here; use target info or existing helpers.

`normalizeChatId()` already ensures numeric IDs match `/^-?\d+$/` and `parseTelegramTarget(to)` typically contains enough structure to determine DM vs group/forum. Re-checking with `RegExp` + `parseInt` adds duplication and can misclassify non-numeric chat identifiers (e.g. `@username`) as `forum` even if they’re private/user chats; if `messageThreadId` is present in that case, we’ll send it with `scope: "forum"` and may incorrectly drop `id=1`.

Also appears in `src/telegram/send.ts:705-708`.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc9f363fd:

  • Removed regex/parseInt re-parsing and the as const assertion.
  • Added resolveTelegramThreadScope(chatId) that uses Number(chatId) (finite + >0 => dm, else forum). Non-numeric chat IDs (e.g. @username) are treated as non-DM.
    This preserves DM topic thread ids (including message_thread_id=1) while still omitting forum general-topic thread id.

Comment on lines 599 to 618
it("omits message_thread_id=1 for forum general topic", async () => {
const chatId = "-1001234567890";
const sendMessage = vi.fn().mockResolvedValue({
message_id: 59,
chat: { id: chatId },
});
const api = { sendMessage } as unknown as {
sendMessage: typeof sendMessage;
};

await sendMessageTelegram(chatId, "general topic message", {
token: "tok",
api,
messageThreadId: 1,
});

expect(sendMessage).toHaveBeenCalledWith(chatId, "general topic message", {
parse_mode: "HTML",
// message_thread_id should be omitted for forum general topic (id=1)
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This assertion doesn’t actually verify omission of message_thread_id.

toHaveBeenCalledWith(chatId, "general topic message", { parse_mode: "HTML" }) will pass even if the implementation adds extra keys (e.g. message_thread_id: 1) unless you assert on the exact object or explicitly assert the absence via expect.objectContaining + checking keys. As written, the comment in the expected object is ignored at runtime.

Also impacts the sticker general-topic test at src/telegram/send.returns-undefined-empty-input.test.ts:748-766 (it uses undefined params, but could still miss cases where a params object is passed without message_thread_id).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/send.returns-undefined-empty-input.test.ts
Line: 599:618

Comment:
[P3] This assertion doesn’t actually verify omission of `message_thread_id`.

`toHaveBeenCalledWith(chatId, "general topic message", { parse_mode: "HTML" })` will pass even if the implementation adds extra keys (e.g. `message_thread_id: 1`) unless you assert on the exact object or explicitly assert the absence via `expect.objectContaining` + checking keys. As written, the comment in the expected object is ignored at runtime.

Also impacts the sticker general-topic test at `src/telegram/send.returns-undefined-empty-input.test.ts:748-766` (it uses `undefined` params, but could still miss cases where a params object is passed without `message_thread_id`).

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the tests in dc9f363fd:

  • Use expect.objectContaining({ parse_mode: "HTML" }) for the params bag.
  • Explicitly assert message_thread_id is omitted: expect(params?.message_thread_id).toBeUndefined().
    This makes the “forum general topic (id=1) omits thread id” behavior actually enforced.

@ViffyGwaanl
Copy link
Author

ViffyGwaanl commented Feb 3, 2026

更新:我已经把分支 rebase 到最新 main 并解决了冲突(冲突点在 tsconfig.json,上游最近合并了 tsconfig/ui 相关改动)。当前 PR 状态已变为 MERGEABLE,不再需要 ‘Resolve conflicts’ 操作。

我之前提到的 allowImportingTsExtensions / bun build 报错是基于当时那一轮 CI 的输出;上游最新 ci.yml 已调整(目前只跑 bun 的 vitest,不再跑 bun 的 tsc build),所以我这边没有再修改 tsconfig.json

我已 force-push 更新分支并触发了新的 CI(现在 checks 都在 pending 跑)。谢谢!

@ViffyGwaanl ViffyGwaanl force-pushed the fix/telegram-dm-topic-media-threadid branch from 76487ec to 00b9b9e Compare February 3, 2026 14:47
@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 3, 2026
@ViffyGwaanl
Copy link
Author

CI update: Windows checks-windows (node, test) was failing with:

  • src/agents/pi-tools.workspace-paths.test.ts ENOENT during process.chdir(...) (worker crash + timeout)

I pushed a stabilization fix to the PR branch:

  • commit b8e9d20a6 (plus 1e2e4a995)

This wraps the CWD mutation in a helper that restores cwd best-effort (and resets to GITHUB_WORKSPACE after each test) so Windows runners don't crash if cwd becomes invalid.

New CI run has been triggered (run: 21636153404).

@ViffyGwaanl ViffyGwaanl force-pushed the fix/telegram-dm-topic-media-threadid branch from b8e9d20 to bb15e98 Compare February 6, 2026 13:04
@openclaw-barnacle openclaw-barnacle bot removed the agents Agent runtime and tooling label Feb 6, 2026
@ViffyGwaanl ViffyGwaanl force-pushed the fix/telegram-dm-topic-media-threadid branch from bb15e98 to eb1cd04 Compare February 9, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments