* fix(skill-extractor): walk all brace candidates so stray braces in prose do not swallow valid JSON
The extractor sliced from the FIRST brace to the LAST brace to recover
JSON embedded in surrounding commentary. When the model emits stray
braces before the JSON object, the slice produces invalid JSON,
json.loads raises, and the exception is swallowed -- the skill is
silently lost.
Fix: walk each brace candidate left-to-right and attempt json.loads on
each slice. The first candidate that parses successfully wins. If none
parse, json.loads on the original text raises and the existing
JSONDecodeError handler logs and returns None as before.
Tested locally -- 8/8 tests passed:
tests/test_extract_skill_json_nonstring.py (2 passed)
tests/test_skill_extractor_rows.py (1 passed)
tests/test_search_content_extraction_parity.py (2 passed)
tests/test_deep_research_search_error.py (3 passed)
Closes#2199
* test(skill-extractor): add focused repro for stray-brace JSON recovery
* test(skill-extractor): add regression test for leading invalid-brace fragment
Addresses the remaining edge case from review: a response that *starts*
with a brace but the leading fragment isn't valid JSON (e.g.
'{not json}\n{"title": "Valid later", ...}') still needs to recover
the valid skill object that follows.
_extract_json_object (already on dev) handles this correctly — it tries
the whole de-fenced string first, then walks each '{' candidate left-to-
right regardless of whether the response begins with '{', so the leading
invalid fragment no longer short-circuits recovery of the real object.
Updates the comment at the call site to call this out explicitly and adds
a regression test covering exactly the scenario described in review.
Three IMAP connection leaks were recently fixed via try/finally
(#1325, #1330, #1423). This commit applies the same pattern to the
remaining callsites that still used inline logout-only cleanup.
routes/email_helpers.py:
- _fetch_sender_thread_context: conn was uninitialized when the outer
try/except returned early on connect failure, causing the finally
block to crash on conn.close()/conn.logout(). Merged the two
separate try blocks into one and added conn=None guard.
- _pre_retrieve_context: ctx_conn.logout() was inside the loop body
with no finally, so any exception in the folder/search loop leaked
the socket. Moved cleanup into a finally block with ctx_conn=None
guard.
mcp_servers/email_server.py:
- _list_emails: multiple inline conn.logout() calls on early-return
paths; exception between them leaked the socket. Wrapped in
try/finally.
- _read_email: same pattern — four separate logout() calls replaced
by a single finally block.
- _reply_to_email: logout() called before the error check, so an
exception in conn.select() leaked the socket. Wrapped in
try/finally.
- _download_attachment: same pattern as _reply_to_email.
Also adds tests/test_imap_leak_fixes.py with 9 regression tests (one
per function/failure-mode) that monkeypatch _imap_connect and assert
conn.logout() is called exactly once even when IMAP operations raise.
* ci: skip pytest smoke on documentation-only changes
Adding paths-ignore for **.md and docs/** so that PRs that touch only
markdown files do not trigger the full pytest suite. Runner minutes are
spent only when Python or config files change.
Closes#2646.
* ci: detect docs-only changes inside the job instead of paths-ignore
Previously paths-ignore on the pull_request trigger caused the entire
workflow to be skipped, which can leave required checks pending and block
merging. Instead, keep the workflow always-triggered and detect docs-only
changes inside python-tests with a git diff step; if every changed file
is a .md or docs/ path, the step reports success without running pytest.
The syntax jobs (python-syntax, node-syntax) are cheap enough to always run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(cookbook): move _diagnose_serve_output to module level in cookbook_helpers
Extracts the nested _diagnose_serve_output function from inside
setup_cookbook_routes() and moves it to module level in cookbook_helpers.py,
alongside the other helper functions it logically belongs with.
No behaviour change — the function is now importable directly for testing
and by other callers without going through the route factory closure.
* fix(cookbook): surface backend diagnosis when serve fails in background
The background poll (_pollBackgroundStatus) already received `diagnosis`
and `cmd` from /api/cookbook/tasks/status but discarded both. When a serve
job died while the Cookbook modal was closed, reopening it showed only a
red error badge with no context.
- Persist live.diagnosis into task._backendDiagnosis in localStorage so it
survives modal close/reopen and page refresh
- Persist live.cmd into task.payload._cmd for agent-spawned tasks so the
crash report includes the actual command
- After _renderRunningTab(), walk rendered cards and call _showDiagnosis()
for any that have a stored _backendDiagnosis but no panel yet
- In _renderTaskCard(), use _backendDiagnosis as a fallback when the
client-side _terminalServeDiagnosis() finds nothing
* test(cookbook): add coverage for _diagnose_serve_output error patterns
10 tests verifying the 16 serve-failure patterns:
- CUDA OOM, port-in-use, vLLM missing, gated model
- Traceback fallback fires without startup success marker
- Traceback suppressed when server actually started
- Clean/empty output returns None
- trust-remote-code and no-GGUF patterns
PermissionError was not in the except tuple so an unreadable settings.json
would crash the app instead of falling back to defaults. Added alongside the
existing FileNotFoundError/JSONDecodeError/ValueError catches.
Also adds test_settings_error_paths.py covering all four failure modes:
missing file, corrupted JSON, wrong type, and permission denied.
setup.py initialises auth.json and .env on a fresh install but was never
called by the Docker entrypoint, leaving new deployments without admin
credentials or a working config.
Adds a single gosu-wrapped call to setup.py before the final exec drop.
setup.py is fully idempotent (skips existing files) so subsequent starts
are unaffected. || true ensures a setup failure never blocks the app from
starting.
Fixes#1476
Gemma 4 and Phi-4 multimodal are natively vision-capable but their Ollama
tags ("gemma4:12b", "phi-4", "phi4") did not match any keyword in
_VISION_MODEL_KEYWORDS. The image was silently routed to the VL fallback
path instead of being passed directly to the model — users saw the model
respond to a placeholder like "[VL model unavailable - image not analyzed]"
rather than the actual image.
Adds "gemma-4"/"gemma4" and "phi-4"/"phi4" to the keyword list, following
the existing err-toward-True policy (#124): a text-only variant being
treated as vision is the safer failure than dropping a real image.
Fixes#1274 (partial — covers the Gemma 4 + Phi-4 case; the OpenRouter/free
vision fallback path is a separate issue).
Models like gemma4, qwen3.5, and ministral served via Ollama's native
/api/chat respond to OpenAI-style tool schemas by emitting a single
native tool_call chunk and then stopping. The agent loop receives
1 token of round_response and no recognised ToolBlock, so the round
ends immediately — the user sees a one-token response.
Root cause: _is_api_model was True for any endpoint whose host appears
in _API_HOSTS (which includes "host.docker.internal" and "localhost")
OR whose model name matches a keyword like "gemma". Native Ollama
endpoints were never excluded from this path.
Fix: import _is_ollama_native_url from llm_core and treat native Ollama
endpoints (/api/chat, port 11434) as text-only by default — falling back
to the fenced-block tool path the local models are tuned for. The
per-endpoint supports_tools=True toggle (Settings → Endpoints) still
overrides this for users who have explicitly opted in.
Fixes#1567
_getCharacterList() had two bugs that silently dropped every
user-created persona from the group participant picker:
1. The /api/presets/templates endpoint returns a JSON array directly,
but the code read `data.templates` (always undefined). The forEach
over `data.templates || []` iterated over an empty array every time,
so no user templates were ever added.
2. Even if the array had been read correctly, the `t.isCharacter` guard
would have filtered them all out — user templates are saved by
presets.js without that flag, which is only present on built-in
PROMPT_TEMPLATES entries.
Fix: accept both the direct-array and the {templates:[]} shapes, drop
the isCharacter guard (user_templates are personas by definition), and
use the correct field name (system_prompt, not prompt) so the character
prompt actually reaches the group chat.
Fixes#1656
Two related bugs in the Cookbook task lifecycle:
1. "Stop all" fired kills via .click() inside a synchronous forEach but
showed the success toast immediately after — the toast appeared before
any of the async kill requests had been sent, giving the user false
confidence the tasks were stopped.
2. The download auto-retry logic (triggered when DOWNLOAD_FAILED appears
in the task output) had no way to distinguish a network interruption
from a deliberate user stop. A download stopped via "Stop all" or the
individual Stop button could be silently restarted up to two times by
the background monitor.
Fix: persist _userStopped: true to localStorage at the moment the user
clicks Stop (individually) or Stop all. The auto-retry guard checks this
flag before relaunching the download. The flag is written BEFORE the
kill requests fire so there is no window where the monitor can race.
Fixes#1458