feat(telegram): include forum topic name in inbound metadata#36916
feat(telegram): include forum topic name in inbound metadata#36916Sid-Qin wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Agents receiving messages from Telegram forum topics now get the topic display name in MessageTopicName, alongside the existing numeric MessageThreadId. Topic names are captured from forum_topic_created and forum_topic_edited service messages and stored in a bounded in-memory cache (LRU, max 2048 entries). Bot-created topics (createForumTopic) also populate the cache. This lets agents auto-match to topic-specific files by name without requiring manual ID-to-name mapping. Closes openclaw#36260
Greptile SummaryThis PR adds Key observations:
Confidence Score: 4/5
Last reviewed commit: 9749a8e |
| const raw = msg as unknown as Record<string, unknown>; | ||
| const created = raw.forum_topic_created as { name?: string } | undefined; | ||
| if (created?.name && msg.message_thread_id != null) { | ||
| cacheTopicName(chatId, msg.message_thread_id, created.name); | ||
| return; | ||
| } | ||
| const edited = raw.forum_topic_edited as { name?: string } | undefined; | ||
| if (edited?.name && msg.message_thread_id != null) { | ||
| cacheTopicName(chatId, msg.message_thread_id, edited.name); |
There was a problem hiding this comment.
as unknown as Record<string, unknown> cast bypasses available grammy types
The @grammyjs/types package exposes forum_topic_created?: ForumTopicCreated and forum_topic_edited?: ForumTopicEdited directly on the Message type (with ForumTopicCreated.name: string and ForumTopicEdited.name?: string). The double cast to Record<string, unknown> is therefore unnecessary and silently bypasses TypeScript's type checking — for example, it would hide a typo in a field name at compile time.
Consider typing the access directly:
| const raw = msg as unknown as Record<string, unknown>; | |
| const created = raw.forum_topic_created as { name?: string } | undefined; | |
| if (created?.name && msg.message_thread_id != null) { | |
| cacheTopicName(chatId, msg.message_thread_id, created.name); | |
| return; | |
| } | |
| const edited = raw.forum_topic_edited as { name?: string } | undefined; | |
| if (edited?.name && msg.message_thread_id != null) { | |
| cacheTopicName(chatId, msg.message_thread_id, edited.name); | |
| const created = msg.forum_topic_created; | |
| if (created?.name && msg.message_thread_id != null) { | |
| cacheTopicName(chatId, msg.message_thread_id, created.name); | |
| return; | |
| } | |
| const edited = msg.forum_topic_edited; | |
| if (edited?.name && msg.message_thread_id != null) { | |
| cacheTopicName(chatId, msg.message_thread_id, edited.name); | |
| } |
If the installed version of @grammyjs/types doesn't yet expose these fields, a narrowly-scoped augmentation (interface Message { forum_topic_created?: ... }) or a type assertion with an explicit inline type would be safer than the double-unknown escape hatch.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/bot-handlers.ts
Line: 75-83
Comment:
**`as unknown as Record<string, unknown>` cast bypasses available grammy types**
The `@grammyjs/types` package exposes `forum_topic_created?: ForumTopicCreated` and `forum_topic_edited?: ForumTopicEdited` directly on the `Message` type (with `ForumTopicCreated.name: string` and `ForumTopicEdited.name?: string`). The double cast to `Record<string, unknown>` is therefore unnecessary and silently bypasses TypeScript's type checking — for example, it would hide a typo in a field name at compile time.
Consider typing the access directly:
```suggestion
const created = msg.forum_topic_created;
if (created?.name && msg.message_thread_id != null) {
cacheTopicName(chatId, msg.message_thread_id, created.name);
return;
}
const edited = msg.forum_topic_edited;
if (edited?.name && msg.message_thread_id != null) {
cacheTopicName(chatId, msg.message_thread_id, edited.name);
}
```
If the installed version of `@grammyjs/types` doesn't yet expose these fields, a narrowly-scoped augmentation (`interface Message { forum_topic_created?: ... }`) or a type assertion with an explicit inline type would be safer than the double-`unknown` escape hatch.
How can I resolve this? If you propose a fix, please make it concise.| export function cacheTopicName(chatId: number | string, threadId: number, name: string): void { | ||
| const key = cacheKey(chatId, threadId); | ||
| if (cache.size >= CACHE_MAX_SIZE && !cache.has(key)) { | ||
| const oldest = cache.keys().next(); | ||
| if (oldest.done === false) { | ||
| cache.delete(oldest.value); | ||
| } | ||
| } | ||
| cache.set(key, name); |
There was a problem hiding this comment.
Eviction is FIFO, not LRU — renamed topics can be evicted prematurely
JavaScript's Map preserves insertion order, and calling cache.set(existingKey, newValue) updates the value but does not move the key to the end of the Map's iteration order. This means that when cacheTopicName is called for a forum_topic_edited rename, the entry keeps its original (old) insertion position. If the cache later fills up, the just-updated entry could be evicted before entries that were inserted more recently — the opposite of LRU behavior.
To give writes true LRU semantics, delete the key before re-inserting it so it moves to the end of the Map's order:
cache.delete(key); // move to end by re-inserting
cache.set(key, name);
The surrounding guard also needs adjustment: skip eviction when cache.has(key) (since size won't grow), and evict only when inserting a genuinely new key.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/topic-name-cache.ts
Line: 8-16
Comment:
**Eviction is FIFO, not LRU — renamed topics can be evicted prematurely**
JavaScript's `Map` preserves insertion order, and calling `cache.set(existingKey, newValue)` updates the value but **does not move the key to the end** of the Map's iteration order. This means that when `cacheTopicName` is called for a `forum_topic_edited` rename, the entry keeps its original (old) insertion position. If the cache later fills up, the just-updated entry could be evicted before entries that were inserted more recently — the opposite of LRU behavior.
To give writes true LRU semantics, delete the key before re-inserting it so it moves to the end of the Map's order:
```
cache.delete(key); // move to end by re-inserting
cache.set(key, name);
```
The surrounding guard also needs adjustment: skip eviction when `cache.has(key)` (since size won't grow), and evict only when inserting a genuinely new key.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9749a8eb85
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| MessageTopicName: | ||
| isForum && typeof messageThreadId === "number" | ||
| ? getTopicName(chatId, messageThreadId) | ||
| : undefined, |
There was a problem hiding this comment.
Expose topic name in inbound metadata outputs
I traced the default agent prompt path (src/auto-reply/reply/get-reply-run.ts -> buildInboundMetaSystemPrompt / buildInboundUserContextPrefix) and those serializers still only include topic_id/is_forum, not MessageTopicName. Because this new field is only assigned here, forum topic names remain invisible to standard agent runs unless users build custom templates/hooks, so the feature goal (agents auto-matching by topic name) is not met in the default flow.
Useful? React with 👍 / 👎.
Summary
MessageThreadIdbut not the topic's display name. Agents cannot auto-match to topic files without manual ID-to-name mapping.Design-learning.md) without requiring hardcoded numeric mappings.src/auto-reply/templating.ts: AddedMessageTopicName?: stringtoMsgContext.src/telegram/topic-name-cache.ts: New bounded in-memory cache (LRU, max 2048 entries) mappingchatId:threadId→ topic name.src/telegram/bot-handlers.ts: Extracts and caches topic names fromforum_topic_createdandforum_topic_editedservice messages.src/telegram/bot-message-context.ts: Looks up cached topic name and populatesMessageTopicNamein the inbound context.src/telegram/send.ts: Caches topic name when bot creates a topic viacreateForumTopicTelegram.MessageThreadIdandIsForumfields. Non-forum messages. Other channels.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
MessageTopicNameis now available in inbound metadata for Telegram forum topic messages. Example: when a message arrives in a topic named "Design-learning", agents receiveMessageTopicName: "Design-learning"alongsideMessageThreadId: 42.forum_topic_created/forum_topic_editedservice messages and bot-created topics.Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
Steps
MessageTopicNameExpected
MessageTopicNamecontains the topic display nameActual
MessageThreadId(numeric) availableMessageTopicNamepopulated from cached topic nameEvidence
All 51 existing Telegram send tests pass.
Human Verification (required)
Compatibility / Migration
Yes — new optional field, existing behavior unchangedNoNoFailure Recovery (if this breaks)
MessageTopicNamewill be undefinedRisks and Mitigations
getForumTopicInfois not available. The cache populates organically.