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>
Resolve catalog display names and aliases to canonical model IDs when spawning or updating agents, keep provider-specific api_key_env hints in sync when switching providers, and route explicit provider model changes through kernel model normalization instead of directly mutating the registry. This fixes cases like xAI agents carrying stale OpenAI auth hints or UI labels such as 'Grok 4.20' being treated as raw model IDs.
(cherry picked from commit 51ee6a5a927208ab0301a29fd2e40b29c9dfaf7d)
Keep a bounded recent approval history instead of dropping timed-out or resolved requests on the floor, return recent approvals from /api/approvals, and make the dashboard poll and badge pending approvals so shell_exec prompts do not disappear before the user ever sees them.
(cherry picked from commit 78dd9f99cc835e85e452889bf6cfced5137f8a4a)
MatrixAdapter hardcoded `auto_accept_invites: true`, meaning any
Matrix-connected instance would blindly join every room it was invited
to. This is a security concern for public-facing homeservers — a
malicious user could invite the bot into an arbitrary room and interact
with the agent without the operator's consent.
Changes:
- Add `auto_accept_invites: bool` to `MatrixConfig` in openfang-types,
with `#[serde(default)]` defaulting to `false`.
- Thread the field through `MatrixAdapter::new()` instead of hardcoding.
- Wire it in `channel_bridge.rs` from `mx_config.auto_accept_invites`.
- Update tests to pass the new parameter.
Operators who want the old behaviour can set:
```toml
[channels.matrix]
auto_accept_invites = true
```
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Feat: Add Nix support
Adding Nix support. Nixos modules may follow...
Run directly with `nix run github:RightNow-AI/openfang`
There are a bunch of flake outputs (based on cargo workspace)
Focus on these:
* openfang-cli (default)
* openfang-desktop
* nix: cmake depdencency was introduced via llama
* Follow upstream style
Merge lark.rs features (dedup, encryption, group filtering, rich text parsing)
into feishu.rs with FeishuRegion toggle (cn/intl). Single [channels.feishu]
config handles both domestic Feishu and international Lark via region field.
- Expand FeishuConfig: region, webhook_path, verification_token, encrypt_key_env, bot_names
- Add FeishuRegion enum with domain switching (open.feishu.cn / open.larksuite.com)
- Add AES-256-CBC event decryption, message/event dedup, group chat filtering
- Update channel_bridge.rs wiring for full config
- Update routes.rs ChannelMeta with new UI fields (region basic, rest advanced)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds a WebSocket-based DingTalk Stream channel adapter as an alternative
to the existing webhook-based DingTalk adapter.
DingTalk Stream Mode uses a long-lived WebSocket connection to the
DingTalk Gateway, eliminating the need for a public webhook endpoint.
Changes:
- `openfang-types`: add `DingTalkStreamConfig` struct and wire into
`ChannelsConfig` alongside the existing `DingTalkConfig`
- `openfang-channels`: implement `DingTalkStreamAdapter` (WebSocket
connection management, ping/pong, token refresh, send via batchSend API)
- `openfang-api`: register `dingtalk_stream` in the channel registry,
`is_channel_configured`, and `channel_config_values`
- `openfang-api`: wire adapter startup in `channel_bridge.rs`
- `openfang-cli`: add `dingtalk_stream` entry to the TUI channels list
Configuration:
```toml
[channels.dingtalk_stream]
app_key_env = "DINGTALK_APP_KEY" # Enterprise Internal App Key
app_secret_env = "DINGTALK_APP_SECRET" # Enterprise Internal App Secret
robot_code_env = "DINGTALK_ROBOT_CODE" # optional, defaults to app_key
```
Requires an Enterprise Internal App in the DingTalk Open Platform with
Stream Mode enabled. No public endpoint needed.
Made-with: Cursor
Co-authored-by: Wang Hanbin <wanghb@best-inc.com>
* feat: heartbeat auto-recovery for crashed agents
Extend the heartbeat monitor to detect and automatically recover crashed
agents, reducing operator intervention for 24/7 autonomous deployments:
- Add RecoveryTracker: per-agent failure count with configurable cooldown
- Heartbeat now monitors both Running and Crashed agents
- Crashed agents auto-recover up to max_recovery_attempts (default 3)
- After exhausting attempts, agents are marked Terminated
- Unresponsive Running agents marked Crashed for next-cycle recovery
- Increase default timeout from 60s to 180s (browser/LLM tasks need time)
- Add HeartbeatStatus.state field for downstream consumers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: claude code driver — PID tracking and message timeout
Add subprocess lifecycle management to prevent hung CLI processes from
blocking agents indefinitely:
- Track active subprocess PIDs in a concurrent DashMap for external monitoring
- Enforce configurable message timeout (default 300s) with automatic process kill
- Return proper LlmError::Api on non-zero exit in streaming mode (was silently ignored)
- Add with_timeout(), active_pids(), pid_map() public methods
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: /restart endpoint — manual per-agent recovery without daemon bounce
POST /api/agents/{id}/restart and /api/agents/{id}/start both:
- Cancel any active task via stop_agent_run()
- Reset agent state to Running (updates last_active)
- Return JSON with previous state and whether a task was cancelled
Enables operators to recover individual crashed/stuck agents through the
API without restarting the entire daemon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: ZiLLA Dev <dev@zilla.wtf>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>