Fix: telegram replies#436
Conversation
- Introduced a new static variable to track previously warned world-readable config files, preventing duplicate warnings. - Updated the warning logic to only log a warning for each unique world-readable config file, improving log clarity and reducing noise. - Added new `ChannelReactionReceived` and `ChannelReactionSent` events to the DomainEvent enum, expanding event handling capabilities in the event bus. - Included tests for the new reaction events to ensure proper functionality and integration.
- Introduced functions to parse log file constraints from environment variables and filter log events based on these constraints. - Enhanced the `init_for_cli_run` function to apply the new filtering logic, improving log management and clarity. - Updated the `conversation_history_key` function to include thread context for Telegram, ensuring accurate message targeting. - Added a new trait method `supports_reactions` to the `Channel` trait, indicating support for emoji reactions. - Implemented integration tests for Telegram channel features, including reaction handling and thread message forwarding.
…dicators - Added support for emoji reactions in Telegram responses, allowing for contextual acknowledgment of user messages. - Implemented a decision heuristic for when to use reactions, improving user interaction quality. - Introduced a typing indicator that activates immediately upon receiving a message, providing instant feedback to users. - Updated the channel delivery instructions to include new reaction syntax and guidelines for usage. - Enhanced tests to cover new reaction handling and message acknowledgment features, ensuring robust functionality.
📝 WalkthroughWalkthroughAdds end-to-end Telegram emoji reaction support, updates conversation-history keys to account for reply/thread targets, refactors typing indicator behavior and persistence for Telegram settings, introduces environment-driven CLI log filtering, and expands tests and event-bus variants for reaction lifecycle. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant TelegramAPI as Telegram API
participant Channel as TelegramChannel
participant Dispatch as Runtime Dispatch
participant Provider as LLM Provider
participant EventBus as Event Bus
User->>TelegramAPI: Send message (message_id: M)
TelegramAPI->>Channel: Webhook update (message, message_id=M)
Channel->>Channel: parse_update -> thread_ts = M
Channel->>EventBus: publish ChannelMessageReceived
EventBus->>Dispatch: route message
Dispatch->>Dispatch: select_acknowledgment_reaction(msg.content)
alt channel supports reactions && thread_ts present
Dispatch->>Channel: send([REACTION:emoji], thread_ts=M) (spawned)
Channel->>TelegramAPI: setMessageReaction(target=M, emoji)
TelegramAPI->>EventBus: publish ChannelReactionSent
end
Dispatch->>Provider: request reply
Provider-->>Dispatch: reply text (may include [REACTION:...])
Dispatch->>Channel: send(reply text, thread_ts=M)
Channel->>TelegramAPI: sendMessage(reply_to_message_id=M, text)
TelegramAPI->>User: deliver reply
sequenceDiagram
participant Dispatch as Runtime Dispatch
participant Channel as TelegramChannel
participant Timer as Tokio Timer
participant TelegramAPI as Telegram API
Dispatch->>Channel: start_typing(chat_id)
Channel->>TelegramAPI: sendChatAction(typing)
TelegramAPI-->>Channel: OK
Channel->>Timer: spawn_scoped_typing_task (refresh_interval)
loop while task active
Timer-->>Channel: sleep(refresh_interval) elapsed
Channel->>TelegramAPI: sendChatAction(typing)
TelegramAPI-->>Channel: OK / error (logged)
end
Dispatch->>Channel: stop_typing()
Channel->>Timer: cancel task
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/openhuman/channels/providers/telegram/channel.rs (1)
475-485:⚠️ Potential issue | 🟡 MinorCase-sensitivity mismatch with
parse_allowed_userscould cause duplicate config entries.
is_user_allowednow uses case-insensitive matching (u.eq_ignore_ascii_case(&identity)), which is good for user experience. However, theparse_allowed_usersfunction insrc/openhuman/channels/controllers/ops.rs:65uses case-sensitive deduplication:if !out.iter().any(|existing| existing == normalized) { out.push(normalized.to_string()); }This means "Alice" and "alice" could both be stored in the config (not deduplicated during parsing), though both would match at runtime. Consider normalizing to lowercase in
parse_allowed_usersbefore deduplication to prevent duplicate entries inconfig.toml.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/channels/providers/telegram/channel.rs` around lines 475 - 485, parse_allowed_users currently deduplicates case-sensitively while is_user_allowed uses case-insensitive matching; update parse_allowed_users to normalize entries (e.g., via the same normalization used by Self::normalize_identity or to lowercase) before checking/inserting into the output so "Alice" and "alice" cannot both be stored, and ensure the dedupe comparison uses the normalized form to avoid duplicate config entries that differ only by case.
🧹 Nitpick comments (1)
src/openhuman/channels/runtime/dispatch.rs (1)
442-442: Truncation effectively disabled — intentional debug change?The truncation limit was changed from 80 to 10,000,000 characters. If this is intentional for debugging full responses in logs, consider using a named constant or
usize::MAXinstead of a magic number.If this was meant to be temporary, it may warrant reverting before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/channels/runtime/dispatch.rs` at line 442, The call to truncate_with_ellipsis(&response, 10000000) disabled truncation via a magic number; replace the hard-coded 10_000_000 with a named constant (e.g., TRUNCATE_RESPONSE_LIMIT) or use usize::MAX if you intend no truncation, or revert to the original 80 if this was temporary; update the invocation of truncate_with_ellipsis and add the constant near the top of the module (referencing truncate_with_ellipsis and response) so the intent is clear and configurable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/openhuman/channels/controllers/ops.rs`:
- Around line 53-89: The parse_allowed_users function currently deduplicates
using case-sensitive equality which can store "Alice" and "alice" separately;
update the push_identity logic in parse_allowed_users to perform
case-insensitive deduplication (e.g., compare normalized.to_lowercase() against
existing.to_lowercase() or store a canonical lowercase form) so stored entries
match TelegramChannel::is_allowed's eq_ignore_ascii_case behavior and prevent
duplicate persisted entries; ensure you still trim leading '@' and preserve a
consistent canonical value for storage.
In `@src/openhuman/channels/providers/telegram/channel.rs`:
- Line 1636: finalize_draft currently falls back to send_text_chunks with
reply_to_message_id set to None, which loses reply/thread context; update
finalize_draft (and the fallback call sites around send_text_chunks invocations)
to preserve threading by extracting the original thread id
(thread_ts/message_id) and passing it to send_text_chunks as
reply_to_message_id, or modify finalize_draft signature to accept an optional
thread_ts and forward that to send_text_chunks; ensure references to
finalize_draft, send_text_chunks, thread_ts, message_id, and reply_to_message_id
are updated consistently at the noted call sites (1636, 1654, 1663, 1707) so
fallback messages remain attached as replies.
- Around line 2383-2413: The test
parse_update_reaction_extracts_actor_target_and_emoji uses an incorrect nested
"message" object; update the test JSON so "message_reaction" contains "chat"
(with "id": -100200300) and "message_id": 123 directly (not under a nested
"message"), keep "user", "old_reaction", and "new_reaction" as-is, then run the
TelegramChannel::parse_update_reaction test to ensure sender, reply_target,
target_message_id, and emoji assertions still pass.
- Around line 745-809: parse_update_reaction is reading a non-existent nested
"message" object; change it to read "chat" and "message_id" directly from the
reaction Value (replace uses of message.get("chat") and
message.get("message_id") with reaction.get("chat") and
reaction.get("message_id")), keep the rest of the extraction
(actor/user_id/emoji) logic and the TelegramReactionEvent construction
unchanged, and also update the corresponding unit test that currently constructs
a reaction containing a "message" object to instead construct a reaction with
top-level "chat" and "message_id" fields so the test matches the real Telegram
MessageReactionUpdated structure.
In `@src/openhuman/channels/runtime/dispatch.rs`:
- Around line 84-91: The current select_acknowledgment_reaction branch treats
messages matching error keywords (the contains_any call with
"error","bug","broken","issue","problem","fail","not work","crash","stuck") as
deserving emoji (&["🤔","🤯"]), which conflicts with SOUL.md's "SUPPRESS
reactions for: errors"; update select_acknowledgment_reaction so that messages
matching that contains_any set return no reaction (e.g., neutral/None/empty
string) instead of emojis, or remove those keywords from the contains_any check
so they fall through to the neutral/default path; adjust any callers or tests
that assume a non-empty reaction accordingly.
---
Outside diff comments:
In `@src/openhuman/channels/providers/telegram/channel.rs`:
- Around line 475-485: parse_allowed_users currently deduplicates
case-sensitively while is_user_allowed uses case-insensitive matching; update
parse_allowed_users to normalize entries (e.g., via the same normalization used
by Self::normalize_identity or to lowercase) before checking/inserting into the
output so "Alice" and "alice" cannot both be stored, and ensure the dedupe
comparison uses the normalized form to avoid duplicate config entries that
differ only by case.
---
Nitpick comments:
In `@src/openhuman/channels/runtime/dispatch.rs`:
- Line 442: The call to truncate_with_ellipsis(&response, 10000000) disabled
truncation via a magic number; replace the hard-coded 10_000_000 with a named
constant (e.g., TRUNCATE_RESPONSE_LIMIT) or use usize::MAX if you intend no
truncation, or revert to the original 80 if this was temporary; update the
invocation of truncate_with_ellipsis and add the constant near the top of the
module (referencing truncate_with_ellipsis and response) so the intent is clear
and configurable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3c565325-0c25-4c8e-833e-80459f3b9ed6
📒 Files selected for processing (12)
src/core/logging.rssrc/openhuman/agent/prompts/SOUL.mdsrc/openhuman/channels/context.rssrc/openhuman/channels/controllers/ops.rssrc/openhuman/channels/providers/telegram/channel.rssrc/openhuman/channels/runtime/dispatch.rssrc/openhuman/channels/tests/context.rssrc/openhuman/channels/tests/mod.rssrc/openhuman/channels/tests/telegram_integration.rssrc/openhuman/channels/traits.rssrc/openhuman/config/schema/load.rssrc/openhuman/event_bus/events.rs
- Changed the route key in tests from `telegram_alice` to `telegram_alice_chat-1` to match the updated `conversation_history_key` format for Telegram. - This adjustment ensures accurate routing and consistency in message handling tests.
- Refactored the message handling tests to utilize a `ChannelMessage` struct for improved clarity and maintainability. - Updated the route key generation to use the `conversation_history_key` function, ensuring consistency in message routing. - Simplified the invocation of `process_channel_message` by directly passing the constructed message, enhancing readability.
- Updated the `finalize_draft` method in the `Channel` trait and its implementation for `TelegramChannel` to accept an optional `thread_ts` parameter, allowing for message threading. - Adjusted related message handling functions to utilize the new parameter, ensuring proper message context during sending. - Modified tests to reflect changes in the `finalize_draft` method signature, enhancing the robustness of message handling in threaded conversations.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/openhuman/channels/providers/telegram/channel.rs (1)
1756-1776:⚠️ Potential issue | 🟠 MajorAttachment-only replies still lose Telegram reply threading.
parent_message_idis parsed earlier, but neither attachment branch forwards it intosend_attachment(). If the model returns only media markers, or the text portion is empty after stripping markers, the outgoing media message is sent without any reply linkage and won't appear attached to the user's message.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/channels/providers/telegram/channel.rs` around lines 1756 - 1776, The attachment-only branches drop the reply/thread id so media replies lose threading; update calls to forward parent_message_id into send_attachment and ensure send_attachment (and any callers) accept and use parent_message_id (e.g., add a parent_message_id param or Option type) so both the attachments loop (for attachment in &attachments) and the parse_path_only_attachment branch call send_attachment(chat_id, thread_id, attachment, parent_message_id), and adjust send_attachment's implementation to include the reply_to/message threading when sending the media; keep send_text_chunks usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/openhuman/channels/providers/telegram/channel.rs`:
- Around line 54-60: The typing state is single-slot and races across concurrent
recipients: change typing_handle: Mutex<Option<TelegramTypingTask>> to a
per-recipient map (e.g., Mutex<HashMap<String, TelegramTypingTask>> keyed by
chat id) and update the start_typing and stop_typing logic (and any helpers that
reference typing_handle) to insert/start a task for the specific chat id and to
only abort/remove that chat's task on stop_typing(); ensure start_typing does
not overwrite unrelated entries and stop_typing only aborts the owned task for
the given chat id so one chat's stop cannot cancel another's indicator.
- Around line 880-885: The parser currently discards the trailing text when it
sees an empty reaction marker by returning (String::new(), None); change this so
invalid/empty markers degrade to plain text instead of being dropped: in the
branch that checks inner.is_empty(), reconstruct and return the original content
(prepend the removed '[' back to rest) as the remaining text and None for the
reaction (i.e. return (format!("[{}", rest).to_string(), None)), so send() will
see the original text rather than an empty message; adjust the code around the
inner/rest/close_pos logic accordingly.
In `@src/openhuman/channels/runtime/dispatch.rs`:
- Around line 218-227: The early typing indicator started with
target_channel.start_typing(&msg.reply_target) can be left running if
get_or_create_provider() later fails; update the control flow in dispatch.rs so
that either (a) start_typing is called only after get_or_create_provider()
succeeds, or (b) ensure any early start_typing is always matched with
target_channel.stop_typing(&msg.reply_target) on every error return path (e.g.,
inside the provider acquisition error branch and other early-return branches
around get_or_create_provider()), referencing the start_typing, stop_typing,
get_or_create_provider, target_channel and msg.reply_target symbols to find and
fix both the 218–227 and 254–270 sites.
---
Outside diff comments:
In `@src/openhuman/channels/providers/telegram/channel.rs`:
- Around line 1756-1776: The attachment-only branches drop the reply/thread id
so media replies lose threading; update calls to forward parent_message_id into
send_attachment and ensure send_attachment (and any callers) accept and use
parent_message_id (e.g., add a parent_message_id param or Option type) so both
the attachments loop (for attachment in &attachments) and the
parse_path_only_attachment branch call send_attachment(chat_id, thread_id,
attachment, parent_message_id), and adjust send_attachment's implementation to
include the reply_to/message threading when sending the media; keep
send_text_chunks usage unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a4b20e29-7101-41fc-b416-ce7dd2ae55d4
📒 Files selected for processing (4)
src/openhuman/channels/controllers/ops.rssrc/openhuman/channels/providers/telegram/channel.rssrc/openhuman/channels/runtime/dispatch.rssrc/openhuman/channels/traits.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/openhuman/channels/controllers/ops.rs
- src/openhuman/channels/traits.rs
| typing_handle: Mutex<Option<TelegramTypingTask>>, | ||
| stream_mode: StreamMode, | ||
| draft_update_interval_ms: u64, | ||
| last_draft_edit: Mutex<std::collections::HashMap<String, std::time::Instant>>, | ||
| mention_only: bool, | ||
| bot_username: Mutex<Option<String>>, | ||
| recent_updates: Mutex<TelegramUpdateWindow>, |
There was a problem hiding this comment.
Single-slot typing state races across concurrent recipients.
typing_handle only stores one TelegramTypingTask, but the runtime can process multiple Telegram messages in parallel. Starting typing for chat B replaces chat A's task, and when A later calls stop_typing() it aborts whatever task is currently stored, which can cancel B's indicator mid-turn. This needs per-recipient task tracking or ownership-aware stop/start logic.
Also applies to: 1925-1980
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/channels/providers/telegram/channel.rs` around lines 54 - 60,
The typing state is single-slot and races across concurrent recipients: change
typing_handle: Mutex<Option<TelegramTypingTask>> to a per-recipient map (e.g.,
Mutex<HashMap<String, TelegramTypingTask>> keyed by chat id) and update the
start_typing and stop_typing logic (and any helpers that reference
typing_handle) to insert/start a task for the specific chat id and to only
abort/remove that chat's task on stop_typing(); ensure start_typing does not
overwrite unrelated entries and stop_typing only aborts the owned task for the
given chat id so one chat's stop cannot cancel another's indicator.
| let inner = rest[..close_pos].trim(); | ||
| if inner.is_empty() { | ||
| return (String::new(), None); | ||
| } | ||
| let remaining = rest[close_pos + 1..].trim().to_string(); | ||
| (remaining, Some(inner.to_string())) |
There was a problem hiding this comment.
Don't drop reply text on an empty reaction marker.
[REACTION:] hello currently returns (String::new(), None), so the trailing text is discarded and send() falls through to an empty Telegram message. Invalid markers should preserve the original content and degrade to plain text instead of deleting the reply.
🔧 Proposed fix
- if inner.is_empty() {
- return (String::new(), None);
- }
+ if inner.is_empty() {
+ return (content.to_string(), None);
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/channels/providers/telegram/channel.rs` around lines 880 - 885,
The parser currently discards the trailing text when it sees an empty reaction
marker by returning (String::new(), None); change this so invalid/empty markers
degrade to plain text instead of being dropped: in the branch that checks
inner.is_empty(), reconstruct and return the original content (prepend the
removed '[' back to rest) as the remaining text and None for the reaction (i.e.
return (format!("[{}", rest).to_string(), None)), so send() will see the
original text rather than an empty message; adjust the code around the
inner/rest/close_pos logic accordingly.
| // Fire typing indicator as early as possible — before any async I/O — so the | ||
| // user sees feedback immediately regardless of how fast the LLM responds. | ||
| if let Some(channel) = target_channel.as_ref() { | ||
| if let Err(e) = channel.start_typing(&msg.reply_target).await { | ||
| tracing::debug!( | ||
| "[dispatch] Early typing start failed on {}: {e}", | ||
| channel.name() | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Provider-init failures leave Telegram typing running.
Typing is started before get_or_create_provider(), but this error branch returns without a matching stop_typing(). On Telegram, start_typing() owns a long-lived background task, so a provider failure can leave the chat stuck showing typing until a later message clears it. Either stop typing on this branch or move the early start until after provider acquisition succeeds.
🔧 Minimal fix
Err(err) => {
+ if let Some(channel) = target_channel.as_ref() {
+ let _ = channel.stop_typing(&msg.reply_target).await;
+ }
let safe_err = providers::sanitize_api_error(&err.to_string());
let message = format!(
"⚠️ Failed to initialize provider `{}`. Please run `/models` to choose another provider.\nDetails: {safe_err}",
route.provider
);Also applies to: 254-270
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/channels/runtime/dispatch.rs` around lines 218 - 227, The early
typing indicator started with target_channel.start_typing(&msg.reply_target) can
be left running if get_or_create_provider() later fails; update the control flow
in dispatch.rs so that either (a) start_typing is called only after
get_or_create_provider() succeeds, or (b) ensure any early start_typing is
always matched with target_channel.stop_typing(&msg.reply_target) on every error
return path (e.g., inside the provider acquisition error branch and other
early-return branches around get_or_create_provider()), referencing the
start_typing, stop_typing, get_or_create_provider, target_channel and
msg.reply_target symbols to find and fix both the 218–227 and 254–270 sites.
Summary
reply_to_message_id.[REACTION:👍]/[REACTION:👍] reply textmarkers; incoming user reactions are received, allowlist-checked, and published on the event bus.sendChatActionbefore the refresh loop so short model turns still show "typing…"; upgraded toTelegramTypingTaskto track recipient and avoid redundant restarts.TelegramUpdateWindow(4096-entry LRU) prevents the sameupdate_idbeing processed twice across long-poll cycles.edited_message+message_reactionsubscription — long-poll now subscribes to both update types andparse_update_messageacceptsedited_message.TelegramConfigtoconfig.tomlso the listener starts with correct credentials without a restart.Problem
Several Telegram channel reliability issues accumulated after the initial integration:
start_typingonly started a background refresh loop but never sent an immediate action, so the indicator never appeared during short or fast model turns. The Telegram API requires asendChatActioncall at least once before the loop matters.[REACTION:emoji]marker from SOUL.md was documented but had no parsing or dispatch logic; reactions appeared to be sent but no feedback (event bus, logging) confirmed delivery or failure.update_id, causing the agent to respond twice.alicewas rejected whenAlicewas in the allowlist, which surprised users.config.toml, so the Telegram listener would not start until the app was restarted.Solution
Reply threading
thread_tson the inboundChannelMessageis set to the inboundmessage_id(not the reply-to parent).send,send_text_chunks, andstart_draftall readthread_tsand forward it asreply_to_message_idon the first chunk sent to the Bot API.Typing indicator
start_typingnow callssend_typing_action_oncesynchronously before spawning the refresh loop, ensuring at least onesendChatActionfires regardless of model latency. Thread-scoped actions retry withoutmessage_thread_idif the chat rejects thread-level actions (some group types). The handle is wrapped inTelegramTypingTask { recipient, handle }to skip restarting if the same recipient is already active.Reaction support
parse_reaction_markerstrips[REACTION:emoji]or[REACTION:emoji|message_id]from the start of any outbound message. If a target message ID is resolvable (from the marker orthread_ts),send_message_reactioncallssetMessageReactionand publishesDomainEvent::ChannelReactionSent. Incomingmessage_reactionupdates are parsed byparse_update_reaction, allowlist-checked, and published asDomainEvent::ChannelReactionReceived. Reactions from non-allowlisted users are silently dropped.Deduplication
TelegramUpdateWindow(lock-guardedHashSet+VecDequebounded to 4096) tracks every seenupdate_id. Updates belowmax_seen_update_idor already in the set are dropped with adebug-level log before any message parsing.Config persistence
After a successful
connect_channelfortelegram/BotTokenmode, the handler clones the current config, writes the newTelegramConfig(token + allowed users, preserving existing stream settings), and callsconfig.save()before returning the RPC response.Log file constraints
OPENHUMAN_LOG_FILE_CONSTRAINTS(comma-separated file paths or target substrings) filters CLI log output to matching events only, useful for isolating Telegram traffic during debugging.Submission Checklist
parse_update_message_sets_thread_ts_to_current_message_id_for_outbound_reply,parse_update_reaction_extracts_actor_target_and_emoji,parse_reaction_marker_supports_optional_target_id,parse_reaction_marker_allows_inline_reply_text,telegram_user_case_insensitive. Updated:typing_handletests migrated toTelegramTypingTask.track_update_id,parse_reaction_marker, reply-target mapping).Impact
src/openhuman/channels/providers/telegram/).ChannelMessagegains no new required fields;thread_tswas already present.DomainEventvariants (ChannelReactionSent,ChannelReactionReceived) added; existing subscribers unaffected (#[non_exhaustive]).config.tomlis now written on Telegram connect; users with manually-edited configs should verifytelegramsection is not overwritten unexpectedly.message_reactionupdates require the bot to have "reaction" update access enabled in BotFather (or viagetUpdatessubscription). No permission changes are needed for existing message flows.Related
sendChatActionrate limiting / Bot API suppressionChannelReactionReceivedhandler to trigger an agent turn (reactions as commands)Summary by CodeRabbit
New Features
Improvements
Tests