{"record":{"id":"e1cb058418096432","repo":"zeroclaw-labs/zeroclaw","slug":"heartbeat-to-is-required-when-heartbeat-target-is","errorCode":null,"errorMessage":"heartbeat.to is required when heartbeat.target is set","messagePattern":"heartbeat\\.to is required when heartbeat\\.target is set","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/daemon/mod.rs","lineNumber":2210,"sourceCode":"        .target\n        .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","sourceCodeStart":2192,"sourceCodeEnd":2228,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/daemon/mod.rs#L2192-L2228","documentation":"resolve_heartbeat_delivery pairs heartbeat.target (the delivery channel) with heartbeat.to (the recipient). Both unset triggers auto-detection of the first configured channel; both set is validated and used; exactly one set is an unaddressable half-specification, so the worker bails at startup.","triggerScenarios":"[heartbeat] target = \"telegram\" with no to key; or the legacy alias channel = \"telegram\" without a recipient.","commonSituations":"Adding a target channel to route heartbeat pings but forgetting the chat id; migrating older configs that had channel-only delivery.","solutions":["Add to = \"<chat id / recipient>\" next to target in the [heartbeat] table","Or remove target (and to) entirely to fall back to auto-detecting the first configured channel"],"exampleFix":"# before\n[heartbeat]\nenabled = true\nagent = \"main\"\ntarget = \"telegram\"\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.to is required\") {\n        eprintln!(\"config error: add heartbeat.to (recipient) alongside heartbeat.target\");\n    }\n    return Err(err);\n}","preventionTips":["Always set target and to together, or neither — there is no valid half-pair","Remember both-unset is valid: the first configured channel is auto-detected"],"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"}