zeroclaw-labs/zeroclaw · error · anyhow::Error

heartbeat.target is set to {channel} but {channel} is an inp

Error message

heartbeat.target is set to {channel} but {channel} is an input-only channel that cannot deliver outbound messages

What it means

is_channel_deliverable is false for input-only transports — voice_wake, voice_duplex, mqtt, amqp, and filesystem — whose Channel::send is a no-op. Selecting one as heartbeat.target would silently drop every heartbeat ping, so validate_heartbeat_channel_config rejects it even when the channel is known and configured.

Source

Thrown at crates/zeroclaw-runtime/src/daemon/mod.rs:2444

    }
    if !config.channels.mattermost.is_empty() {
        // Mattermost requires explicit target
        return None;
    }
    None
}

fn validate_heartbeat_channel_config(config: &Config, channel: &str) -> Result<()> {
    if !config.channels.is_known_channel(channel) {
        anyhow::bail!("unsupported heartbeat.target channel: {channel}");
    }
    if !config.channels.is_channel_configured(channel) {
        anyhow::bail!(
            "heartbeat.target is set to {channel} but channels.{channel} is not configured"
        );
    }
    if !config.channels.is_channel_deliverable(channel) {
        anyhow::bail!(
            "heartbeat.target is set to {channel} but {channel} is an input-only channel that cannot deliver outbound messages"
        );
    }
    Ok(())
}

fn has_supervised_channels(config: &Config) -> bool {
    config.channels.has_any_enabled()
}

// run_mqtt_sop_listener has been moved to zeroclaw-channels::orchestrator::mqtt.
// The daemon now receives it as a starter via DaemonRegistry::register_mqtt.

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    use zeroclaw_config::schema::MattermostListenMode;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Choose a deliverable outbound channel (telegram, discord, slack, matrix, email, webhook, ...)
  2. If you need the heartbeat in an input-only transport, deliver via a deliverable channel to a webhook bridge that republishes it

Example fix

# before
[heartbeat]
enabled = true
agent = "main"
target = "mqtt"
to = "home/alerts"

# after
[heartbeat]
enabled = true
agent = "main"
target = "telegram"
to = "123456789"
Defensive patterns

Strategy: type-guard

Validate before calling

if let Some(channel) = &config.heartbeat.target {
    if !config.channels.is_channel_deliverable(channel) {
        anyhow::bail!("heartbeat.target = {channel} is input-only and cannot deliver outbound messages");
    }
}

Type guard

fn is_deliverable_heartbeat_channel(config: &zeroclaw_config::schema::Config, channel: &str) -> bool {
    config.channels.is_channel_deliverable(channel)
}

Try / catch

if let Err(err) = run_heartbeat_worker(config.clone()).await {
    if err.to_string().contains("input-only channel") {
        eprintln!("config error: pick a deliverable channel (telegram, discord, slack, email, ...) for heartbeat.target");
    }
    return Err(err);
}

Prevention

When it happens

Trigger: heartbeat.target = "mqtt", "amqp", "voice_wake", "voice_duplex", or "filesystem" with the target/to pair set (the channel may even be fully configured).

Common situations: Home-automation setups that mostly receive over MQTT being reused as a heartbeat sink; picking filesystem intending heartbeats to be written as files.

Understand the failure class

Background: Config validation failed: what "invalid value for {key}" and settings-rejection errors mean across 19 open-source libraries — this error's family across 19 libraries.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/8ebbc32762c65024. Report an issue: GitHub.