Skip to content

fix(telegram): keep regular supergroup replies on the chat thread - #1063

Open
tarasyarema wants to merge 2 commits into
vercel:mainfrom
tarasyarema:fix/telegram-reply-thread-outside-forums
Open

tarasyarema wants to merge 2 commits into
vercel:mainfrom
tarasyarema:fix/telegram-reply-thread-outside-forums

Conversation

@tarasyarema

Copy link
Copy Markdown

Summary

In a regular (non-forum) supergroup, a reply to the bot never reached onSubscribedMessage. Telegram sets message_thread_id on a reply outside forums, to the id of the message it answers, and leaves is_topic_message unset. The adapter treated any message_thread_id as a forum topic, so the reply got its own thread ID, telegram:{chatId}:{messageThreadId}. The bot was subscribed to telegram:{chatId}, so the reply was dropped without an error.

The adapter now treats message_thread_id as a thread only when is_topic_message is true. A new getTopicThreadId helper applies that rule wherever the adapter builds a thread ID from a Telegram message: incoming messages, slash commands, callback queries, parseMessage, and the messages returned by postMessage (which reply uses), editMessage, and streaming. For sent messages, the adapter still falls back to the target thread's topic when the Bot API response omits is_topic_message. Forum topics keep their own threads.

The mention-on-reply check already scopes its forum exclusion to is_topic_message, so it needed no change.

Also in this PR:

  • A patch changeset for @chat-adapter/telegram.
  • A "Threads" section on the Telegram adapter docs page and a note in the package AGENTS.md.
  • packages/integration-tests/src/replies.test.ts now expects a regular supergroup reply on telegram:{chatId}, with no message_thread_id on the bot's answer.

Test plan

Checks ran scoped to the Telegram adapter. The full-repo pnpm validate was skipped: the sandbox that produced this PR has 2 CPUs and 4 GB of RAM, and two earlier runs of the full validate and the examples build ran out of resources and crashed.

  • pnpm turbo run build --filter=@chat-adapter/telegram... passes.
  • pnpm --filter @chat-adapter/telegram typecheck passes.
  • pnpm --filter @chat-adapter/telegram test passes: 382 tests.
  • biome check on the changed files passes.
  • vitest run src/replies.test.ts in packages/integration-tests passes: 9 tests.
  • The two new regression tests fail against the adapter code on main and pass with this change. The forum topic case passes on both.

Checklist

  • All commits are signed and verified
  • All commits are signed off for the DCO (git commit -s)
  • pnpm validate passes (skipped for sandbox memory reasons; see Test plan)
  • Changeset added (or N/A — see CONTRIBUTING.md)
  • Documentation updated (or N/A)

Outside forums, Telegram sets message_thread_id on a reply to the id of
the message it answers. The adapter treated that as a forum topic and
routed the reply to telegram:{chatId}:{messageThreadId}, so a bot
subscribed to the chat thread never received it. Only use
message_thread_id as a thread when is_topic_message is true.

Signed-off-by: Taras' Swarm <tarasyaremabcn@gmail.com>
@vercel

vercel Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@tarasyarema is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@tarasyarema

Copy link
Copy Markdown
Author

@cramforce hi any chance to check this in the near future?

@roboeve roboeve Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for this. The core fix is right: gating message_thread_id on is_topic_message is the same rule grammY and Telegraf apply, and routing every message-to-thread conversion through one helper keeps the inbound and outbound paths consistent.

What I checked locally on d3eab4a

  • pnpm turbo run build --filter=@chat-adapter/telegram...: passes
  • tsc --noEmit in packages/adapter-telegram: passes
  • vitest run in packages/adapter-telegram: 382 passed
  • vitest run src/replies.test.ts in packages/integration-tests: 9 passed
  • biome check on the changed files: clean
  • With src/index.ts restored to the base version, both new regression tests fail and the forum case passes, so the tests do guard the fix.

Requested changes

  1. Document the behavior change for existing deployments (inline on the changeset). Thread IDs of the form telegram:{chatId}:{n} that a bot already stored for a regular supergroup (subscriptions, thread.setState, anything keyed on thread.id) stop matching after this ships, and subscribing from a reply chain now subscribes to the whole chat. That is the intended outcome, but it should be spelled out for upgraders.
  2. Ground the fix in a captured payload. The repo AGENTS.md asks for sample-messages.md to be extended when fixing parsers. The existing captured sample, "Reply to a bot message in a group", has no message_thread_id at all, so the repo currently has no real payload showing the shape this PR fixes. Please add a sanitized regular-supergroup reply that carries message_thread_id without is_topic_message (and, if you have it, the matching sendMessage response).
  3. Tighten the message_thread_id wording (inline). It's described in several places as "the id of the message it answers". The Bot API documents it as the id of "a message thread or forum topic". For a reply to a reply it's my understanding that this is the thread's root message rather than the direct parent, so the claim only holds for one level of reply. The code doesn't depend on it, but the docs, changeset, AGENTS.md, JSDoc and test comments do.

Nits (non-blocking)

  • Test coverage only exercises the incoming webhook and reply/postMessage. parseMessage, callback queries, and editMessage/streaming also changed; a parseMessage case and a callback-query case would be cheap to add.
  • handleMessageReactionUpdate still uses reactionUpdate.message_thread_id as-is (not in this diff, around src/index.ts:1488). MessageReactionUpdated carries no is_topic_message, so it can't use the helper. That's fine to leave for now, but the PR description's "wherever the adapter builds a thread ID" slightly overstates it.
  • pnpm validate hasn't run (knip in particular). CI should cover it, just flagging that the checklist item is still open.

"@chat-adapter/telegram": patch
---

Keep replies in a regular supergroup on the chat's thread. Telegram sets `message_thread_id` on a reply outside forums, to the id of the message it answers, and the adapter turned that into a separate `telegram:{chatId}:{messageThreadId}` thread. A bot subscribed to the chat never saw the reply. The adapter now uses `message_thread_id` as a thread only when `is_topic_message` is `true`, so forum topics keep their own threads.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add an upgrade note. Bots that already stored telegram:{chatId}:{n} thread IDs for a regular supergroup (subscriptions, thread state) lose them after this ships: follow-ups now arrive on telegram:{chatId} and go to onNewMention/onNewMessage instead of onSubscribedMessage. Subscribing from inside a reply chain also now subscribes to the whole chat. One or two sentences here (and in the docs Threads section) would cover it. It's also worth a maintainer's call on whether patch is still the right bump for a change to thread IDs.

Comment thread packages/adapter-telegram/src/index.ts Outdated
* The forum topic a message belongs to, or `undefined` outside topics.
*
* Only a topic message (`is_topic_message`) gets its own thread. In a regular
* supergroup Telegram also sets `message_thread_id` on a reply, to the id of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit on accuracy: "to the id of the message it answers" is only true for a direct reply to a top-level message. The Bot API calls this "a message thread or forum topic", and for a reply to a reply I believe it's the root of the reply thread. Something like "Telegram also sets message_thread_id on replies, identifying the reply thread" stays correct either way. The same wording appears in the changeset, the docs page, AGENTS.md, and the two test comments.

});

it.each([
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: both cases use message_thread_id === reply_to_message.message_id. A case where they differ (a reply to a reply, with message_thread_id set to the thread root) would show the routing doesn't depend on that coincidence.

markdown: "hello",
});

expect(posted.threadId).toBe("telegram:-100123");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: worth asserting the outgoing request too, as the integration test does, so this also proves the bot doesn't send message_thread_id back to a regular supergroup:

const body = JSON.parse(
  String((mockFetch.mock.calls[1]?.[1] as RequestInit).body)
);
expect(body.message_thread_id).toBeUndefined();


## Threads

Each Telegram chat is one thread, `telegram:{chatId}`. In a forum supergroup, each topic is its own thread, `telegram:{chatId}:{messageThreadId}`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: "Each Telegram chat is one thread" leaves out two existing forms: topics in private chats with the bot (the is_topic_message doc in types.ts covers these) and business threads, telegram:biz:{connectionId}:{chatId}. Either list them or phrase this as the default and link to the full list.

@roboeve

roboeve Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

hi any chance to check this in the near future?

Hi @tarasyarema, thanks for the contribution, and we appreciate that you're keen to get this reviewed soon. Please don't tag individual maintainers to ask for a review, though. It won't make the review happen any sooner, and PRs that do this may be closed without notice.

Open PRs are triaged by the team as capacity allows. A review with requested changes is already on this one, so addressing that feedback is the best way to move it forward.

Bots that stored telegram:{chatId}:{n} thread IDs for a regular
supergroup lose them once replies move to telegram:{chatId}, so the
changeset and the docs Threads section now spell that out.

message_thread_id identifies the reply thread by its root message, so
it is not the id of the message a reply answers once replies nest. The
docs, AGENTS.md, JSDoc and test comments no longer claim it is.

New tests cover a reply to a reply, parseMessage, callback queries, and
the request reply sends to a regular supergroup.

Signed-off-by: Taras' Swarm <tarasyaremabcn@gmail.com>

This branch was successfully deployed

1 active (outdated) deployment
Preview — d3eab4ae Deployed Oct 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant