zeroclaw-labs/zeroclaw · error · anyhow::Error
AcpChannel.request_multi_choice requires at least one choice
Error message
AcpChannel.request_multi_choice requires at least one choice
What it means
request_multi_choice rejects an empty choices slice before consulting capabilities. Note the ordering: the emptiness check runs first, so even a client without elicitation.form — which would otherwise receive Ok(None) so the poll tool can take its own non-ACP fallback — still gets an error on an empty list.
Source
Thrown at crates/zeroclaw-channels/src/acp_channel.rs:369
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>>> {
if choices.is_empty() {
anyhow::bail!("AcpChannel.request_multi_choice requires at least one choice")
}
if !self.client_caps.form {
// No legacy fallback for multi-select — session/request_permission
// is single-select-only. Signal Ok(None) so the caller (poll tool)
// takes its own non-ACP fallback path.
return Ok(None);
}
let req = ElicitationRequest {
session_id: self.session_id.clone(),
mode: ElicitationMode::Form,
message: question.to_string(),
requested_schema: multi_select_schema(choices, min_items, max_items),
};
let params = serde_json::to_value(&req)?;
let call = self.rpc.request("elicitation/create", params);
let response_value = match tokio::time::timeout(timeout, call).await {
Ok(Ok(value)) => value,View on GitHub (pinned to 88bb9c8533)
Solutions
- Guard the call: if the list is empty, return early or skip the question rather than invoking request_multi_choice.
- Fix the upstream list producer so multi-select questions always have candidates.
- Remember the adjacent contract: Ok(None) from this API means capability-missing fallback, so keep the empty-list check separate from that path.
Example fix
// before
let picks = ch.request_multi_choice("Pick tags", &tags, 1, 3, timeout).await?;
// after
if tags.is_empty() {
return Ok(None); // nothing to select; let the caller skip
}
let picks = ch.request_multi_choice("Pick tags", &tags, 1, 3, timeout).await?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_multi_choices(choices: &[String]) -> anyhow::Result<()> {
if choices.is_empty() {
anyhow::bail!("multi-select requires at least one option");
}
Ok(())
} Type guard
fn has_options(choices: &[String]) -> bool {
!choices.is_empty()
} Prevention
- Keep the empty-list guard separate from the Ok(None) capability-missing fallback so the two paths are not confused
- Derive min/max bounds from the choices length so an empty list never reaches the prompt
- Test the poll tool with empty candidate sets
When it happens
Trigger: The poll tool or another caller builds a multi-select list dynamically and passes an empty slice to request_multi_choice, regardless of whether the client advertised elicitation.form.
Common situations: Tag/category pickers whose source list is empty after filtering; batch-selection prompts over an empty result set; refactors that drop the guard at the call site.
Related errors
- AcpChannel.request_choice requires at least one choice
- ACP elicitation/create (multi) failed: {} ({})
- ACP elicitation/create (multi) timed out after {timeout:?}
- ACP returned unexpected outcome: {other}
- ACP elicitation/create failed: {} ({})
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/84ff0bd00aaa4b9d.
Report an issue: GitHub.