Skip to content

feat(telegram): include forum topic name in inbound metadata#36916

Closed
Sid-Qin wants to merge 1 commit intoopenclaw:mainfrom
Sid-Qin:feat/36260-telegram-topic-name-metadata
Closed

feat(telegram): include forum topic name in inbound metadata#36916
Sid-Qin wants to merge 1 commit intoopenclaw:mainfrom
Sid-Qin:feat/36260-telegram-topic-name-metadata

Conversation

@Sid-Qin
Copy link
Copy Markdown
Contributor

@Sid-Qin Sid-Qin commented Mar 6, 2026

Summary

  • Problem: When receiving messages from Telegram forum topics, the inbound metadata only includes the numeric MessageThreadId but not the topic's display name. Agents cannot auto-match to topic files without manual ID-to-name mapping.
  • Why it matters: Agents need the topic name to automatically load the correct context file (e.g., Design-learning.md) without requiring hardcoded numeric mappings.
  • What changed:
    • src/auto-reply/templating.ts: Added MessageTopicName?: string to MsgContext.
    • src/telegram/topic-name-cache.ts: New bounded in-memory cache (LRU, max 2048 entries) mapping chatId:threadId → topic name.
    • src/telegram/bot-handlers.ts: Extracts and caches topic names from forum_topic_created and forum_topic_edited service messages.
    • src/telegram/bot-message-context.ts: Looks up cached topic name and populates MessageTopicName in the inbound context.
    • src/telegram/send.ts: Caches topic name when bot creates a topic via createForumTopicTelegram.
  • What did NOT change: Existing MessageThreadId and IsForum fields. Non-forum messages. Other channels.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • MessageTopicName is now available in inbound metadata for Telegram forum topic messages. Example: when a message arrives in a topic named "Design-learning", agents receive MessageTopicName: "Design-learning" alongside MessageThreadId: 42.
  • Topic names are cached from forum_topic_created/forum_topic_edited service messages and bot-created topics.
  • Cache is in-memory only; topic names are re-learned after gateway restart when the next service message or topic creation occurs.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS
  • Runtime: Node.js 22
  • Integration/channel: Telegram (forum supergroup)

Steps

  1. Create a forum topic in a Telegram supergroup where the bot is admin
  2. Send a message in that topic
  3. Check inbound metadata for MessageTopicName

Expected

  • MessageTopicName contains the topic display name

Actual

  • Before fix: Only MessageThreadId (numeric) available
  • After fix: MessageTopicName populated from cached topic name

Evidence

 src/auto-reply/templating.ts        |  2 ++
 src/telegram/bot-handlers.ts        | 16 +++++++++++++++
 src/telegram/bot-message-context.ts |  5 +++++
 src/telegram/send.ts                |  3 +++
 src/telegram/topic-name-cache.ts    | 21 +++++++++++++++++++++
 5 files changed, 47 insertions(+)

All 51 existing Telegram send tests pass.

Human Verification (required)

  • Verified scenarios: forum_topic_created caching, forum_topic_edited caching, bot-created topic caching, lookup for forum messages, undefined for non-forum messages
  • Edge cases checked: Missing message_thread_id, non-forum chats, cache eviction (LRU at 2048), topic rename via forum_topic_edited
  • What I did not verify: Live Telegram forum with bot in supergroup

Compatibility / Migration

  • Backward compatible? Yes — new optional field, existing behavior unchanged
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert: Revert this commit; MessageTopicName will be undefined
  • Files/config to restore: 5 files listed above
  • Known bad symptoms: None — additive field, undefined when unavailable

Risks and Mitigations

  • Risk: Topic names not available until after the first service message or topic creation (cold cache)
  • Mitigation: This matches the Telegram Bot API limitation; getForumTopicInfo is not available. The cache populates organically.

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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 6, 2026

Greptile Summary

This PR adds MessageTopicName to the inbound Telegram metadata by introducing a bounded in-memory cache that is populated from forum_topic_created / forum_topic_edited service messages and bot-created topics. The change is purely additive and backward-compatible — existing fields and non-forum paths are untouched.

Key observations:

  • Eviction strategy is FIFO, not LRU: Map.set on an existing key preserves its original insertion position. A renamed topic (written via forum_topic_edited) retains its old position and can be evicted before entries that were inserted later, which is the inverse of LRU semantics. Consider cache.delete(key) before cache.set(key, name) to move updated entries to the end of the Map's iteration order.
  • Type-unsafe cast in bot-handlers.ts: msg as unknown as Record<string, unknown> bypasses TypeScript to reach forum_topic_created / forum_topic_edited, but @grammyjs/types exposes these as typed optional fields directly on Message. Using the typed fields directly eliminates the escape hatch.
  • Cold-cache gap is documented and expected: topic names are only available after the first relevant service message or bot-created topic; this matches the Telegram Bot API limitation.
  • All 51 existing Telegram send tests continue to pass.

Confidence Score: 4/5

  • Safe to merge — purely additive feature with no changes to existing paths; two minor style issues with the cache eviction order and a type-unsafe cast.
  • The feature is well-scoped and backward-compatible. The only issues are: (1) the eviction strategy is FIFO rather than the claimed LRU (low practical impact given the 2048-entry limit), and (2) a double-unknown cast that could be replaced by grammy's existing typed fields. Neither is a runtime correctness bug for the core feature.
  • src/telegram/topic-name-cache.ts (eviction order), src/telegram/bot-handlers.ts (type cast)

Last reviewed commit: 9749a8e

Comment on lines +75 to +83
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);
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.

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:

Suggested change
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.

Comment on lines +8 to +16
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);
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.

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +860 to +863
MessageTopicName:
isForum && typeof messageThreadId === "number"
? getTopicName(chatId, messageThreadId)
: undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

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: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Include Telegram forum topic name in inbound metadata

2 participants