- #825: Doctor now surfaces blocked workspace skills count in injection scan
- #828: Skill install detects Git URLs (https://, git@) and clones before install
- #856: Custom model names preserved — user-defined models take priority over builtins
- #770: Dashboard WS streaming now triggers Alpine.js reactivity via splice()
- #774: tool_use.input always normalized to JSON object (fixes Anthropic API errors)
- #851/#808: Global skills loaded for all agents; workspace skills properly override globals
- #785: Gemini streaming SSE parser handles \r\n line endings (fixes empty response loop)
All 2,186 tests passing. Live tested with daemon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- #845: Model fallback chain now retries with fallback_models on ModelNotFound
- #844: Heartbeat skips idle agents that never received a message (no more crash loops)
- #823: Doctor --json outputs clean JSON to stdout, tracing to stderr, BrokenPipe handled
- #767: Workflows page scrollable with flex layout fix
- #802: Model dropdown handles object options (no more [object Object] for Ollama)
- #816: Spawn wizard provider dropdown loads dynamically from /api/providers (43 providers)
All 829+ tests passing. Live tested with daemon.
- #834: Remove 3 decommissioned Groq models (gemma2-9b-it, llama-3.2-1b/3b-preview)
- #805: Ollama streaming parser now checks both reasoning_content and reasoning fields
- #820: Browser Hand checks python3 before python, fix optional dep logic
- #848: Hand continuous interval changed from 60s to 3600s to prevent credit waste
- #826: Doctor command no longer reports all_ok when provider key is rejected
- #836: WebSocket tool events now include tool call ID for concurrent call correlation
All 825+ tests passing. Verified live with daemon.
- Implement create-with-PATCH-on-conflict push pattern (POST 409 → PATCH)
- Migrate push and delete endpoints from deprecated v3 to v4 API
- Replace workspaceId with projectId in push/delete API calls
Claude CLI ≥2.x emits type=assistant events where the response text is
inside message.content[{"type":"text","text":"..."}] rather than a flat
content string. The old handler only checked event.content, so every
token was silently dropped and streaming always returned an empty response.
The handler now checks the flat content field first (backward-compatible),
then falls back to joining all text blocks from message.content[].
Refs: RightNow-AI/openfang#295
Mirror the same environment fixes applied to complete(): inject HOME so
the CLI locates ~/.claude/credentials when running as a service, and set
stdin to null so the process does not block on interactive input.
Refs: RightNow-AI/openfang#295
When complete() called child.wait() before reading stdout/stderr, large
responses (>64 KB) caused a deadlock: the subprocess blocked on write()
because the OS pipe buffer was full, and wait() never returned.
Fix by spawning two tokio tasks to drain stdout/stderr concurrently with
child.wait(), then collecting after the process exits.
Also inject HOME from home_dir() so the CLI finds ~/.claude/credentials
when OpenFang runs as a service, and set stdin to null so the CLI does
not stall waiting for interactive input.
Refs: RightNow-AI/openfang#295
Newer Claude CLI versions (≥2.x) emit assistant responses inside a nested
`message.content[].text` structure in stream-json events, rather than a
flat `content` string.
Add ClaudeMessageBlock and ClaudeAssistantMessage structs, plus a new
`message` field on ClaudeStreamEvent, so the stream handler can extract
text from both layouts.
Refs: RightNow-AI/openfang#295
Without this attribute, serde treats a missing `result` field as a
deserialization error even though `Option<T>` implies the field is
optional. Some Claude CLI versions emit the response in `content` or
`text` rather than `result`; the silent parse failure caused the
driver to fall through to a plain-text read which could be empty,
triggering the "model returned an empty response" guard in the agent
loop.
Closes#295.
Co-Authored-By: Claude <noreply@anthropic.com>
Add sanitize_gemini_turns() to enforce Gemini's strict turn-ordering
constraints after message history is trimmed. This merges consecutive
same-role turns, drops orphaned functionCall/functionResponse parts,
and removes empty turns. Also adds #[serde(default)] on GeminiContent.parts
and fixes two tests that were missing required ToolResult messages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bridge now prefixes messages with [From: Name <email>] so agents
know who is speaking. Essential for multi-user rooms and for agents
that need to act on behalf of specific users (e.g., checking the
correct email account or calendar).
Updated bridge integration tests to match the new format.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add #[serde(default)] to GeminiContent.parts so responses with
empty or missing parts arrays deserialize as empty Vec instead
of failing with "missing field parts".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dashboard CSP uses 'unsafe-inline' for script-src, which permits
any inline <script> block to execute — including attacker-injected
scripts if any endpoint reflects user input (agent names, message
content, channel descriptions, etc.).
Replace with a per-request cryptographic nonce (UUID v4):
- webchat_page generates a unique nonce on every request
- All <script> tags embed the nonce at compile time via __NONCE__ placeholder
- CSP becomes: script-src 'self' 'nonce-{nonce}' 'unsafe-eval'
('unsafe-eval' is still required for Alpine.js x-data expressions)
- API endpoints receive a strict default-src 'none'; frame-ancestors 'none'
policy instead of the permissive dashboard policy
This is a standard CSP Level 2 hardening; all modern browsers support nonces.
PUT /api/budget casts &Arc<AppState> to *mut KernelConfig and mutates
the budget fields through a raw pointer. This is unsound: AppState is
shared across Tokio worker threads, so two concurrent PUT /api/budget
requests cause a data race on the same memory location.
Replace with Arc<tokio::sync::RwLock<BudgetConfig>> stored on AppState,
initialized from kernel.config.budget at startup. All readers use
.read().await and all writers use .write().await. No unsafe code remains
in the budget update path.
Fixes: data race / undefined behaviour under concurrent budget updates
MCP servers using Streamable HTTP (e.g., Hindsight) wrap JSON-RPC
responses in SSE framing (event: message\ndata: {...}\n\n). The SSE
transport handler expected raw JSON, causing 'Invalid MCP SSE JSON-RPC
response' errors when connecting to these servers.
Extract the JSON payload from SSE data: lines before deserializing.
Falls back to raw body parsing for servers that return plain JSON.
Fixes connection to MCP servers implementing the Streamable HTTP
transport (MCP spec 2025-03-26).