- Fix silent parse failures: message_id.parse().unwrap_or(0) replaced
with match + tracing::warn on parse error (update_draft, finalize_draft)
- Fix UTF-8 panic: byte-based truncation replaced with char_indices()
safe boundary detection for TELEGRAM_MAX_MESSAGE_LENGTH
- Fix global rate limiter: Mutex<Option<Instant>> replaced with
Mutex<HashMap<String, Instant>> for per-chat rate limiting so
concurrent conversations don't interfere with each other
- Document Block variant: clarify it's reserved for future use and
currently behaves the same as Partial
Wire the existing provider-layer streaming infrastructure through the
channel trait and agent loop so Telegram users see tokens arrive
progressively via editMessageText, instead of waiting for the full
response.
Changes:
- Add StreamMode enum (off/partial/block) and draft_update_interval_ms
to TelegramConfig (backward-compatible defaults: off, 1000ms)
- Add supports_draft_updates/send_draft/update_draft/finalize_draft to
Channel trait with no-op defaults (zero impact on existing channels)
- Implement draft methods on TelegramChannel using sendMessage +
editMessageText with rate limiting and Markdown fallback
- Add on_delta mpsc::Sender<String> parameter to run_tool_call_loop
(None preserves existing behavior)
- Wire streaming in process_channel_message: when channel supports
drafts, send initial draft, spawn updater task, finalize on completion
Edge cases handled:
- 4096-char limit: finalize draft and fall back to chunked send
- Broken Markdown: use no parse_mode during streaming, apply on finalize
- Edit failures: fall back to sending complete response as new message
- Rate limiting: configurable draft_update_interval_ms (default 1s)
Problem:
The iMessage listener opened a new SQLite connection to the Messages
database on every ~3-second poll cycle via get_max_rowid() and
fetch_new_messages(), creating ~40 connection open/close cycles per
minute. Each cycle incurs filesystem syscalls, WAL header reads,
and potential page cache cold starts.
Fix:
Open a single read-only connection before the poll loop and reuse it
across iterations using the 'shuttle' pattern: the connection is moved
into each spawn_blocking closure and returned alongside the results,
then reassigned for the next iteration. This eliminates per-poll
connection overhead while preserving the spawn_blocking pattern that
keeps SQLite I/O off the Tokio runtime thread.
The standalone get_max_rowid() and fetch_new_messages() helper
functions are retained for use by tests and other callers.
Ref: zeroclaw-labs/zeroclaw#710 (Item 9)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three fixes for conversation quality issues:
1. loop_.rs and channels now read max_tool_iterations from AgentConfig
instead of using a hardcoded constant of 10, making it configurable.
2. Memory recall now filters entries below a configurable
min_relevance_score threshold (default 0.4), preventing unrelated
memories from bleeding into conversation context.
3. Default hybrid search weights rebalanced from 70/30 vector/keyword
to 40/60, reducing cross-topic semantic bleed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
run_tool_call_loop used a hardcoded MAX_TOOL_ITERATIONS (10) and
trim_history/auto_compact_history used a hardcoded MAX_HISTORY_MESSAGES (50),
ignoring the user-configurable agent.max_tool_iterations and
agent.max_history_messages values in config.toml.
Meanwhile, agent.rs correctly reads from config — creating an inconsistency
where CLI single-shot mode respected config but the channel runtime and
interactive CLI loop silently ignored it.
Changes:
- Rename constants to DEFAULT_* to clarify they are fallback defaults
- Add max_tool_iterations parameter to run_tool_call_loop
- Add max_history parameter to trim_history and auto_compact_history
- Thread config.agent.max_tool_iterations through ChannelRuntimeContext
- Both CLI code paths now pass config values to run_tool_call_loop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Discord channel was setting msg.channel to the numeric Discord
channel ID instead of the literal string 'discord'. This caused
process_channel_message() to fail the channels_by_name lookup since
the map is keyed by channel name (e.g. 'discord', 'telegram', 'slack').
The result: the bot receives messages and generates LLM responses but
never sends them back -- target_channel resolves to None so the send
call is silently skipped.
Every other channel (telegram, slack, whatsapp, matrix, signal, irc,
imessage, lark, dingtalk, qq, email, mattermost) correctly sets this
field to its channel name string. Discord was the only one using the
platform-specific ID.
Add start_typing/stop_typing overrides to TelegramChannel following the
same pattern as DiscordChannel: spawn a tokio task that sends
sendChatAction every 4 seconds (Telegram typing expires after 5s),
and abort it on stop_typing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement Telegram Forum (topic) support to allow the bot to respond
in the same topic where it was mentioned/called.
Changes:
- parse_update_message(): Extract message_thread_id and format reply_target as 'chat_id:thread_id'
- send(): Parse recipient to extract chat_id and optional thread_id
- All send methods now pass thread_id parameter and include message_thread_id in API requests
- Added test for forum topic message parsing
This ensures bot replies stay within the same forum topic thread.
Include DingTalk in daemon supervised channel detection so the listener starts in daemon mode.
Handle CALLBACK stream frames, subscribe to bot message topic, and improve session webhook routing for private/group replies.
Add regression tests for supervised-channel detection and DingTalk payload/chat-id parsing.
Strip XML-style tool call tags from messages before sending to Telegram to prevent Markdown parsing failures (status 400).
Fixes#503
Co-Authored-By: ayush-thakur02 <ayush.th2002@gmail.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactor the Channel trait to accept a SendMessage struct instead of
separate message and recipient string parameters. This enables passing
additional metadata like email subjects.
Changes:
- Add SendMessage struct with content, recipient, and optional subject
- Update Channel::send() signature to accept &SendMessage
- Update all 12 channel implementations
- Update call sites in channels/mod.rs and gateway/mod.rs
Subject field usage:
- Email: uses subject for email subject line
- DingTalk: uses subject as markdown message title
- All others: ignore subject (no native platform support)
Implement QQ Official messaging channel using OAuth2 authentication
with Discord-like WebSocket gateway protocol for events.
- Add QQChannel with send/listen/health_check support
- Add QQConfig (app_id, app_secret, allowed_users)
- OAuth2 token refresh and WebSocket heartbeat management
- Message deduplication with capacity-based eviction
- Support both C2C (private) and group AT messages
- Integrate with onboard wizard, integrations registry, and channel
list/doctor commands
- Include unit tests for user allowlist, deduplication, and config
* fix(providers): add CN/global endpoint variants for Chinese vendors
* fix(onboard): deduplicate provider key-url match arms
* chore(i18n): normalize non-English literals to English
Adds a new Signal messaging channel that connects to a running
signal-cli daemon's native HTTP API (JSON-RPC + SSE).
[channels_config.signal]
http_url = "http://127.0.0.1:8686"
account = "+1234567890"
group_id = "group_id" # optional, omit for all
allowed_from = ["+1111111111"]
ignore_attachments = true
ignore_stories = true
Implementation:
- SSE listener at /api/v1/events for incoming messages
- JSON-RPC sends via /api/v1/rpc (method: send)
- Health check via /api/v1/check
- Typing indicators via sendTyping RPC
- Supports DMs and group messages (room_id filtering)
- Allowlist-based sender filtering (E.164 or wildcard)
- Optional attachment/story filtering
- Fixed has_supervised_channels() to include signal + irc/lark/dingtalk
Registered in channel list, doctor, start, integrations registry, and
daemon supervisor gate. Includes unit tests for config serde, sender
filtering, room matching, envelope processing, and deserialization.
No new dependencies (uses existing uuid, futures-util, reqwest).
fix(misc): complete parking_lot::Mutex migration (fixes#505)
- DiscordChannel: store actual channel_id in ChannelMessage.channel
instead of hardcoded "discord" string
- channels/mod.rs: use msg.channel instead of msg.sender for replies
- Migrate all std::sync::Mutex to parking_lot::Mutex:
* src/security/audit.rs
* src/memory/sqlite.rs
* src/memory/response_cache.rs
* src/memory/lucid.rs
* src/channels/email_channel.rs
* src/gateway/mod.rs
* src/observability/traits.rs
* src/providers/reliable.rs
* src/providers/router.rs
* src/agent/agent.rs
- Remove all .lock().unwrap() and .map_err(PoisonError) patterns
since parking_lot::Mutex never poisons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(channels): add channel capabilities to system prompt
Add channel capabilities section to system prompt so the agent knows
it can send Discord messages directly without asking permission.
Also reminds agent not to repeat or echo credentials.
Co-authored-by: Vernon Stinebaker <vernon.stinebaker@gmail.com>
* chore: fix formatting and clippy warnings
When mention_only is true, the bot only responds to messages that
@-mention the bot. Other messages in the guild are silently ignored.
Also strips the bot mention from content before processing.
Co-authored-by: Will Sarg <12886992+willsarg@users.noreply.github.com>
* feat(memory): add session_id isolation to Memory trait
Add optional session_id parameter to store(), recall(), and list()
methods across the Memory trait and all four backends (sqlite, markdown,
lucid, none). This enables per-session memory isolation so different
agent sessions cannot cross-read each other's stored memories.
Changes:
- traits.rs: Add session_id: Option<&str> to store/recall/list
- sqlite.rs: Schema migration (ALTER TABLE ADD COLUMN session_id),
index, persist/filter by session_id in all query paths
- markdown.rs, lucid.rs, none.rs: Updated signatures
- All callers pass None for backward compatibility
- 5 new tests: session-filtered recall, cross-session isolation,
session-filtered list, no-filter returns all, migration idempotency
Closes#518
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(channels): fix discord _channel_id typo and lark missing reply_to
Pre-existing compilation errors on main after reply_to was added to
ChannelMessage: discord.rs used _channel_id (underscore prefix) but
referenced channel_id, and lark.rs was missing the reply_to field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
ChannelMessage.sender was used both for display (username) and as the
reply target in Channel::send(). For Telegram, sender is the username
(e.g. "unknown") while send() requires the numeric chat_id, causing
"Bad Request: chat not found" errors.
Add a dedicated reply_to field to ChannelMessage that stores the
channel-specific reply address (Telegram chat_id, Discord channel_id,
Slack channel, etc.). Update all channel implementations and dispatch
code to use reply_to for send/start_typing/stop_typing calls.
This also fixes the same latent bug in Discord and Slack channels where
sender (user ID) was incorrectly passed as the reply target.