- TurnContext now carries a turn_id (session_key:time_ns)
- All state transition debug logs include [turn_id] prefix
- RuntimeError messages also include turn_id for observability
- State handlers now return event strings ('ok', 'dispatch', 'shortcut')
- Driver loop uses _TRANSITIONS lookup table: (state, event) -> next_state
- State graph is centralized and visible at a glance
- Added StateTraceEntry to record per-state timing and events
- Driver loop logs state duration + event at debug level
- Exception paths are traced with error field for observability
- Fix _assemble_outbound on_stream type annotation (Callable[[str], Awaitable[None]] | None)
- Use last_msg consistently in _state_save instead of re-indexing
- Remove dead fallback in _state_respond (guaranteed non-None by _state_save)
- Change pending_summary type from Any to str | None
- Make session optional in TurnContext to avoid redundant fetch
- Add defensive dispatch with RuntimeError for missing handlers
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.