Previously the WeChat channel's send() method only handled text messages,
completely ignoring msg.media. When the agent called message(media=[...]),
the file was never delivered to the user.
Implement the full WeChat CDN upload protocol following the reference
@tencent-weixin/openclaw-weixin v1.0.2:
1. Generate a client-side AES-128 key (16 random bytes)
2. Call getuploadurl with file metadata + hex-encoded AES key
3. AES-128-ECB encrypt the file and POST to CDN with filekey param
4. Read x-encrypted-param from CDN response header as download param
5. Send message with the media item (image/video/file) referencing
the CDN upload
Also adds:
- _encrypt_aes_ecb() for AES-128-ECB encryption (reverse of existing
_decrypt_aes_ecb)
- Media type detection from file extension (image/video/file)
- Graceful error handling: failed media sends notify the user via text
without blocking subsequent text delivery
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During testing, we discovered that when a user requests the agent to
send a file (e.g., "send me IMG_1115.png"), the agent would call
read_file to view the content and then reply with text claiming
"file sent" — but never actually deliver the file to the user.
Root cause: The system prompt stated "Reply directly with text for
conversations. Only use the 'message' tool to send to a specific
chat channel", which led the LLM to believe text replies were
sufficient for all responses, including file delivery.
Fix: Add an explicit IMPORTANT instruction in the system prompt
telling the LLM it MUST use the 'message' tool with the 'media'
parameter to send files, and that read_file only reads content
for its own analysis.
Co-Authored-By: qulllee <qullkui@tencent.com>
Add a new WeChat (微信) channel that connects to personal WeChat using
the ilinkai.weixin.qq.com HTTP long-poll API. Protocol reverse-engineered
from @tencent-weixin/openclaw-weixin v1.0.2.
Features:
- QR code login flow (nanobot weixin login)
- HTTP long-poll message receiving (getupdates)
- Text message sending with proper WeixinMessage format
- Media download with AES-128-ECB decryption (image/voice/file/video)
- Voice-to-text from WeChat + Groq Whisper fallback
- Quoted message (ref_msg) support
- Session expiry detection and auto-pause
- Server-suggested poll timeout adaptation
- Context token caching for replies
- Auto-discovery via channel registry
No WebSocket, no Node.js bridge, no local WeChat client needed — pure
HTTP with a bot token obtained via QR code scan.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the single _processing_lock (asyncio.Lock) with per-session locks
so that different sessions can process LLM requests concurrently, while
messages within the same session remain serialised.
An optional global concurrency cap is available via the
NANOBOT_MAX_CONCURRENT_REQUESTS env var (default 3, <=0 for unlimited).
Also re-binds tool context before each tool execution round to prevent
concurrent sessions from clobbering each other's routing info.
Tested in production and manually reviewed.
(cherry picked from commit c397bb4229e8c3b7f99acea7ffe4bea15e73e957)
Trigger token consolidation before prompt usage reaches the full context window so response tokens and tokenizer estimation drift still fit safely within the model budget.
Made-with: Cursor
Register Mistral as a first-class provider with LiteLLM routing,
MISTRAL_API_KEY env var, and https://api.mistral.ai/v1 default base.
Includes schema field, registry entry, and tests.
- Add strip_think() to helpers.py as single source of truth
- Filter deltas in agent loop before dispatching to consumers
- Implement send_delta in TelegramChannel with progressive edit_message_text
- Remove duplicate think filtering from CLI stream.py and telegram.py
- Remove legacy fake streaming (send_message_draft) from Telegram
- Default Telegram streaming to true
- Update CHANNEL_PLUGIN_GUIDE.md with streaming documentation
Made-with: Cursor
Move ThinkingSpinner and StreamRenderer into a dedicated module to keep
commands.py focused on orchestration. Uses Rich Live with manual refresh
(auto_refresh=False) and ellipsis overflow for stable streaming output.
Made-with: Cursor
Preserve the provider and agent-loop streaming primitives plus the CLI experiment scaffolding so this work can be resumed later without blocking urgent bug fixes on main.
Made-with: Cursor
Resolve conflict in context.py: keep main's build_messages which already
merges runtime context into user message (achieving the same cache goal).
The real value-add from this PR is the second cache breakpoint in
litellm_provider.py.
Made-with: Cursor
estimate_prompt_tokens() only counted the `content` text field, completely
missing tool_calls JSON (~72% of actual payload), reasoning_content,
tool_call_id, name, and per-message framing overhead. This caused the
memory consolidator to never trigger for tool-heavy sessions (e.g. cron
jobs), leading to context window overflow errors from the LLM provider.
Also adds reasoning_content counting and proper per-message overhead to
estimate_message_tokens() for consistent boundary detection.
Made-with: Cursor
Merge process_direct() and process_direct_outbound() into a single
interface returning OutboundMessage | None. This eliminates the
dual-path detection logic in CLI single-message mode that relied on
inspect.iscoroutinefunction to distinguish between the two APIs.
Extract status rendering into a pure function build_status_content()
in utils/helpers.py, decoupling it from AgentLoop internals.
Made-with: Cursor
Only use process_direct_outbound when the agent loop actually exposes it as an async method, and otherwise fall back to the legacy process_direct path. This keeps the new CLI render-metadata flow without breaking existing test doubles or older direct-call implementations.
Made-with: Cursor
Keep status output responsive while estimating current context from session history, dropping low-value queue/subagent counters, and marking command-style replies for plain-text rendering in CLI. Also route direct CLI calls through outbound metadata so help/status formatting stays explicit instead of relying on content heuristics.
Made-with: Cursor
Handle /status at the run-loop level so it can return immediately while the agent is busy, and reset last-usage stats when providers omit usage data. Also keep Telegram help/menu coverage for /status without changing the existing final-response send path.
Made-with: Cursor
Only normalize nullable MCP tool schemas for OpenAI-compatible providers so optional params still work without collapsing unrelated unions. Also teach local validation to honor nullable flags and add regression coverage for nullable and non-nullable schemas.
Made-with: Cursor
Add native image content blocks for read_file and web_fetch, preserve the multimodal tool-result path through the agent loop, and keep session history compact with image placeholders. Also harden web_fetch against redirect-based SSRF bypasses and add regression coverage for image reads and blocked private redirects.
Keep multimodal tool outputs on the native content-block path while
restoring redirect SSRF checks for web_fetch image responses. Also share
image block construction, simplify persisted history sanitization, and
add regression tests for image reads and blocked private redirects.
Made-with: Cursor
Exclude openai_codex alongside github_copilot from generated config,
filter OAuth-only providers out of the onboarding wizard, and clarify in
README that OAuth login stores session state outside config. Also unify
the GitHub Copilot login command spelling and add regression tests.
Made-with: Cursor
Keep messages already collected in the current poll cycle when a stale
IMAP connection dies mid-fetch, so retrying once does not drop emails
that were already parsed and marked seen. Add a regression test covering
a mid-cycle disconnect after the first message succeeds.
Made-with: Cursor
- Add openssh-client to apt dependencies for git operations
- Configure git to use HTTPS instead of SSH for github.com to avoid
SSH key requirements during Docker build
Made-with: Cursor
Record run_at_ms, status, duration_ms and error for each execution,
keeping the last 20 entries per job in jobs.json. Adds CronRunRecord
dataclass, get_job() lookup, and four regression tests covering
success, error, trimming and persistence.
Closes#1837
Made-with: Cursor
- wire tools.exec.enable and deny_patterns into the current AgentLoop
- preserve the current WebSearchTool config-based registration path
- treat deny_patterns=[] as an explicit override instead of falling back
to the default blacklist
- add regression coverage for disabled exec registration and custom deny
patterns
Made-with: Cursor
- Catch asyncio.CancelledError separately from generic exceptions
- Re-raise CancelledError only when loop is shutting down (_running is False)
- Continue processing messages if CancelledError occurs during normal operation
- Prevents anyio/MCP cancel scopes from prematurely terminating the agent loop