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

AcpChannel.request_choice requires at least one choice

Error message

AcpChannel.request_choice requires at least one choice

What it means

request_choice rejects an empty choices slice before doing any protocol work — both downstream paths (form elicitation and the legacy permission overload) need at least one option to present. The source comment notes callers should already gate on this; the bail is a defensive invariant for callers that do not.

Source

Thrown at crates/zeroclaw-channels/src/acp_channel.rs:349

        &self,
        _channel_id: &str,
        _message_id: &str,
        _emoji: &str,
    ) -> anyhow::Result<()> {
        anyhow::bail!("AcpChannel does not support reactions")
    }

    async fn request_choice(
        &self,
        question: &str,
        choices: &[String],
        timeout: Duration,
    ) -> anyhow::Result<Option<String>> {
        if choices.is_empty() {
            // Caller should already gate on this via supports_free_form_ask,
            // but be defensive — both downstream paths require at least one
            // option to present.
            anyhow::bail!("AcpChannel.request_choice requires at least one choice")
        }
        if self.client_caps.form {
            self.request_choice_via_elicitation(question, choices, timeout)
                .await
        } else {
            self.request_choice_via_permission(question, choices, timeout)
                .await
        }
    }

    async fn request_multi_choice(
        &self,
        question: &str,
        choices: &[String],
        min_items: usize,
        max_items: usize,
        timeout: Duration,
    ) -> anyhow::Result<Option<Vec<String>>> {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Check choices.is_empty() before calling and take a caller-side action: skip the question, use a default, or answer free-form if the channel supports it.
  2. Fix the upstream producer so a question that must be asked never has an empty option set.
  3. If the question is optional, skip asking it entirely rather than prompting with no options.

Example fix

// before
let pick = ch.request_choice("Which release?", &options, timeout).await?;

// after
if options.is_empty() {
    tracing::warn!("no release candidates; skipping question");
    return Ok(None);
}
let pick = ch.request_choice("Which release?", &options, timeout).await?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_choices(choices: &[String]) -> anyhow::Result<()> {
    if choices.is_empty() {
        anyhow::bail!("question requires at least one option");
    }
    Ok(())
}

Type guard

fn has_options(choices: &[String]) -> bool {
    !choices.is_empty()
}

Prevention

When it happens

Trigger: An agent tool dynamically builds the option list (search results, filtered items) and the list comes back empty, then request_choice is called with an empty slice — the check fires identically on both the form-capable and legacy paths.

Common situations: Dynamic option sources that can legitimately return zero items (empty search results, filtered schedules); refactors that move the emptiness check away from the call site; generated prompts whose choices template rendered nothing.

Related errors


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