22 KiB
Messenger Connectors - Uncertainty Pass 31
Date: 2026-04-29 Scope: Telegram private-chat topics, one-topic-per-team topology, reply-to teammate routing, topic registry recoverability, and local inbox alignment
Executive Delta
The next lowest-confidence area is not whether Telegram supports topics. It does.
The weak point is whether topics can be used as a stable product navigation layer without losing routing correctness:
Telegram private topic id
-> app team route
-> lead or teammate recipient
-> durable local message
-> agent reply
-> Telegram message in the same topic
-> user replies to a concrete teammate message
The correct approach is:
One Telegram topic per team.
Route the topic to the team.
Route the recipient inside the team by reply-to message ledger, explicit command, or UI buttons.
Default no-reply messages to the lead.
Never create one bot or one topic per teammate as the default.
⚠️ Main new finding: Telegram Bot API exposes create/edit/delete topic operations, but I do not see a Bot API method for listing all private-chat topics and recovering their ids. That means our app must treat topic ids as durable provider state and store them locally/backend-side from creation time. If the registry is lost, topic recovery is weak and may require creating replacement topics.
Source Facts Rechecked
Telegram official facts checked on 2026-04-29:
- Bot API 9.3, dated December 31, 2025, added private-chat topic mode support.
- Bot API 9.3 added
User.has_topics_enabled,Message.message_thread_id, andMessage.is_topic_messagesupport for private chats with topic mode enabled. - Bot API 9.3 added
message_thread_idsupport in private chats forsendMessage, media methods,sendMediaGroup,copyMessage,forwardMessage,sendChatAction, and topic-management methods. - Bot API 9.4, dated February 9, 2026, allowed bots to create topics in private chats with
createForumTopic. - Bot API 9.4 added a BotFather setting that can prevent users from creating and deleting topics in private chats.
User.has_topics_enabledis returned bygetMeand means the bot has forum topic mode enabled in private chats.User.allows_users_to_create_topicsis returned bygetMeand indicates whether users may create/delete topics in private chats.createForumTopiccan create a topic in a forum supergroup chat or a private chat with a user.editForumTopic,deleteForumTopic, andunpinAllForumTopicMessagessupport private chats with a user.sendMessage.message_thread_idroutes a message to a forum/private-chat topic.- Incoming
Messageincludes optionalmessage_thread_id,is_topic_message,reply_to_message,media_group_id, and text/media fields. ReplyParameterslets a bot reply to a specific message id in the current chat or a specified chat.direct_messages_topic_idis for channel direct messages chats and should not be confused with forum/private-chatmessage_thread_id.- Telegram forum topics are conceptually message threads. Nested message threads inside topics are not supported.
- Telegram clients can have a "View as messages" setting for forums that shows messages from all topics in one stream. Treat this as a warning that visible topic grouping is a UX layer, not a routing authority.
Sources:
- https://core.telegram.org/bots/api-changelog
- https://core.telegram.org/bots/api#getme
- https://core.telegram.org/bots/api#user
- https://core.telegram.org/bots/api#message
- https://core.telegram.org/bots/api#sendmessage
- https://core.telegram.org/bots/api#replyparameters
- https://core.telegram.org/bots/api#createforumtopic
- https://core.telegram.org/bots/api#editforumtopic
- https://core.telegram.org/bots/api#deleteforumtopic
- https://core.telegram.org/api/forum
Local code facts:
InboxMessagealready hasfrom,to,messageId,relayOfMessageId,conversationId, andreplyToConversationId.TeamDataService.sendMessagepassesconversationIdandreplyToConversationIdinto the message controller.CrossTeamServicealready usesconversationIdandreplyToConversationIdfor cross-team threads.- OpenCode runtime delivery writes direct replies to either
user_sent_messagesormember_inbox. MessagesFilterPopoveralready derives participants from messagefromandto.MessagesPanelpending reply logic already treatsfrom=user -> to=memberandfrom=member -> to=useras meaningful route signals.- Current message model is string-name based, not stable-id based. Prior passes already identified stable route identity as a required feature layer.
Implication:
The app can represent the desired conversation shape,
but messenger connectors need a provider-neutral route registry
and provider message link ledger before Telegram topics are safe.
1. Topic Is Team Scope, Not Recipient Scope
One topic should map to one team:
chatId + messageThreadId -> teamRouteId
Recipient should be resolved inside that team:
incoming message in team topic
-> if it replies to a known bot message from teammate X, route to teammate X
-> else if it contains explicit recipient command/control, route to that recipient
-> else route to lead
Do not use topic title to route.
Topic title is display state:
"Frontend - Acme"
"API - Acme"
"API - Acme (archived)"
Route identity must be persisted as:
type MessengerTeamTopicRoute = {
routeId: string;
provider: 'telegram';
botScope: 'official' | 'own_bot';
botId: string;
telegramChatIdHash: string;
telegramChatIdEncrypted?: string;
telegramMessageThreadId: number;
teamId: string;
teamGeneration: number;
projectId: string | null;
projectGeneration: number | null;
displayTitle: string;
status:
| 'active'
| 'create_pending'
| 'create_ambiguous'
| 'renaming'
| 'renamed'
| 'delete_seen'
| 'replaced'
| 'disabled'
| 'error';
createdAt: string;
updatedAt: string;
};
For official shared bot, backend needs this route registry. For own-bot local mode, desktop can own it locally.
2. The Recoverability Problem
Creation is straightforward:
user starts bot
desktop/backend knows Telegram chat id
app creates topic for a team
Telegram returns ForumTopic
app stores message_thread_id
The low-confidence part is recovery:
What if our route registry is lost?
What if topic creation succeeded but the process crashed before storing topic id?
What if user deletes or renames a topic?
What if app creates a duplicate topic after a timeout?
I do not see a Bot API method equivalent to "list my private-chat topics". Telegram's MTProto API has forum topic listing for forums, but Bot API docs expose topic creation/edit/delete operations and no simple list method. We should not build a core invariant on being able to reconstruct topic state from Telegram later.
Therefore:
Topic registry is authoritative local/backend product state.
Telegram is an external projection.
Creation must use a two-phase state:
create_pending -> active
create_pending -> create_ambiguous
create_ambiguous -> replaced
If creation response is lost:
- do not keep retrying blindly;
- show diagnostics in app;
- allow "Create replacement topic";
- optionally send a message in the general/default bot chat asking the user to pick the right topic if we can design a safe verification flow later.
3. User Topic Deletion And BotFather Settings
Bot API 9.4 added a setting to prevent users from creating and deleting topics in private chats.
Recommended official bot configuration:
Private chat topics enabled.
Users cannot create/delete topics.
Bot manages team topics.
Why:
- fewer orphan routes;
- fewer topic id invalidation bugs;
- fewer accidental duplicates;
- cleaner support story.
But the app must still handle deletion or invalid topic errors:
sendMessage(chatId, message_thread_id) fails
-> mark topic route as error or delete_seen
-> do not fallback silently to general chat
-> create replacement topic only behind an explicit repair flow
Silent fallback to general chat is dangerous because the user may read a message outside the intended team context.
4. Reply-To Teammate Routing
The desired product behavior:
User opens team topic.
Bot posts messages from lead and teammates.
User replies to a concrete message.
App routes the reply to that concrete teammate.
This is viable if we store provider message links:
type ProviderMessageLink = {
provider: 'telegram';
routeId: string;
providerChatIdHash: string;
providerMessageThreadId: number;
providerMessageId: number;
internalMessageId: string;
internalTeamId: string;
internalFromMemberId: string;
internalToMemberId: string | null;
direction: 'telegram_to_app' | 'app_to_telegram';
createdAt: string;
};
Incoming reply resolution:
if update.message.reply_to_message.message_id exists:
lookup ProviderMessageLink by chatId + messageThreadId + reply_to_message.message_id
if found and linked internal message came from teammate:
route to that teammate
if found and linked internal message came from lead:
route to lead
if found and linked internal message came from user:
route to lead or use explicit reply target from that internal row
else:
use explicit recipient control or default to lead
Important edge case:
Telegram topics cannot have nested message threads.
Reply-to is only a pointer to a message, not a durable sub-thread per teammate.
Therefore, reply-to should be a routing hint, not the entire conversation model.
5. Explicit Recipient Controls
Reply-to is natural but insufficient.
Users will send plain messages into a topic without replying. For those messages, the app needs a deterministic default and optional controls:
Default:
message without reply -> team lead
Explicit route:
/to teammate-name message
or inline button "Reply to Alice"
or short command menu
Do not rely on Telegram mentions for routing:
- teammate names may not be Telegram users;
- agents are not Telegram accounts;
- inline mention semantics depend on Telegram user privacy and previous contact conditions;
- local app member names can change.
Suggested official MVP:
No global "active recipient" state at first.
Use reply-to for specific teammate replies.
Use /to for explicit direct messages.
Default to lead.
This is less magical but safer than hidden mutable state.
6. Message Text Format In Telegram
Because client topic grouping can be changed by the user and messages can appear in flattened views, every bot message should carry lightweight context.
Example:
[Frontend] Alice
I pushed the fix and need review on the auth callback.
For lead:
[Frontend] Lead
I will ask Alice to check the failing test.
For user-sent routed message acknowledgements:
[Frontend] to Alice
Forwarded.
Rules:
- include team label in the first line;
- include member display name for agent replies;
- keep prefixes short;
- do not include internal ids;
- do not rely only on topic title;
- avoid markdown complexity unless using explicit Telegram entities.
This makes flattened Telegram views survivable.
7. Topic Lifecycle State Machine
Suggested route lifecycle:
not_created
-> create_pending
-> active
-> renaming
-> active
-> disabled
create_pending
-> create_ambiguous
-> replaced
active
-> send_failed_topic_missing
-> repair_required
-> replacement_pending
-> active
active
-> archived
-> disabled
Do not delete topics automatically when a team is archived.
Recommended archive behavior:
- rename topic to include a compact archived marker;
- send one final "team archived" message;
- stop routing new user messages or route them to lead with a clear archived response;
- keep local route state for historical provider links.
Deletion destroys user-visible history in Telegram and makes provider message links harder to explain.
8. Rename And Duplicate Teams
Current app still relies heavily on teamName, while prior research recommended stable team ids and route generations.
Telegram topic routing should not follow only team name.
If team is renamed:
teamId stays stable
topic route stays stable
displayTitle is updated
editForumTopic is best-effort
message prefix changes after local commit
If two projects have same team name:
topic title must include a compact project discriminator
routeId must include project/team stable ids
Example topic title:
Frontend - acme-web
Frontend - mobile-app
Title length is capped, so the full identity must be in the registry, not in Telegram title.
9. Topic Creation Timing
Three possible creation timings:
Lazy create on first outbound/inbound use
Pros:
- fewer unused topics;
- less setup friction.
Cons:
- first message may be slower;
- creation failure blocks communication at the worst moment;
- ambiguous creation state can happen during a real user message.
Eager create during connect wizard
Pros:
- setup verifies topic capability early;
- failures are visible before real traffic;
- topic registry is ready.
Cons:
- creates topics for teams user may never use;
- can clutter Telegram.
Hybrid
Recommended:
Create a topic for selected/active teams during connect wizard.
Lazy-create for other teams when user enables them.
This matches "minimum user actions" without creating too many topics.
10. Route Ambiguity Cases
Inbound ambiguity cases:
- message has no
message_thread_id; - message has a thread id not in registry;
- message has a known thread id but route is disabled;
- message replies to a provider message id not in ledger;
- reply target maps to a deleted/renamed teammate;
- reply target maps to an old team generation;
- user uses
/tofor an unknown teammate; - topic title was manually changed;
- duplicate topic exists for the same team;
- user forwards/copies messages between topics;
- media group spans a topic but parts arrive separately;
- update contains
direct_messages_topicfrom channel direct messages, not private chat topic; - bot receives a message outside private chat if added to a group.
Resolution policy:
Unknown topic -> do not deliver to agent, send repair/unknown-topic notice.
Known topic + unknown reply target -> route to lead with quoted context.
Known topic + stale teammate -> route to lead and mention stale target in internal metadata.
No topic id -> onboarding/default command handling only.
Never guess a team by topic title.
11. Local UI Implications
The current Messages panel can already show participant flows from from and to. For messenger connectors, add a feature-local projection rather than rewriting the existing panel first:
type MessengerConversationProjection = {
routeId: string;
teamId: string;
provider: 'telegram';
providerTopicTitle: string;
messages: Array<{
internalMessageId: string;
providerMessageId?: number;
fromMemberId: string;
toMemberId?: string;
replyToInternalMessageId?: string;
direction: 'inbound' | 'outbound';
deliveryState: 'pending' | 'sent' | 'failed' | 'ambiguous';
}>;
};
The renderer can keep using participant filters, but messenger-specific state should live in src/features/messenger-connectors/renderer:
messenger feature hook
-> maps route/thread state into view model
-> existing MessagesPanel can show the durable local messages
-> optional connector status panel shows Telegram topic health
Do not put Telegram concepts directly into shared InboxMessage unless they are provider-neutral.
Provider-specific fields belong in a feature table/store:
provider_message_links
provider_route_registry
provider_delivery_ledger
12. Architecture Fit
This feature clearly qualifies for the canonical feature architecture:
src/features/messenger-connectors/
contracts/
core/
domain/
route.ts
topic.ts
recipient-resolution.ts
provider-message-link.ts
application/
ports.ts
connect-messenger.ts
receive-provider-update.ts
send-provider-reply.ts
repair-topic-route.ts
main/
composition/
adapters/
input/
ipc/
telegram-webhook/
desktop-relay/
output/
telegram/
team-messages/
local-store/
infrastructure/
preload/
renderer/
Core domain invariants:
1. Provider topic title never determines route identity.
2. Provider thread id maps to exactly one active team route per bot/chat.
3. Recipient resolution is deterministic and auditable.
4. Unknown topic never reaches an agent as a normal user message.
5. Every outbound Telegram message that can be replied to has a ProviderMessageLink.
6. Topic repair never silently changes the user's message destination.
13. Top 3 Options
Option 1 - One topic per team, reply-to ledger, default to lead, /to escape hatch
🎯 8 🛡️ 9 🧠 6
Approx changed LOC: 2500-5500.
What it means:
- each team has one Telegram private topic;
- inbound messages in that topic route to the lead by default;
- replying to a known teammate message routes to that teammate;
/to teammate messageprovides explicit routing;- topic id and provider message links are stored durably;
- unknown/stale topics enter repair flow.
Why this is best:
- matches the user's selected model;
- avoids topic explosion;
- works with current
from/tomessage model; - scales to many teams better than per-teammate topics;
- keeps routing deterministic.
Risk:
- users must learn reply-to or
/tofor teammate-specific messages; - if provider message link ledger is missing, teammate routing falls back to lead;
- requires solid route registry.
Option 2 - One topic per team with mutable active recipient controls
🎯 6 🛡️ 7 🧠 7
Approx changed LOC: 3500-7000.
What it means:
- each topic has controls such as "Active recipient: Alice";
- user taps inline buttons or commands to switch active recipient;
- plain messages route to current active recipient until changed.
Why it is tempting:
- fewer reply-to requirements;
- feels convenient on mobile;
- user can have a visible selected target.
Risk:
- hidden mutable state across desktop and phone is easy to misunderstand;
- two devices/users can change active recipient unexpectedly;
- stale controls can route messages incorrectly;
- callback handling and status messages add complexity.
This can be added later after Option 1, but I would not make it the first model.
Option 3 - One topic per teammate or per internal conversation
🎯 4 🛡️ 6 🧠 8
Approx changed LOC: 4000-9000.
What it means:
- team lead has one topic;
- each teammate has a separate topic;
- or each conversation creates a topic.
Why it looks reliable:
- recipient is obvious from topic;
- fewer reply-to resolution rules.
Why it is worse:
- topic count explodes;
- Telegram UI becomes cluttered;
- team context fragments;
- archiving/renaming/recovering many topics is painful;
- cross-team/project grouping becomes harder;
- user wanted one team context, not dozens of technical threads.
Use only for a future "power mode" if users explicitly ask for per-agent topics.
14. Decision Update
Recommended design:
Default official bot:
one private topic per team
topic id maps to team route
default route to lead
reply-to route to teammate through ProviderMessageLink
`/to` command as explicit escape hatch
no mutable active recipient in MVP
Required build blocks before implementation:
1. Stable TeamRoute identity independent of teamName.
2. MessengerTopicRegistry with route generations and repair states.
3. ProviderMessageLink ledger for every Telegram outbound message.
4. RecipientResolver pure domain service.
5. UnknownTopicPolicy that never sends unknown messages to agents.
6. TopicRepair use case.
7. Tests for duplicate, deleted, renamed, stale, and unknown topics.
The most important invariant:
Telegram topic/thread id chooses team.
Provider reply-to message id chooses teammate.
Plain topic message chooses lead.
15. Tests To Write First
Domain tests:
- known topic + no reply -> lead;
- known topic + reply to lead message -> lead;
- known topic + reply to teammate message -> teammate;
- known topic + reply to user message -> lead;
- known topic + unknown reply message id -> lead with ambiguity metadata;
- unknown topic -> repair/notice, not agent delivery;
- disabled topic -> archived/disabled response, not agent delivery;
- duplicate topic route -> terminal config error;
- renamed team -> same route id, updated display title;
- deleted teammate -> lead fallback with stale target metadata;
/to Alice hello-> Alice;/to unknown hello-> lead or error notice by policy;- media group in known topic -> same team route for all parts.
Adapter tests:
createForumTopicsuccess persistsmessage_thread_id;- create response lost enters
create_ambiguous; sendMessageincludes correctmessage_thread_id;sendMessagefailure for topic not found marks repair-required;- inbound update stores provider message link before local delivery ACK;
- outbound provider message id is stored before considering Telegram delivery complete;
- duplicate webhook with same provider message id returns existing local route.
Renderer tests:
- connector status panel shows topic healthy/error/repair-required;
- message row prefix includes team/member context for Telegram projection;
- participant filters still work with messenger-originated messages;
- reply-to unavailable shows lead fallback reason.
16. Remaining Low-Confidence Areas
Still worth deeper research next:
- exact Telegram client UX for private-chat topics on mobile and desktop after Bot API 9.3/9.4;
- whether BotFather private topic settings can be configured programmatically or only manually;
- exact error codes returned when a private topic is deleted or disabled;
- whether Telegram private topics expose enough update events to detect user rename/delete promptly;
- how long topic titles can remain readable with many projects and similar team names;
- whether
sendMessageDraftcould improve "agent is typing" UX per team topic without creating noisy messages; - how to migrate a user from official shared bot topics to own-bot topics without losing local route history.