_extract_absolute_paths() only matched paths starting with / or drive
letters, missing ~ paths that expand to the home directory. This
allowed agents to bypass restrictToWorkspace by using commands like
cat ~/.nanobot/config.json to access files outside the workspace.
Add tilde path extraction regex and use expanduser() before resolving.
Also switch from manual parent-chain check to is_relative_to() for
more robust path containment validation.
Fixes#1817
When a cron job fires, the agent processes the scheduled message and
has access to the cron tool. If the original message resembles a
scheduling instruction (e.g. "remind me in 10 seconds"), the agent
would call cron.add again, creating an infinite feedback loop.
Add a cron-context flag to CronTool that blocks add operations during
cron job execution. The flag is set before process_direct() and cleared
in a finally block to ensure cleanup even on errors.
Fixes#1441
Some LLM providers (Minimax, Dashscope) strictly reject consecutive
messages with the same role. build_messages() was emitting two separate
user messages back-to-back: the runtime context and the actual user
content.
Merge them into a single user message, handling both plain text and
multimodal (image) content. Update _save_turn() to strip the runtime
context prefix from the merged message when persisting to session
history.
Fixes#1414Fixes#1344
The bare keyword "codex" causes false positive matches when any model
name happens to contain "codex" (e.g. "gpt-5.3-codex" on a custom
provider). This incorrectly routes the request through the OAuth-based
OpenAI Codex provider, producing "OAuth credentials not found" errors
even when a valid custom api_key and api_base are configured.
Keep only the explicit "openai-codex" keyword so that auto-detection
requires the canonical prefix. Users can still set provider: "custom"
to force the custom endpoint, but auto-detection should not collide.
Closes#1311
When an LLM returns content: null on a plain assistant message (no
tool_calls), the null gets saved to session history and causes
permanent 400 errors on every subsequent request.
- Sanitize None content on plain assistant messages to "(empty)" in
_sanitize_empty_content(), matching the existing empty-string handling
- Skip persisting error responses (finish_reason="error") to the
message history in _run_agent_loop(), preventing poison loops
Closes#1303
The typing indicator loop catches all exceptions with bare
except/pass, so a permanent HTTP failure (client closed, auth
error, etc.) causes the loop to spin every 8 seconds doing
nothing until the channel is explicitly stopped.
Log the error and exit the loop instead, letting the task
clean up naturally.
- _split_message: return empty list for empty/None content instead
of a list with one empty string (Discord rejects empty content)
- _split_message: use pos <= 0 fallback to prevent empty chunks
when content starts with a newline or space
- _send_payload: return bool to indicate success/failure
- send: abort remaining chunks when a chunk fails to send,
preventing partial/corrupted message delivery
Follow-up to #864. Three f-string logger calls in base.py and dingtalk.py
were missed in the original sweep. These can cause KeyError if interpolated
values contain curly braces, since loguru interprets them as format placeholders.
list_sessions() previously reconstructed the session key by replacing all
underscores in the filename with colons. This is lossy: a key like
'cli:user_name' became 'cli:user:name' after round-tripping.
Now the actual key is persisted in the metadata line during save() and read
back in list_sessions(). Legacy files without the key field fall back to
replacing only the first underscore, which handles the common channel:chat_id
pattern correctly.
Closes#899
Discord's API rejects messages longer than 2000 characters with HTTP 400.
Previously, long agent responses were silently lost after retries exhausted.
Adds _split_message() (matching Telegram's approach) to chunk content at
line boundaries before sending. Only the first chunk carries the reply
reference. Retry logic extracted to _send_payload() for reuse across chunks.
Closes#898
Addresses Codex review: concurrent callers could both pass the
_mcp_connected guard and race through _connect_mcp(). Added
_mcp_connecting flag set immediately to serialize attempts.
The flag was set before the connection attempt, so if any MCP server
was temporarily unavailable, the flag stayed True and MCP tools were
permanently lost for the session.
Closes#889
When a shell command times out, process.kill() is called but the
process object was never awaited after that. This leaves subprocess
pipes undrained and file descriptors open. If many commands time out,
fd leaks accumulate.
Add a bounded wait (5s) after kill to let the process fully terminate
and release its resources.
Address review feedback:
- Remove on_progress call for interim text to prevent duplicate
messages when the model simply answers a direct question
- Reset final_content to None before continue to avoid stale
interim text leaking as the final response on empty retry
Closes#705
Some LLM providers (MiniMax, Gemini Flash, GPT-4.1, etc.) send an
initial text-only response like "Let me investigate..." before actually
making tool calls. The agent loop previously broke immediately on any
text response without tool calls, preventing these models from ever
using tools.
Now, when the model responds with text but hasn't used any tools yet,
the loop forwards the text as progress to the user and gives the model
one additional iteration to make tool calls. This is limited to a
single retry to prevent infinite loops.
Closes#705
The /help command was routed through _forward_command → _handle_message
→ is_allowed(), which denied access to users not in the allowFrom list.
Since /help is purely informational, it should be accessible to all
users — similar to how /start already works with its own handler.
Add a dedicated _on_help handler that replies directly without going
through the message bus access control.
Closes#687
Add a `_consolidating` set to track which sessions have an active
consolidation task. Skip creating a new task if one is already in
progress for the same session key, and clean up the flag when done.
This prevents the excessive API calls reported when messages exceed
the memory_window threshold — previously every single message after
the threshold triggered a new background consolidation.
Closes#751