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