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

heartbeat worker requires `[heartbeat] agent = "<alias>"` na

Error message

heartbeat worker requires `[heartbeat] agent = "<alias>"` naming a configured agent

What it means

resolve_heartbeat_workspace_dir resolves which agent workspace the heartbeat worker runs in. HeartbeatConfig.agent defaults to an empty string and there is deliberately no default-agent fallback, so enabling the heartbeat without [heartbeat] agent = "<alias>" fails fast at worker startup instead of pinging from an unintended workspace.

Source

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

                    if ran_for >= stable_run {
                        backoff = initial_backoff_secs.max(1);
                    }
                }
            }

            crate::health::bump_component_restart(name);
            crate::util::release_freed_heap();
            tokio::time::sleep(Duration::from_secs(backoff)).await;
            // Double backoff AFTER sleeping so first error uses initial_backoff
            backoff = backoff.saturating_mul(2).min(max_backoff);
        }
    })
}

fn resolve_heartbeat_workspace_dir(config: &Config) -> Result<(String, PathBuf)> {
    let agent_alias = config.heartbeat.agent.trim().to_string();
    if agent_alias.is_empty() {
        anyhow::bail!(
            "heartbeat worker requires `[heartbeat] agent = \"<alias>\"` naming a configured agent"
        );
    }
    if config.agent(&agent_alias).is_none() {
        anyhow::bail!(
            "[heartbeat] agent = {agent_alias:?} is not configured ([agents.{agent_alias}] missing)"
        );
    }
    let workspace_dir = config.agent_workspace_dir(&agent_alias);
    Ok((agent_alias, workspace_dir))
}

/// Test-only hook for [`connect_heartbeat_mcp_registry`]. The daemon
/// heartbeat worker builds an `Arc<McpRegistry>` once at worker start
/// and shares it across every tick so that stdio MCP children live
/// for the daemon's lifetime. Tests inject a hook here to
/// count invocations and assert the registry is constructed at most
/// once for N simulated ticks.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set [heartbeat] agent = "<alias>" where <alias> matches an existing [agents.<alias>] table
  2. Verify the referenced agent exists: check the [agents.<alias>] section in zeroclaw.toml
  3. Restart the daemon so the heartbeat worker starts cleanly

Example fix

# zeroclaw.toml — before
[heartbeat]
enabled = true
interval_minutes = 30

# after
[heartbeat]
enabled = true
agent = "main"
interval_minutes = 30

# and the referenced agent must exist
[agents.main]
model = "..."
Defensive patterns

Strategy: validation

Validate before calling

let alias = config.heartbeat.agent.trim();
if config.heartbeat.enabled && alias.is_empty() {
    anyhow::bail!("[heartbeat] agent is required when heartbeat is enabled");
}

Type guard

fn heartbeat_agent_is_set(config: &zeroclaw_config::schema::Config) -> bool {
    !config.heartbeat.enabled || !config.heartbeat.agent.trim().is_empty()
}

Try / catch

if let Err(err) = run_heartbeat_worker(config.clone()).await {
    if err.to_string().contains("[heartbeat] agent") {
        eprintln!("config error: set [heartbeat] agent = \"<alias>\" naming an [agents.<alias>] entry");
    }
    return Err(err);
}

Prevention

When it happens

Trigger: [heartbeat] enabled = true with no agent key, agent = "", or agent = " " (trimmed empty). Raised when the daemon spawns run_heartbeat_worker at startup or in the heartbeat workspace tests.

Common situations: Copying a minimal heartbeat example that omits agent; re-enabling heartbeat after agent tables were renamed; upgrading from a setup that assumed an implicit default agent.

Related errors


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