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

{failed_deletes} stale skill command delete(s) failed; recon

Error message

{failed_deletes} stale skill command delete(s) failed; reconcile not recorded, next READY retries

What it means

After upserting, reconcile deletes stale skill commands that no longer match the desired set. If any delete fails, it bails with this message and deliberately does NOT record a successful reconcile: the persisted fingerprint stays stale, so the next gateway READY retries the whole reconcile. This is a designed soft failure, not data loss.

Source

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

            return Ok(ReconcileOutcome::RateLimited { until });
        }
        if !resp.status().is_success() {
            let status = resp.status();
            let err = resp.text().await.unwrap_or_default();
            anyhow::bail!("slash command registration failed for '{name}' ({status}): {err}");
        }
        upserted += 1;
    }
    if upserted > 0 {
        ::zeroclaw_log::record!(
            INFO,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                .with_attrs(::serde_json::json!({"upserted": upserted})),
            "discord slash commands upserted"
        );
    }
    if failed_deletes > 0 {
        anyhow::bail!(
            "{failed_deletes} stale skill command delete(s) failed; \
             reconcile not recorded, next READY retries"
        );
    }
    Ok(ReconcileOutcome::Reconciled)
}

#[cfg(test)]
mod typed_option_tests {
    use super::super::slash_options::{OptKind, OptionSpec};
    use super::*;

    fn spec_with(options: Vec<OptionSpec>) -> DiscordSlashCommandSpec {
        DiscordSlashCommandSpec {
            skill_name: "s".to_string(),
            slug: "s".to_string(),
            description: "d".to_string(),
            description_localizations: Default::default(),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Do nothing for transient causes — the next READY or restart re-runs the reconcile (the success fingerprint was not recorded)
  2. If it repeats across READYs, check the bot token and application id
  3. Ensure a single writer reconciles this application; concurrent reconciles with different desired sets will fight
Defensive patterns

Strategy: retry

Try / catch

match reconcile_slash_commands(&client, &token, &app_id, &desired, base, scope, &guilds).await {
    Ok(outcome) => Ok(outcome),
    Err(e) if e.to_string().contains("reconcile not recorded, next READY retries") => {
        // designed soft failure: state intentionally left stale so READY re-runs it
        tracing::warn!("slash reconcile deferred to next READY: {e}");
        Ok(ReconcileOutcome::RateLimited { until: next_ready() })
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: DELETE of a stale application command returns non-success (401/403/5xx) during a reconcile triggered by a change in the skill command set; 429 is converted to RateLimited before deletes run.

Common situations: Removing or renaming skills while credentials are degraded; Discord 5xx during cleanup; token lacking permission to modify the application's commands; two processes reconciling the same application with different desired sets.

Related errors


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