QQ channel's start() created a background task and returned immediately,
violating the base Channel contract which specifies start() should be
"a long-running async task". This caused the gateway to exit prematurely
when QQ was the only enabled channel.
Now directly awaits _run_bot() to stay alive like other channels.
Fixes#894
When _processed_uids exceeds 100k entries, the entire set was cleared
with .clear(), allowing all previously seen emails to be re-processed.
Now evicts the oldest 50% of entries, keeping recent UIDs to prevent
duplicate processing while still bounding memory usage.
Fixes#890
shutil.move() in _load() can fail due to permissions, disk full, or
concurrent access. Without error handling, the exception propagates up
and prevents the session from loading entirely.
Wrap in try/except so migration failures are logged as warnings and the
session falls back to loading from the legacy path on next attempt.
Fixes#863
_handle_message() in _on_socket_request() had no try/except. If it
throws (bus full, permission error, etc.), the exception propagates up
and crashes the Socket Mode event loop, causing missed messages.
Other channels like Telegram already have explicit error handlers.
Fixes#895
`startswith` string comparison allows bypassing directory restrictions.
For example, `/home/user/workspace_evil` passes the check against
`/home/user/workspace` because the string starts with the allowed path.
Replace with `Path.relative_to()` which correctly validates that the
resolved path is actually inside the allowed directory tree.
Fixes#888
PR #947 fixed the consumer side (context.py) but the root cause is at
the provider level — getattr returns "" (empty string) instead of None
when reasoning_content is empty. This causes DeepSeek API to reject the
request with "Missing reasoning_content field" error.
`"" or None` evaluates to None, preventing empty strings from
propagating downstream.
Fixes#946
When MCP tools return empty content, messages may contain empty-string
text blocks. OpenAI-compatible providers reject these with HTTP 400.
Changes:
- Add _prevent_empty_text_blocks() to filter empty text items from
content lists and handle empty string content
- For assistant messages with tool_calls, set content to None (valid)
- For other messages, replace with '(empty)' placeholder
- Only copy message dict when modification is needed (zero-copy path
for normal messages)
Co-Authored-By: nanobot <noreply@anthropic.com>