Merge main into fix/ssrf-check, keeping both the auto-recover
logic for error-state agents and the async streaming support
for the send_message endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `stream: bool` parameter to `POST /v1/managed-agents/{id}/messages`.
When `stream=true`, the agent processes the message synchronously and
returns an SSE stream (OpenAI-compatible format) with token-by-token
response, tool result events, and usage metadata.
This enables real-time voice assistants and chat UIs to receive agent
responses as they are generated, rather than polling for completion.
- Extend SendMessageRequest with `stream` field (default: false)
- Add _stream_managed_agent() helper using asyncio.to_thread()
- Build AgentContext from conversation history for multi-turn support
- Emit tool_results as named SSE events
- Persist agent response in DB after streaming completes
- Add 6 new tests covering streaming behavior
- Update agents.md documentation with streaming examples
- Acquire tick BEFORE spawning thread to prevent race condition on concurrent Run Now clicks
- Auto-recover agents in error/needs_attention state when Run Now is clicked
- Auto-recover error-state agents when receiving immediate messages
- End tick on system build failure to avoid stuck running state
- Add test verifying concurrent start_tick raises ValueError
* feat: model catalogue with download/delete and auto-pull Qwen3.5
- Desktop boot: start server immediately with fallback model (qwen3:0.6b),
then pull preferred model (qwen3.5:4b) and remaining Qwen3.5 variants
that fit in RAM in the background. No more broken "Select model" state.
- Backend: add POST /v1/models/pull and DELETE /v1/models/{name} endpoints
so the frontend can trigger model downloads and deletions via Ollama.
- Frontend: redesign CommandPalette (Cmd+K) with two tabs — "Installed"
shows pulled models with select/delete, "Download Models" shows a
catalogue of popular models plus a custom model input field.
- Fix ollama_has_model() to use exact tag matching instead of prefix
matching, preventing false positives.
* fix: streaming, model switching, second-largest default, and tests
- Streaming: use direct engine streaming for non-tool requests so tokens
arrive in real-time instead of being batched by the agent bridge.
Add error handling to _handle_stream so engine errors surface as
content chunks instead of silent failures.
- Model selection: pick the second-largest Qwen3.5 model that fits
(leaves headroom for OS/apps) instead of the absolute largest.
- Model switching: abort in-flight stream when the user changes models
mid-generation, preventing stale-model errors. Improve error messages
in catch blocks.
- Tests: add tests/server/test_model_management.py with 11 tests
covering model pull/delete endpoints, streaming error resilience,
direct-engine streaming bypass, and model listing. All 100 server
tests pass.
The SecurityHeadersMiddleware ran before CORSMiddleware (Starlette
executes middleware in LIFO order) and added headers to OPTIONS
preflight requests. The Content-Security-Policy: default-src 'self'
header told the browser to reject cross-origin connections, so fetch()
from the Tauri webview (https://tauri.localhost) to the API server
(http://127.0.0.1) was blocked — causing "Failed to get response" on
every chat message in the desktop app.
Two fixes:
- Skip security headers on OPTIONS requests so CORS preflight works
- Remove Content-Security-Policy from API responses — it is a
document-level browser policy irrelevant to JSON API responses and
breaks any cross-origin API consumer