Add a new WATI channel for WhatsApp Business API integration via the
WATI managed platform. WATI simplifies WhatsApp integration with its
own REST API and webhook system.
- New WatiChannel implementation (webhook mode, REST send)
- WatiConfig with api_token, api_url, tenant_id, allowed_numbers
- Gateway routes: GET/POST /wati for webhook verification and messages
- Flexible webhook parsing handles WATI's variable field names
- 15 unit tests covering parsing, allowlist, timestamps, phone normalization
Add file extension validation before generating [IMAGE:] markers for
incoming Telegram attachments. Non-image files (e.g. .md, .txt, .pdf)
now always use [Document:] format regardless of how Telegram classifies
them, preventing false vision capability errors.
Extract format_attachment_content() and is_image_extension() helpers
to centralize the logic and make it testable.
Fixes#1274
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the progress streaming code from the fork's 75fdeb0 commit.
The upstream run_tool_call_loop only uses on_delta for final response
streaming, missing real-time feedback during tool execution.
Added progress sends at 4 points in the tool loop:
- "Thinking..." / "Thinking (round N)..." before each LLM call
- "Got N tool call(s) (Xs)" after LLM responds with tool calls
- Tool start: "⏳ tool_name: hint..." before each tool execution
- Tool complete: "✅ tool_name (Xs)" or "❌ tool_name (Xs)" after
Also added DRAFT_CLEAR_SENTINEL handling in the channel draft updater
so progress lines are cleared before the final answer streams in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix 'Current Date & Time' section only emitting timezone string (e.g. 'Timezone: +08:00'), omitting actual date and time values.
- Caused AI to hallucinate incorrect dates when asked about current time.
- Emit full datetime in format 'YYYY-MM-DD HH:MM:SS (TZ)' instead.
When a channel message triggers an LLM error or idle timeout, the user
turn was already appended to conversation history (line 1517) but no
assistant turn was recorded. This orphan user turn caused the LLM to
treat the failed request as unfinished context on subsequent messages,
leading to unrelated replies (e.g., re-executing a timed-out GitHub
search when the user asked about WAL checkpoints).
Append a short assistant marker ("[Task failed — not continuing this
request]" or "[Task timed out — ...]") in the error and timeout
branches so the conversation history stays properly alternating and the
LLM sees the prior request as closed.
The cancel and context-overflow paths are intentionally left unchanged:
cancel is superseded by a newer message, and context-overflow prompts
the user to resend.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add markdown_to_telegram_html() to TelegramChannel: converts **bold**,
*italic*, `code`, ```blocks```, [text](url) links, and ## headers
to Telegram HTML tags (<b>, <i>, <code>, <pre>, <a href>)
- Switch send_text_chunks() and finalize_draft() from parse_mode=Markdown
to parse_mode=HTML — more reliable and supports richer formatting
- Update channel_delivery_instructions() for Telegram: guide model to use
bold, emoji, and concise style (mirrors OpenClaw SOUL.md approach)
- Add wildcard support to http_request allowlist: allowed_domains=["*"]
now bypasses domain filtering entirely
- Expand system prompt URL fetching guidance: jina.ai reader-mode proxy
as fallback for paywalled/403 content
- C1: Use real tool success boolean instead of starts_with("Error")
heuristic in after_tool_call hook
- C2: Wire HookRunner from config into ChannelRuntimeContext so hooks
actually fire in daemon/channel mode (was hardcoded to None)
- I1: Suppress unused_imports warning on HookHandler public API re-export
- I3: Remove session_memory and boot_script config fields that had no
backing implementation (YAGNI); keep only command_logger which is wired
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread Option<&HookRunner> into run_tool_call_loop with hook fire points
for LLM input, before/after tool calls. Add hooks field to
ChannelRuntimeContext for message received/sending interception.
Build HookRunner from config in run_gateway and fire gateway_start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a full NostrChannel implementation enabling ZeroClaw to send and
receive private messages over the Nostr protocol via user-configured
relay WebSocket connections.
Key design decisions:
- Implements the Channel trait in src/channels/nostr.rs; registered via
the existing factory in channels/mod.rs
- Supports both NIP-04 (legacy encrypted DMs) and NIP-17 (gift-wrapped
private messages); replies automatically mirror the sender's protocol
- Deny-by-default allowlist (allowed_pubkeys = [] denies all)
- Private key encrypted at rest via SecretStore (ChaCha20-Poly1305 AEAD)
when secrets.encrypt = true (the default)
- nostr-sdk added with default-features = false and only nip04 + nip59
features to minimise binary size impact
- health_check() returns true if any relay reports is_connected()
Wiring:
- New NostrConfig struct and optional field in ChannelsConfig
- has_supervised_channels() in daemon updated to include nostr
- Onboarding wizard extended with a dedicated Nostr step (key
validation, relay selection, allowlist configuration)
Docs compliance:
- channels-reference.md: channel matrix, delivery modes table, allowlist
field names, numbered config section (4.12), log keyword table (7.2),
and log filter command all updated
- config-reference.md: [channels_config.nostr] sub-section with key
table and security notes added
- network-deployment.md and README.md updated
- .github/pull_request_template.md: resolved stale conflict markers from
chore/labeler-spacing-trusted-tier
Extract d.attachments from MESSAGE_CREATE payloads and fetch text/*
content from Discord CDN URLs, appending it to ChannelMessage.content
before the agent loop receives the message.
- Add process_attachments() async helper: fetches text/* attachments,
skips all other MIME types with debug log, warns on fetch errors
- Reuse existing build_runtime_proxy_client HTTP client (no new deps)
- Format inlined content as [filename]\n<content>, joined by ---
- Add unit tests: empty list, unsupported MIME type skip
Closes#1169
Move strip_tool_call_tags to channels/mod.rs as shared utility and
call it in Discord's send method. Telegram already stripped these tags
but Discord sent raw LLM output including <tool_call>...</tool_call>
XML, which leaked internal protocol to end users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the 200-char truncation of quoted reply text in Telegram
channel. The agent benefits from seeing the complete original message
when replying to a conversation thread.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Photos now use [IMAGE:/path] format instead of [Photo] /path, so the
existing multimodal pipeline validates vision capability and rejects
unsupported providers (Groq, OpenAI-compatible) with a user-facing
error before calling the LLM.
Tests added (all offline, no API keys required):
- attachment_photo_content_uses_image_marker
- attachment_document_content_uses_document_label
- photo_image_marker_detected_by_multimodal
- photo_image_marker_with_caption
- e2e_attachment_saves_file_and_formats_content
- groq_provider_rejects_photo_with_vision_error
- e2e_photo_attachment_rejected_by_non_vision_provider
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add an ignored integration test that exercises the full voice
transcription pipeline: load a pre-recorded MP3 fixture, transcribe via
Groq Whisper API, verify the result contains "hello", cache it in
TelegramChannel.voice_transcriptions, and assert extract_reply_context
returns "[Voice] <transcription>" instead of the fallback placeholder.
The test gracefully skips when GROQ_API_KEY is not set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user swipes to reply to a specific message, the agent now
receives the quoted original message as a blockquote prefix, e.g.:
> @alice:
> original message text
translate this
This makes reply-to-voice ("translate this" → previous transcription)
and other reply-aware interactions work correctly.
Changes:
- Extract `extract_sender_info` helper (DRY: was duplicated in
parse_update_message and try_parse_voice_message)
- Add `extract_reply_context` helper: parses reply_to_message,
handles text/voice/photo/document/video/sticker, truncates >200
chars, falls back from username to first_name
- Wire reply context into both parse_update_message and
try_parse_voice_message
- Add 8 unit tests covering all branches
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>