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

desired command set is not an array

Error message

desired command set is not an array

What it means

reconcile_slash_commands takes the desired command set as a serde_json::Value and immediately requires it to be a JSON array of command objects (each with a name). In the shipped runtime the desired set is built internally from skill specs plus the built-in ask command, so this bail means a forked/patched builder or a hand-built payload produced a non-array.

Source

Thrown at crates/zeroclaw-channels/src/discord/slash.rs:472

async fn rate_limit_deadline(resp: reqwest::Response) -> i64 {
    let now = crate::discord_slash_state::now_unix();
    let headers = resp.headers().clone();
    let body = resp.json::<serde_json::Value>().await.ok();
    crate::discord_slash_state::retry_after_deadline(&headers, body.as_ref(), now)
}

pub(crate) async fn reconcile_slash_commands(
    client: &reqwest::Client,
    bot_token: &str,
    app_id: &str,
    desired: &serde_json::Value,
    api_base: &str,
    scope: SlashScope,
    guild_ids: &[String],
) -> anyhow::Result<ReconcileOutcome> {
    let auth = format!("Bot {bot_token}");
    let Some(desired) = desired.as_array() else {
        anyhow::bail!("desired command set is not an array");
    };
    let desired_names: std::collections::HashSet<&str> = desired
        .iter()
        .filter_map(|c| c.get("name").and_then(|n| n.as_str()))
        .collect();

    let global_base = format!("{api_base}/applications/{app_id}/commands");
    let guild_base = |g: &str| format!("{api_base}/applications/{app_id}/guilds/{g}/commands");
    let (active, inactive): (Vec<String>, Vec<String>) = match scope {
        SlashScope::Global => (
            vec![global_base],
            guild_ids.iter().map(|g| guild_base(g)).collect(),
        ),
        SlashScope::Guild => (
            guild_ids.iter().map(|g| guild_base(g)).collect(),
            vec![global_base],
        ),
    };

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Make the desired set a JSON array of command objects: [{\"name\": ..., ...}, ...]
  2. Validate that the skills → command mapping emits an array before reconciling
  3. Fix test fixtures that pass an object with a commands key instead of the bare array

Example fix

// before
let desired = serde_json::json!({ "commands": specs }); // object: reconcile bails

// after
let desired = serde_json::json!(specs); // array of command objects
Defensive patterns

Strategy: type-guard

Validate before calling

if !desired.is_array() {
    anyhow::bail!("desired command set must be a JSON array of command objects");
}

Type guard

// Rust — narrow the desired set before reconciling
fn is_valid_desired_set(desired: &serde_json::Value) -> bool {
    desired.as_array().is_some_and(|cmds| {
        cmds.iter().all(|c| c.get("name").is_some_and(|n| n.is_string()))
    })
}

Prevention

When it happens

Trigger: Calling reconcile_slash_commands with a desired value that is an object, string, or null instead of an array — e.g. a fork building desired straight from config whose schema drifted, or a test fixture with the wrong top-level shape.

Common situations: Forks that derive the command set from user config where the config shape changed; hand-written test payloads wrapping the commands in an object instead of the array itself.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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