3801 Commits

Author SHA1 Message Date
chengyongru
5e0ef36cf2 fix(anthropic): preserve SDK and dated model compatibility 2026-08-04 10:50:26 +08:00
chengyongru
39b2294ecf fix(anthropic): support Opus 5 effort controls 2026-08-04 09:54:09 +08:00
chengyongru
44b7e1bf41 fix(providers): keep serde errors explicit 2026-08-03 18:06:45 +08:00
arcdrake22
6eda67b50c fix(providers): keep reasoning items wire-valid for DeepSeek Responses
convert_messages() emitted reasoning items with ``content`` as a plain
string whenever preserve_reasoning was enabled (the DeepSeek spec).
DeepSeek's Responses gateway rejects that shape with a serde error
("input: invalid type: string ..., expected a sequence"), which surfaced
only after token consolidation cleared provider_state and forced the
full-history conversion path; replayed server items already carry list
content, which is why normal multi-turn requests never failed. Serialize
reasoning content as a list of output_text parts, matching the OpenAI
Responses schema and DeepSeek's accepted wire shape (verified live against
api.deepseek.com/responses).

The serde fallback classifier introduced in the previous commit remains as
a last-resort safeguard for any remaining wire incompatibility.

Tests: extend test_preserves_deepseek_reasoning_content to the array shape;
add a full-history regression with the observed failing item, a
replay/consolidation regression covering both replayed and converted
reasoning items, and provider-level request fixtures for both paths.
Full suite: 5773 passed, 22 skipped (only the known local-only
channels/sms packaging failure remains).
2026-08-03 18:06:45 +08:00
arcdrake22
fb2688fd37 fix(providers): fall back to chat completions on serde body rejections
DeepSeek's new Responses endpoint (deepseek-v4-flash) intermittently rejects valid request bodies with serde deserialization errors such as 'input: invalid type: string ..., expected a sequence'. These were not classified as compatibility errors, so affected conversations died instead of falling back to Chat Completions.

The wire format is correct (input serializes as a list), so this is a server-side Responses compatibility issue; Chat Completions is strictly more permissive, making fallback safe. Extend the fallback classifier to recognize serde body-parsing markers. Repeated failures still trip the existing circuit breaker.
2026-08-03 18:06:45 +08:00
chengyongru
2b63715282 fix(webui): complete i18n audit 2026-08-03 17:53:33 +08:00
Xubin Ren
df11fd92a6 docs(providers): link ModelScope setup sources 2026-08-03 16:57:10 +08:00
Xubin Ren
b29f9dcbcb docs(providers): align ModelScope setup with current config 2026-08-03 16:57:10 +08:00
Krislu1221
02df20cd55 docs(providers): add ModelScope (魔搭) section
ModelScope is a fully implemented provider (nanobot/providers/registry.py,
image_generation.py, schema.py) with async image-generation task submission
and polling, but was previously undocumented in docs/providers.md.

This patch adds a ModelScope entry under 'Common Provider Patterns',
covering:

- Default base URL: https://api-inference.modelscope.cn/v1
- OpenAI-compatible chat/completions endpoint
- Async image-generation flow (task submit + status poll)
- Automatic 'modelscope/' prefix stripping when calling the API
- A minimal nanobot.yaml example

No code changes; docs-only.
2026-08-03 16:57:10 +08:00
chengyongru
f11710a578
fix(webui): show actual local trigger messages (#5228) 2026-08-03 16:43:01 +08:00
chengyongru
eeecfac538 fix(webui): stabilize thread during IME input 2026-08-03 16:41:08 +08:00
Xubin Ren
ac216c3e94 docs(providers): document Eden AI setup and WebUI parity 2026-08-03 16:40:13 +08:00
Xubin Ren
e7ec981f79 test(providers): verify Eden AI gateway contract 2026-08-03 16:40:13 +08:00
Victor M. SMITH
f42a44817a feat(providers): add Eden AI as an OpenAI-compatible gateway provider
Eden AI (https://www.edenai.co) is an EU-hosted, OpenAI-compatible gateway exposing 100+ models from many providers through a single endpoint and API key. Models use the provider/model naming scheme (the full id is sent upstream, like OpenRouter).

Adds it following the registry's documented two-step recipe:
- a ProviderSpec in providers/registry.py (backend openai_compat, gateway, default_api_base https://api.edenai.run/v3, EDENAI_API_KEY, reasoning_effort)
- the matching field in ProvidersConfig (config/schema.py)

API key via EDENAI_API_KEY only; never hardcoded.

Signed-off-by: Victor M. SMITH <72023257+MVS-source@users.noreply.github.com>
2026-08-03 16:40:13 +08:00
Xubin Ren
84f98f5e92 test(cron): cover invalid schedule expressions 2026-08-03 16:20:22 +08:00
ferkans-amir
73a0080484 fix(cron): validate expression syntax in _validate_schedule_for_add 2026-08-03 16:20:22 +08:00
arcdrake22
c6bd5f0075 test(gateway): align runtime-tasks gather tests with bounded retrieval
The helper never waits on the runtime-tasks gather after cancelling it
(its children are bounded individually), so the finished-gather test must
hand the helper an already-complete gather to exercise the bounded
retrieval path, and the cancelled-gather test must settle the gather
itself instead of expecting the helper to await a still-pending future.

Use a pre-completed child for the finished case and suppress(await) for
the cancelled case; both now assert done() and a single close.
2026-08-03 16:00:39 +08:00
Xubin Ren
39e1533c3b fix(gateway): make resource teardown cancellation-safe 2026-08-03 16:00:39 +08:00
arcdrake22
a91ce900ef test(gateway): add shutdown teardown regression coverage
Covers the lifecycle contract of _close_gateway_runtime: runtime tasks are
cancelled before shared resources close, pending background work is drained
before the close returns, cancellation-swallowing tasks and hanging cleanup
are bounded by their timeouts, a failing close is logged without blocking the
stop, duplicate cleanup is idempotent, and the runtime_tasks gather await path
is exercised for both completed and cancelled gathers.
2026-08-03 16:00:39 +08:00
arcdrake22
8942c22d86 fix(gateway): close agent resources deterministically on shutdown
The gateway shutdown path never closed agent resources explicitly: it relied
on the agent loop task's own finally to run close_mcp() when that task is
cancelled. When the service stops with an in-flight exec session or MCP
subprocess, that path can be skipped or cut short, leaving asyncio subprocess
transports alive after the event loop closes. They are then finalized by
__del__ against a closed loop, producing "RuntimeError: Event loop is closed"
noise in the shutdown log, and in the worst case orphaned subprocesses with
the stop stalling until systemd's timeout kills the cgroup.

The teardown is now extracted into _close_gateway_runtime() with explicit
ordering and bounds:

- Runtime tasks (including the agent loop and any in-flight turn) are
  cancelled and awaited -- bounded -- before exec sessions, subagents, and MCP
  servers are closed, so no active turn is using a shared resource when it
  closes.
- Channel transports are closed before waiting for their runners to exit, since
  some SDKs swallow task cancellation while attempting to reconnect.
- agent.close_mcp() is invoked explicitly, bounded to 15s, and is idempotent:
  it is a no-op when the agent loop's own cleanup already ran, and the
  guaranteed final close otherwise.
- A coroutine that swallows cancellation (e.g. an SDK reconnect loop) can no
  longer hold the stop open until systemd's timeout kills the cgroup; cleanup
  failures are logged instead of blocking shutdown.
2026-08-03 16:00:39 +08:00
chengyongru
a9bb39b833 fix(webui): dismiss mobile keyboard after send 2026-08-03 15:53:05 +08:00
KDB
52bc79d3a0 fix(plugins): use uv when pip is unavailable 2026-08-03 15:42:26 +08:00
chengyongru
5c72fdcd88 fix(webui): remove unused bot identity settings 2026-08-03 14:06:44 +08:00
yang1
f7a6bc2d21 fix(webui): globally register correct MIME types for static assets (#5190)
On Windows, mimetypes.guess_type() reads the Content Type value from
HKEY_CLASSES_ROOT\.js (and other extensions) in the registry, which is
commonly set to text/plain because .js is associated with Windows Script
Host rather than web JavaScript. The registry value overrides Python's
built-in mapping and causes browsers to reject ES module scripts.

Fix by explicitly registering correct MIME types via mimetypes.add_type()
at module import time for common web static extensions (.js, .mjs, .css,
.html, .json, .svg, .wasm). Using strict=True ensures the values replace
the registry-backed standard mappings used by mimetypes.guess_type(). This
benefits all callers of mimetypes.guess_type() in the gateway process, not
just _serve_static.

Closes #5190

Co-authored-by: amkile <44280409+amkile@users.noreply.github.com>
2026-08-03 11:33:02 +08:00
arcdrake22
08fe9f7b3a fix(image): send Gemini Flash hints via generationConfig.imageConfig
The live v1beta API rejects the legacy responseFormat.image block
(enum-based aspectRatio/imageSize fields) for gemini-3.1-flash-lite-image
with INVALID_ARGUMENT, even for documented plain-string values. Gemini
Flash image models accept plain-string hints under
generationConfig.imageConfig instead (e.g. aspectRatio 16:9, imageSize
1K), which the API accepts. Switch the flash path to imageConfig and
update the provider tests accordingly. Other providers (aihubmix,
ollama, imagen) are untouched.
2026-08-03 11:22:09 +08:00
chengyongru
8fde956c64 fix(webui): show timestamps for replayed messages 2026-08-03 10:41:50 +08:00
chengyongru
580824a15a
perf(webui): accelerate JSONL session list and thread loading (#5194) 2026-08-03 09:51:35 +08:00
Xubin Ren
db6c9effc3 fix(webui): position sidebar highlight on mount 2026-08-01 23:01:43 +08:00
Xubin Ren
0cb7dd5cc9 refactor(webui): reuse sidebar selection highlight 2026-08-01 23:01:43 +08:00
Xubin Ren
e1894d6f0b fix(providers): respect explicit cloud namespaces 2026-08-01 20:25:58 +08:00
NearlCrews
5eb818e800 fix(providers): require api_base before local provider wins on keyword match
Ollama's spec keeps "nemotron" as a keyword so bare `nemotron-3-nano`
auto-routes to a configured Ollama install (PR #1863). NVIDIA NIM was
later registered with the same "nemotron" keyword (commit 046d0831),
creating the only keyword collision in the registry.

In `_match_provider`, the keyword loop accepted any local provider on
`spec.is_local` alone — no api_base check. Models like
`nvidia/nemotron-3-super-120b-a12b` (intended for OpenRouter or NVIDIA
NIM) were therefore hijacked to http://localhost:11434/v1 even when the
user had never configured Ollama, causing silent connection errors at
runtime.

Add the same api_base gate the local-fallback loop already uses: a local
provider only wins by keyword when the user has actually set its
api_base. Preserves PR #1863's intent for users who configured Ollama;
fixes the silent hijack for everyone else.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-01 20:25:58 +08:00
santhreal
4c387f6633 fix(memory): handle non-string timestamp and missing role in raw_archive 2026-08-01 20:14:28 +08:00
Xubin Ren
e152e7bc0b test(cron): cover stop during manual execution 2026-08-01 20:03:19 +08:00
yu-xin-c
e26e09c205 fix(cron): preserve manual run completion state 2026-08-01 20:03:19 +08:00
KDB
f3bbb543d0 refactor(cli): narrow Pyright suppressions 2026-08-01 19:52:08 +08:00
KDB
b1030ab131 fix(exec): preserve wait targets across response truncation 2026-08-01 19:40:36 +08:00
KDB
39bb20c76b fix(session): tolerate malformed persisted session summary
AutoCompact.prepare_session runs on the turn hot path
(AgentLoop._compact_session) and read the persisted _last_summary metadata
with an unguarded meta['text'] and datetime.fromisoformat(meta['last_active']).
A _last_summary dict that was hand-edited or written by another version
(missing text/last_active, or a non-ISO last_active) raised KeyError/ValueError
out of the turn.

Sibling readers already tolerate the same data: estimate_session_prompt_tokens
uses .get('text') and _archive parses inside try/except. Mirror that tolerance:
skip when text is unusable, and fall back to the session's own updated_at (the
value the writer persists) when last_active is missing or unparseable, so the
archived summary is preserved instead of crashing the turn.
2026-08-01 19:29:16 +08:00
chengyongru
cdb75f8e7d
feat(providers): support DeepSeek Responses API (#5197) 2026-08-01 11:53:51 +08:00
chengyongru
971b977a84
fix(weixin): recover refreshed state after session expiry (#5196) 2026-08-01 00:28:21 +08:00
Pablo Cabeza García
54650332fb fix(slack): scope channel thread openers to their own session
A top-level channel message that opens a thread fell back to the
channel-wide session, because the session key required `raw_thread_ts` —
which Slack only sets on messages that already arrived inside a thread.
Every new thread therefore began life in one shared channel session and
only became thread-scoped from its first reply onward, so unrelated
threads saw each other's opening turns.

Key off `thread_ts` instead. It is set both for messages arriving inside
a thread and for channel messages that `reply_in_thread` opens a thread
for. DM roots never get a `thread_ts`, so they keep the default per-chat
session and the DM routing from 82c5083 is preserved; with
`reply_in_thread` disabled no thread exists and the channel session is
still used.

This restores the per-thread isolation introduced in #1048.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:47:26 +08:00
chengyongru
172fe4f991
fix(webui): preserve user scroll ownership near tail (#5193) 2026-07-31 23:37:26 +08:00
shixi-li
dda9b61b1e fix(config): install timezone data on all platforms 2026-07-31 19:55:22 +08:00
chengyongru
6a1a45d07a
feat: preserve Responses reasoning state and compact context (#5172) 2026-07-30 22:39:43 +08:00
Solaris-star
511c764f45 fix(agent): route finish_reason='length' with blank content to length recovery
When an LLM response arrives with finish_reason='length' and has_tool_calls
but blank text content (e.g. the model spent its whole output budget on a
tool call whose closing tag was truncated), the runner dropped the tool
calls and then misrouted the blank response into the empty-response retry
branch. Retrying the same prompt cannot recover from output-budget
exhaustion, so every retry hit the same length ceiling and the turn ended
in the generic apology.

The length-recovery branch was gated on 'finish_reason == length and not
is_blank_text(clean)', so a blank-but-truncated turn could never reach it.

- The empty-response retry branch now excludes finish_reason == 'length'
  (in addition to 'error').
- The length-recovery branch no longer requires non-blank content, so a
  blank-but-truncated turn enters recovery and appends
  build_length_recovery_message (which handles a blank tail safely).

Adds a regression test asserting the length-recovery path is taken; it
fails on the unfixed code and passes with the fix.

Fixes #5133
2026-07-30 19:55:19 +08:00
Xubin Ren
0eac82984c test(mcp): stabilize idle reconnect timing 2026-07-30 19:44:09 +08:00
yu-xin-c
5e67fbf93e fix(exec): bound buffered session output 2026-07-30 19:44:09 +08:00
yu-xin-c
9ec4420104 fix(agent): release idle session locks 2026-07-30 19:17:37 +08:00
KDB
52680dbe19 fix(pairing): keep approvals across transient store read failures
_load() treated any OSError like corruption and returned an empty store. When pairing.json was transiently unreadable, an unapproved DM could deny the sender, generate a pairing code from the empty view, and overwrite the store without its approved senders.

Keep the existing JSONDecodeError reset behavior, but propagate OSError so mutations cannot persist unreadable state. Read-only checks fail closed without writing; mutating /pairing subcommands report temporary unavailability; and the DM pairing path skips one reply instead of crashing the handler.

This mirrors the refuse-to-overwrite strategy used by the cron and trigger stores.
2026-07-30 19:02:37 +08:00
KDB
e633f867e8 fix(session): tolerate invalid idle-compaction timestamps 2026-07-30 18:52:16 +08:00
KDB
07c2677eed fix(webui): drop malformed token-usage day keys
normalize_token_usage_state only length-checked persisted day keys, so a
hand-edited or foreign 10-char key (e.g. "not-a-dat3" or "2026-13-01") in
token-usage.json survived reads and atomic rewrites. token_usage_payload
then parsed every day key with an unguarded datetime.fromisoformat, so one
such key failed every /api/settings and /api/settings/usage request until
the file was repaired by hand.

Validate day keys in normalize_token_usage_state, the shared boundary that
every read, record, and rewrite already funnels through. Malformed keys are
dropped like other malformed rows and scrubbed from the file on the next
write; valid state is unchanged.
2026-07-30 18:41:44 +08:00