Replace the tuple(list[dict], int) return of
Session.retain_recent_legal_suffix with a named RetentionResult
dataclass that exposes retained, dropped,
already_consolidated_count, and new_last_consolidated fields.
The tuple return was easy to misuse because the second value only
made sense relative to the first and the old last_consolidated
cursor. The named fields make the archive-skip semantics explicit
at every call site.
No behavior change. All existing tests pass unchanged in semantics.
Refs #4136
Signed-off-by: axelray-dev <110029405+axelray-dev@users.noreply.github.com>
Treat max_messages as a last-resort replay guard now that consolidation and idle auto-compact own normal history reduction. Raising the default avoids frequent sliding-window prefix churn in moderate conversations without adding a new cache-policy knob.
Combine malformed tool-call handling with placeholder filtering and a
no-tools fallback so a relay that returns tool_use blocks with null
id/name/input can no longer crash a turn or permanently wedge a session.
Adapted to the ContextGovernor architecture (context governance now lives
in nanobot/agent/context_governance.py, not runner.py):
- ToolCallRequest.has_valid_name(): single source of truth for "usable
name" (non-empty string).
- tool_hints.format_tool_hints(): skip tool calls with a non-string/empty
name instead of raising AttributeError on the whole turn.
- ContextGovernor.strip_placeholder_assistant_messages() and
strip_malformed_tool_calls() (plus the _tool_call_name_is_valid helper):
history-cleaning staticmethods invoked at the START of
prepare_for_model() — strip_placeholder, then strip_malformed, then the
existing drop_orphan/backfill chain. Both only repair the model-facing
copy and leave persisted history untouched (return a copy, or the same
list when nothing changes). Also wired into runner's minimal-repair path.
- AgentRunner._drop_malformed_tool_calls(): returns
(dropped, all_dropped, original_finish_reason); clears finish_reason to
"stop" when all calls are dropped.
- AgentRunner._malformed_tool_call_retry_messages() + _request_model
malformed_retry flag: when an all-dropped tool_calls response comes back,
retry once with a corrective note; if the retry STILL comes back
all-dropped, fall back to _request_no_tools for graceful text degradation.
Tests for the history-cleaning methods live with ContextGovernor in
tests/agent/test_runner_governance.py; response-layer and tool-hint tests
stay on AgentRunner / tool_hints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract model-facing context governance from AgentRunner.
Only compact in-flight tool results when the model request is over budget, keep compacted IDs stable within a turn, and allow the newest result to be compacted as a last resort when it is the remaining source of overflow.
Add fail_on_tool_error to AgentDefaults and wire it through
AgentLoop -> SubagentManager -> AgentRunSpec.
Previously hardcoded to True in SubagentManager._run_subagent.
Now configurable via config.json with default True for backward
compatibility. When set to False, subagents can retry on minor
tool errors instead of immediately failing.
Changes:
- nanobot/config/schema.py: add fail_on_tool_error field (default True)
- nanobot/agent/subagent.py: accept and forward fail_on_tool_error
- nanobot/agent/loop.py: pass config through to SubagentManager
- tests/agent/test_subagent.py: add regression test
Signed-off-by: axelray-dev <110029405+axelray-dev@users.noreply.github.com>
When dream.enabled is false, the Dream cron job never runs, so the
dream cursor (.dream_cursor) stays at its initial value (0). This
causes read_recent_history_for_prompt() to treat every history entry
as unprocessed, injecting the full chat history into every system
prompt and growing without bound.
Fix: fast-forward the dream cursor to the latest history entry at
gateway startup when Dream is disabled.
The onboard wizard dispatched field handlers by bare field name, so
WebSearchConfig.provider was hijacked by the LLM-provider handler and
showed LLM providers instead of search engines. Keenable was also never
wired into the CLI wizard when it landed in the WebUI.
- Add a single source of truth for selectable search providers
(SEARCH_PROVIDER_OPTIONS in web.py); WebUI settings now import it.
- Add a WebSearchConfig-aware search-provider picker to the wizard and
resolve handlers by (model type, field name) so the LLM and search
provider fields no longer collide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maintainer edit: native reasoning streams can split <thinking> wrapper tags across chunks. Buffer the stream and emit only cleaned incremental reasoning so raw partial tags do not reach WebUI.
maintainer edit: move duplicate tool_use history repair out of AgentRunner and into Anthropic message conversion, reusing the OpenAI-compatible queue-mapping approach locally without broadening the shared runner path.
maintainer edit: remap duplicate tool_use/tool_call ids instead of dropping later calls, so Anthropic-compatible providers that reuse ids for distinct parallel tool calls keep all requested work while still sending unique ids.
Anthropic rejects any request where two tool_use blocks share an id
("messages.N.content.M: tool_use ids must be unique"). A mis-assembled
stream could surface the same tool_use block twice in one assistant turn;
the runner persisted it verbatim, so the malformed message was re-sent on
every subsequent turn and permanently bricked the session — the agent
silently stopped replying.
Fix at two layers:
- AnthropicProvider._parse_response: drop duplicate tool_use ids (keep
first) as the response enters nanobot, so corruption is never persisted.
- AgentRunner._dedup_tool_calls: a new context-governance pass that dedupes
assistant tool_calls and tool results by id before each send, healing any
history that was already corrupted.
Add regression tests covering both the dedup and the no-op fast path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Quick Start endpoint choices for subscription plan providers, require explicit WebSocket confirmation, and require a WebUI password when enabling the WebSocket channel. Update docs to route Quick Start users through the WebUI instead of agent -m.
Drive Quick Start provider choices from the provider registry instead of a short allowlist. Clean up parenthetical wizard labels and keep the beginner docs in sync.
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.
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.
SessionManager._load() migrates sessions from the legacy directory
(~/.nanobot/sessions/) to the workspace path, but delete_session only
checked the workspace path. A user deleting a session could therefore
see its history come back the next time the session was loaded.
- delete_session now attempts to unlink both paths
- returns True if at least one file was removed
- added regression tests: legacy-only, both-paths, and no-revival
When the primary model (e.g. DeepSeek during peak hours) returns an empty
choices response with HTTP 200, the error carries no status code or
structured error metadata. The existing _FALLBACK_ERROR_TOKENS had no
matching token, so _should_fallback() returned False and fallback models
were never tried.
Changes:
- Add 'empty' token to _FALLBACK_ERROR_TOKENS so 'Error: API returned
empty choices.' text matches the fallback path
- Set error_kind='empty' in openai_compat_provider when returning
the empty-choices error, making the classification explicit
- Add test coverage for both text-only and error_kind matching paths
Fixes: glebov reported primary never falls back when DeepSeek returns
empty responses
Maintainer edit: keep capability-specific allowed_dir boundaries active even when the outer workspace scope is full access, and cover Dream plus ordinary full-access filesystem behavior.