fix: add intentional silence capability for gateway bots#2133
fix: add intentional silence capability for gateway bots#2133praisonai-triage-agent[bot] wants to merge 2 commits into
Conversation
- Add core silence protocol module with NO_REPLY token detection - Update wrapper's shared delivery path to suppress messages on silence - Add allow_silence and silence_token config options - Initialize _allow_silence in all bot adapters - Add CLI support with --allow-silence and --group-policy flags - Enable agents to selectively stay silent in group chats Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
|
@coderabbitai review |
|
/review |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more β On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
β Action performedReview finished.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the βοΈ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
π WalkthroughWalkthroughAdds a first-class intentional-silence protocol across the bot gateway stack. A new core module defines ChangesBot Gateway Intentional Silence Protocol
Estimated code review effortπ― 2 (Simple) | β±οΈ ~12 minutes Suggested reviewers
Poem
π₯ Pre-merge checks | β 5β Passed checks (5 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first β incorporate their findings. Review areas:
|
Greptile SummaryThis PR adds first-class intentional silence support to all gateway bot adapters, allowing agents to return a
Confidence Score: 3/5The feature works end-to-end for email/AgentMail, but all three real-time adapters (Telegram, Discord, Slack) leave the ack reaction emoji permanently stuck on messages whenever the agent is silent, because the early return bypasses the cleanup call. Telegram, Discord, and Slack all set an ack emoji on inbound messages and are expected to swap or remove it when processing completes. Now that The ack cleanup gap is present in all three real-time adapters: Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant BotAdapter as Bot Adapter<br/>(Telegram/Discord/Slack)
participant Mixin as fire_message_sending()<br/>_protocol_mixin.py
participant Agent
participant Silence as silence.py<br/>is_intentional_silence_response()
participant HookRunner
User->>BotAdapter: Incoming message
BotAdapter->>Agent: session.chat(text)
Agent-->>BotAdapter: response (e.g. "NO_REPLY")
BotAdapter->>Mixin: fire_message_sending(channel, response)
alt allow_silence is True
Mixin->>Silence: is_intentional_silence_response(response)
Silence-->>Mixin: True
Mixin-->>BotAdapter: "{cancel: True, silent: True}"
BotAdapter->>BotAdapter: return early β οΈ ack emoji not cleaned up
else allow_silence is False or not a silence token
Mixin->>HookRunner: execute MESSAGE_SENDING hooks
HookRunner-->>Mixin: hook results
Mixin-->>BotAdapter: "{content: modified_text, cancel: False}"
BotAdapter->>User: Send message
BotAdapter->>BotAdapter: ack.done() β cleanup emoji
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant BotAdapter as Bot Adapter<br/>(Telegram/Discord/Slack)
participant Mixin as fire_message_sending()<br/>_protocol_mixin.py
participant Agent
participant Silence as silence.py<br/>is_intentional_silence_response()
participant HookRunner
User->>BotAdapter: Incoming message
BotAdapter->>Agent: session.chat(text)
Agent-->>BotAdapter: response (e.g. "NO_REPLY")
BotAdapter->>Mixin: fire_message_sending(channel, response)
alt allow_silence is True
Mixin->>Silence: is_intentional_silence_response(response)
Silence-->>Mixin: True
Mixin-->>BotAdapter: "{cancel: True, silent: True}"
BotAdapter->>BotAdapter: return early β οΈ ack emoji not cleaned up
else allow_silence is False or not a silence token
Mixin->>HookRunner: execute MESSAGE_SENDING hooks
HookRunner-->>Mixin: hook results
Mixin-->>BotAdapter: "{content: modified_text, cancel: False}"
BotAdapter->>User: Send message
BotAdapter->>BotAdapter: ack.done() β cleanup emoji
end
|
| # Check for intentional silence first (if enabled) | ||
| if getattr(self, '_allow_silence', False): | ||
| try: | ||
| from praisonaiagents.bots.silence import is_intentional_silence_response, SILENT_REPLY_TOKEN | ||
| # Check against custom token if configured, otherwise use default | ||
| custom_token = getattr(self.config, 'silence_token', None) if hasattr(self, 'config') else None | ||
| if custom_token: | ||
| # Exact match for custom token | ||
| if content and content.strip() == custom_token: | ||
| return {"content": "", "cancel": True, "silent": True} | ||
| else: | ||
| # Use default silence detection | ||
| if is_intentional_silence_response(content): | ||
| return {"content": "", "cancel": True, "silent": True} | ||
| except ImportError: | ||
| # Fallback if core module not available - just check for NO_REPLY | ||
| if content and content.strip() == "NO_REPLY": | ||
| return {"content": "", "cancel": True, "silent": True} |
There was a problem hiding this comment.
Silent replies bypass all
MESSAGE_SENDING hooks
When a silence token is detected the method returns early before the hook runner is consulted. Any MESSAGE_SENDING hook registered for auditing, logging, or rate-limiting never fires for silenced responses. If a hook is expected to record every outbound decision (including the "do not reply" decision), it will silently miss these events. Consider calling hooks first or emitting a dedicated MESSAGE_SILENCED hook before returning.
| if not text: | ||
| return False # blank != deliberate silence | ||
| return text.strip().strip("[]").upper() in _MARKERS No newline at end of file |
There was a problem hiding this comment.
Missing newline at end of file
The file is missing a trailing newline, which is required by POSIX and causes minor noise in some diff tools.
| if not text: | |
| return False # blank != deliberate silence | |
| return text.strip().strip("[]").upper() in _MARKERS | |
| if not text: | |
| return False # blank != deliberate silence | |
| return text.strip().strip("[]").upper() in _MARKERS |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 6
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/praisonai-agents/praisonaiagents/bots/silence.py`:
- Line 39: The `strip("[]")` method in the return statement of the marker
checking function is too lenient because it removes any combination of bracket
characters from both ends, allowing malformed tokens like "[[TOKEN]" or
"[TOKEN]]" to match when they shouldn't. Replace the `strip("[]")` call with
stricter validation that only removes exactly one opening bracket from the start
and one closing bracket from the end if both are present, ensuring exact bracket
matching. This prevents false positives from tokens with mismatched or extra
brackets.
In `@src/praisonai/praisonai/bots/_protocol_mixin.py`:
- Around line 170-185: The ImportError fallback only checks for the hardcoded
"NO_REPLY" string and ignores any custom silence_token configured in
self.config. To fix this, move the custom_token retrieval logic (the lines that
get custom_token from self.config using getattr) outside and before the try
block so it's accessible to both the try and except paths. Then in the except
ImportError block, replicate the custom token check logic that matches
custom_token against content before falling back to the "NO_REPLY" check,
ensuring configured custom tokens are respected even when the import fails.
In `@src/praisonai/praisonai/bots/agentmail.py`:
- Around line 115-117: The send_message() method calls fire_message_sending()
but discards its result, so the silence token mechanism does not actually
prevent message sends. Modify send_message() to capture the return value from
fire_message_sending() and check whether the sending should be suppressed before
proceeding with the actual message transmission. If fire_message_sending()
indicates cancellation due to a silence token, return early without sending the
message.
In `@src/praisonai/praisonai/bots/email.py`:
- Around line 103-105: The send_message() method calls fire_message_sending() at
line 456 but ignores its return value, so intentional silence requests are not
being honored and emails are still sent. In the send_message() method, capture
and check the return value from fire_message_sending() to determine whether the
message delivery should be suppressed, and only proceed with sending the email
if the method indicates that delivery should continue. This will respect the
allow_silence configuration that was initialized in self._allow_silence.
In `@src/praisonai/praisonai/bots/slack.py`:
- Around line 93-95: The `handle_mention` method sends messages directly to
Slack without respecting the `_allow_silence` gate that is initialized in the
constructor. Ensure that the `handle_mention` method applies the same silence
check before sending any messages, either by routing through the
`fire_message_sending` method (which presumably enforces the silence gate) or by
explicitly checking `self._allow_silence` before executing the direct message
send logic to maintain consistency with the configured silence behavior.
In `@src/praisonai/praisonai/cli/features/bots_cli.py`:
- Around line 81-85: The new fields group_policy, allow_silence, and
silence_token have been added to BotCapabilities but are not being serialized or
deserialized. You need to add these three fields to the to_dict() method so they
are included when converting BotCapabilities to a dictionary for serialization.
Additionally, in the start_from_config() method, map the corresponding values
from the channel configuration (channel.group_policy, channel.allow_silence,
channel.silence_token) when constructing the BotCapabilities object, ensuring
that YAML configuration values are properly loaded into these fields at runtime.
πͺ 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: 59ac853a-24f1-4d9a-a663-5286ee1bce9b
π Files selected for processing (11)
src/praisonai-agents/praisonaiagents/bots/silence.pysrc/praisonai/praisonai/bots/_config_schema.pysrc/praisonai/praisonai/bots/_protocol_mixin.pysrc/praisonai/praisonai/bots/agentmail.pysrc/praisonai/praisonai/bots/discord.pysrc/praisonai/praisonai/bots/email.pysrc/praisonai/praisonai/bots/slack.pysrc/praisonai/praisonai/bots/telegram.pysrc/praisonai/praisonai/bots/whatsapp.pysrc/praisonai/praisonai/cli/commands/bot.pysrc/praisonai/praisonai/cli/features/bots_cli.py
| """ | ||
| if not text: | ||
| return False # blank != deliberate silence | ||
| return text.strip().strip("[]").upper() in _MARKERS No newline at end of file |
There was a problem hiding this comment.
Tighten silence-token matching to avoid malformed-token false positives.
Using strip("[]") allows non-exact bracket variants to match (e.g., extra brackets), which breaks strict token matching.
Proposed fix
- return text.strip().strip("[]").upper() in _MARKERS
+ normalized = text.strip().upper()
+ return normalized in _MARKERSπ Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return text.strip().strip("[]").upper() in _MARKERS | |
| normalized = text.strip().upper() | |
| return normalized in _MARKERS |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-agents/praisonaiagents/bots/silence.py` at line 39, The
`strip("[]")` method in the return statement of the marker checking function is
too lenient because it removes any combination of bracket characters from both
ends, allowing malformed tokens like "[[TOKEN]" or "[TOKEN]]" to match when they
shouldn't. Replace the `strip("[]")` call with stricter validation that only
removes exactly one opening bracket from the start and one closing bracket from
the end if both are present, ensuring exact bracket matching. This prevents
false positives from tokens with mismatched or extra brackets.
| try: | ||
| from praisonaiagents.bots.silence import is_intentional_silence_response, SILENT_REPLY_TOKEN | ||
| # Check against custom token if configured, otherwise use default | ||
| custom_token = getattr(self.config, 'silence_token', None) if hasattr(self, 'config') else None | ||
| if custom_token: | ||
| # Exact match for custom token | ||
| if content and content.strip() == custom_token: | ||
| return {"content": "", "cancel": True, "silent": True} | ||
| else: | ||
| # Use default silence detection | ||
| if is_intentional_silence_response(content): | ||
| return {"content": "", "cancel": True, "silent": True} | ||
| except ImportError: | ||
| # Fallback if core module not available - just check for NO_REPLY | ||
| if content and content.strip() == "NO_REPLY": | ||
| return {"content": "", "cancel": True, "silent": True} |
There was a problem hiding this comment.
Preserve custom silence_token behavior in the ImportError fallback.
Current fallback only matches "NO_REPLY", so configured custom tokens stop working when the import fails.
Proposed fix
if getattr(self, '_allow_silence', False):
+ custom_token = getattr(self.config, 'silence_token', None) if hasattr(self, 'config') else None
try:
from praisonaiagents.bots.silence import is_intentional_silence_response, SILENT_REPLY_TOKEN
# Check against custom token if configured, otherwise use default
- custom_token = getattr(self.config, 'silence_token', None) if hasattr(self, 'config') else None
if custom_token:
# Exact match for custom token
if content and content.strip() == custom_token:
return {"content": "", "cancel": True, "silent": True}
else:
# Use default silence detection
if is_intentional_silence_response(content):
return {"content": "", "cancel": True, "silent": True}
except ImportError:
- # Fallback if core module not available - just check for NO_REPLY
- if content and content.strip() == "NO_REPLY":
+ # Fallback if core module not available
+ if custom_token and content and content.strip() == custom_token:
+ return {"content": "", "cancel": True, "silent": True}
+ if content and content.strip() == "NO_REPLY":
return {"content": "", "cancel": True, "silent": True}π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/bots/_protocol_mixin.py` around lines 170 - 185, The
ImportError fallback only checks for the hardcoded "NO_REPLY" string and ignores
any custom silence_token configured in self.config. To fix this, move the
custom_token retrieval logic (the lines that get custom_token from self.config
using getattr) outside and before the try block so it's accessible to both the
try and except paths. Then in the except ImportError block, replicate the custom
token check logic that matches custom_token against content before falling back
to the "NO_REPLY" check, ensuring configured custom tokens are respected even
when the import fails.
| # Initialize allow_silence from config | ||
| self._allow_silence = getattr(self.config, 'allow_silence', False) | ||
|
|
There was a problem hiding this comment.
_allow_silence is initialized, but AgentMail send flow still ignores cancellation.
send_message() calls fire_message_sending(...) and discards its result (Line 719), so silence tokens do not actually suppress sends for AgentMail.
Proposed fix
- # Fire sending hook
- self.fire_message_sending(channel_id, body)
+ # Fire sending hook
+ send_result = self.fire_message_sending(channel_id, body, reply_to=reply_to)
+ if send_result.get("cancel"):
+ # Honor intentional silence / hook cancellation
+ return BotMessage(
+ message_id=str(uuid.uuid4()),
+ content="",
+ message_type=MessageType.TEXT,
+ sender=self._bot_user,
+ channel=BotChannel(channel_id=channel_id, name=channel_id),
+ reply_to=reply_to,
+ thread_id=thread_id,
+ metadata={"subject": subject, "silent": True},
+ )
+ body = send_result.get("content", body)π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/bots/agentmail.py` around lines 115 - 117, The
send_message() method calls fire_message_sending() but discards its result, so
the silence token mechanism does not actually prevent message sends. Modify
send_message() to capture the return value from fire_message_sending() and check
whether the sending should be suppressed before proceeding with the actual
message transmission. If fire_message_sending() indicates cancellation due to a
silence token, return early without sending the message.
| # Initialize allow_silence from config | ||
| self._allow_silence = getattr(self.config, 'allow_silence', False) | ||
|
|
There was a problem hiding this comment.
Honor fire_message_sending cancel/content in Email outbound path.
self._allow_silence is now initialized, but send_message() currently ignores the return from fire_message_sending (Line 456), so intentional silence still sends an email instead of suppressing delivery.
π§ Proposed fix
- content_str = content.get("body", "") if isinstance(content, dict) else str(content)
- self.fire_message_sending(channel_id, content_str)
+ content_str = content.get("body", "") if isinstance(content, dict) else str(content)
+ send_result = self.fire_message_sending(channel_id, content_str)
+ if send_result.get("cancel"):
+ return BotMessage(
+ message_id="",
+ content="",
+ message_type=MessageType.TEXT,
+ sender=self._bot_user,
+ channel=BotChannel(channel_id=channel_id, name=channel_id),
+ reply_to=reply_to,
+ thread_id=thread_id,
+ metadata={"silent": True},
+ )
+ content_str = send_result["content"]
@@
- body = content.get("body", "")
+ body = content_str
@@
- body = str(content)
+ body = content_strπ€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/bots/email.py` around lines 103 - 105, The
send_message() method calls fire_message_sending() at line 456 but ignores its
return value, so intentional silence requests are not being honored and emails
are still sent. In the send_message() method, capture and check the return value
from fire_message_sending() to determine whether the message delivery should be
suppressed, and only proceed with sending the email if the method indicates that
delivery should continue. This will respect the allow_silence configuration that
was initialized in self._allow_silence.
| # Initialize allow_silence from config | ||
| self._allow_silence = getattr(self.config, 'allow_silence', False) | ||
|
|
There was a problem hiding this comment.
Slack mention flow bypasses the silence gate.
_allow_silence is set, but handle_mention sends directly without fire_message_sending, so intentional silence does not apply there.
Proposed fix
try:
user_id = event.get("user", "unknown")
logger.info(f"`@mention` received: {text[:100]}...")
response = await self._session.chat(
@@
logger.info(f"Response sent: {response[:100]}...")
+ send_result = self.fire_message_sending(event.get("channel", ""), str(response))
+ if send_result["cancel"]:
+ return
+ response = send_result["content"]
# Determine if we should reply in thread
thread_ts = None
if self.config.reply_in_thread:
thread_ts = event.get("thread_ts") or event.get("ts")π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/bots/slack.py` around lines 93 - 95, The
`handle_mention` method sends messages directly to Slack without respecting the
`_allow_silence` gate that is initialized in the constructor. Ensure that the
`handle_mention` method applies the same silence check before sending any
messages, either by routing through the `fire_message_sending` method (which
presumably enforces the silence gate) or by explicitly checking
`self._allow_silence` before executing the direct message send logic to maintain
consistency with the configured silence behavior.
| # Group behavior | ||
| group_policy: str = "mention_only" # respond_all, mention_only, command_only | ||
| allow_silence: bool = False # Allow agent to return NO_REPLY to stay silent | ||
| silence_token: Optional[str] = None # Custom silence token | ||
|
|
There was a problem hiding this comment.
Complete YAML/serialization wiring for the new silence/group capability fields.
The new BotCapabilities fields are declared, but they are not carried through two key paths:
to_dict()omitsgroup_policy,allow_silence,silence_token(Lines 92-120).start_from_config()never mapschannel.group_policy,channel.allow_silence,channel.silence_tokenwhen constructingBotCapabilities(Lines 217-228).
This causes --config YAML values to be ignored at runtime.
π§ Proposed fix
@@
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
@@
"stream": self.stream,
"stream_edit_interval": self.stream_edit_interval,
+ "group_policy": self.group_policy,
+ "allow_silence": self.allow_silence,
+ "silence_token": self.silence_token,
"session_id": self.session_id,
"user_id": self.user_id,
}
@@
capabilities = BotCapabilities(
@@
stream=channel.streaming.mode != "off" if channel.streaming else False,
stream_edit_interval=int(channel.streaming.min_interval * 1000) if channel.streaming else 700,
+ group_policy=channel.group_policy,
+ allow_silence=channel.allow_silence,
+ silence_token=channel.silence_token,
)π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/cli/features/bots_cli.py` around lines 81 - 85, The
new fields group_policy, allow_silence, and silence_token have been added to
BotCapabilities but are not being serialized or deserialized. You need to add
these three fields to the to_dict() method so they are included when converting
BotCapabilities to a dictionary for serialization. Additionally, in the
start_from_config() method, map the corresponding values from the channel
configuration (channel.group_policy, channel.allow_silence,
channel.silence_token) when constructing the BotCapabilities object, ensuring
that YAML configuration values are properly loaded into these fields at runtime.
|
@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task ββ View job Architecture Review & Fixes βI've completed the final architecture review and fixed all critical issues found by the reviewers. Todo List
Architecture Review Resultsβ
Protocol-driven design: Correctly implemented with protocols in core SDK and heavy implementations in wrapper Fixed Issues
Files Modified
Issues NOT Fixed (Out of Scope)
Final Verdictβ APPROVED - All critical and major issues have been resolved. The implementation now:
The PR is ready to merge. |
- Add missing allow_silence and silence_token fields to BotConfig dataclass - Fix silence detection logic to avoid false positives with malformed brackets - Fix ImportError fallback to respect custom silence tokens - Fix bot adapters to honor fire_message_sending cancel results - Fix YAML serialization for new silence fields in BotCapabilities These fixes resolve the TypeError at runtime and ensure the silence feature works correctly across all bot platforms. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
Summary
This PR adds first-class support for intentional silence in gateway bots, allowing agents to decide when NOT to reply in group/ambient channels. Previously, bots could only filter messages on the inbound side (group_policy), but once triggered, they always sent a reply.
Changes
Core SDK (praisonaiagents)
Wrapper (praisonai)
Usage
YAML Configuration
channels:
telegram:
token: TELEGRAM_BOT_TOKEN
group_policy: respond_all # Bot sees all messages
allow_silence: true # But can choose not to reply
CLI
praisonai bot telegram --allow-silence --group-policy respond_all
When enabled, agents can return NO_REPLY to stay silent:
Testing
Verified silence detection works correctly:
Fixes #2132
Summary by CodeRabbit
allow_silencesetting--allow-silenceand--silence-tokenCLI options for Telegram and Discord bot deployments