{"record":{"id":"b9eb06d8537cfbb8","repo":"zeroclaw-labs/zeroclaw","slug":"heartbeat-target-is-required-when-heartbeat-to-is","errorCode":null,"errorMessage":"heartbeat.target is required when heartbeat.to is set","messagePattern":"heartbeat\\.target is required when heartbeat\\.to is set","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/daemon/mod.rs","lineNumber":2211,"sourceCode":"        .as_deref()\n        .map(str::trim)\n        .filter(|value| !value.is_empty());\n    let target = config\n        .heartbeat\n        .to\n        .as_deref()\n        .map(str::trim)\n        .filter(|value| !value.is_empty());\n\n    match (channel, target) {\n        // Both explicitly set — validate and use.\n        (Some(channel), Some(target)) => {\n            validate_heartbeat_channel_config(config, channel)?;\n            Ok(Some((channel.to_string(), target.to_string())))\n        }\n        // Only one set — error.\n        (Some(_), None) => anyhow::bail!(\"heartbeat.to is required when heartbeat.target is set\"),\n        (None, Some(_)) => anyhow::bail!(\"heartbeat.target is required when heartbeat.to is set\"),\n        // Neither set — try auto-detect the first configured channel.\n        (None, None) => Ok(auto_detect_heartbeat_channel(config)),\n    }\n}\n\nconst HEARTBEAT_SESSION_CONTEXT_MESSAGES: usize = 20;\n\nfn load_heartbeat_session_context(config: &Config) -> Option<String> {\n    use zeroclaw_providers::traits::ChatMessage;\n\n    let channel = config\n        .heartbeat\n        .target\n        .as_deref()\n        .map(str::trim)\n        .filter(|v| !v.is_empty())?;\n    let to = config\n        .heartbeat","sourceCodeStart":2193,"sourceCodeEnd":2229,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/daemon/mod.rs#L2193-L2229","documentation":"resolve_heartbeat_delivery requires heartbeat.target and heartbeat.to to be set together. A recipient without a channel cannot be routed, so setting to while target is unset bails at heartbeat worker startup instead of guessing a channel for an explicitly given recipient.","triggerScenarios":"[heartbeat] to = \"123456789\" with no target key; or the legacy alias recipient = \"...\" without channel.","commonSituations":"Configuring the recipient first and intending to add the channel later; commenting out the target line while debugging delivery.","solutions":["Add target = \"<channel>\" next to to in the [heartbeat] table","Or remove both keys to fall back to auto-detection of the first configured channel"],"exampleFix":"# before\n[heartbeat]\nenabled = true\nagent = \"main\"\nto = \"123456789\"\n\n# after\n[heartbeat]\nenabled = true\nagent = \"main\"\ntarget = \"telegram\"\nto = \"123456789\"","handlingStrategy":"validation","validationCode":"match (&config.heartbeat.target, &config.heartbeat.to) {\n    (Some(_), None) => anyhow::bail!(\"heartbeat.to is required when heartbeat.target is set\"),\n    (None, Some(_)) => anyhow::bail!(\"heartbeat.target is required when heartbeat.to is set\"),\n    _ => {}\n}","typeGuard":"fn heartbeat_delivery_pair_is_complete(config: &zeroclaw_config::schema::Config) -> bool {\n    config.heartbeat.target.is_some() == config.heartbeat.to.is_some()\n}","tryCatchPattern":"if let Err(err) = run_heartbeat_worker(config.clone()).await {\n    if err.to_string().contains(\"heartbeat.target is required\") {\n        eprintln!(\"config error: add heartbeat.target (channel) alongside heartbeat.to\");\n    }\n    return Err(err);\n}","preventionTips":["Do not comment out only one of target/to when debugging delivery — remove both or keep both","The legacy aliases channel/recipient map to the same pair and obey the same rule"],"tags":["heartbeat","channels","config-validation"],"backgroundTag":"config-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}