{"record":{"id":"8ebbc32762c65024","repo":"zeroclaw-labs/zeroclaw","slug":"heartbeat-target-is-set-to-channel-but-channel","errorCode":null,"errorMessage":"heartbeat.target is set to {channel} but {channel} is an input-only channel that cannot deliver outbound messages","messagePattern":"heartbeat\\.target is set to (.+?) but (.+?) is an input-only channel that cannot deliver outbound messages","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/daemon/mod.rs","lineNumber":2444,"sourceCode":"    }\n    if !config.channels.mattermost.is_empty() {\n        // Mattermost requires explicit target\n        return None;\n    }\n    None\n}\n\nfn validate_heartbeat_channel_config(config: &Config, channel: &str) -> Result<()> {\n    if !config.channels.is_known_channel(channel) {\n        anyhow::bail!(\"unsupported heartbeat.target channel: {channel}\");\n    }\n    if !config.channels.is_channel_configured(channel) {\n        anyhow::bail!(\n            \"heartbeat.target is set to {channel} but channels.{channel} is not configured\"\n        );\n    }\n    if !config.channels.is_channel_deliverable(channel) {\n        anyhow::bail!(\n            \"heartbeat.target is set to {channel} but {channel} is an input-only channel that cannot deliver outbound messages\"\n        );\n    }\n    Ok(())\n}\n\nfn has_supervised_channels(config: &Config) -> bool {\n    config.channels.has_any_enabled()\n}\n\n// run_mqtt_sop_listener has been moved to zeroclaw-channels::orchestrator::mqtt.\n// The daemon now receives it as a starter via DaemonRegistry::register_mqtt.\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tempfile::TempDir;\n    use zeroclaw_config::schema::MattermostListenMode;","sourceCodeStart":2426,"sourceCodeEnd":2462,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/daemon/mod.rs#L2426-L2462","documentation":"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.","triggerScenarios":"heartbeat.target = \"mqtt\", \"amqp\", \"voice_wake\", \"voice_duplex\", or \"filesystem\" with the target/to pair set (the channel may even be fully configured).","commonSituations":"Home-automation setups that mostly receive over MQTT being reused as a heartbeat sink; picking filesystem intending heartbeats to be written as files.","solutions":["Choose a deliverable outbound channel (telegram, discord, slack, matrix, email, webhook, ...)","If you need the heartbeat in an input-only transport, deliver via a deliverable channel to a webhook bridge that republishes it"],"exampleFix":"# before\n[heartbeat]\nenabled = true\nagent = \"main\"\ntarget = \"mqtt\"\nto = \"home/alerts\"\n\n# after\n[heartbeat]\nenabled = true\nagent = \"main\"\ntarget = \"telegram\"\nto = \"123456789\"","handlingStrategy":"type-guard","validationCode":"if let Some(channel) = &config.heartbeat.target {\n    if !config.channels.is_channel_deliverable(channel) {\n        anyhow::bail!(\"heartbeat.target = {channel} is input-only and cannot deliver outbound messages\");\n    }\n}","typeGuard":"fn is_deliverable_heartbeat_channel(config: &zeroclaw_config::schema::Config, channel: &str) -> bool {\n    config.channels.is_channel_deliverable(channel)\n}","tryCatchPattern":"if let Err(err) = run_heartbeat_worker(config.clone()).await {\n    if err.to_string().contains(\"input-only channel\") {\n        eprintln!(\"config error: pick a deliverable channel (telegram, discord, slack, email, ...) for heartbeat.target\");\n    }\n    return Err(err);\n}","preventionTips":["Remember voice_wake, voice_duplex, mqtt, amqp, and filesystem cannot send — their send is a no-op","Bridge instead: deliver to a webhook that republishes into the input-only transport"],"tags":["heartbeat","channels","input-only"],"backgroundTag":"config-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}