* fix(telegram): clean up thread bindings to stale/failed ACP sessions on startup
When loading persisted thread bindings on manager creation, validate each
ACP session against the session store. Remove bindings where:
- Session entry doesn't exist (deleted externally)
- Session status is failed/killed/timeout
- ACP runtime state is 'error'
This addresses issue #60102 where Telegram DMs remained routed to stale
ACP sessions even after restart, because the binding file persisted
across restarts without validating the target session was still valid.
* fix(telegram): guard against null session entry and transient store read failures
Address review comments on PR #67822:
1. Skip bindings when readAcpSessionEntry returns null or when
session store is temporarily unreadable (storeReadFailed: true).
Without this, a transient I/O error would mark all ACP bindings
as stale and delete them on every startup.
2. Only set needsPersist when bindings were actually removed.
Previously, stale session keys from OTHER accounts could set
needsPersist=true even when zero bindings were removed for
the current account — causing spurious disk writes.
Also clean up redundant optional chaining on entry.status now
that we guard against undefined/nullable sessionEntry.
* perf(telegram): dedupe ACP session reads in startup cleanup
Cache readAcpSessionEntry calls by targetSessionKey. Multiple bindings
to the same ACP session now result in a single session store read instead
of one read per binding.
Addresses chatgpt-codex-connector P2 review comment on PR #67822.
* fix(telegram): skip non-ACP session keys in stale binding cleanup
Address chatgpt-codex-connector P1 review comment on PR #67822:
Plugin-bound Telegram conversations use "plugin-binding:*" keys
with targetKind === "acp", but these are NOT ACP runtime sessions.
readAcpSessionEntry() returns no entry for them, so !sessionEntry.entry
would classify them as stale and delete them on every startup.
Now checks isAcpSessionKey(binding.targetSessionKey) to skip plugin-bound
sessions from the stale session cleanup scan.
Also clarifies the comment to explain why we use targetKind === "acp"
// together with isAcpSessionKey() check.
* fix(telegram): import isAcpSessionKey from sessions/session-key-utils
isAcpSessionKey is not re-exported from openclaw/plugin-sdk/routing.
Fix import to use the correct subpath: openclaw/sessions/session-key-utils.
Addresses chatgpt-codex-connector P1 review comment on PR #67822.
* fix(telegram): import from relative path, remove unused variable
- Import isAcpSessionKey from relative path ../../sessions/session-key-utils.js
(not openclaw/sessions/session-key-utils which doesn't exist)
- Remove unused 'bindings' variable in for-of loop
Addresses CI failures on PR #67822.
* fix(telegram): export isAcpSessionKey from plugin-sdk/routing
isAcpSessionKey lives in src/routing/session-key.ts, which is already
exported via openclaw/plugin-sdk/routing. Re-export it from routing.ts
so extensions can import via the public plugin-sdk path.
Fixes chatgpt-codex-connector P1: relative path ../../sessions/session-key-utils.js
doesn't exist in the build output, making the Telegram extension fail
module resolution before startup cleanup can run.
* test(telegram): cover startup ACP binding cleanup
* fix: clear stale telegram ACP bindings on startup (#67822) (thanks @chinar-amrutkar)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix: use process-scoped cache for Telegram command sync to fix missing menu after restart
Fixes openclaw#66714, openclaw#66682
Root cause: The command hash cache was persisted to disk across gateway
restarts. When the hash matched (commands unchanged), setMyCommands was
skipped entirely. But Telegram bot commands can be cleared by external
factors, so the cached state becomes stale after restart.
Fix: Replace file-based hash cache with a process-scoped Map. This preserves
the rapid-restart rate-limit protection within a single process, but ensures
commands are always re-registered after a gateway restart.
* fix(telegram): drop stale async command cache calls
* fix: keep Telegram command sync process-local (#66730) (thanks @nightq)
---------
Co-authored-by: nightq <zengwei@nightq.cn>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* Telegram: filter binary content from msg.caption to prevent token explosion (#66647)
When a user sends a binary document (e.g. .mobi, .epub) via Telegram, raw
binary bytes can leak into msg.caption. getTelegramTextParts() passes this
through to the LLM prompt, causing catastrophic token explosion (~460K tokens).
Add isBinaryContent() that detects non-printable control characters (0x00-0x08,
0x0E-0x1F) and use it to sanitize the text in getTelegramTextParts() before it
reaches the prompt pipeline. When binary content is detected, the text and
entities are both replaced with empty values so the message is still processed
(media placeholder still works) but the binary junk is dropped.
Made-with: Cursor
* fix: distill telegram binary caption filtering
* fix: filter telegram binary caption text (#66663) (thanks @joelnishanth)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(telegram): expose forum topic names in agent context
Telegram Bot API does not provide a method to look up forum topic names
by thread ID. This adds an in-memory LRU cache that learns topic names
from service messages (forum_topic_created, forum_topic_edited,
forum_topic_closed, forum_topic_reopened) and seeds from
reply_to_message.forum_topic_created as a fallback for pre-existing
topics.
The resolved topic name is surfaced as:
- TopicName in MsgContext (available to {{TopicName}} in templates)
- topic_name in the agent prompt metadata block
- topicName in plugin hook event metadata
Includes unit tests for the topic-name-cache module (11 tests including
eviction and read-recency).
Known limitation: cache is in-memory only; after a restart it falls back
to the creation-time name until a rename event is observed.
* refactor(telegram): distill topic name flow
* fix: expose telegram topic names in agent context (#65973) (thanks @ptahdunbar)
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>