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>
Read response lines until finding a JSON-RPC response matching the
request ID. Previously, the bridge read one line and assumed it was
the response, causing "No result from MCP tools/call" when MCP
servers send notifications or log lines before the actual result.
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).
- Ensure all templates have manifest_toml field
- Use spawnFromTemplate for templates with manifest_toml
- Fix spawnBuiltin to handle missing fields gracefully
- Update HTML template to call correct spawn method
- Replace hardcoded list of 6 templates with all 30+ available templates
- Add category information to templates
- Combine static and dynamic templates with static templates displayed first
- Add loading and error states for template list
- Fix showDetail method for agent configuration
Add a [heartbeat] section to KernelConfig so users can tune the
inactivity timeout that determines when agents are marked unresponsive.
Reactive agents (hands) that sit idle between infrequent requests were
getting marked as crashed after the hardcoded 180s default, causing
the first request after idle to fail.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Modified agents.js to fetch templates dynamically from /api/templates endpoint
- Updated API endpoint to include category information for templates
- Added category mapping logic for proper template categorization
- Updated HTML template to handle loading and error states
- Implemented fallback to hardcoded templates if API fails
This change replaces the hardcoded list of 6 templates with all 32 available
agent templates from the agents/ directory, making the web interface more
dynamic and maintainable.
Two fixes for the Matrix bot stuck in infinite reply loop (#757):
1. Use validated user ID from /whoami instead of config value for
self-message filtering. Matrix server delegation or casing
differences can cause the configured user_id to not match the
sender field in timeline events, so the bot processes its own
replies and enters an infinite loop.
2. Add event ID dedup set to prevent re-processing the same event
on sync token races or reconnects. This is a defense-in-depth
measure that also protects against edge cases where /sync returns
overlapping event windows.
Fixes#757
- Add MiniMax-M2.7 model entry (Frontier tier, 1M context, vision+tools)
- Update default 'minimax' alias to resolve to MiniMax-M2.7
- Add 'minimax-m2.7' alias for explicit model selection
- Add M2.7 pricing in metering (same as M2.5: $1.10/$4.40 per 1M tokens)
- Update model catalog tests for M2.7 as new default
- Increment MiniMax model count from 6 to 7
- record_failure() now only recomputes next_run when the job is already
overdue (next_run <= now), preserving the scheduled fire time when a
manual run fails before the job's natural next_run
- Remove premature job.last_run update in scheduler.js — the job runs
asynchronously so last_run should only reflect the server-side
completion timestamp on the next data refresh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GET /api/cron/jobs: show actual {jobs: [...], total} wrapper and
document the ?agent_id query filter
- POST /api/cron/jobs: fix status code to 201 Created, show the actual
{result: "<stringified-json>"} response shape
- GET /api/cron/jobs/{id}/status: show full JobMeta structure with
nested job object, one_shot, last_status, consecutive_errors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>