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
- Do nothing for transient causes — the next READY or restart re-runs the reconcile (the success fingerprint was not recorded)
- If it repeats across READYs, check the bot token and application id
- 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
- Expect reconcile to converge over multiple READY events; do not alarm on a single failure
- Keep a single writer for slash-command reconcile per application
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
- listing commands failed ({})
- interaction defer failed ({status}): {err}
- Discord remove reaction failed ({status}): {err}
- desired command set is not an array
- slash command registration failed for '{name}' ({status}): {
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9d5a08dab852deec.
Report an issue: GitHub.