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

heartbeat.to is required when heartbeat.target is set

Error message

heartbeat.to is required when heartbeat.target is set

What it means

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.

Source

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

        .target
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty());
    let target = config
        .heartbeat
        .to
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty());

    match (channel, target) {
        // Both explicitly set — validate and use.
        (Some(channel), Some(target)) => {
            validate_heartbeat_channel_config(config, channel)?;
            Ok(Some((channel.to_string(), target.to_string())))
        }
        // Only one set — error.
        (Some(_), None) => anyhow::bail!("heartbeat.to is required when heartbeat.target is set"),
        (None, Some(_)) => anyhow::bail!("heartbeat.target is required when heartbeat.to is set"),
        // Neither set — try auto-detect the first configured channel.
        (None, None) => Ok(auto_detect_heartbeat_channel(config)),
    }
}

const HEARTBEAT_SESSION_CONTEXT_MESSAGES: usize = 20;

fn load_heartbeat_session_context(config: &Config) -> Option<String> {
    use zeroclaw_providers::traits::ChatMessage;

    let channel = config
        .heartbeat
        .target
        .as_deref()
        .map(str::trim)
        .filter(|v| !v.is_empty())?;
    let to = config

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Add to = "<chat id / recipient>" next to target in the [heartbeat] table
  2. Or remove target (and to) entirely to fall back to auto-detecting the first configured channel

Example fix

# before
[heartbeat]
enabled = true
agent = "main"
target = "telegram"

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

Strategy: validation

Validate before calling

match (&config.heartbeat.target, &config.heartbeat.to) {
    (Some(_), None) => anyhow::bail!("heartbeat.to is required when heartbeat.target is set"),
    (None, Some(_)) => anyhow::bail!("heartbeat.target is required when heartbeat.to is set"),
    _ => {}
}

Type guard

fn heartbeat_delivery_pair_is_complete(config: &zeroclaw_config::schema::Config) -> bool {
    config.heartbeat.target.is_some() == config.heartbeat.to.is_some()
}

Try / catch

if let Err(err) = run_heartbeat_worker(config.clone()).await {
    if err.to_string().contains("heartbeat.to is required") {
        eprintln!("config error: add heartbeat.to (recipient) alongside heartbeat.target");
    }
    return Err(err);
}

Prevention

When it happens

Trigger: [heartbeat] target = "telegram" with no to key; or the legacy alias channel = "telegram" without a recipient.

Common situations: Adding a target channel to route heartbeat pings but forgetting the chat id; migrating older configs that had channel-only delivery.

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/e1cb058418096432. Report an issue: GitHub.