A refactoring in commit 132807a introduced a regression where the final
response was silently discarded whenever the message tool was used,
regardless of the target. This restored the original logic from PR #832
that only suppresses the final reply when the message tool sends to the
same (channel, chat_id) as the original message.
Changes:
- message.py: Replace _sent_in_turn: bool with _turn_sends: list[tuple]
to track actual send targets, add get_turn_sends() method
- loop.py: Check if (msg.channel, msg.chat_id) is in sent_targets before
suppressing final reply. Also move the "Response to" log after the
suppress check to avoid misleading logs.
- Add unit tests for the suppress logic
This ensures:
- Email sent via message tool → Feishu still gets confirmation
- Message tool sends to same Feishu chat → No duplicate (suppressed)
HeartbeatService was refactored from free-text HEARTBEAT_OK token
matching to a structured two-phase design (LLM tool call for
skip/run decision, then execution). The tests still used the old
on_heartbeat callback constructor and HEARTBEAT_OK_TOKEN import.
- Remove obsolete test_heartbeat_ok_detection test
- Update test_start_is_idempotent to use new provider+model constructor
- Add tests for _decide() skip path, trigger_now() run/skip paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SubagentManager tracks _session_tasks: session_key -> {task_id, ...}
- cancel_by_session() cancels all subagents for a session
- SpawnTool passes session_key through to SubagentManager
- /stop response reports subagent cancellation count
- Cleanup callback removes from both _running_tasks and _session_tasks
Builds on #1179
- Add commands.py with CommandDef registry, parse_command(), get_help_text()
- Refactor run() to dispatch messages as asyncio tasks (non-blocking)
- /stop is an 'immediate' command: handled inline, cancels active task
- Global processing lock serializes message handling (safe for shared state)
- _pending_tasks set prevents GC of dispatched tasks before lock acquisition
- _dispatch() registers/clears active tasks, catches CancelledError gracefully
- /help now auto-generated from COMMANDS registry
Closes#849
Previously, `autoReplyEnabled=false` would block ALL email sends,
including proactive emails triggered from other channels (e.g., asking
nanobot on Feishu to send an email).
Now `autoReplyEnabled` only controls automatic replies to incoming
emails, not proactive sends. This allows users to disable auto-replies
while still being able to ask nanobot to send emails on demand.
Changes:
- Check if recipient is in `_last_subject_by_chat` to determine if
it's a reply
- Only skip sending when it's a reply AND auto_reply_enabled is false
- Add test for proactive send with auto_reply_enabled=false
- Update existing test to verify reply behavior
Fixes#1042. When the LLM returns tool call arguments as a dict or
JSON string instead of parsed values, memory consolidation would fail
with "TypeError: data must be str, not dict".
Changes:
- Add type guard in MemoryStore.consolidate() to parse string arguments
and reject unexpected types gracefully
- Add regression tests covering dict args, string args, and edge cases
Propagate Matrix thread metadata from inbound events and attach
m.relates_to
(rel_type=m.thread, m.in_reply_to, is_falling_back=true) to outbound
messages
including attachments. Add tests for thread metadata and thread replies.
- extend message tool with optional media paths for channel delivery
- switch Matrix uploads to stream providers and handle encrypted-room payloads
- add/expand tests for message tool media forwarding and Matrix upload edge cases
- Replaces the explicit media event tuple with MATRIX_MEDIA_EVENT_FILTER
based on
media base classes: (RoomMessageMedia, RoomEncryptedMedia).
- Keeps MatrixMediaEvent as the static typing alias for media-specific
handlers.
- Removes MatrixInboundEvent and uses RoomMessage in mention-related
logic.
- Adds regression tests for:
- callback registration using MATRIX_MEDIA_EVENT_FILTER
- ensuring RoomMessageText is not matched by the media filter.