Clarify nanobot's preference for small core changes, reviewable PR boundaries, and careful handling of prompt/context surfaces so AI contributors preserve the project's maintenance philosophy.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add CLAUDE.md at the repository root to orient future Claude Code
instances, and split detailed constraints into .agent/:
- .agent/design.md — architectural constraints (core small, duplication
over abstraction, minimal changes, explicit over magical)
- .agent/security.md — workspace/SSRF/shell sandbox boundaries
- .agent/gotchas.md — config ${VAR}, Windows compat, templates,
heartbeat virtual tool call, atomic writes, ruff format warning,
skills extension point
Also updates .gitignore to not ignore .agent/.
The previous version changed return fail/pass to raise, which broke
graceful degradation — tests expect upload/content failures to be
caught and handled, not propagated.
Now logs errors with exc_info=True while preserving existing control
flow (return fail for upload/content send, stop typing for stream).
The Matrix channel had 4 bare except blocks that silently swallowed
transport errors with no logging — stream send/edit failures, media
upload failures, server config fetch failures, and room content send
failures. The Weixin channel had 1 silent state-load failure.
This mirrors commit 98c2f7cc ('fix(weixin): raise exceptions instead
of silently dropping messages') for the Matrix channel and adds a
warning for the remaining silent catch in Weixin's _load_state.
All failures now log at warning level with exc_info=True so operators
can diagnose intermittent Matrix/Weixin transport issues.
On Windows, prompt_toolkit produces lone surrogate code points (e.g.
🐈) for emoji input. These propagate through the message bus
and crash at json.dumps() / file write time because surrogates cannot
be encoded as UTF-8.
Extract _sanitize_surrogates() that round-trips through UTF-16 to
reconstruct paired surrogates into real characters (e.g. 🐈
→ 🐈), replacing unpaired surrogates with U+FFFD. Apply it at the CLI
input path and reuse in SafeFileHistory.
Fixes two related input-handling bugs in the onboard wizard:
1. _input_text treated "" as None, preventing users from clearing
optional string fields or entering empty strings intentionally.
2. _input_model_with_autocomplete used `if value else None`, which
discarded falsy values such as empty strings or 0.
To support clearing optional string fields, add _is_str_or_none() and
normalize empty strings to None inside _configure_pydantic_model only
when the field annotation is `str | None`. Required str fields keep
"" as a valid value.
Also included:
- Remember last selected item in provider/channel/model menus for
better UX when configuring multiple items.
- Rename _SIMPLE_TYPES and _MENU_DISPATCH to lowercase to follow
Python naming conventions (they are local variables, not constants).
- Remove unused imports in test file.
Extracted from PR #3358.
The HTTP compression buffer in aiohttp held all SSE chunks until
the stream ended, making streaming appear batched instead of
incremental. SSE payloads are small and frequent, so compression
provides negligible benefit while breaking real-time delivery.
PR introduced module-level logger in static methods, which drops
the channel context bound by BaseChannel.__init__. Revert to
self._channel.logger / self.logger to preserve log labels.
Also remove @staticmethod since these methods legitimately need
instance access (F821 was the real issue, not the logger source).
Follow-up to PR #3651:
- Replace logger.error with logger.exception inside except blocks
so stack traces are no longer lost:
- providers/transcription.py (5 occurrences)
- agent/tools/mcp.py (1 occurrence)
- Replace stdlib logging.getLogger with loguru logger in
providers/openai_compat_provider.py for consistency.
Track the Dream cursor in memory versioning so restores do not skip history after rolling back Dream commits.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a session-scoped slash command palette sourced from backend command metadata, and keep welcome-page quick actions localized across all WebUI languages.
Co-authored-by: Cursor <cursoragent@cursor.com>
_send_text() swallowed API errors (non-zero errcode) with just a
warning log, and send() had three silent return paths (no client,
session paused, no context_token). Neither triggered ChannelManager's
retry logic, causing persistent message loss until a new inbound
message refreshed the context_token.
Now all failure paths raise RuntimeError, matching BaseChannel's
contract and enabling proper retry behavior.
When host is set to 0.0.0.0, the gateway now enforces that either token
or token_issue_secret must be configured — it refuses to start otherwise.
Bootstrap endpoint behavior:
- token_issue_secret configured: always validate regardless of source IP
(handles reverse-proxy scenarios where all connections appear as localhost)
- No secret: only localhost can bootstrap (local dev mode)
The frontend shows an authentication form when bootstrap returns 401/403,
persists the secret in localStorage, and retries automatically on reload.
The previous LAN-access fix (PR #3656) relaxed the bootstrap localhost
check when host was 0.0.0.0, but did not require any authentication —
any device on the network could obtain a token without credentials.
New behavior:
- token_issue_secret configured: always validate, regardless of source
IP (handles reverse-proxy scenarios where all connections appear as
localhost).
- No secret configured: only localhost can bootstrap (local dev mode).
This supersedes the host-based check from PR #3656.
The webui bootstrap endpoint (/webui/bootstrap) rejected all non-localhost
connections with HTTP 403, preventing the embedded webui from working when
accessed from another device on the LAN — even when host was set to 0.0.0.0.
Skip the localhost check when the server is explicitly bound to 0.0.0.0 or ::,
since that signals intent to accept external connections.
Align the WebUI sidebar and chat chrome with the updated design, and generate WebUI session titles asynchronously without blocking turns.
Co-authored-by: Cursor <cursoragent@cursor.com>
asyncio.create_task in connect_mcp_servers creates child tasks for
each MCP server, but close_mcp calls stack.aclose() from the main
task. anyio CancelScope requires enter/exit in the same task, so the
cross-task exit raises RuntimeError which gets silently caught. The
orphaned cancel scope keeps retrying via call_soon on every event
loop tick, consuming 100% CPU.
Fix: remove create_task/gather and connect servers sequentially in the
caller task. MCP servers are typically 1-2, so parallel connection
provides negligible benefit while introducing the cancel scope hazard.
Closes#3638
The is_path branch in _fmt_known was not passing max_length to
abbreviate_path, so read_file, write_file, edit, list_dir, and
web_fetch always truncated paths at 40 chars regardless of config.
Now all three branches (is_path, is_command, fallback) honor the
configured toolHintMaxLength.
The config field was added but never passed from config to AgentLoop.
The value was always falling back to the default (40) regardless of
what was set in config.json.
Now passes tool_hint_max_length through all AgentLoop() call sites:
- nanobot/nanobot.py (main bot)
- nanobot/cli/commands.py (CLI agent, dev, webui commands)
Also adds documentation in docs/configuration.md.
Add to config (default: 40, range: 20-500).
Controls how many characters of tool hints are shown in progress updates
(e.g. '$ cd …/project && npm test').
Set to 120+ to see full commands instead of truncated hints:
```json
{
"agents": {
"defaults": {
"toolHintMaxLength": 120
}
}
}
```
- Thread max_length through format_tool_hints → _fmt_known/_fmt_mcp/_fmt_fallback
- Make path abbreviation in _abbreviate_command proportional to max_length
- Add TestToolHintMaxLength test class with 5 tests
- All 41 existing tests pass
- Correct api_key type hint to str | None in _post_transcription_with_retry
- Remove unreachable final return ""
- Fix test_openai_missing_api_key_short_circuits to actually test
missing-key path (use audio_file fixture so file exists)
- Fix PermissionError patch for Windows (patch class method instead
of instance attribute)
A single transient failure between the agent and an OpenAI/Groq Whisper
endpoint currently vanishes as `return ""` in transcribe(). The voice
message arrives as the empty string and there is no way to tell real
silence apart from a failed upload. A malformed but successful response
body is even worse: the JSON-decode error escapes the helper unhandled.
Add a shared `_post_transcription_with_retry` used by both providers.
Retry behaviour:
- exponential backoff 1s -> 2s -> 4s, up to 3 retries (4 attempts)
- retryable HTTP statuses: 408, 429, 500, 502, 503, 504
- retryable exceptions: TimeoutException, ConnectError, ReadError,
WriteError, RemoteProtocolError
Non-transient failures short-circuit to "" on the first attempt --
retrying a misconfigured key or a broken upload only burns rate-limit
quota. Branches that short-circuit:
- missing API key, missing audio file
- file-read errors (PermissionError, OSError) on the audio path,
preserving the nightly contract for direct provider callers
- HTTP auth/4xx body issues via raise_for_status()
- response.json() parse failures
- non-dict JSON payloads
Sharing one helper means OpenAI and Groq cannot drift apart silently.
Thread `language` through the helper. The multipart files dict is rebuilt
inside the per-attempt loop, so when a caller sets self.language the
`language` field is sent on every attempt -- not just the first.
Tests cover:
- every advertised retryable status and exception, parameterized
- language present on attempts 1 and 2 of a 503->200 sequence
- language absent when unset; present when set (both providers)
- malformed JSON body and non-dict JSON body short-circuit to ""
- PermissionError on file read short-circuits with no HTTP attempt
- max-attempts give-up, exponential-backoff schedule, auth no-retry,
missing-key / missing-file short-circuit
Test stub fix: the _StubResponse in tests/channels/test_channel_plugins.py
declared no status_code, which the new helper reads for retry classification.
Set status_code = 200 so the stub advertises the successful response that
those tests already simulate. Also moved the two transcription-provider
imports to the top of that file (previously placed mid-file) so the file
is ruff-clean (E402).
Disable the externally updated cron job before yielding to the event loop so slow Windows CI cannot run the short-interval job before the test writes the update.
Keep private URL access blocked at the tool boundary, but return a clear non-retryable hint so the agent can recover conversationally instead of aborting the turn.
Co-authored-by: Cursor <cursoragent@cursor.com>
``Nanobot.run()`` has always documented ``RunResult.tools_used`` and
``RunResult.messages`` but actually returned ``[]`` for both, so SDK
consumers could never inspect which tools fired or what the final
message list looked like — the only useful field was ``content``.
This threads the data out via a tiny ``_SDKCaptureHook`` that installs
alongside any user-supplied hooks. The capture hook accumulates tool
names across iterations and snapshots the message list on each
``after_iteration`` call; the last snapshot reflects end-of-turn state.
Only the SDK facade is touched: ``AgentLoop.process_direct`` and
``AgentRunner`` signatures are unchanged, so channels / CLI / API paths
are unaffected.