Remove automatic Quick Start model discovery. Users now explicitly enter the model ID after choosing the provider and API key, and incomplete Quick Start input does not leave partial provider config behind.
Cleanup-only simplification: remove a one-entry dispatch table, avoid unused provider tuple unpacking, and strip the selected model once before storing it.
Remove the PR changes that made nanobot onboard default to the wizard, added --defaults, added non-TTY fallback behavior, and changed the Docker smoke command. Keep Quick Start available through nanobot onboard --wizard.
Maintainer edit: replace Quick Start key/base detection with an explicit provider-first flow. Users choose the provider that issued the API key, paste the key, and only custom OpenAI-compatible setups ask for a base URL.
Maintainer edit: restart the Quick Start flow around an API-key-first path without recommending OpenRouter or DeepSeek. Detect unique key prefixes locally, fall back to a user-provided base URL, and only fetch models from that approved URL.
Maintainer edit: restart the Quick Start default around a mainland-friendly provider instead of making OpenRouter the first-run dependency. Update wizard copy, default preset, focused tests, and beginner docs to use DeepSeek with the current deepseek-v4-flash model.
Maintainer edit: continue the wizard simplification pass by making Quick Start save after the API key path, hiding save/summary actions until they are needed, removing failed-key side effects, and aligning beginner docs with the local WebUI path.
Maintainer edit: collapse the first-run wizard to an API-key-only Quick Start and move lower-frequency provider, model, channel, gateway, and tool settings behind Advanced Settings.
Maintainer edit: reduce the default onboarding path to a recommended local WebUI setup that only asks for an OpenRouter key, while keeping the detailed provider/channel flow available for advanced setup.
Maintainer edit: make explicit --defaults refresh existing configs without prompting when no TTY is available, preserving user values for CI and Docker runs.
maintainer edit: make the exact-file allowlist check express the invariant directly: the resolved target must still equal the requested logical path before the logical path can match the allowlist.
maintainer edit: compare exact-file allowlist entries using logical paths and require the resolved target to stay at that canonical path, so symlinks or junctions cannot redirect canonical memory files to an external write target.
Treat the current live user message as the replay boundary for normal user turns, while keeping user-turn extension for history and consolidation paths that need it. Add regression coverage for the user-triggered long tool-turn case.
Maintainer edit: after #4314 moved shared tool config models to nanobot.config_base, keep FileToolsConfig on the new dependency boundary so the PR passes the architecture guard.
Maintainer edit: subagents rebuilt their scoped ToolsConfig without carrying tools.file, which re-enabled built-in file tools after the parent agent disabled them. Preserve the file config and add loader/subagent coverage for the disabled path.
Avoid resubmitting unchanged schedules from the automation edit dialog so completed one-time automations can still have their content updated. Treat unchanged schedules as already-valid on the backend while preserving validation for actual schedule changes.
Maintainer edit: redact external channel chat identifiers from the WebUI automation payload and reject malformed or unschedulable automation updates before they mutate cron jobs.
maintainer edit: rename the idle compact archive inputs so the code distinguishes messages being removed from messages being summarized. This keeps the #4264 behavior unchanged while making the retained-suffix summary rule easier to read.
maintainer edit: reject arbitrary custom provider keys that normalize to built-in provider names so runtime and WebUI settings cannot disagree about whether a provider is dynamic or built in.
maintainer edit: preserve provider-prefix CLI routing for named custom providers by stripping only the matched dynamic route prefix before sending the model id to OpenAI-compatible endpoints. This keeps ordinary namespaced model ids intact when the provider is selected explicitly.
maintainer edit: make gateway execution, WebUI automation listing, and delete protection agree on the new bound cron shape. Legacy delivery payloads that carry sessionKey are excluded from the WebUI-bound automation surface.
maintainer edit: keep existing cron jobs with legacy delivery fields on the legacy execution path, even when they already carry a sessionKey. This preserves deliver=false behavior and channel-specific routing metadata for upgraded jobs.
maintainer edit: treat job-level CancelledError as a failed cron run so bound automation cancellations update run history and do not break subsequent scheduling.
Maintainer edit: make the unsafe redirect regression go through connect_mcp_servers so both SSE and streamable HTTP prove that the request hook is attached to the MCP clients before redirects are followed.
Maintainer edit: explain that HTTP/SSE MCP now uses the shared SSRF guard before connecting and before following redirects, so local or private HTTP MCP endpoints require an explicit tools.ssrfWhitelist entry.
maintainer edit: add SDK-object and tool-call history regressions so the empty-string reasoning_content fix is covered across both parse branches and the sanitized request path.
maintainer edit: keep cancellation out of on_error so shutdown paths do not look like run failures, and let the SDK capture hook use the authoritative after_run snapshot.
Extract all HTTP route handling (bootstrap, sessions, settings, media,
commands, sidebar state, static serving, token management) into a new
GatewayHTTPHandler class in nanobot/channels/ws_http.py.
WebSocketChannel is reduced from 1907 to 1372 lines (-28%), retaining
only WebSocket connection management and message dispatch.
No behavior change. 3730 tests pass, 0 failures.
Shared HTTP utility functions (path parsing, response builders, auth
helpers) now live in ws_http.py with websocket.py importing from there,
avoiding circular dependencies.
Backwards-compat property aliases on WebSocketChannel ensure existing
tests continue to work without modification.
maintainer edit: track background handler tasks, surface failed OneBot actions, reject image redirects, and add focused unit coverage for group routing and edge cases.
- Remove ## Completed section from HEARTBEAT.md template; completed
tasks should be deleted, not accumulated
- Change in_active_section from tri-state (None/True/False) to bool
(True/False) so stray text before any ## heading no longer triggers
heartbeat
- Add test cases for stray pre-heading text and ## Notes section
- Update docs/chat-commands.md to reference ## Active Tasks
- Apply _normalize_addr in _is_allowed_loopback_target so
::ffff:127.0.0.1 is correctly identified as loopback
- Add test for contains_internal_url with IPv6-mapped addresses
- Add test for whitelist + IPv6-mapped CGNAT interaction
Replace asyncio.sleep(0.05) with an asyncio.Event + patched Lock.acquire
to guarantee the waiting task has reached the lock before asserting. Add
a test confirming LongTaskTool and CompleteGoalTool ContextVars are
isolated, and document the design intent in _GoalToolsMixin.
The import was moved to module top in nanobot/cli/commands.py,
so tests must patch nanobot.cli.commands.evaluate_response instead
of nanobot.utils.evaluator.evaluate_response.
Remove standalone nanobot/heartbeat/ service and replace it with an
auto-registered system cron job on gateway startup. Key behaviors preserved:
- HeartbeatConfig (enabled, interval_s, keep_recent_messages) remains in
GatewayConfig for backward compatibility.
- On startup, if enabled, a system cron job "heartbeat" is registered with
schedule derived from interval_s.
- HEARTBEAT.md is checked on each tick; empty/template-identical files skip
to avoid wasting LLM calls.
- Post-run evaluate_response and session history truncation
(keep_recent_messages) are retained.
- Delivery target selection, deliverable filtering, and preamble guidance
are preserved.
Files removed:
- nanobot/heartbeat/__init__.py
- nanobot/heartbeat/service.py
- tests/heartbeat/*
- tests/agent/test_heartbeat_service.py
Templates and docs updated to reflect cron-based usage.
Override _handle_message to publish directly to the bus for messages
that have already passed _check_inbound_policy. The denied DM pairing
path calls super()._handle_message() to issue pairing codes via the
base class. This avoids cross-policy leakage where e.g. group open
policy would cause is_allowed to incorrectly allow denied DM senders.
Also includes:
- SSE: strip one optional leading space after 'data:' per spec
- Convert 20+ f-string log calls to loguru lazy formatting
- Add end-to-end tests for DM/group routing through the full chain
- Add cross-policy test (dm allowlist + group open) for pairing
- Add Signal channel documentation to docs/chat-apps.md
Channel lazy load: discover_enabled() only imports enabled channel
modules instead of all 18 modules with heavy SDKs (telegram, discord,
slack, etc). discover_all() now delegates to discover_enabled().
Lazy OpenAI client: defer AsyncOpenAI() + httpx construction to
_ensure_client() with asyncio.Lock double-checked locking. openai
and httpx imports moved from module-level into _ensure_client().
Minor: lazy Nanobot/RunResult and CronService exports via __getattr__.
Benchmark: 6910ms → 460ms (-93.3%)
- Remove generated_image_paths_from_messages() and _extract_text_payload() from artifacts.py (no runtime callers)
- Remove session_attachments.py entirely (merge_turn_media_into_last_assistant and stage_media_paths_for_session_replay had no runtime callers)
- Remove test_session_media_persist.py and the orphaned test in test_artifacts.py
The runtime media-attachment mechanism was broken for streaming channels
(e.g. WebSocket): the _streamed flag caused _send_once to skip the final
OutboundMessage that carried generated media, so images were never delivered.
Rather than adding complex coordination between streaming and media delivery,
delegate image delivery to the LLM: after generate_image returns artifact
paths, the next_step prompt now instructs the LLM to call the message tool
with the paths in the media parameter. This works uniformly across all
channels, streaming or not.
Remove generated_media from TurnContext, _assemble_outbound, and _state_save.
Update prompts in identity.md, SKILL.md, message tool description, and
artifacts.py to reflect the new flow.
Replace AutoCompact._archive() direct session mutation with delegation
to Consolidator.compact_idle_session(). Remove _split_unconsolidated()
method since that logic now lives inside compact_idle_session.
All session mutation for idle compaction now goes through the
Consolidator's lock, eliminating the race condition between
background token consolidation and idle TTL compaction.
Changes:
- autocompact.py: rewrite _archive() to call compact_idle_session,
remove _split_unconsolidated(), clean up unused imports
- test_autocompact_unit.py: replace TestArchive/TestSplitUnconsolidated
with TestArchiveDelegates that verifies delegation behavior
- test_auto_compact.py: convert all consolidator.archive mocks to
consolidator.compact_idle_session mocks via _make_fake_compact helper
When background consolidation runs with a stale session reference (captured
before AutoCompact replaced the session via compact_idle_session), it could
operate on outdated data. Now, after acquiring the per-session lock, the
method refreshes its session reference from SessionManager.get_or_create().
If the session was replaced, it swaps in the fresh reference before doing
any consolidation work.
This prevents a race where AutoCompact truncates an idle session while a
background maybe_consolidate_by_tokens call is in flight with the old
session object.
Add Consolidator.compact_idle_session(session_key, max_suffix=8) that
performs hard-truncation of idle sessions under the per-session
consolidation lock. This is the single lock-protected path for AutoCompact
to use instead of modifying session state directly, fixing the race
condition between AutoCompact and Consolidator.
Behavior:
- Acquires per-session consolidation lock
- Invalidates cache and reloads fresh from disk
- Splits unconsolidated tail into archive prefix and retained suffix
- Archives prefix via LLM (with raw_archive fallback on failure)
- Persists _last_summary in session metadata on success
- Returns summary text, None on LLM failure, or '' if nothing to archive
Tests: 6 new tests covering prefix archival, empty session timestamp
refresh, (nothing) summary exclusion, LLM failure fallback,
last_consolidated offset, and lock acquisition verification.
_drain_pending injected a full runtime context block (including goal
state) into every injected user message, but the initial message already
carries runtime context via build_messages(). This caused goal state to
appear multiple times in the LLM context window within a single turn,
wasting tokens (up to 4000 chars per duplicate).
Now _drain_pending only passes the raw user content without runtime
context. The initial turn message remains the sole carrier.
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.
_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.