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
- Make the desired set a JSON array of command objects: [{\"name\": ..., ...}, ...]
- Validate that the skills → command mapping emits an array before reconciling
- 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
- Keep command-set builders returning Vec<Value> and serialize the vector itself, never a wrapper object
- Assert the top-level shape in any test that feeds reconcile_slash_commands
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
- interaction defer failed ({status}): {err}
- listing commands failed ({})
- slash command registration failed for '{name}' ({status}): {
- {failed_deletes} stale skill command delete(s) failed; recon
- Schema missing required 'type' field
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/18e1eb7ed9b84dda.
Report an issue: GitHub.